From 732b0044f2af24d80a52146062cddab0d605fffc Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 08:05:53 +0200 Subject: [PATCH 01/96] restore sharing behavior --- ultraplot/axes/cartesian.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index f55380502..251bf9af5 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -2,6 +2,7 @@ """ The standard Cartesian axes used for most ultraplot figures. """ +from cProfile import label import copy import inspect @@ -434,8 +435,7 @@ def _apply_axis_sharing_for_axis( labels._transfer_label(axis.label, shared_axis_obj.label) axis.label.set_visible(False) - # Handle tick label sharing (level > 2) - if level > 2: + if level >= 1: label_visibility = self._determine_tick_label_visibility( axis, shared_axis, @@ -528,8 +528,10 @@ def _convert_label_param(label_param: str) -> str: is_parent_tick_on = sharing_ticks[label_param_trans] if is_panel: label_visibility[label_param] = is_parent_tick_on - elif is_border: - label_visibility[label_param] = is_this_tick_on + elif is_border or getattr(self.figure, f"_share{axis_name}") < 3: + label_visibility[label_param] = ( + is_this_tick_on or sharing_ticks[label_param_trans] + ) return label_visibility def _add_alt(self, sx, **kwargs): From fbd946a3ca48c6c344f8b731b49c9dd6977beb31 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 08:18:31 +0200 Subject: [PATCH 02/96] add unittest submodule intended for testing sharing related functions --- ultraplot/axes/cartesian.py | 2 + ultraplot/figure.py | 2 +- ultraplot/tests/test_sharing.py | 86 +++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 ultraplot/tests/test_sharing.py diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 251bf9af5..935453c2b 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -529,6 +529,8 @@ def _convert_label_param(label_param: str) -> str: if is_panel: label_visibility[label_param] = is_parent_tick_on elif is_border or getattr(self.figure, f"_share{axis_name}") < 3: + # turn on sharing when on border + # or sharing is below 3 label_visibility[label_param] = ( is_this_tick_on or sharing_ticks[label_param_trans] ) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d44f31e61..9170b81db 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1058,7 +1058,7 @@ def _get_sharing_level(self): """ We take the average here as the sharex and sharey should be the same value. In case this changes in the future we can track down the error easily """ - return 0.5 * (self.figure._sharex + self.figure._sharey) + return min(self.figure._sharex, self.figure._sharey) def _add_axes_panel(self, ax, side=None, **kwargs): """ diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py new file mode 100644 index 000000000..9ce7433aa --- /dev/null +++ b/ultraplot/tests/test_sharing.py @@ -0,0 +1,86 @@ +import pytest, ultraplot as uplt + +""" +Sharing levels for subplots determine the visbility of the axis labels and tick labels. + +Axis labels are pushed to the border subplots when the sharing level is greater than 1. + +Ticks are visible only on the border plots when the sharing levels is greater than 2. + +Or more verbosely: + sharey = 0: no sharing, all labels and ticks visible + sharey = 1: share axis, all labels and ticks visible + sharey = 2: share limits + sharey = 3 or True, share both ticks and labels +A similar story holds for sharex. +""" + + +@pytest.mark.parametrize("share_level", [0, "labels", "labs", 1, True]) +@pytest.mark.mpl_image_compare +def test_sharing_levels_y(share_level): + """ + Test sharing levels for y-axis: left and right ticks/labels. + """ + fig, axs = uplt.subplots(None, 2, 3, sharey=share_level) + axs.format(ylabel="Y") + axs.format(title=f"sharey = {share_level}") + fig.canvas.draw() # needed for checks + + if fig._sharey < 3: + border_axes = set(axs) + else: + # Reduce border_axes to a set of axes for left and right + border_axes = set() + for direction in ["left", "right"]: + axes = fig._get_border_axes().get(direction, []) + if isinstance(axes, (list, tuple, set)): + border_axes.update(axes) + else: + border_axes.add(axes) + for axi in axs: + tick_params = axi.yaxis.get_tick_params() + for direction in ["left", "right"]: + label_key = f"label{direction}" + visible = tick_params.get(label_key, False) + is_border = axi in fig._get_border_axes().get(direction, []) + if direction == "left" and (fig._sharey < 3 or is_border): + assert visible + else: + assert not visible + return fig + + +@pytest.mark.parametrize("share_level", [0, "labels", "labs", 1, True]) +@pytest.mark.mpl_image_compare +def test_sharing_levels_x(share_level): + """ + Test sharing levels for x-axis: top and bottom ticks/labels. + """ + fig, axs = uplt.subplots(None, 2, 3, sharex=share_level) + axs.format(xlabel="X") + axs.format(title=f"sharex = {share_level}") + fig.canvas.draw() # needed for checks + + if fig._sharex < 3: + border_axes = set(axs) + else: + # Reduce border_axes to a set of axes for top and bottom + border_axes = set() + for direction in ["top", "bottom"]: + axes = fig._get_border_axes().get(direction, []) + if isinstance(axes, (list, tuple, set)): + border_axes.update(axes) + else: + border_axes.add(axes) + for axi in axs: + tick_params = axi.xaxis.get_tick_params() + for direction in ["top", "bottom"]: + label_key = f"label{direction}" + visible = tick_params.get(label_key, False) + is_border = axi in fig._get_border_axes().get(direction, []) + if direction == "bottom" and (fig._sharex < 3 or is_border): + assert visible + else: + assert not visible + return fig From 923f509f2a3bb7c34d3259a57441ee4520e71e70 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 08:49:34 +0200 Subject: [PATCH 03/96] rm auto add import --- ultraplot/axes/cartesian.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 935453c2b..f81bd2ecd 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -2,7 +2,6 @@ """ The standard Cartesian axes used for most ultraplot figures. """ -from cProfile import label import copy import inspect From 639695d1c8dc171e48e8544493739716f4292bb7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 08:50:47 +0200 Subject: [PATCH 04/96] restore figure share --- ultraplot/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 9170b81db..d44f31e61 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1058,7 +1058,7 @@ def _get_sharing_level(self): """ We take the average here as the sharex and sharey should be the same value. In case this changes in the future we can track down the error easily """ - return min(self.figure._sharex, self.figure._sharey) + return 0.5 * (self.figure._sharex + self.figure._sharey) def _add_axes_panel(self, ax, side=None, **kwargs): """ From 6b1dffe133db56374bac3a9ccdf9ad101d8109e1 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 08:58:23 +0200 Subject: [PATCH 05/96] rm _get_sharing_level --- ultraplot/axes/geo.py | 22 +++++++++++++++++----- ultraplot/figure.py | 10 ++-------- ultraplot/tests/test_geographic.py | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index ab95a661e..d2fb2a440 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -671,7 +671,7 @@ def _apply_axis_sharing(self): # build chain. if not self.stale: return - if self.figure._get_sharing_level() == 0: + if self.figure._sharex == 0 and self.figure._sharey == 0: return def _get_gridliner_labels( @@ -719,10 +719,22 @@ def _handle_axis_sharing( target_axis: The target axis to apply sharing to """ # Copy view interval and minor locator from source to target - - if self.figure._get_sharing_level() >= 2: - target_axis.set_view_interval(*source_axis.get_view_interval()) - target_axis.set_minor_locator(source_axis.get_minor_locator()) + source_view_interval = source_axis.get_view_interval() + source_locator = source_axis.get_minor_locator() + + target_view_interval = target_axis.get_view_interval() + target_locator = target_axis.get_minor_locator() + if self.figure._sharex >= 2: + target_view_interval[0] = source_view_interval[0] + target_view_interval[1] = source_view_interval[1] + target_locator = source_locator + if self.figure._sharey >= 2: + target_view_interval[0] = source_view_interval[1] + target_view_interval[1] = source_view_interval[1] + + target_locator = source_locator + target_axis.set_view_interval(*target_view_interval) + target_axis.set_minor_locator(target_locator) @override def draw(self, renderer=None, *args, **kwargs): diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d44f31e61..97bc7d086 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1054,12 +1054,6 @@ def _get_renderer(self): renderer = canvas.get_renderer() return renderer - def _get_sharing_level(self): - """ - We take the average here as the sharex and sharey should be the same value. In case this changes in the future we can track down the error easily - """ - return 0.5 * (self.figure._sharex + self.figure._sharey) - def _add_axes_panel(self, ax, side=None, **kwargs): """ Add an axes panel. @@ -1270,7 +1264,7 @@ def _share_labels_with_others(self, *, which="both"): """ # Only apply sharing of labels when we are # actually sharing labels. - if self._get_sharing_level() == 0: + if self._sharex == 0 and self._sharey == 0: return # Turn all labels off # Note: this action performs it for all the axes in @@ -1971,7 +1965,7 @@ def format( # When we apply formatting to all axes, we need # to potentially adjust the labels. - if len(axs) == len(self.axes) and self._get_sharing_level() > 0: + if len(axs) == len(self.axes) and (self._sharex or self._sharey): self._share_labels_with_others() # Warn unused keyword argument(s) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 4b95a9384..05e1a4c92 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -551,7 +551,7 @@ def assert_views_are_sharing(ax): l2 = np.linalg.norm( np.asarray(latview) - np.asarray(target_lat), ) - level = ax.figure._get_sharing_level() + level = ax.figure._sharex if level <= 1: share_x = share_y = False assert np.allclose(l1, 0) == share_x From b3a37ae56430753554805624c1bc81b10309d3b3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 09:04:10 +0200 Subject: [PATCH 06/96] update figure format --- ultraplot/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 97bc7d086..04d7eb9be 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1965,7 +1965,7 @@ def format( # When we apply formatting to all axes, we need # to potentially adjust the labels. - if len(axs) == len(self.axes) and (self._sharex or self._sharey): + if len(axs) == len(self.axes) and (self._sharex >= 3 and self._sharey >= 3): self._share_labels_with_others() # Warn unused keyword argument(s) From 724d0da437ea3be2c2983549ae86533db6a51e86 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 09:09:28 +0200 Subject: [PATCH 07/96] bump handle axis sharing geo --- ultraplot/axes/geo.py | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index d2fb2a440..fa134f956 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -653,18 +653,15 @@ def _apply_axis_sharing(self): the leftmost and bottommost is the *figure* sharing level. """ # Handle X axis sharing - if self._sharex: - self._handle_axis_sharing( - source_axis=self._sharex._lonaxis, - target_axis=self._lonaxis, - ) - + self._handle_axis_sharing( + source_axis=self._sharex._lonaxis, + target_axis=self._lonaxis, + which="x", + ) # Handle Y axis sharing - if self._sharey: - self._handle_axis_sharing( - source_axis=self._sharey._lataxis, - target_axis=self._lataxis, - ) + self._handle_axis_sharing( + source_axis=self._sharey._lataxis, target_axis=self._lataxis, which="y" + ) # This block is apart of the draw sequence as the # gridliner object is created late in the @@ -710,6 +707,8 @@ def _handle_axis_sharing( self, source_axis: "GeoAxes", target_axis: "GeoAxes", + *, + which: str, ): """ Helper method to handle axis sharing for both X and Y axes. @@ -719,22 +718,9 @@ def _handle_axis_sharing( target_axis: The target axis to apply sharing to """ # Copy view interval and minor locator from source to target - source_view_interval = source_axis.get_view_interval() - source_locator = source_axis.get_minor_locator() - - target_view_interval = target_axis.get_view_interval() - target_locator = target_axis.get_minor_locator() - if self.figure._sharex >= 2: - target_view_interval[0] = source_view_interval[0] - target_view_interval[1] = source_view_interval[1] - target_locator = source_locator - if self.figure._sharey >= 2: - target_view_interval[0] = source_view_interval[1] - target_view_interval[1] = source_view_interval[1] - - target_locator = source_locator - target_axis.set_view_interval(*target_view_interval) - target_axis.set_minor_locator(target_locator) + if getattr(self.figure, f"_share{which}") >= 2: + target_axis.set_view_interval(*source_axis.get_view_interval()) + target_axis.set_minor_locator(source_axis.get_minor_locator()) @override def draw(self, renderer=None, *args, **kwargs): From 60a28487a35349442acf482719947d68c9765242 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 09:18:29 +0200 Subject: [PATCH 08/96] bump handle axis sharing geo --- ultraplot/axes/geo.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index fa134f956..675396a20 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -653,15 +653,19 @@ def _apply_axis_sharing(self): the leftmost and bottommost is the *figure* sharing level. """ # Handle X axis sharing - self._handle_axis_sharing( - source_axis=self._sharex._lonaxis, - target_axis=self._lonaxis, - which="x", - ) + # + if self._sharex: + self._handle_axis_sharing( + source_axis=self._sharex._lonaxis, + target_axis=self._lonaxis, + which="x", + ) + # Handle Y axis sharing - self._handle_axis_sharing( - source_axis=self._sharey._lataxis, target_axis=self._lataxis, which="y" - ) + if self._sharey: + self._handle_axis_sharing( + source_axis=self._sharey._lataxis, target_axis=self._lataxis, which="y" + ) # This block is apart of the draw sequence as the # gridliner object is created late in the From 010001e76c4e23012ff3395196678c3a8b8320f1 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 09:31:51 +0200 Subject: [PATCH 09/96] refactor and update test --- ultraplot/tests/test_geographic.py | 69 +++++++++++++++--------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 05e1a4c92..5c8cc159f 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -491,7 +491,8 @@ def test_get_gridliner_labels_cartopy(): uplt.close(fig) -def test_sharing_levels(): +@pytest.mark.parametrize("level", [0, 1, 2, 3, 4]) +def test_sharing_levels(level): """ We can share limits or labels. We check if we can do both for the GeoAxes. @@ -515,7 +516,6 @@ def test_sharing_levels(): x = np.array([0, 10]) y = np.array([0, 10]) - sharing_levels = [0, 1, 2, 3, 4] lonlim = latlim = np.array((-10, 10)) def assert_views_are_sharing(ax): @@ -557,40 +557,39 @@ def assert_views_are_sharing(ax): assert np.allclose(l1, 0) == share_x assert np.allclose(l2, 0) == share_y - for level in sharing_levels: - fig, ax = uplt.subplots(ncols=2, nrows=2, proj="cyl", share=level) - ax.format(labels="both") - for axi in ax: - axi.format( - lonlim=lonlim * axi.number, - latlim=latlim * axi.number, - ) + fig, ax = uplt.subplots(ncols=2, nrows=2, proj="cyl", share=level) + ax.format(labels="both") + for axi in ax: + axi.format( + lonlim=lonlim * axi.number, + latlim=latlim * axi.number, + ) - fig.canvas.draw() - for idx, axi in enumerate(ax): - axi.plot(x * (idx + 1), y * (idx + 1)) - - fig.canvas.draw() # need this to update the labels - # All the labels should be on - for axi in ax: - side_labels = axi._get_gridliner_labels( - left=True, - right=True, - top=True, - bottom=True, - ) - s = 0 - for dir, labels in side_labels.items(): - s += any([label.get_visible() for label in labels]) - - assert_views_are_sharing(axi) - # When we share the labels but not the limits, - # we expect all ticks to be on - if level == 0: - assert s == 4 - else: - assert s == 2 - uplt.close(fig) + fig.canvas.draw() + for idx, axi in enumerate(ax): + axi.plot(x * (idx + 1), y * (idx + 1)) + + fig.canvas.draw() # need this to update the labels + # All the labels should be on + for axi in ax: + side_labels = axi._get_gridliner_labels( + left=True, + right=True, + top=True, + bottom=True, + ) + s = 0 + for dir, labels in side_labels.items(): + s += any([label.get_visible() for label in labels]) + + assert_views_are_sharing(axi) + # When we share the labels but not the limits, + # we expect all ticks to be on + if level < 3: + assert s == 4 + else: + assert s == 2 + uplt.close(fig) @pytest.mark.mpl_image_compare From d63d9330ecc0f6b6845226b59f9feeb554163526 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 09:47:09 +0200 Subject: [PATCH 10/96] change x test to be mpl specific --- ultraplot/tests/test_sharing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index 9ce7433aa..925c7fe3e 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -75,6 +75,14 @@ def test_sharing_levels_x(share_level): border_axes.add(axes) for axi in axs: tick_params = axi.xaxis.get_tick_params() + from ultraplot.internals.versions import _mpl_version + from packaging import version + + directions = ( + ["top", "bottom"] + if version.parse(str(_mpl_version)) < version.parse("3.10") + else ["left", "right"] + ) for direction in ["top", "bottom"]: label_key = f"label{direction}" visible = tick_params.get(label_key, False) From 452533e0386e87fca2d173e3a27084c89e5bf72f Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 09:48:10 +0200 Subject: [PATCH 11/96] change x test to be mpl specific --- ultraplot/tests/test_sharing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index 925c7fe3e..e0a6e4028 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -75,12 +75,12 @@ def test_sharing_levels_x(share_level): border_axes.add(axes) for axi in axs: tick_params = axi.xaxis.get_tick_params() - from ultraplot.internals.versions import _mpl_version + from ultraplot.internals.versions import _version_mpl from packaging import version directions = ( ["top", "bottom"] - if version.parse(str(_mpl_version)) < version.parse("3.10") + if version.parse(str(_version_mpl)) < version.parse("3.10") else ["left", "right"] ) for direction in ["top", "bottom"]: From 7394633ef87c7a4cc9003ef19a3989cf48bd79a0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 17:41:33 +0200 Subject: [PATCH 12/96] revert check --- ultraplot/axes/cartesian.py | 170 +++++------------------------------- ultraplot/tests/conftest.py | 3 + 2 files changed, 23 insertions(+), 150 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index f81bd2ecd..03811e9ed 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -2,6 +2,7 @@ """ The standard Cartesian axes used for most ultraplot figures. """ +from cProfile import label import copy import inspect @@ -384,156 +385,25 @@ def _apply_axis_sharing(self): # bottommost or to the *right* of the leftmost panel. But the sharing level # used for the leftmost and bottommost is the *figure* sharing level. - # Get border axes once for efficiency - border_axes = self.figure._get_border_axes() - - # Apply X axis sharing - self._apply_axis_sharing_for_axis("x", border_axes) - - # Apply Y axis sharing - self._apply_axis_sharing_for_axis("y", border_axes) - - def _apply_axis_sharing_for_axis( - self, - axis_name: str, - border_axes: dict[str, plot.PlotAxes], - ) -> None: - """ - Apply axis sharing for a specific axis (x or y). - - Parameters - ---------- - axis_name : str - Either 'x' or 'y' - border_axes : dict - Dictionary from _get_border_axes() containing border information - """ - if axis_name == "x": - axis = self.xaxis - shared_axis = self._sharex - panel_group = self._panel_sharex_group - sharing_level = self.figure._sharex - label_params = ["labeltop", "labelbottom"] - border_sides = ["top", "bottom"] - else: # axis_name == 'y' - axis = self.yaxis - shared_axis = self._sharey - panel_group = self._panel_sharey_group - sharing_level = self.figure._sharey - label_params = ["labelleft", "labelright"] - border_sides = ["left", "right"] - - if shared_axis is None or not axis.get_visible(): - return - - level = 3 if panel_group else sharing_level - - # Handle axis label sharing (level > 0) - if level > 0: - shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") - labels._transfer_label(axis.label, shared_axis_obj.label) - axis.label.set_visible(False) - - if level >= 1: - label_visibility = self._determine_tick_label_visibility( - axis, - shared_axis, - axis_name, - label_params, - border_sides, - border_axes, - ) - axis.set_tick_params(which="both", **label_visibility) - # Turn minor ticks off - axis.set_minor_formatter(mticker.NullFormatter()) - - def _determine_tick_label_visibility( - self, - axis: maxis.Axis, - shared_axis: maxis.Axis, - axis_name: str, - label_params: list[str], - border_sides: list[str], - border_axes: dict[str, list[plot.PlotAxes]], - ) -> dict[str, bool]: - """ - Determine which tick labels should be visible based on sharing rules and borders. - - Parameters - ---------- - axis : matplotlib axis - The current axis object - shared_axis : Axes - The axes this one shares with - axis_name : str - Either 'x' or 'y' - label_params : list - List of label parameter names (e.g., ['labeltop', 'labelbottom']) - border_sides : list - List of border side names (e.g., ['top', 'bottom']) - border_axes : dict - Dictionary from _get_border_axes() - - Returns - ------- - dict - Dictionary of label visibility parameters - """ - ticks = axis.get_tick_params() - shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") - sharing_ticks = shared_axis_obj.get_tick_params() - - label_visibility = {} - - def _convert_label_param(label_param: str) -> str: - # Deal with logic not being consistent - # in prior mpl versions - if version.parse(str(_version_mpl)) <= version.parse("3.9"): - if label_param == "labeltop" and axis_name == "x": - label_param = "labelright" - elif label_param == "labelbottom" and axis_name == "x": - label_param = "labelleft" - return label_param - - for label_param, border_side in zip(label_params, border_sides): - # Check if user has explicitly set label location via format() - label_visibility[label_param] = False - has_panel = False - for panel in self._panel_dict[border_side]: - # Check if the panel is a colorbar - colorbars = [ - values - for key, values in self._colorbar_dict.items() - if border_side in key # key is tuple (side, top | center | lower) - ] - if not panel in colorbars: - # Skip colorbar as their - # yaxis is not shared - has_panel = True - break - # When we have a panel, let the panel have - # the labels and turn-off for this axis + side. - if has_panel: - continue - is_border = self in border_axes.get(border_side, []) - is_panel = ( - self in shared_axis._panel_dict[border_side] - and self == shared_axis._panel_dict[border_side][-1] - ) - # Use automatic border detection logic - # if we are a panel we "push" the labels outwards - label_param_trans = _convert_label_param(label_param) - is_this_tick_on = ticks[label_param_trans] - is_parent_tick_on = sharing_ticks[label_param_trans] - if is_panel: - label_visibility[label_param] = is_parent_tick_on - elif is_border or getattr(self.figure, f"_share{axis_name}") < 3: - # turn on sharing when on border - # or sharing is below 3 - label_visibility[label_param] = ( - is_this_tick_on or sharing_ticks[label_param_trans] - ) - return label_visibility + if self._sharex is not None and self.xaxis.get_visible(): + # If we are sharing with a panel + # turn all labels off + level = 3 if self._panel_sharex_group else self.figure._sharex + if level > 0: + labels._transfer_label(self.xaxis.label, self._sharex.xaxis.label) + if level > 2: + # WARNING: Cannot set NullFormatter because shared axes share the + # same Ticker(). Instead use approach copied from mpl subplots(). + ticks_visible = dict(labeltop=False, labelbottom=False) + self.xaxis.set_tick_params(which="both", **ticks_visible) + + if self._sharey is not None and self.yaxis.get_visible(): + level = 3 if self._panel_sharey_group else self.figure._sharey + if level > 0: + labels._transfer_label(self.yaxis.label, self._sharey.yaxis.label) + if level > 2: + ticks_visible = dict(labelleft=False, labelright=False) + self.yaxis.set_tick_params(which="both", **ticks_visible) def _add_alt(self, sx, **kwargs): """ diff --git a/ultraplot/tests/conftest.py b/ultraplot/tests/conftest.py index 0a76ac245..1dff5edb1 100644 --- a/ultraplot/tests/conftest.py +++ b/ultraplot/tests/conftest.py @@ -2,6 +2,9 @@ from pathlib import Path import warnings, logging +logging.getLogger("matplotlib").setLevel(logging.ERROR) + + SEED = 51423 From cdc220cd42cb07d054afdb040b6b895fce70b1eb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 22:37:34 +0200 Subject: [PATCH 13/96] reversion to new --- ultraplot/axes/cartesian.py | 166 +++++++++++++++++++++++++++++++----- 1 file changed, 147 insertions(+), 19 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 03811e9ed..02167fcb4 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -385,25 +385,153 @@ def _apply_axis_sharing(self): # bottommost or to the *right* of the leftmost panel. But the sharing level # used for the leftmost and bottommost is the *figure* sharing level. - if self._sharex is not None and self.xaxis.get_visible(): - # If we are sharing with a panel - # turn all labels off - level = 3 if self._panel_sharex_group else self.figure._sharex - if level > 0: - labels._transfer_label(self.xaxis.label, self._sharex.xaxis.label) - if level > 2: - # WARNING: Cannot set NullFormatter because shared axes share the - # same Ticker(). Instead use approach copied from mpl subplots(). - ticks_visible = dict(labeltop=False, labelbottom=False) - self.xaxis.set_tick_params(which="both", **ticks_visible) - - if self._sharey is not None and self.yaxis.get_visible(): - level = 3 if self._panel_sharey_group else self.figure._sharey - if level > 0: - labels._transfer_label(self.yaxis.label, self._sharey.yaxis.label) - if level > 2: - ticks_visible = dict(labelleft=False, labelright=False) - self.yaxis.set_tick_params(which="both", **ticks_visible) + # Get border axes once for efficiency + border_axes = self.figure._get_border_axes() + + # Apply X axis sharing + self._apply_axis_sharing_for_axis("x", border_axes) + + # Apply Y axis sharing + self._apply_axis_sharing_for_axis("y", border_axes) + + def _apply_axis_sharing_for_axis( + self, + axis_name: str, + border_axes: dict[str, plot.PlotAxes], + ) -> None: + """ + Apply axis sharing for a specific axis (x or y). + + Parameters + ---------- + axis_name : str + Either 'x' or 'y' + border_axes : dict + Dictionary from _get_border_axes() containing border information + """ + if axis_name == "x": + axis = self.xaxis + shared_axis = self._sharex + panel_group = self._panel_sharex_group + sharing_level = self.figure._sharex + label_params = ["labeltop", "labelbottom"] + border_sides = ["top", "bottom"] + else: # axis_name == 'y' + axis = self.yaxis + shared_axis = self._sharey + panel_group = self._panel_sharey_group + sharing_level = self.figure._sharey + label_params = ["labelleft", "labelright"] + border_sides = ["left", "right"] + + if shared_axis is None or not axis.get_visible(): + return + + level = 3 if panel_group else sharing_level + + # Handle axis label sharing (level > 0) + if level > 0: + shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") + labels._transfer_label(axis.label, shared_axis_obj.label) + axis.label.set_visible(False) + + # Handle tick label sharing (level > 2) + if level > 2: + label_visibility = self._determine_tick_label_visibility( + axis, + shared_axis, + axis_name, + label_params, + border_sides, + border_axes, + ) + axis.set_tick_params(which="both", **label_visibility) + # Turn minor ticks off + axis.set_minor_formatter(mticker.NullFormatter()) + + def _determine_tick_label_visibility( + self, + axis: maxis.Axis, + shared_axis: maxis.Axis, + axis_name: str, + label_params: list[str], + border_sides: list[str], + border_axes: dict[str, list[plot.PlotAxes]], + ) -> dict[str, bool]: + """ + Determine which tick labels should be visible based on sharing rules and borders. + + Parameters + ---------- + axis : matplotlib axis + The current axis object + shared_axis : Axes + The axes this one shares with + axis_name : str + Either 'x' or 'y' + label_params : list + List of label parameter names (e.g., ['labeltop', 'labelbottom']) + border_sides : list + List of border side names (e.g., ['top', 'bottom']) + border_axes : dict + Dictionary from _get_border_axes() + + Returns + ------- + dict + Dictionary of label visibility parameters + """ + ticks = axis.get_tick_params() + shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") + sharing_ticks = shared_axis_obj.get_tick_params() + + label_visibility = {} + + def _convert_label_param(label_param: str) -> str: + # Deal with logic not being consistent + # in prior mpl versions + if version.parse(str(_version_mpl)) <= version.parse("3.9"): + if label_param == "labeltop" and axis_name == "x": + label_param = "labelright" + elif label_param == "labelbottom" and axis_name == "x": + label_param = "labelleft" + return label_param + + for label_param, border_side in zip(label_params, border_sides): + # Check if user has explicitly set label location via format() + label_visibility[label_param] = False + has_panel = False + for panel in self._panel_dict[border_side]: + # Check if the panel is a colorbar + colorbars = [ + values + for key, values in self._colorbar_dict.items() + if border_side in key # key is tuple (side, top | center | lower) + ] + if not panel in colorbars: + # Skip colorbar as their + # yaxis is not shared + has_panel = True + break + # When we have a panel, let the panel have + # the labels and turn-off for this axis + side. + if has_panel: + continue + is_border = self in border_axes.get(border_side, []) + is_panel = ( + self in shared_axis._panel_dict[border_side] + and self == shared_axis._panel_dict[border_side][-1] + ) + # Use automatic border detection logic + # if we are a panel we "push" the labels outwards + label_param_trans = _convert_label_param(label_param) + is_this_tick_on = ticks[label_param_trans] + is_parent_tick_on = sharing_ticks[label_param_trans] + if is_panel: + label_visibility[label_param] = is_parent_tick_on + elif is_border: + label_visibility[label_param] = is_this_tick_on + return label_visibility def _add_alt(self, sx, **kwargs): """ From 10e329a10c3d05516bd907006b4603bd11db3ff5 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 22:44:21 +0200 Subject: [PATCH 14/96] formatting --- ultraplot/axes/geo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 675396a20..8c901d934 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -664,7 +664,9 @@ def _apply_axis_sharing(self): # Handle Y axis sharing if self._sharey: self._handle_axis_sharing( - source_axis=self._sharey._lataxis, target_axis=self._lataxis, which="y" + source_axis=self._sharey._lataxis, + target_axis=self._lataxis, + which="y", ) # This block is apart of the draw sequence as the From 13702c061b29b6094b20bd45c8fcb770e129c09c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 22:44:43 +0200 Subject: [PATCH 15/96] rm added import --- ultraplot/axes/cartesian.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 02167fcb4..f55380502 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -2,7 +2,6 @@ """ The standard Cartesian axes used for most ultraplot figures. """ -from cProfile import label import copy import inspect From 08e77e3596019ec2b8955331d8d6c5c8d424dfce Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 23:01:47 +0200 Subject: [PATCH 16/96] fix test --- ultraplot/tests/test_sharing.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index e0a6e4028..b2658a477 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -62,6 +62,7 @@ def test_sharing_levels_x(share_level): axs.format(title=f"sharex = {share_level}") fig.canvas.draw() # needed for checks + # Get the border axes if fig._sharex < 3: border_axes = set(axs) else: @@ -73,20 +74,24 @@ def test_sharing_levels_x(share_level): border_axes.update(axes) else: border_axes.add(axes) + + # Run tests for axi in axs: tick_params = axi.xaxis.get_tick_params() + # Get correct directions depending on mpl version from ultraplot.internals.versions import _version_mpl from packaging import version - directions = ( - ["top", "bottom"] - if version.parse(str(_version_mpl)) < version.parse("3.10") - else ["left", "right"] - ) + if version.parse(str(_version_mpl)) >= version.parse("3.10"): + direction_label_map = {"top": "labeltop", "bottom": "labelbottom"} + else: + direction_label_map = {"top": "labelright", "bottom": "labelleft"} + for direction in ["top", "bottom"]: - label_key = f"label{direction}" + label_key = direction_label_map[direction] visible = tick_params.get(label_key, False) is_border = axi in fig._get_border_axes().get(direction, []) + print(axi.number, is_border, share_level, visible, tick_params) if direction == "bottom" and (fig._sharex < 3 or is_border): assert visible else: From f962859c018c2434876b82437673cef0f90a0b19 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 23:04:49 +0200 Subject: [PATCH 17/96] minor formatting --- ultraplot/axes/geo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 8c901d934..2a258e84d 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -653,7 +653,6 @@ def _apply_axis_sharing(self): the leftmost and bottommost is the *figure* sharing level. """ # Handle X axis sharing - # if self._sharex: self._handle_axis_sharing( source_axis=self._sharex._lonaxis, From 3aea23600ab42c6bc2cf22365573a7faa71c4356 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 23:24:20 +0200 Subject: [PATCH 18/96] satisfy codecov --- ultraplot/tests/test_figure.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ultraplot/tests/test_figure.py b/ultraplot/tests/test_figure.py index 0e92f8f2f..2bde251f1 100644 --- a/ultraplot/tests/test_figure.py +++ b/ultraplot/tests/test_figure.py @@ -58,6 +58,41 @@ def test_unsharing_different_rectilinear(): """ with pytest.warns(uplt.internals.warnings.UltraPlotWarning): fig, ax = uplt.subplots(ncols=2, proj=("cyl", "merc"), share="all") + + +def test_get_renderer_basic(): + """ + Test that _get_renderer returns a renderer object. + """ + fig, ax = uplt.subplots() + renderer = fig._get_renderer() + # Renderer should not be None and should have draw_path method + assert renderer is not None + assert hasattr(renderer, "draw_path") + + +def test_share_labels_with_others_no_sharing(): + """ + Test that _share_labels_with_others returns early when no sharing is set. + """ + fig, ax = uplt.subplots() + fig._sharex = 0 + fig._sharey = 0 + # Should simply return without error + result = fig._share_labels_with_others() + assert result is None + + +def test_share_labels_with_others_with_sharing(): + """ + Test that _share_labels_with_others runs when sharing is enabled. + """ + fig, ax = uplt.subplots(ncols=2, sharex=1, sharey=1) + fig._sharex = 1 + fig._sharey = 1 + # Should not return early + fig._share_labels_with_others() + # No assertion, just check for coverage and no error uplt.close(fig) From ddb1ef7ecddeceff1bb7b2779d4dd431dcaf8890 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Fri, 19 Sep 2025 13:00:13 +0200 Subject: [PATCH 19/96] Update ultraplot/tests/test_sharing.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultraplot/tests/test_sharing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index b2658a477..1c026d398 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -91,7 +91,6 @@ def test_sharing_levels_x(share_level): label_key = direction_label_map[direction] visible = tick_params.get(label_key, False) is_border = axi in fig._get_border_axes().get(direction, []) - print(axi.number, is_border, share_level, visible, tick_params) if direction == "bottom" and (fig._sharex < 3 or is_border): assert visible else: From 6341074da51178bc51fc5ec32b509234def0d4bf Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Fri, 19 Sep 2025 13:00:24 +0200 Subject: [PATCH 20/96] Update ultraplot/tests/test_sharing.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultraplot/tests/test_sharing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index 1c026d398..199ec7f9b 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -5,7 +5,7 @@ Axis labels are pushed to the border subplots when the sharing level is greater than 1. -Ticks are visible only on the border plots when the sharing levels is greater than 2. +Ticks are visible only on the border plots when the sharing level is greater than 2. Or more verbosely: sharey = 0: no sharing, all labels and ticks visible From 345009e8829bf8c4cfcfbb10fa887acb9086820d Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Fri, 19 Sep 2025 13:00:32 +0200 Subject: [PATCH 21/96] Update ultraplot/tests/test_sharing.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultraplot/tests/test_sharing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index 199ec7f9b..7f884eedb 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -1,7 +1,7 @@ import pytest, ultraplot as uplt """ -Sharing levels for subplots determine the visbility of the axis labels and tick labels. +Sharing levels for subplots determine the visibility of the axis labels and tick labels. Axis labels are pushed to the border subplots when the sharing level is greater than 1. From d65c14132476ff1f8d15a043cf338eb85217997e Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 19 Sep 2025 21:12:43 +0200 Subject: [PATCH 22/96] restore typo with sharing level --- ultraplot/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 04d7eb9be..2c48c9215 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1965,7 +1965,7 @@ def format( # When we apply formatting to all axes, we need # to potentially adjust the labels. - if len(axs) == len(self.axes) and (self._sharex >= 3 and self._sharey >= 3): + if len(axs) == len(self.axes) and (self._sharex > 0 or self._sharey > 0): self._share_labels_with_others() # Warn unused keyword argument(s) From b1a8b06bbd9d0c393af9b41faff514682bd78690 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 19 Sep 2025 21:13:37 +0200 Subject: [PATCH 23/96] clarify comment --- ultraplot/tests/test_sharing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index 7f884eedb..9d2ef59e0 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -9,7 +9,7 @@ Or more verbosely: sharey = 0: no sharing, all labels and ticks visible - sharey = 1: share axis, all labels and ticks visible + sharey = 1: share axis labels, tick labels are still independent sharey = 2: share limits sharey = 3 or True, share both ticks and labels A similar story holds for sharex. From f720d09ab586981e4697383100c0e3725b6632f0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 19 Sep 2025 21:14:27 +0200 Subject: [PATCH 24/96] specify sharing limit --- ultraplot/tests/test_sharing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_sharing.py b/ultraplot/tests/test_sharing.py index 9d2ef59e0..620e879f8 100644 --- a/ultraplot/tests/test_sharing.py +++ b/ultraplot/tests/test_sharing.py @@ -10,7 +10,7 @@ Or more verbosely: sharey = 0: no sharing, all labels and ticks visible sharey = 1: share axis labels, tick labels are still independent - sharey = 2: share limits + sharey = 2: share data limits sharey = 3 or True, share both ticks and labels A similar story holds for sharex. """ From 579cadde24158dfe75df7f249f803bb7f67262d2 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 21 Sep 2025 15:01:04 +0200 Subject: [PATCH 25/96] update geosharing --- ultraplot/axes/geo.py | 10 +++--- ultraplot/figure.py | 82 ++++++++++++------------------------------- 2 files changed, 28 insertions(+), 64 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 2a258e84d..bf9099fef 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -1437,16 +1437,18 @@ def _is_ticklabel_on(self, side: str) -> bool: """ # Deal with different cartopy versions left_labels, right_labels, bottom_labels, top_labels = self._get_side_labels() + if self.gridlines_major is None: return False + elif side == "labelleft": - return getattr(self.gridlines_major, left_labels) + return getattr(self.gridlines_major, left_labels) == "y" elif side == "labelright": - return getattr(self.gridlines_major, right_labels) + return getattr(self.gridlines_major, right_labels) == "y" elif side == "labelbottom": - return getattr(self.gridlines_major, bottom_labels) + return getattr(self.gridlines_major, bottom_labels) == "x" elif side == "labeltop": - return getattr(self.gridlines_major, top_labels) + return getattr(self.gridlines_major, top_labels) == "x" else: raise ValueError(f"Invalid side: {side}") diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 2c48c9215..fc30cc068 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1281,26 +1281,36 @@ def _share_labels_with_others(self, *, which="both"): for axi in axes: recoded[axi] = recoded.get(axi, []) + [direction] - are_ticks_on = False default = dict( - labelleft=are_ticks_on, - labelright=are_ticks_on, - labeltop=are_ticks_on, - labelbottom=are_ticks_on, + labelleft=False, + labelright=False, + labeltop=False, + labelbottom=False, ) + sides = "top bottom left right".split() for axi in self._iter_axes(hidden=False, panels=False, children=False): # Turn the ticks on or off depending on the position - sides = recoded.get(axi, []) turn_on_or_off = default.copy() - for side in sides: sidelabel = f"label{side}" is_label_on = axi._is_ticklabel_on(sidelabel) - if is_label_on: - # When we are a border an the labels are on - # we keep them on - assert sidelabel in turn_on_or_off - turn_on_or_off[sidelabel] = True + match side: + case "left" | "right": + if self._sharey < 3: + turn_on_or_off[sidelabel] = is_label_on + else: + # When we are a border an the labels are on + # we keep them on + if side in recoded.get(axi, []): + turn_on_or_off[sidelabel] = is_label_on + case "top" | "bottom": + if self._sharex < 3: + turn_on_or_off[sidelabel] = is_label_on + else: + # When we are a border an the labels are on + # we keep them on + if side in recoded.get(axi, []): + turn_on_or_off[sidelabel] = is_label_on if isinstance(axi, paxes.GeoAxes): axi._toggle_gridliner_labels(**turn_on_or_off) @@ -1816,7 +1826,6 @@ def _align_content(): # noqa: E306 # subsequent tight layout really weird. Have to resize twice. _draw_content() if not gs: - print("hello") return if aspect: gs._auto_layout_aspect() @@ -1979,53 +1988,6 @@ def format( f"Ignoring unused projection-specific format() keyword argument(s): {kw}" # noqa: E501 ) - def _share_labels_with_others(self, *, which="both"): - """ - Helpers function to ensure the labels - are shared for rectilinear GeoAxes. - """ - # Turn all labels off - # Note: this action performs it for all the axes in - # the figure. We use the stale here to only perform - # it once as it is an expensive action. - border_axes = self._get_border_axes(same_type=False) - # Recode: - recoded = {} - for direction, axes in border_axes.items(): - for axi in axes: - recoded[axi] = recoded.get(axi, []) + [direction] - - # We turn off the tick labels when the scale and - # ticks are shared (level > 0) - are_ticks_on = False - default = dict( - labelleft=are_ticks_on, - labelright=are_ticks_on, - labeltop=are_ticks_on, - labelbottom=are_ticks_on, - ) - for axi in self._iter_axes(hidden=False, panels=False, children=False): - # Turn the ticks on or off depending on the position - sides = recoded.get(axi, []) - turn_on_or_off = default.copy() - # The axis will be a border if it is either - # (a) on the edge - # (b) not next to a subplot - # (c) not next to a subplot of the same kind - for side in sides: - sidelabel = f"label{side}" - is_label_on = axi._is_ticklabel_on(sidelabel) - if is_label_on: - # When we are a border an the labels are on - # we keep them on - assert sidelabel in turn_on_or_off - turn_on_or_off[sidelabel] = True - - if isinstance(axi, paxes.GeoAxes): - axi._toggle_gridliner_labels(**turn_on_or_off) - else: - axi.tick_params(which=which, **turn_on_or_off) - @docstring._concatenate_inherited @docstring._snippet_manager def colorbar( From e18b02af69d32974ff43375c7692fb891b9f1415 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 21 Sep 2025 15:21:21 +0200 Subject: [PATCH 26/96] propagate shared axis for border without shared axis --- ultraplot/axes/cartesian.py | 26 +++++++++++++++++--------- ultraplot/tests/test_axes.py | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 46685b5df..28e74d1ea 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -425,13 +425,15 @@ def _apply_axis_sharing_for_axis( label_params = ["labelleft", "labelright"] border_sides = ["left", "right"] - if shared_axis is None or not axis.get_visible(): + if not axis.get_visible(): return level = 3 if panel_group else sharing_level # Handle axis label sharing (level > 0) - if level > 0: + # If we are a border axis, @shared_axis may be None + # We propagate this through the _determine_tick_label_visiblity() logic + if level > 0 and shared_axis: shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") labels._transfer_label(axis.label, shared_axis_obj.label) axis.label.set_visible(False) @@ -483,8 +485,11 @@ def _determine_tick_label_visibility( Dictionary of label visibility parameters """ ticks = axis.get_tick_params() - shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") - sharing_ticks = shared_axis_obj.get_tick_params() + + sharing_ticks = {} + if shared_axis: + shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") + sharing_ticks = shared_axis_obj.get_tick_params() label_visibility = {} @@ -519,19 +524,22 @@ def _convert_label_param(label_param: str) -> str: if has_panel: continue is_border = self in border_axes.get(border_side, []) - is_panel = ( - self in shared_axis._panel_dict[border_side] - and self == shared_axis._panel_dict[border_side][-1] - ) + is_panel = False + if shared_axis: + is_panel = ( + self in shared_axis._panel_dict[border_side] + and self == shared_axis._panel_dict[border_side][-1] + ) # Use automatic border detection logic # if we are a panel we "push" the labels outwards label_param_trans = _convert_label_param(label_param) is_this_tick_on = ticks[label_param_trans] - is_parent_tick_on = sharing_ticks[label_param_trans] + is_parent_tick_on = sharing_ticks.get(label_param_trans, False) if is_panel: label_visibility[label_param] = is_parent_tick_on elif is_border: label_visibility[label_param] = is_this_tick_on + print(self.number, label_visibility) return label_visibility def _add_alt(self, sx, **kwargs): diff --git a/ultraplot/tests/test_axes.py b/ultraplot/tests/test_axes.py index a04c2233a..f94dc23c1 100644 --- a/ultraplot/tests/test_axes.py +++ b/ultraplot/tests/test_axes.py @@ -352,7 +352,7 @@ def test_sharing_labels_top_right(): [3, 4, 5], [3, 4, 0], ], - 3, # default sharing level + True, # default sharing level {"xticklabelloc": "t", "yticklabelloc": "r"}, [1, 3, 4], # y-axis labels visible indices [0, 1, 4], # x-axis labels visible indices From c52f0e657debd5943f9468cb8d306feebce738f2 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 22 Sep 2025 16:37:27 +0200 Subject: [PATCH 27/96] move apply axis to each subtype --- ultraplot/axes/base.py | 6 +++++ ultraplot/axes/geo.py | 27 +++++++++++++++++++ ultraplot/axes/polar.py | 5 ++++ ultraplot/figure.py | 60 ----------------------------------------- 4 files changed, 38 insertions(+), 60 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 22e489d18..db5aab3c9 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1518,6 +1518,12 @@ def _apply_title_above(self): for name in names: labels._transfer_label(self._title_dict[name], pax._title_dict[name]) + def _apply_axis_sharing(self): + """ + Should be implemented by subclasses but silently pass if not, e.g. for polar axes + """ + raise ImplementationError("Axis sharing not implemented for this axes type.") + def _apply_auto_share(self): """ Automatically configure axis sharing based on the horizontal and diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index bf9099fef..957a59a12 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -722,6 +722,33 @@ def _handle_axis_sharing( source_axis: The source axis to share from target_axis: The target axis to apply sharing to """ + + # Turn the ticks on or off depending on the position + sides = "top bottom".split() if which == "x" else "left right".split() + border_to_ax = self.figure._get_border_axes() + turn_on_or_off = {} + for side in sides: + sidelabel = f"label{side}" + is_label_on = self._is_ticklabel_on(sidelabel) + turn_on_or_off[sidelabel] = False # default is False + match side: + case "left" | "right": + if self.figure._sharey < 3: + turn_on_or_off[sidelabel] = is_label_on + else: + # When we are a border an the labels are on + # we keep them on + if self in border_to_ax.get(side, False): + turn_on_or_off[sidelabel] = is_label_on + case "top" | "bottom": + if self.figure._sharex < 3: + turn_on_or_off[sidelabel] = is_label_on + else: + # When we are a border an the labels are on + # we keep them on + if self in border_to_ax.get(side, False): + turn_on_or_off[sidelabel] = is_label_on + # Copy view interval and minor locator from source to target if getattr(self.figure, f"_share{which}") >= 2: target_axis.set_view_interval(*source_axis.get_view_interval()) diff --git a/ultraplot/axes/polar.py b/ultraplot/axes/polar.py index d66e3e2ea..ae8a78d68 100644 --- a/ultraplot/axes/polar.py +++ b/ultraplot/axes/polar.py @@ -138,6 +138,11 @@ def __init__(self, *args, **kwargs): for axis in (self.xaxis, self.yaxis): axis.set_tick_params(which="both", size=0) + @override + def _apply_axis_sharing(self): + # Not implemented. Silently pass + return + def _update_formatter(self, x, *, formatter=None, formatter_kw=None): """ Update the gridline label formatter. diff --git a/ultraplot/figure.py b/ultraplot/figure.py index fc30cc068..cb03af74c 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1257,66 +1257,6 @@ def _unshare_axes(self): if isinstance(ax, paxes.GeoAxes) and hasattr(ax, "set_global"): ax.set_global() - def _share_labels_with_others(self, *, which="both"): - """ - Helpers function to ensure the labels - are shared for rectilinear GeoAxes. - """ - # Only apply sharing of labels when we are - # actually sharing labels. - if self._sharex == 0 and self._sharey == 0: - return - # Turn all labels off - # Note: this action performs it for all the axes in - # the figure. We use the stale here to only perform - # it once as it is an expensive action. - # The axis will be a border if it is either - # (a) on the edge - # (b) not next to a subplot - # (c) not next to a subplot of the same kind - border_axes = self._get_border_axes() - # Recode: - recoded = {} - for direction, axes in border_axes.items(): - for axi in axes: - recoded[axi] = recoded.get(axi, []) + [direction] - - default = dict( - labelleft=False, - labelright=False, - labeltop=False, - labelbottom=False, - ) - sides = "top bottom left right".split() - for axi in self._iter_axes(hidden=False, panels=False, children=False): - # Turn the ticks on or off depending on the position - turn_on_or_off = default.copy() - for side in sides: - sidelabel = f"label{side}" - is_label_on = axi._is_ticklabel_on(sidelabel) - match side: - case "left" | "right": - if self._sharey < 3: - turn_on_or_off[sidelabel] = is_label_on - else: - # When we are a border an the labels are on - # we keep them on - if side in recoded.get(axi, []): - turn_on_or_off[sidelabel] = is_label_on - case "top" | "bottom": - if self._sharex < 3: - turn_on_or_off[sidelabel] = is_label_on - else: - # When we are a border an the labels are on - # we keep them on - if side in recoded.get(axi, []): - turn_on_or_off[sidelabel] = is_label_on - - if isinstance(axi, paxes.GeoAxes): - axi._toggle_gridliner_labels(**turn_on_or_off) - else: - axi._apply_axis_sharing() - def _toggle_axis_sharing( self, *, From e31cf678b5a4a0a69adc085880e5014cc65bcc84 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 22 Sep 2025 16:37:43 +0200 Subject: [PATCH 28/96] add import on polar --- ultraplot/axes/polar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ultraplot/axes/polar.py b/ultraplot/axes/polar.py index ae8a78d68..74d083040 100644 --- a/ultraplot/axes/polar.py +++ b/ultraplot/axes/polar.py @@ -3,6 +3,7 @@ Polar axes using azimuth and radius instead of *x* and *y*. """ import inspect +from typing import override import matplotlib.projections.polar as mpolar import numpy as np From 1c62fefa7138aa163fef818cf1411ec715a1414a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 22 Sep 2025 16:37:58 +0200 Subject: [PATCH 29/96] remove redundant function --- ultraplot/figure.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index cb03af74c..485afd05e 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1914,9 +1914,6 @@ def format( # When we apply formatting to all axes, we need # to potentially adjust the labels. - if len(axs) == len(self.axes) and (self._sharex > 0 or self._sharey > 0): - self._share_labels_with_others() - # Warn unused keyword argument(s) kw = { key: value From f76e4af5fff9705eb201a6bdc6c118fb809d51c1 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 22 Sep 2025 16:38:54 +0200 Subject: [PATCH 30/96] refactor cartesian sharing --- ultraplot/axes/cartesian.py | 69 ++++++++++--------------------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 28e74d1ea..202cc9548 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -415,15 +415,11 @@ def _apply_axis_sharing_for_axis( shared_axis = self._sharex panel_group = self._panel_sharex_group sharing_level = self.figure._sharex - label_params = ["labeltop", "labelbottom"] - border_sides = ["top", "bottom"] else: # axis_name == 'y' axis = self.yaxis shared_axis = self._sharey panel_group = self._panel_sharey_group sharing_level = self.figure._sharey - label_params = ["labelleft", "labelright"] - border_sides = ["left", "right"] if not axis.get_visible(): return @@ -440,55 +436,41 @@ def _apply_axis_sharing_for_axis( # Handle tick label sharing (level > 2) if level > 2: - label_visibility = self._determine_tick_label_visibility( - axis, - shared_axis, - axis_name, - label_params, - border_sides, - border_axes, - ) + label_visibility = self._determine_tick_label_visibility(which=axis_name) axis.set_tick_params(which="both", **label_visibility) # Turn minor ticks off axis.set_minor_formatter(mticker.NullFormatter()) def _determine_tick_label_visibility( self, - axis: maxis.Axis, - shared_axis: maxis.Axis, - axis_name: str, - label_params: list[str], - border_sides: list[str], - border_axes: dict[str, list[plot.PlotAxes]], + *, + which: str, ) -> dict[str, bool]: """ Determine which tick labels should be visible based on sharing rules and borders. Parameters ---------- - axis : matplotlib axis - The current axis object - shared_axis : Axes - The axes this one shares with - axis_name : str - Either 'x' or 'y' - label_params : list - List of label parameter names (e.g., ['labeltop', 'labelbottom']) - border_sides : list - List of border side names (e.g., ['top', 'bottom']) - border_axes : dict - Dictionary from _get_border_axes() + axis: str ('x' or 'y') Returns ------- dict Dictionary of label visibility parameters """ + axis = getattr(self, f"{which}axis") + shared_axis = getattr(self, f"_share{which}") + label_params = ( + ("labeltop", "labelbottom") if which == "x" else ("labelleft", "labelright") + ) + border_sides = ("top", "bottom") if which == "x" else ("left", "right") + border_axes = self.figure._get_border_axes() + ticks = axis.get_tick_params() sharing_ticks = {} if shared_axis: - shared_axis_obj = getattr(shared_axis, f"{axis_name}axis") + shared_axis_obj = getattr(shared_axis, f"{which}axis") sharing_ticks = shared_axis_obj.get_tick_params() label_visibility = {} @@ -497,32 +479,15 @@ def _convert_label_param(label_param: str) -> str: # Deal with logic not being consistent # in prior mpl versions if version.parse(str(_version_mpl)) <= version.parse("3.9"): - if label_param == "labeltop" and axis_name == "x": + if label_param == "labeltop" and which == "x": label_param = "labelright" - elif label_param == "labelbottom" and axis_name == "x": + elif label_param == "labelbottom" and which == "x": label_param = "labelleft" return label_param for label_param, border_side in zip(label_params, border_sides): # Check if user has explicitly set label location via format() label_visibility[label_param] = False - has_panel = False - for panel in self._panel_dict[border_side]: - # Check if the panel is a colorbar - colorbars = [ - values - for key, values in self._colorbar_dict.items() - if border_side in key # key is tuple (side, top | center | lower) - ] - if not panel in colorbars: - # Skip colorbar as their - # yaxis is not shared - has_panel = True - break - # When we have a panel, let the panel have - # the labels and turn-off for this axis + side. - if has_panel: - continue is_border = self in border_axes.get(border_side, []) is_panel = False if shared_axis: @@ -535,11 +500,13 @@ def _convert_label_param(label_param: str) -> str: label_param_trans = _convert_label_param(label_param) is_this_tick_on = ticks[label_param_trans] is_parent_tick_on = sharing_ticks.get(label_param_trans, False) + # print(self.number, is_panel, is_legend, is_colorbar, border_side) if is_panel: label_visibility[label_param] = is_parent_tick_on + elif self.number is None: # for legend, colorbars + label_visibility[label_param] = is_this_tick_on elif is_border: label_visibility[label_param] = is_this_tick_on - print(self.number, label_visibility) return label_visibility def _add_alt(self, sx, **kwargs): From c656a17e2e2ac2a480916ce89b7f3cf1e4396f3b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 09:43:03 +0200 Subject: [PATCH 31/96] fixed sharing edge cases cartesian --- ultraplot/axes/cartesian.py | 44 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 202cc9548..a85c368d0 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -412,8 +412,8 @@ def _apply_axis_sharing_for_axis( """ if axis_name == "x": axis = self.xaxis - shared_axis = self._sharex - panel_group = self._panel_sharex_group + shared_axis = self._sharex # do we share the xaxis? + panel_group = self._panel_sharex_group # do we have a panel? sharing_level = self.figure._sharex else: # axis_name == 'y' axis = self.yaxis @@ -490,6 +490,7 @@ def _convert_label_param(label_param: str) -> str: label_visibility[label_param] = False is_border = self in border_axes.get(border_side, []) is_panel = False + is_colorbar = True if self._colorbar_fill else False if shared_axis: is_panel = ( self in shared_axis._panel_dict[border_side] @@ -500,12 +501,41 @@ def _convert_label_param(label_param: str) -> str: label_param_trans = _convert_label_param(label_param) is_this_tick_on = ticks[label_param_trans] is_parent_tick_on = sharing_ticks.get(label_param_trans, False) - # print(self.number, is_panel, is_legend, is_colorbar, border_side) - if is_panel: - label_visibility[label_param] = is_parent_tick_on - elif self.number is None: # for legend, colorbars + are_we_sharing_labels_on_ax = ( + self._panel_sharex_group if which == "x" else self._panel_sharey_group + ) + # To share all axes we need to consider a few cases. + + # Case 1 and 2: Sharing top and right labels only on the + # figure borders or when we are not an alternate axis + + if is_colorbar: label_visibility[label_param] = is_this_tick_on - elif is_border: + elif ( + not self._altx_parent + and border_side == "top" + and self.figure._sharex > 2 + ): + label_visibility[label_param] = is_border and is_this_tick_on + elif ( + not self._alty_parent + and border_side == "right" + and self.figure._sharey > 2 + ): + label_visibility[label_param] = is_border and is_this_tick_on + # Case 3: share axis labels when we are sharing axes set + elif (which == "x" and self._sharex) or (which == "y" and self._sharey): + # Shared subplot. Labels are off unless it's a border. + # On the border, respect the local tick setting. + label_visibility[label_param] = False + # or when we are sharing the labels + elif are_we_sharing_labels_on_ax: + # Panel sharing. Labels are off unless it's a border or the outermost panel. + label_visibility[label_param] = False + # Case 4: singular axes we check if the ticks are on + else: + # print("here", self.number, which, is_this_tick_on) + # Not sharing. label_visibility[label_param] = is_this_tick_on return label_visibility From e2c2989d09317a2f9c741b4e905f213b9fb18ff5 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 10:14:36 +0200 Subject: [PATCH 32/96] refactor sharing handler to be similar to cartesian --- ultraplot/axes/geo.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 957a59a12..bffc9064f 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -652,19 +652,26 @@ def _apply_axis_sharing(self): or to the *right* of the leftmost panel. But the sharing level used for the leftmost and bottommost is the *figure* sharing level. """ + + # Share interval x + if self._sharex and self.figure._sharex >= 2: + self._lonaxis.set_view_interval(*self._sharex._lonaxis.get_view_interval()) + self._lonaxis.set_minor_locator(self._sharex._lonaxis.get_minor_locator()) + # Handle X axis sharing - if self._sharex: + if self.figure._sharex > 2: self._handle_axis_sharing( - source_axis=self._sharex._lonaxis, - target_axis=self._lonaxis, which="x", ) + # Share interval y + if self._sharey and self.figure._sharey >= 2: + self._lataxis.set_view_interval(*self._sharey._lataxis.get_view_interval()) + self._lataxis.set_minor_locator(self._sharey._lataxis.get_minor_locator()) + # Handle Y axis sharing - if self._sharey: + if self.figure._sharey > 2: self._handle_axis_sharing( - source_axis=self._sharey._lataxis, - target_axis=self._lataxis, which="y", ) @@ -710,8 +717,6 @@ def _toggle_gridliner_labels( def _handle_axis_sharing( self, - source_axis: "GeoAxes", - target_axis: "GeoAxes", *, which: str, ): @@ -722,15 +727,16 @@ def _handle_axis_sharing( source_axis: The source axis to share from target_axis: The target axis to apply sharing to """ - - # Turn the ticks on or off depending on the position - sides = "top bottom".split() if which == "x" else "left right".split() - border_to_ax = self.figure._get_border_axes() + # Turn all labels off + # Note: this action performs it for all the axes in + # the figure. We use the stale here to only perform + # it once as it is an expensive action. + border_axes = self.figure._get_border_axes(same_type=False) turn_on_or_off = {} + sides = ("left", "right", "top", "bottom") for side in sides: sidelabel = f"label{side}" is_label_on = self._is_ticklabel_on(sidelabel) - turn_on_or_off[sidelabel] = False # default is False match side: case "left" | "right": if self.figure._sharey < 3: @@ -738,7 +744,7 @@ def _handle_axis_sharing( else: # When we are a border an the labels are on # we keep them on - if self in border_to_ax.get(side, False): + if self in border_axes.get(side, []): turn_on_or_off[sidelabel] = is_label_on case "top" | "bottom": if self.figure._sharex < 3: @@ -746,13 +752,9 @@ def _handle_axis_sharing( else: # When we are a border an the labels are on # we keep them on - if self in border_to_ax.get(side, False): + if self in border_axes.get(side, []): turn_on_or_off[sidelabel] = is_label_on - - # Copy view interval and minor locator from source to target - if getattr(self.figure, f"_share{which}") >= 2: - target_axis.set_view_interval(*source_axis.get_view_interval()) - target_axis.set_minor_locator(source_axis.get_minor_locator()) + self._toggle_gridliner_labels(**turn_on_or_off) @override def draw(self, renderer=None, *args, **kwargs): From 4ea7867b2d500b30568c6314fc21de6acb91e672 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:38:55 +0200 Subject: [PATCH 33/96] further fixes for cartesian sharing --- ultraplot/axes/cartesian.py | 33 +++++++++++++----------- ultraplot/axes/geo.py | 51 ++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index a85c368d0..89003b932 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -487,15 +487,9 @@ def _convert_label_param(label_param: str) -> str: for label_param, border_side in zip(label_params, border_sides): # Check if user has explicitly set label location via format() - label_visibility[label_param] = False is_border = self in border_axes.get(border_side, []) - is_panel = False + is_panel = True if self._panel_parent else False is_colorbar = True if self._colorbar_fill else False - if shared_axis: - is_panel = ( - self in shared_axis._panel_dict[border_side] - and self == shared_axis._panel_dict[border_side][-1] - ) # Use automatic border detection logic # if we are a panel we "push" the labels outwards label_param_trans = _convert_label_param(label_param) @@ -508,7 +502,6 @@ def _convert_label_param(label_param: str) -> str: # Case 1 and 2: Sharing top and right labels only on the # figure borders or when we are not an alternate axis - if is_colorbar: label_visibility[label_param] = is_this_tick_on elif ( @@ -516,25 +509,35 @@ def _convert_label_param(label_param: str) -> str: and border_side == "top" and self.figure._sharex > 2 ): - label_visibility[label_param] = is_border and is_this_tick_on + if is_panel: + if self._panel_sharex_group: + panels = self._panel_parent._panel_dict.get(border_side, []) + if panels and self == panels[-1]: + label_visibility[label_param] = is_parent_tick_on + else: + label_visibility[label_param] = is_border and is_this_tick_on elif ( not self._alty_parent and border_side == "right" and self.figure._sharey > 2 ): - label_visibility[label_param] = is_border and is_this_tick_on + if is_panel: + # check if we are sharing hte axis labels + if self._panel_sharey_group: + panels = self._panel_parent._panel_dict.get(border_side, []) + if panels and self == panels[-1]: + label_visibility[label_param] = is_parent_tick_on + else: + label_visibility[label_param] = is_this_tick_on + else: + label_visibility[label_param] = is_border and is_this_tick_on # Case 3: share axis labels when we are sharing axes set elif (which == "x" and self._sharex) or (which == "y" and self._sharey): # Shared subplot. Labels are off unless it's a border. # On the border, respect the local tick setting. label_visibility[label_param] = False - # or when we are sharing the labels - elif are_we_sharing_labels_on_ax: - # Panel sharing. Labels are off unless it's a border or the outermost panel. - label_visibility[label_param] = False # Case 4: singular axes we check if the ticks are on else: - # print("here", self.number, which, is_this_tick_on) # Not sharing. label_visibility[label_param] = is_this_tick_on return label_visibility diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index bffc9064f..83837f1c4 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -733,27 +733,36 @@ def _handle_axis_sharing( # it once as it is an expensive action. border_axes = self.figure._get_border_axes(same_type=False) turn_on_or_off = {} - sides = ("left", "right", "top", "bottom") - for side in sides: - sidelabel = f"label{side}" - is_label_on = self._is_ticklabel_on(sidelabel) - match side: - case "left" | "right": - if self.figure._sharey < 3: - turn_on_or_off[sidelabel] = is_label_on - else: - # When we are a border an the labels are on - # we keep them on - if self in border_axes.get(side, []): - turn_on_or_off[sidelabel] = is_label_on - case "top" | "bottom": - if self.figure._sharex < 3: - turn_on_or_off[sidelabel] = is_label_on - else: - # When we are a border an the labels are on - # we keep them on - if self in border_axes.get(side, []): - turn_on_or_off[sidelabel] = is_label_on + for label_param, border_side in zip(label_params, border_sides): + is_border = self in border_axes.get(border_side, []) + is_this_tick_on = self._is_ticklabel_on(label_param) + + # Case 1: Top-side of a shared X-axis (for primary axes). + if ( + which == "x" + and not getattr(self, "_altx_parent", None) + and border_side == "top" + and self.figure._sharex > 2 + ): + + # Case 2: Right-side of a shared Y-axis (for primary axes). + elif ( + which == "y" + and not getattr(self, "_alty_parent", None) + and border_side == "right" + and self.figure._sharey > 2 + ): + turn_on_or_off[label_param] = is_border and is_this_tick_on + + # Case 3: Standard bottom/left shared axes. + elif which == "x" and not self._sharex is None and self.figure._sharex > 2: + turn_on_or_off[label_param] = is_border and is_this_tick_on + elif which == "y" and not self._sharey is None and self.figure._sharey > 2: + turn_on_or_off[label_param] = is_border and is_this_tick_on + # Case 4: Standalone axes (no sharing). + else: + turn_on_or_off[label_param] = is_this_tick_on + self._toggle_gridliner_labels(**turn_on_or_off) @override From 7fa21201d49000a940ecfe3bc997b2c03f8f352e Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:39:20 +0200 Subject: [PATCH 34/96] update gridliner sharing --- ultraplot/axes/geo.py | 47 ++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 83837f1c4..0e5c7378a 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -659,10 +659,7 @@ def _apply_axis_sharing(self): self._lonaxis.set_minor_locator(self._sharex._lonaxis.get_minor_locator()) # Handle X axis sharing - if self.figure._sharex > 2: - self._handle_axis_sharing( - which="x", - ) + self._handle_axis_sharing(which="x") # Share interval y if self._sharey and self.figure._sharey >= 2: @@ -670,10 +667,7 @@ def _apply_axis_sharing(self): self._lataxis.set_minor_locator(self._sharey._lataxis.get_minor_locator()) # Handle Y axis sharing - if self.figure._sharey > 2: - self._handle_axis_sharing( - which="y", - ) + self._handle_axis_sharing(which="y") # This block is apart of the draw sequence as the # gridliner object is created late in the @@ -719,7 +713,7 @@ def _handle_axis_sharing( self, *, which: str, - ): + ) -> None: """ Helper method to handle axis sharing for both X and Y axes. @@ -727,11 +721,22 @@ def _handle_axis_sharing( source_axis: The source axis to share from target_axis: The target axis to apply sharing to """ - # Turn all labels off - # Note: this action performs it for all the axes in - # the figure. We use the stale here to only perform - # it once as it is an expensive action. - border_axes = self.figure._get_border_axes(same_type=False) + if self.figure._sharex == 0 and which == "x": + return + if self.figure._sharey == 0 and which == "y": + return + # This logic is adapted from CartesianAxes._determine_tick_label_visibility + # to provide consistent tick label sharing behavior for GeoAxes. + # Per user guidance, it excludes panel and colorbar logic. + + axis = getattr(self, f"{which}axis") + ticks = axis.get_tick_params() + label_params = ( + ("labeltop", "labelbottom") if which == "x" else ("labelleft", "labelright") + ) + border_sides = ("top", "bottom") if which == "x" else ("left", "right") + border_axes = self.figure._get_border_axes() + turn_on_or_off = {} for label_param, border_side in zip(label_params, border_sides): is_border = self in border_axes.get(border_side, []) @@ -745,6 +750,8 @@ def _handle_axis_sharing( and self.figure._sharex > 2 ): + turn_on_or_off[label_param] = is_border and is_this_tick_on + # Case 2: Right-side of a shared Y-axis (for primary axes). elif ( which == "y" @@ -759,6 +766,7 @@ def _handle_axis_sharing( turn_on_or_off[label_param] = is_border and is_this_tick_on elif which == "y" and not self._sharey is None and self.figure._sharey > 2: turn_on_or_off[label_param] = is_border and is_this_tick_on + # Case 4: Standalone axes (no sharing). else: turn_on_or_off[label_param] = is_this_tick_on @@ -1480,13 +1488,13 @@ def _is_ticklabel_on(self, side: str) -> bool: return False elif side == "labelleft": - return getattr(self.gridlines_major, left_labels) == "y" + return getattr(self.gridlines_major, left_labels) elif side == "labelright": return getattr(self.gridlines_major, right_labels) == "y" elif side == "labelbottom": - return getattr(self.gridlines_major, bottom_labels) == "x" + return getattr(self.gridlines_major, bottom_labels) elif side == "labeltop": - return getattr(self.gridlines_major, top_labels) == "x" + return getattr(self.gridlines_major, top_labels) else: raise ValueError(f"Invalid side: {side}") @@ -1508,8 +1516,9 @@ def _toggle_gridliner_labels( togglers = (labelleft, labelright, labelbottom, labeltop) gl = self.gridlines_major for toggle, side in zip(togglers, side_labels): - if getattr(gl, side) != toggle: - setattr(gl, side, toggle) + if toggle is None: + continue + setattr(gl, side, toggle) if geo is not None: # only cartopy 0.20 supported but harmless setattr(gl, "geo_labels", geo) From 57a5f334f7bc31b5e3e09ee85d248c16ddd3bf30 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:39:41 +0200 Subject: [PATCH 35/96] update tests --- ultraplot/tests/test_axes.py | 1 + ultraplot/tests/test_geographic.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ultraplot/tests/test_axes.py b/ultraplot/tests/test_axes.py index f94dc23c1..75ccb3aa3 100644 --- a/ultraplot/tests/test_axes.py +++ b/ultraplot/tests/test_axes.py @@ -405,6 +405,7 @@ def check_state(ax, numbers, state, which): # Format axes with the specified tick label locations ax.format(**tick_loc) + fig.canvas.draw() # needed for sharing labels # Calculate the indices where labels should be hidden all_indices = list(range(len(ax))) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 5c8cc159f..e2564bd63 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -296,6 +296,7 @@ def are_labels_on(ax, which=["top", "bottom", "right", "left"]) -> tuple[bool]: settings = dict(land=True, ocean=True, labels="both") fig, ax = uplt.subplots(layout, share="all", proj="cyl") ax.format(**settings) + fig.canvas.draw() # needed for sharing labels for axi in ax: state = are_labels_on(axi) expectation = expectations[axi.number - 1] @@ -314,8 +315,8 @@ def test_toggle_gridliner_labels(): gl = ax[0].gridlines_major assert gl.left_labels == False - assert gl.right_labels == None # initially these are none - assert gl.top_labels == None + assert gl.right_labels == False + assert gl.top_labels == False assert gl.bottom_labels == False ax[0]._toggle_gridliner_labels(labeltop=True) assert gl.top_labels == True @@ -572,22 +573,18 @@ def assert_views_are_sharing(ax): fig.canvas.draw() # need this to update the labels # All the labels should be on for axi in ax: - side_labels = axi._get_gridliner_labels( - left=True, - right=True, - top=True, - bottom=True, + + s = sum( + [ + 1 if axi._is_ticklabel_on(side) else 0 + for side in "labeltop labelbottom labelleft labelright".split() + ] ) - s = 0 - for dir, labels in side_labels.items(): - s += any([label.get_visible() for label in labels]) assert_views_are_sharing(axi) # When we share the labels but not the limits, # we expect all ticks to be on - if level < 3: - assert s == 4 - else: + if level > 2: assert s == 2 uplt.close(fig) @@ -616,7 +613,9 @@ def test_cartesian_and_geo(rng): ax[0].pcolormesh(rng.random((10, 10))) ax[1].scatter(*rng.random((2, 100))) ax[0]._apply_axis_sharing() - assert mocked.call_count == 1 + assert ( + mocked.call_count == 2 + ) # needs to be called at least twice; one for each axis return fig From 7e3b5a1b3e9dc310163502dffff0162b3393d92b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:39:49 +0200 Subject: [PATCH 36/96] update tests part 2 --- ultraplot/tests/test_geographic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index e2564bd63..349a3b385 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -586,6 +586,8 @@ def assert_views_are_sharing(ax): # we expect all ticks to be on if level > 2: assert s == 2 + else: + assert s == 4 uplt.close(fig) @@ -805,6 +807,7 @@ def are_labels_on(ax, which=("top", "bottom", "right", "left")) -> tuple[bool]: h = ax.imshow(data)[0] ax.format(land=True, labels="both") # need this otherwise no labels are printed fig.colorbar(h, loc="r") + fig.canvas.draw() # needed to invoke axis sharing expectations = ( [True, False, False, True], From b4e8bcf1c0c828b95925af16b6124950994bd839 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:40:04 +0200 Subject: [PATCH 37/96] remove redundant draw --- ultraplot/tests/test_geographic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 349a3b385..1f243f13f 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -570,7 +570,6 @@ def assert_views_are_sharing(ax): for idx, axi in enumerate(ax): axi.plot(x * (idx + 1), y * (idx + 1)) - fig.canvas.draw() # need this to update the labels # All the labels should be on for axi in ax: From 05515a38b827ab526c8024863157eeb89b0689d8 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:40:11 +0200 Subject: [PATCH 38/96] remove redundant tests --- ultraplot/tests/test_figure.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/ultraplot/tests/test_figure.py b/ultraplot/tests/test_figure.py index 2bde251f1..cffa3c7f6 100644 --- a/ultraplot/tests/test_figure.py +++ b/ultraplot/tests/test_figure.py @@ -71,31 +71,6 @@ def test_get_renderer_basic(): assert hasattr(renderer, "draw_path") -def test_share_labels_with_others_no_sharing(): - """ - Test that _share_labels_with_others returns early when no sharing is set. - """ - fig, ax = uplt.subplots() - fig._sharex = 0 - fig._sharey = 0 - # Should simply return without error - result = fig._share_labels_with_others() - assert result is None - - -def test_share_labels_with_others_with_sharing(): - """ - Test that _share_labels_with_others runs when sharing is enabled. - """ - fig, ax = uplt.subplots(ncols=2, sharex=1, sharey=1) - fig._sharex = 1 - fig._sharey = 1 - # Should not return early - fig._share_labels_with_others() - # No assertion, just check for coverage and no error - uplt.close(fig) - - def test_figure_sharing_toggle(): """ Check if axis sharing and unsharing works From 20e292ce68d711966065a0e4f19c81e7f9677cda Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:40:29 +0200 Subject: [PATCH 39/96] forgot this --- ultraplot/axes/geo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 0e5c7378a..d69a92403 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -1490,7 +1490,7 @@ def _is_ticklabel_on(self, side: str) -> bool: elif side == "labelleft": return getattr(self.gridlines_major, left_labels) elif side == "labelright": - return getattr(self.gridlines_major, right_labels) == "y" + return getattr(self.gridlines_major, right_labels) elif side == "labelbottom": return getattr(self.gridlines_major, bottom_labels) elif side == "labeltop": From 211a100e77b9fcb931291df982f72703fca9b13a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 23 Sep 2025 12:45:14 +0200 Subject: [PATCH 40/96] add override import --- ultraplot/axes/polar.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/polar.py b/ultraplot/axes/polar.py index 74d083040..94950179d 100644 --- a/ultraplot/axes/polar.py +++ b/ultraplot/axes/polar.py @@ -3,7 +3,11 @@ Polar axes using azimuth and radius instead of *x* and *y*. """ import inspect -from typing import override + +try: + from typing import override +except: + from typing_extensions import override import matplotlib.projections.polar as mpolar import numpy as np From 5eb11e275526fb2e9f1579be769531185e7bc963 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 25 Sep 2025 16:55:22 +0200 Subject: [PATCH 41/96] Move label sharing to axis object --- .gitignore | 1 + ultraplot/axes/cartesian.py | 105 -------------------- ultraplot/figure.py | 154 ++++++++++++++++++++++++++--- ultraplot/gridspec.py | 11 ++- ultraplot/tests/test_geographic.py | 2 +- ultraplot/utils.py | 49 +++++++-- 6 files changed, 189 insertions(+), 133 deletions(-) diff --git a/.gitignore b/.gitignore index bbd6bf100..0bf4ea4e1 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ sources # Python extras .ipynb_checkpoints *.log +*.ipnyb *.pyc .*.pyc __pycache__ diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 89003b932..692fbef29 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -434,114 +434,9 @@ def _apply_axis_sharing_for_axis( labels._transfer_label(axis.label, shared_axis_obj.label) axis.label.set_visible(False) - # Handle tick label sharing (level > 2) - if level > 2: - label_visibility = self._determine_tick_label_visibility(which=axis_name) - axis.set_tick_params(which="both", **label_visibility) # Turn minor ticks off axis.set_minor_formatter(mticker.NullFormatter()) - def _determine_tick_label_visibility( - self, - *, - which: str, - ) -> dict[str, bool]: - """ - Determine which tick labels should be visible based on sharing rules and borders. - - Parameters - ---------- - axis: str ('x' or 'y') - - Returns - ------- - dict - Dictionary of label visibility parameters - """ - axis = getattr(self, f"{which}axis") - shared_axis = getattr(self, f"_share{which}") - label_params = ( - ("labeltop", "labelbottom") if which == "x" else ("labelleft", "labelright") - ) - border_sides = ("top", "bottom") if which == "x" else ("left", "right") - border_axes = self.figure._get_border_axes() - - ticks = axis.get_tick_params() - - sharing_ticks = {} - if shared_axis: - shared_axis_obj = getattr(shared_axis, f"{which}axis") - sharing_ticks = shared_axis_obj.get_tick_params() - - label_visibility = {} - - def _convert_label_param(label_param: str) -> str: - # Deal with logic not being consistent - # in prior mpl versions - if version.parse(str(_version_mpl)) <= version.parse("3.9"): - if label_param == "labeltop" and which == "x": - label_param = "labelright" - elif label_param == "labelbottom" and which == "x": - label_param = "labelleft" - return label_param - - for label_param, border_side in zip(label_params, border_sides): - # Check if user has explicitly set label location via format() - is_border = self in border_axes.get(border_side, []) - is_panel = True if self._panel_parent else False - is_colorbar = True if self._colorbar_fill else False - # Use automatic border detection logic - # if we are a panel we "push" the labels outwards - label_param_trans = _convert_label_param(label_param) - is_this_tick_on = ticks[label_param_trans] - is_parent_tick_on = sharing_ticks.get(label_param_trans, False) - are_we_sharing_labels_on_ax = ( - self._panel_sharex_group if which == "x" else self._panel_sharey_group - ) - # To share all axes we need to consider a few cases. - - # Case 1 and 2: Sharing top and right labels only on the - # figure borders or when we are not an alternate axis - if is_colorbar: - label_visibility[label_param] = is_this_tick_on - elif ( - not self._altx_parent - and border_side == "top" - and self.figure._sharex > 2 - ): - if is_panel: - if self._panel_sharex_group: - panels = self._panel_parent._panel_dict.get(border_side, []) - if panels and self == panels[-1]: - label_visibility[label_param] = is_parent_tick_on - else: - label_visibility[label_param] = is_border and is_this_tick_on - elif ( - not self._alty_parent - and border_side == "right" - and self.figure._sharey > 2 - ): - if is_panel: - # check if we are sharing hte axis labels - if self._panel_sharey_group: - panels = self._panel_parent._panel_dict.get(border_side, []) - if panels and self == panels[-1]: - label_visibility[label_param] = is_parent_tick_on - else: - label_visibility[label_param] = is_this_tick_on - else: - label_visibility[label_param] = is_border and is_this_tick_on - # Case 3: share axis labels when we are sharing axes set - elif (which == "x" and self._sharex) or (which == "y" and self._sharey): - # Shared subplot. Labels are off unless it's a border. - # On the border, respect the local tick setting. - label_visibility[label_param] = False - # Case 4: singular axes we check if the ticks are on - else: - # Not sharing. - label_visibility[label_param] = is_this_tick_on - return label_visibility - def _add_alt(self, sx, **kwargs): """ Add an alternate axes. diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 485afd05e..8aa3bce46 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -20,6 +20,11 @@ import matplotlib.transforms as mtransforms import numpy as np +try: + from typing import override +except: + from typing_extensions import override + from . import axes as paxes from . import constructor from . import gridspec as pgridspec @@ -477,6 +482,21 @@ def _canvas_preprocess(self, *args, **kwargs): return canvas +def _clear_border_cache(func): + """ + Decorator that clears the border cache after function execution. + """ + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + if hasattr(self, "_cache_border_axes"): + delattr(self, "_cache_border_axes") + return result + + return wrapper + + class Figure(mfigure.Figure): """ The `~matplotlib.figure.Figure` subclass used by ultraplot. @@ -801,6 +821,95 @@ def __init__( # NOTE: This ignores user-input rc_mode. self.format(rc_kw=rc_kw, rc_mode=1, skip_axes=True, **kw_format) + @override + def draw(self, renderer): + # implement the tick sharing here + # should be shareable --> either all cartesian or all geographic + # but no mixing (panels can be mixed) + # check which ticks are on for x or y and push the labels to the + # outer most on a given column or row. + # we can use get_border_axes for the outermost plots and then collect their outermost panels that are not colorbars + self._share_ticklabels(axis="x") + self._share_ticklabels(axis="y") + super().draw(renderer) + + def _share_ticklabels(self, *, axis: str) -> None: + """ + Tick label sharing is determined at the figure level. While + each subplot controls the limits, we are dealing with the ticklabels + here as the complexity is easiier to deal with. + axis: str 'x' or 'y', row or columns to update + """ + if not self.stale: + return + outer_axes = self._get_border_axes() + true_outer = {} + + sides = ("top", "bottom") if axis == "x" else ("left", "right") + # for panels + other_axis = "x" if axis == "y" else "y" + other_sides = ("left", "right") if axis == "x" else ("top", "bottom") + # Outer_axes contains the main grid but we need + # to add the panels that are on these axes potentially + + tick_params = ( + {"labeltop": False, "labelbottom": False} + if axis == "x" + else {"labelleft": False, "labelright": False} + ) + + # Check if any of the ticks are set to on for @axis + subplot_types = set() + for axi in self._iter_axes(panels=True, hidden=False): + if not type(axi) in (paxes.CartesianAxes, paxes.GeoAxes): + warnings._warn_ultraplot( + f"Tick label sharing not implemented for {type(axi)} subplots." + ) + return + subplot_types.add(type(axi)) + match axis: + # Handle x + case "x" if isinstance(axi, paxes.CartesianAxes): + tmp = axi.xaxis.get_tick_params() + if tmp.get("labeltop"): + tick_params["labeltop"] = tmp["labeltop"] + if tmp.get("labelbottom"): + tick_params["labelbottom"] = tmp["labelbottom"] + + # TODO: + case "x" if isinstance(axi, paxes.GeoAxes): + pass + + # Handle y + case "y" if isinstance(axi, paxes.CartesianAxes): + tmp = axi.yaxis.get_tick_params() + if tmp.get("labelleft"): + tick_params["labelleft"] = tmp["labelleft"] + if tmp.get("labelright"): + tick_params["labelright"] = tmp["labelright"] + + # TODO: + case "y" if isinstance(axi, paxes.GeoAxes): + pass + + # We cannot mix types (yet) + if len(subplot_types) > 1: + warnings._warn_ultraplot( + "Tick label sharing not implemented for mixed subplot types." + ) + return + for axi in self._iter_axes(panels=True, hidden=False): + tmp = tick_params.copy() + # For sharing limits and or axis labels we + # can leave the ticks as found + for side in sides: + label = f"label{side}" + if axi not in outer_axes[side]: + tmp[label] = False + + axi.tick_params(**tmp) + self.stale = True + def _context_adjusting(self, cache=True): """ Prevent re-running auto layout steps due to draws triggered by figure @@ -928,8 +1037,9 @@ def _get_border_axes( if gs is None: return border_axes - # Skip colorbars or panels etc - all_axes = [axi for axi in self.axes if axi.number is not None] + all_axes = [] + for axi in self._iter_axes(panels=True): + all_axes.append(axi) # Handle empty cases nrows, ncols = gs.nrows, gs.ncols @@ -941,26 +1051,45 @@ def _get_border_axes( # Reconstruct the grid based on axis locations. Note that # spanning axes will fit into one of the boxes. Check # this with unittest to see how empty axes are handles - grid, grid_axis_type, seen_axis_type = _get_subplot_layout( - gs, - all_axes, - same_type=same_type, - ) + + gs = self.axes[0].get_gridspec() + shape = (gs.nrows_total, gs.ncols_total) + grid = np.zeros(shape, dtype=object) + grid.fill(None) + grid_axis_type = np.zeros(shape, dtype=int) + seen_axis_type = dict() + for axi in self._iter_axes(panels=True): + gs = axi.get_subplotspec() + x, y = np.unravel_index(gs.num1, shape) + span = gs._get_rows_columns() + + xleft, xright, yleft, yright = span + xspan = xright - xleft + 1 + yspan = yright - yleft + 1 + number = axi.number + if type(axi) not in seen_axis_type: + seen_axis_type[type(axi)] = len(seen_axis_type) + type_number = seen_axis_type[type(axi)] + grid[x : x + xspan, y : y + yspan] = axi + grid_axis_type[x : x + xspan, y : y + yspan] = type_number # We check for all axes is they are a border or not # Note we could also write the crawler in a way where # it find the borders by moving around in the grid, without spawning on each axis point. We may change # this in the future for axi in all_axes: axis_type = seen_axis_type.get(type(axi), 1) + number = axi.number + if axi.number is None: + number = -axi._panel_parent.number crawler = _Crawler( ax=axi, grid=grid, - target=axi.number, + target=number, axis_type=axis_type, grid_axis_type=grid_axis_type, ) for direction, is_border in crawler.find_edges(): - if is_border: + if is_border and axi not in border_axes[direction]: border_axes[direction].append(axi) self._cached_border_axes = border_axes return border_axes @@ -1054,6 +1183,7 @@ def _get_renderer(self): renderer = canvas.get_renderer() return renderer + @_clear_border_cache def _add_axes_panel(self, ax, side=None, **kwargs): """ Add an axes panel. @@ -1098,6 +1228,7 @@ def _add_axes_panel(self, ax, side=None, **kwargs): axis.set_label_position(side) # set label position return pax + @_clear_border_cache def _add_figure_panel( self, side=None, span=None, row=None, col=None, rows=None, cols=None, **kwargs ): @@ -1132,6 +1263,7 @@ def _add_figure_panel( pax._panel_parent = None return pax + @_clear_border_cache def _add_subplot(self, *args, **kwargs): """ The driver function for adding single subplots. @@ -1240,9 +1372,6 @@ def _add_subplot(self, *args, **kwargs): if ax.number: self._subplot_dict[ax.number] = ax - # Invalidate border axes cache - if hasattr(self, "_cached_border_axes"): - delattr(self, "_cached_border_axes") return ax def _unshare_axes(self): @@ -1672,6 +1801,7 @@ def _update_super_title(self, title, **kwargs): if title is not None: self._suptitle.set_text(title) + @_clear_border_cache @docstring._concatenate_inherited @docstring._snippet_manager def add_axes(self, rect, **kwargs): diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 0d642505e..3183aa1d3 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -195,7 +195,7 @@ def _get_rows_columns(self, ncols=None): row2, col2 = divmod(self.num2, ncols) return row1, row2, col1, col2 - def _get_grid_span(self, hidden=False) -> (int, int, int, int): + def _get_grid_span(self, hidden=True) -> (int, int, int, int): """ Retrieve the location of the subplot within the gridspec. When hidden is False we only consider @@ -203,11 +203,12 @@ def _get_grid_span(self, hidden=False) -> (int, int, int, int): """ gs = self.get_gridspec() nrows, ncols = gs.nrows_total, gs.ncols_total - if not hidden: + if hidden: + x, y = np.unravel_index(self.num1, (nrows, ncols)) + else: nrows, ncols = gs.nrows, gs.ncols - # Use num1 or num2 - decoded = gs._decode_indices(self.num1) - x, y = np.unravel_index(decoded, (nrows, ncols)) + decoded = gs._decode_indices(self.num1) + x, y = np.unravel_index(decoded, (nrows, ncols)) span = self._get_rows_columns() xspan = span[1] - span[0] + 1 # inclusive diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 1f243f13f..105103873 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -742,7 +742,7 @@ def test_geo_with_panels(rng): length=0.5, ), ) - ax.format(oceancolor="blue", coast=True) + ax.format(oceancolor="blue", coast=True, latticklabels="r") return fig diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 1b1b97a95..5f04be8ff 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -918,7 +918,8 @@ def _get_subplot_layout( axis types. This function is used internally to determine the layout of axes in a GridSpec. """ - grid = np.zeros((gs.nrows, gs.ncols)) + grid = np.zeros((gs.nrows_total, gs.ncols_total), dtype=object) + grid.fill(None) grid_axis_type = np.zeros((gs.nrows, gs.ncols)) # Collect grouper based on kinds of axes. This # would allow us to share labels across types @@ -936,7 +937,7 @@ def _get_subplot_layout( grid[ slice(*rowspan), slice(*colspan), - ] = axi.number + ] = axi # Allow grouping of mixed types axis_type = 1 @@ -1004,13 +1005,19 @@ def find_edge_for( # Retrieve where the axis is in the grid spec = self.ax.get_subplotspec() - spans = spec._get_grid_span() + shape = (spec.get_gridspec().nrows_total, spec.get_gridspec().ncols_total) + x, y = np.unravel_index(spec.num1, shape) + spans = spec._get_rows_columns() rowspan = spans[:2] colspan = spans[-2:] - xs = range(*rowspan) - ys = range(*colspan) + + a = rowspan[1] - rowspan[0] + b = colspan[1] - colspan[0] + xs = range(x, x + a + 1) + ys = range(y, y + b + 1) + is_border = False - for x, y in product(xs, ys): + for xl, yl in product(xs, ys): pos = (x, y) if self.is_border(pos, d): is_border = True @@ -1037,11 +1044,11 @@ def is_border( elif y > self.grid.shape[1] - 1: return True - if self.grid[x, y] == 0 or self.grid_axis_type[x, y] != self.axis_type: + if self.grid[x, y] is None or self.grid_axis_type[x, y] != self.axis_type: return True # Check if we reached a plot or an internal edge - if self.grid[x, y] != self.target and self.grid[x, y] > 0: + if self.grid[x, y] != self.ax: return self._check_ranges(direction, other=self.grid[x, y]) dx, dy = direction @@ -1065,14 +1072,15 @@ def _check_ranges( can share x. """ this_spec = self.ax.get_subplotspec() - other_spec = self.ax.figure._subplot_dict[other].get_subplotspec() + other_spec = other.get_subplotspec() # Get the row and column spans of both axes - this_span = this_spec._get_grid_span() + this_span = this_spec._get_rows_columns() this_rowspan = this_span[:2] this_colspan = this_span[-2:] other_span = other_spec._get_grid_span() + other_span = other_spec._get_rows_columns() other_rowspan = other_span[:2] other_colspan = other_span[-2:] @@ -1089,6 +1097,27 @@ def _check_ranges( other_start, other_stop = other_rowspan if this_start == other_start and this_stop == other_stop: + # Check if it is a panel + mapper = { + (0, -1): "left", + (0, 1): "right", + (1, 0): "top", + (-1, 0): "bottom", + } + d = mapper[direction] + if self.ax.number is None: + parent = self.ax._panel_parent + if panels := parent._panel_dict.get(d, []): + if self.ax == panels[-1]: + if d in ("left", "right") and parent._sharey: + return True + elif d in ("top", "bottom") and parent._sharex: + return True + elif self.ax.number is not None: + if d in ("left", "right") and not self.ax._sharey: + return True + elif d in ("top", "bottom") and not self.ax._sharex: + return True return False # not a border return True From cf5ba69ffb57daab55221e7f5086a74be6eaaee1 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 29 Sep 2025 14:19:50 +0200 Subject: [PATCH 42/96] Add geo back in --- ultraplot/axes/geo.py | 76 ------------------------------------------- ultraplot/figure.py | 22 +++++++++---- ultraplot/utils.py | 13 +++++--- 3 files changed, 25 insertions(+), 86 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index d69a92403..f46ad544f 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -658,25 +658,11 @@ def _apply_axis_sharing(self): self._lonaxis.set_view_interval(*self._sharex._lonaxis.get_view_interval()) self._lonaxis.set_minor_locator(self._sharex._lonaxis.get_minor_locator()) - # Handle X axis sharing - self._handle_axis_sharing(which="x") - # Share interval y if self._sharey and self.figure._sharey >= 2: self._lataxis.set_view_interval(*self._sharey._lataxis.get_view_interval()) self._lataxis.set_minor_locator(self._sharey._lataxis.get_minor_locator()) - # Handle Y axis sharing - self._handle_axis_sharing(which="y") - - # This block is apart of the draw sequence as the - # gridliner object is created late in the - # build chain. - if not self.stale: - return - if self.figure._sharex == 0 and self.figure._sharey == 0: - return - def _get_gridliner_labels( self, bottom=None, @@ -709,68 +695,6 @@ def _toggle_gridliner_labels( for label in gridlabels.get(direction, []): label.set_visible(toggle) - def _handle_axis_sharing( - self, - *, - which: str, - ) -> None: - """ - Helper method to handle axis sharing for both X and Y axes. - - Args: - source_axis: The source axis to share from - target_axis: The target axis to apply sharing to - """ - if self.figure._sharex == 0 and which == "x": - return - if self.figure._sharey == 0 and which == "y": - return - # This logic is adapted from CartesianAxes._determine_tick_label_visibility - # to provide consistent tick label sharing behavior for GeoAxes. - # Per user guidance, it excludes panel and colorbar logic. - - axis = getattr(self, f"{which}axis") - ticks = axis.get_tick_params() - label_params = ( - ("labeltop", "labelbottom") if which == "x" else ("labelleft", "labelright") - ) - border_sides = ("top", "bottom") if which == "x" else ("left", "right") - border_axes = self.figure._get_border_axes() - - turn_on_or_off = {} - for label_param, border_side in zip(label_params, border_sides): - is_border = self in border_axes.get(border_side, []) - is_this_tick_on = self._is_ticklabel_on(label_param) - - # Case 1: Top-side of a shared X-axis (for primary axes). - if ( - which == "x" - and not getattr(self, "_altx_parent", None) - and border_side == "top" - and self.figure._sharex > 2 - ): - - turn_on_or_off[label_param] = is_border and is_this_tick_on - - # Case 2: Right-side of a shared Y-axis (for primary axes). - elif ( - which == "y" - and not getattr(self, "_alty_parent", None) - and border_side == "right" - and self.figure._sharey > 2 - ): - turn_on_or_off[label_param] = is_border and is_this_tick_on - - # Case 3: Standard bottom/left shared axes. - elif which == "x" and not self._sharex is None and self.figure._sharex > 2: - turn_on_or_off[label_param] = is_border and is_this_tick_on - elif which == "y" and not self._sharey is None and self.figure._sharey > 2: - turn_on_or_off[label_param] = is_border and is_this_tick_on - - # Case 4: Standalone axes (no sharing). - else: - turn_on_or_off[label_param] = is_this_tick_on - self._toggle_gridliner_labels(**turn_on_or_off) @override diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 8aa3bce46..62e158540 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -861,7 +861,11 @@ def _share_ticklabels(self, *, axis: str) -> None: # Check if any of the ticks are set to on for @axis subplot_types = set() for axi in self._iter_axes(panels=True, hidden=False): - if not type(axi) in (paxes.CartesianAxes, paxes.GeoAxes): + if not type(axi) in ( + paxes.CartesianAxes, + paxes._CartopyAxes, + paxes._BasemapAxes, + ): warnings._warn_ultraplot( f"Tick label sharing not implemented for {type(axi)} subplots." ) @@ -876,9 +880,9 @@ def _share_ticklabels(self, *, axis: str) -> None: if tmp.get("labelbottom"): tick_params["labelbottom"] = tmp["labelbottom"] - # TODO: case "x" if isinstance(axi, paxes.GeoAxes): - pass + tick_params["labeltop"] = axi._is_ticklabel_on("labeltop") + tick_params["labelbottom"] = axi._is_ticklabel_on("labelbottom") # Handle y case "y" if isinstance(axi, paxes.CartesianAxes): @@ -888,9 +892,9 @@ def _share_ticklabels(self, *, axis: str) -> None: if tmp.get("labelright"): tick_params["labelright"] = tmp["labelright"] - # TODO: case "y" if isinstance(axi, paxes.GeoAxes): - pass + tick_params["labelleft"] = axi._is_ticklabel_on("labelleft") + tick_params["labelright"] = axi._is_ticklabel_on("labelright") # We cannot mix types (yet) if len(subplot_types) > 1: @@ -907,7 +911,13 @@ def _share_ticklabels(self, *, axis: str) -> None: if axi not in outer_axes[side]: tmp[label] = False - axi.tick_params(**tmp) + if isinstance(axi, paxes.GeoAxes): + # TODO: move this to tick_params? + # Deal with backends as tick_params is still a + # function + axi._toggle_gridliner_labels(**tmp) + else: + axi.tick_params(**tmp) self.stale = True def _context_adjusting(self, cache=True): diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 5f04be8ff..06da84140 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1114,10 +1114,15 @@ def _check_ranges( elif d in ("top", "bottom") and parent._sharex: return True elif self.ax.number is not None: - if d in ("left", "right") and not self.ax._sharey: - return True - elif d in ("top", "bottom") and not self.ax._sharex: - return True + # Defer import to prevent circular import + from . import axes as paxes + + if isinstance(self.ax, paxes.CartesianAxes): + if d in ("left", "right") and not self.ax._sharey: + return True + elif d in ("top", "bottom") and not self.ax._sharex: + return True + # GeoAxes or Polar return False # not a border return True From b2c9fe1a2b05487feb7594f041eb3985cea7cd6e Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 29 Sep 2025 23:00:30 +0200 Subject: [PATCH 43/96] Fix order of label transfer (#353) * prefer dest over src * add preference only on first pass * add fontproperties --- ultraplot/internals/labels.py | 34 +++++++++++++++++++++++++++++++--- ultraplot/tests/test_format.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/ultraplot/internals/labels.py b/ultraplot/internals/labels.py index c12c528e8..8b7cb851e 100644 --- a/ultraplot/internals/labels.py +++ b/ultraplot/internals/labels.py @@ -4,19 +4,47 @@ """ import matplotlib.patheffects as mpatheffects import matplotlib.text as mtext +from matplotlib.font_manager import FontProperties + from . import ic # noqa: F401 -def _transfer_label(src, dest): +def merge_font_properties( + dest_fp: FontProperties, src_fp: FontProperties +) -> FontProperties: + # Prefer dest_fp's values if set, otherwise use src_fp's + return FontProperties( + family=dest_fp.get_family() or src_fp.get_family(), + style=dest_fp.get_style() or src_fp.get_style(), + variant=dest_fp.get_variant() or src_fp.get_variant(), + weight=dest_fp.get_weight() or src_fp.get_weight(), + stretch=dest_fp.get_stretch() or src_fp.get_stretch(), + size=dest_fp.get_size() or src_fp.get_size(), + ) + + +def _transfer_label(src: mtext.Text, dest: mtext.Text) -> None: """ Transfer the input text object properties and content to the destination text object. Then clear the input object text. """ text = src.get_text() dest.set_color(src.get_color()) # not a font property - dest.set_fontproperties(src.get_fontproperties()) # size, weight, etc. - if not text.strip(): # WARNING: must test strip() (see _align_axis_labels()) + src_fp = src.get_font_properties() + dest_fp = dest.get_font_properties() + + # Track if we've already transferred to this dest + if not hasattr(dest, "_label_transferred"): + # First transfer: copy all from src + dest.set_fontproperties(src_fp) + dest._label_transferred = True + else: + # Subsequent transfers: preserve dest's manual changes + merged_fp = merge_font_properties(dest_fp, src_fp) # dest takes precedence + dest.set_fontproperties(merged_fp) + + if not text.strip(): return dest.set_text(text) src.set_text("") diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index c2248d7a8..3a45fd66b 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -140,6 +140,34 @@ def test_inner_title_zorder(): return fig +def test_transfer_label_preserves_dest_font_properties(): + """ + Test that repeated _transfer_label calls do not overwrite dest's updated font properties. + """ + import matplotlib.pyplot as plt + from ultraplot.internals.labels import _transfer_label + + fig, ax = plt.subplots() + src = ax.text(0.1, 0.5, "Source", fontsize=10, fontweight="bold", color="red") + dest = ax.text(0.9, 0.5, "Dest", fontsize=12, fontweight="normal", color="blue") + + # First transfer: dest gets src's font properties + _transfer_label(src, dest) + assert dest.get_fontsize() == 10 + assert dest.get_fontweight() == "bold" + assert dest.get_text() == "Source" + + # Change dest's font size + dest.set_fontsize(20) + + # Second transfer: dest's font size should be preserved + src.set_text("New Source") + _transfer_label(src, dest) + assert dest.get_fontsize() == 20 # Should not be overwritten by src + assert dest.get_fontweight() == "bold" # Still from src originally + assert dest.get_text() == "New Source" + + @pytest.mark.mpl_image_compare def test_font_adjustments(): """ From 876d1eca145bdf0cf642b7e501d063f43b937444 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:58:11 +0200 Subject: [PATCH 44/96] Bump the github-actions group with 2 updates (#354) Bumps the github-actions group with 2 updates: [mamba-org/setup-micromamba](https://github.com/mamba-org/setup-micromamba) and [actions/setup-python](https://github.com/actions/setup-python). Updates `mamba-org/setup-micromamba` from 2.0.5 to 2.0.7 - [Release notes](https://github.com/mamba-org/setup-micromamba/releases) - [Commits](https://github.com/mamba-org/setup-micromamba/compare/v2.0.5...v2.0.7) Updates `actions/setup-python` from 5 to 6 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: mamba-org/setup-micromamba dependency-version: 2.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-ultraplot.yml | 4 ++-- .github/workflows/main.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-ultraplot.yml b/.github/workflows/build-ultraplot.yml index 79a5f2db9..8185af587 100644 --- a/.github/workflows/build-ultraplot.yml +++ b/.github/workflows/build-ultraplot.yml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - - uses: mamba-org/setup-micromamba@v2.0.5 + - uses: mamba-org/setup-micromamba@v2.0.7 with: environment-file: ./environment.yml init-shell: bash @@ -59,7 +59,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: mamba-org/setup-micromamba@v2.0.5 + - uses: mamba-org/setup-micromamba@v2.0.7 with: environment-file: ./environment.yml init-shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f1660c47..9e9a3519f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.11" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 724f3fe7c..1eda57ccb 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -23,7 +23,7 @@ jobs: run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* shell: bash - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" From 39be7e8b6fc97d569cdf15017ebef0c326a386cb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 16 Sep 2025 17:41:33 +0200 Subject: [PATCH 45/96] revert check --- ultraplot/axes/cartesian.py | 3 --- ultraplot/tests/conftest.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 692fbef29..86a726e99 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -386,9 +386,6 @@ def _apply_axis_sharing(self): # bottommost or to the *right* of the leftmost panel. But the sharing level # used for the leftmost and bottommost is the *figure* sharing level. - # Get border axes once for efficiency - border_axes = self.figure._get_border_axes() - # Apply X axis sharing self._apply_axis_sharing_for_axis("x", border_axes) diff --git a/ultraplot/tests/conftest.py b/ultraplot/tests/conftest.py index e6848abaa..8296fa82c 100644 --- a/ultraplot/tests/conftest.py +++ b/ultraplot/tests/conftest.py @@ -4,6 +4,10 @@ logging.getLogger("matplotlib").setLevel(logging.ERROR) +<<<<<<< HEAD +======= + +>>>>>>> 7394633ef (revert check) SEED = 51423 From 4610d8414d66cb1bc087832fe1fb724263d458e5 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 29 Sep 2025 15:23:01 +0200 Subject: [PATCH 46/96] stash --- ultraplot/figure.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 62e158540..d0325b883 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1068,7 +1068,7 @@ def _get_border_axes( grid.fill(None) grid_axis_type = np.zeros(shape, dtype=int) seen_axis_type = dict() - for axi in self._iter_axes(panels=True): + for axi in self._iter_axes(panels=True, hidden=True): gs = axi.get_subplotspec() x, y = np.unravel_index(gs.num1, shape) span = gs._get_rows_columns() @@ -1080,12 +1080,14 @@ def _get_border_axes( if type(axi) not in seen_axis_type: seen_axis_type[type(axi)] = len(seen_axis_type) type_number = seen_axis_type[type(axi)] - grid[x : x + xspan, y : y + yspan] = axi + if axi.get_visible(): + grid[x : x + xspan, y : y + yspan] = axi grid_axis_type[x : x + xspan, y : y + yspan] = type_number # We check for all axes is they are a border or not # Note we could also write the crawler in a way where # it find the borders by moving around in the grid, without spawning on each axis point. We may change # this in the future + print(grid, grid.shape) for axi in all_axes: axis_type = seen_axis_type.get(type(axi), 1) number = axi.number @@ -1099,6 +1101,7 @@ def _get_border_axes( grid_axis_type=grid_axis_type, ) for direction, is_border in crawler.find_edges(): + # print(">>", axi.number, direction, is_border) if is_border and axi not in border_axes[direction]: border_axes[direction].append(axi) self._cached_border_axes = border_axes From 685c2f7ee13d446a0730fb8b240adcef8ae4b993 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 1 Oct 2025 23:49:42 +0200 Subject: [PATCH 47/96] refactor geo toggling --- ultraplot/axes/geo.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index f46ad544f..2b75f3fef 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -680,22 +680,36 @@ def _toggle_gridliner_labels( labelright=None, geo=None, ): - # For BasemapAxes the gridlines are dicts with key as the coordinate and keys the line and label - # We override the dict here assuming the labels are mut excl due to the N S E W extra chars + """ + Toggle visibility of gridliner labels for each direction. + + Parameters + ---------- + labeltop, labelbottom, labelleft, labelright : bool or None + Whether to show labels on each side. If None, do not change. + geo : optional + Not used in this method. + """ + # Ensure gridlines_major is fully initialized if any(i is None for i in self.gridlines_major): return + gridlabels = self._get_gridliner_labels( bottom=labelbottom, top=labeltop, left=labelleft, right=labelright ) - bools = [labelbottom, labeltop, labelleft, labelright] - directions = "bottom top left right".split() - for direction, toggle in zip(directions, bools): + + toggles = { + "bottom": labelbottom, + "top": labeltop, + "left": labelleft, + "right": labelright, + } + + for direction, toggle in toggles.items(): if toggle is None: continue - for label in gridlabels.get(direction, []): - label.set_visible(toggle) - - self._toggle_gridliner_labels(**turn_on_or_off) + if label := gridlabels.get(direction, None): + label.set_visible(bool(toggle) or toggle in ("x", "y")) @override def draw(self, renderer=None, *args, **kwargs): From 7ea819c93d306416bdb1a67c17eb87eff46c17e4 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 2 Oct 2025 17:41:40 +0200 Subject: [PATCH 48/96] more refactoring --- ultraplot/axes/geo.py | 3 +- ultraplot/figure.py | 50 +++++++++++++++++++++--------- ultraplot/tests/conftest.py | 5 --- ultraplot/tests/test_geographic.py | 4 +-- ultraplot/utils.py | 8 ++++- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 2b75f3fef..558292c20 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -708,7 +708,7 @@ def _toggle_gridliner_labels( for direction, toggle in toggles.items(): if toggle is None: continue - if label := gridlabels.get(direction, None): + for label in gridlabels.get(direction, []): label.set_visible(bool(toggle) or toggle in ("x", "y")) @override @@ -1424,7 +1424,6 @@ def _is_ticklabel_on(self, side: str) -> bool: if self.gridlines_major is None: return False - elif side == "labelleft": return getattr(self.gridlines_major, left_labels) elif side == "labelright": diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d0325b883..d9c5f526f 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -842,6 +842,7 @@ def _share_ticklabels(self, *, axis: str) -> None: """ if not self.stale: return + outer_axes = self._get_border_axes() true_outer = {} @@ -851,12 +852,7 @@ def _share_ticklabels(self, *, axis: str) -> None: other_sides = ("left", "right") if axis == "x" else ("top", "bottom") # Outer_axes contains the main grid but we need # to add the panels that are on these axes potentially - - tick_params = ( - {"labeltop": False, "labelbottom": False} - if axis == "x" - else {"labelleft": False, "labelright": False} - ) + tick_params = {} # Check if any of the ticks are set to on for @axis subplot_types = set() @@ -881,8 +877,10 @@ def _share_ticklabels(self, *, axis: str) -> None: tick_params["labelbottom"] = tmp["labelbottom"] case "x" if isinstance(axi, paxes.GeoAxes): - tick_params["labeltop"] = axi._is_ticklabel_on("labeltop") - tick_params["labelbottom"] = axi._is_ticklabel_on("labelbottom") + if axi._is_ticklabel_on("labeltop"): + tick_params["labeltop"] = axi._is_ticklabel_on("labeltop") + if axi._is_ticklabel_on("labelbottom"): + tick_params["labelbottom"] = axi._is_ticklabel_on("labelbottom") # Handle y case "y" if isinstance(axi, paxes.CartesianAxes): @@ -893,8 +891,10 @@ def _share_ticklabels(self, *, axis: str) -> None: tick_params["labelright"] = tmp["labelright"] case "y" if isinstance(axi, paxes.GeoAxes): - tick_params["labelleft"] = axi._is_ticklabel_on("labelleft") - tick_params["labelright"] = axi._is_ticklabel_on("labelright") + if axi._is_ticklabel_on("labelleft"): + tick_params["labelleft"] = axi._is_ticklabel_on("labelleft") + if axi._is_ticklabel_on("labelright"): + tick_params["labelright"] = axi._is_ticklabel_on("labelright") # We cannot mix types (yet) if len(subplot_types) > 1: @@ -911,6 +911,20 @@ def _share_ticklabels(self, *, axis: str) -> None: if axi not in outer_axes[side]: tmp[label] = False + # Determine sharing level + level = getattr(self, f"_share{axis}") + if axis == "y": + # For panels + if hasattr(axi, "_panel_sharey_group") and axi._panel_sharey_group: + level = 3 + else: # x-axis + # For panels + if hasattr(axi, "_panel_sharex_group") and axi._panel_sharex_group: + level = 3 + + # Don't update when we are not sharing axis ticks + if level <= 2: + continue if isinstance(axi, paxes.GeoAxes): # TODO: move this to tick_params? # Deal with backends as tick_params is still a @@ -1068,6 +1082,7 @@ def _get_border_axes( grid.fill(None) grid_axis_type = np.zeros(shape, dtype=int) seen_axis_type = dict() + ax_type_mapping = dict() for axi in self._iter_axes(panels=True, hidden=True): gs = axi.get_subplotspec() x, y = np.unravel_index(gs.num1, shape) @@ -1077,9 +1092,13 @@ def _get_border_axes( xspan = xright - xleft + 1 yspan = yright - yleft + 1 number = axi.number - if type(axi) not in seen_axis_type: - seen_axis_type[type(axi)] = len(seen_axis_type) - type_number = seen_axis_type[type(axi)] + axis_type = type(axi) + if isinstance(axi, (paxes.GeoAxes)): + axis_type = axi.projection + if axis_type not in seen_axis_type: + seen_axis_type[axis_type] = len(seen_axis_type) + type_number = seen_axis_type[axis_type] + ax_type_mapping[axi] = type_number if axi.get_visible(): grid[x : x + xspan, y : y + yspan] = axi grid_axis_type[x : x + xspan, y : y + yspan] = type_number @@ -1087,9 +1106,10 @@ def _get_border_axes( # Note we could also write the crawler in a way where # it find the borders by moving around in the grid, without spawning on each axis point. We may change # this in the future - print(grid, grid.shape) + # print(grid, grid.shape) + # print(grid_axis_type) for axi in all_axes: - axis_type = seen_axis_type.get(type(axi), 1) + axis_type = ax_type_mapping[axi] number = axi.number if axi.number is None: number = -axi._panel_parent.number diff --git a/ultraplot/tests/conftest.py b/ultraplot/tests/conftest.py index 8296fa82c..db2482d90 100644 --- a/ultraplot/tests/conftest.py +++ b/ultraplot/tests/conftest.py @@ -3,11 +3,6 @@ import warnings, logging logging.getLogger("matplotlib").setLevel(logging.ERROR) - -<<<<<<< HEAD -======= - ->>>>>>> 7394633ef (revert check) SEED = 51423 diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 105103873..2d436fd17 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -613,7 +613,7 @@ def test_cartesian_and_geo(rng): ax.format(land=True, lonlim=(-10, 10), latlim=(-10, 10)) ax[0].pcolormesh(rng.random((10, 10))) ax[1].scatter(*rng.random((2, 100))) - ax[0]._apply_axis_sharing() + fig.canvas.draw() assert ( mocked.call_count == 2 ) # needs to be called at least twice; one for each axis @@ -742,7 +742,7 @@ def test_geo_with_panels(rng): length=0.5, ), ) - ax.format(oceancolor="blue", coast=True, latticklabels="r") + ax.format(oceancolor="blue", coast=True) return fig diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 06da84140..c6864f665 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1044,8 +1044,14 @@ def is_border( elif y > self.grid.shape[1] - 1: return True - if self.grid[x, y] is None or self.grid_axis_type[x, y] != self.axis_type: + if self.grid[x, y] is None: return True + if self.grid_axis_type[x, y] != self.axis_type: + if ( + hasattr(self.grid[x, y], "_panel_side") + and self.grid[x, y]._panel_side is None + ): + return True # Check if we reached a plot or an internal edge if self.grid[x, y] != self.ax: From 267bcdcfd698c81e7d66a2a7d3afba9174c6c0f7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 10:47:57 +0200 Subject: [PATCH 49/96] update test to reflect new sharing changes --- ultraplot/tests/test_subplots.py | 46 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index dead27f3b..f698ee420 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -290,27 +290,27 @@ def test_panel_sharing_top_right(layout): for dir in "left right top bottom".split(): pax = ax[0].panel(dir) fig.canvas.draw() # force redraw tick labels - for dir, paxs in ax[0]._panel_dict.items(): - # Since we are sharing some of the ticks - # should be hidden depending on where the panel is - # in the grid - for pax in paxs: - match dir: - case "left": - assert pax._is_ticklabel_on("labelleft") - assert pax._is_ticklabel_on("labelbottom") - case "top": - assert pax._is_ticklabel_on("labeltop") == False - assert pax._is_ticklabel_on("labelbottom") == False - assert pax._is_ticklabel_on("labelleft") - case "right": - print(pax._is_ticklabel_on("labelright")) - assert pax._is_ticklabel_on("labelright") == False - assert pax._is_ticklabel_on("labelbottom") - case "bottom": - assert pax._is_ticklabel_on("labelleft") - assert pax._is_ticklabel_on("labelbottom") == False - - # The sharing axis is not showing any ticks - assert ax[0]._is_ticklabel_on(dir) == False + border_axes = fig._get_border_axes() + + for axi in fig._iter_axes(panels=True): + assert ( + axi._is_ticklabel_on("labelleft") + if axi in border_axes["left"] + else not axi._is_ticklabel_on("labelleft") + ) + assert ( + axi._is_ticklabel_on("labeltop") + if axi in border_axes["top"] + else not axi._is_ticklabel_on("labeltop") + ) + assert ( + axi._is_ticklabel_on("labelright") + if axi in border_axes["right"] + else not axi._is_ticklabel_on("labelright") + ) + assert ( + axi._is_ticklabel_on("labelbottom") + if axi in border_axes["bottom"] + else not axi._is_ticklabel_on("labelbottom") + ) return fig From ab231827d6d3b09db829d87ac832208181ae70ac Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 10:49:02 +0200 Subject: [PATCH 50/96] simplify internal border detection --- ultraplot/utils.py | 57 ++++++++++++---------------------------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index c6864f665..5dfe1dd95 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1003,6 +1003,8 @@ def find_edge_for( Setup search for a specific direction. """ + from itertools import product + # Retrieve where the axis is in the grid spec = self.ax.get_subplotspec() shape = (spec.get_gridspec().nrows_total, spec.get_gridspec().ncols_total) @@ -1033,19 +1035,15 @@ def is_border( Recursively move over the grid by following the direction. """ x, y = pos - # Check if we are at an edge of the grid (out-of-bounds). - if x < 0: - return True - elif x > self.grid.shape[0] - 1: + # Edge of grid (out-of-bounds) + if not (0 <= x < self.grid.shape[0] and 0 <= y < self.grid.shape[1]): return True - if y < 0: - return True - elif y > self.grid.shape[1] - 1: - return True + cell = self.grid[x, y] + dx, dy = direction + if cell is None: + return self.is_border((x + dx, y + dy), direction) - if self.grid[x, y] is None: - return True if self.grid_axis_type[x, y] != self.axis_type: if ( hasattr(self.grid[x, y], "_panel_side") @@ -1053,13 +1051,12 @@ def is_border( ): return True - # Check if we reached a plot or an internal edge - if self.grid[x, y] != self.ax: - return self._check_ranges(direction, other=self.grid[x, y]) + # Internal edge or plot reached + if cell != self.ax: + print(x, y, direction, self.ax, cell) + return self._check_ranges(direction, other=cell) - dx, dy = direction - pos = (x + dx, y + dy) - return self.is_border(pos, direction) + return self.is_border((x + dx, y + dy), direction) def _check_ranges( self, @@ -1103,33 +1100,7 @@ def _check_ranges( other_start, other_stop = other_rowspan if this_start == other_start and this_stop == other_stop: - # Check if it is a panel - mapper = { - (0, -1): "left", - (0, 1): "right", - (1, 0): "top", - (-1, 0): "bottom", - } - d = mapper[direction] - if self.ax.number is None: - parent = self.ax._panel_parent - if panels := parent._panel_dict.get(d, []): - if self.ax == panels[-1]: - if d in ("left", "right") and parent._sharey: - return True - elif d in ("top", "bottom") and parent._sharex: - return True - elif self.ax.number is not None: - # Defer import to prevent circular import - from . import axes as paxes - - if isinstance(self.ax, paxes.CartesianAxes): - if d in ("left", "right") and not self.ax._sharey: - return True - elif d in ("top", "bottom") and not self.ax._sharex: - return True - # GeoAxes or Polar - return False # not a border + return False # internal border return True From 21f124dcaf186dbea34ea2df57549f3bd8e8a8f8 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 10:49:14 +0200 Subject: [PATCH 51/96] minor refactor --- ultraplot/utils.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 5dfe1dd95..92fc57831 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -997,8 +997,6 @@ def find_edge_for( direction: str, d: tuple[int, int], ) -> tuple[str, bool]: - from itertools import product - """ Setup search for a specific direction. """ @@ -1045,10 +1043,7 @@ def is_border( return self.is_border((x + dx, y + dy), direction) if self.grid_axis_type[x, y] != self.axis_type: - if ( - hasattr(self.grid[x, y], "_panel_side") - and self.grid[x, y]._panel_side is None - ): + if getattr(cell, "_panel_side", None) is None: return True # Internal edge or plot reached From 53c3b805f4ced6c14893113f8ab469d38f38b8bc Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 10:51:35 +0200 Subject: [PATCH 52/96] more refactoring --- ultraplot/figure.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d9c5f526f..53dec12fb 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1106,8 +1106,6 @@ def _get_border_axes( # Note we could also write the crawler in a way where # it find the borders by moving around in the grid, without spawning on each axis point. We may change # this in the future - # print(grid, grid.shape) - # print(grid_axis_type) for axi in all_axes: axis_type = ax_type_mapping[axi] number = axi.number @@ -1121,7 +1119,6 @@ def _get_border_axes( grid_axis_type=grid_axis_type, ) for direction, is_border in crawler.find_edges(): - # print(">>", axi.number, direction, is_border) if is_border and axi not in border_axes[direction]: border_axes[direction].append(axi) self._cached_border_axes = border_axes From 56dfb9509c83ab8bc64c19f3a8770935cb610479 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 10:53:53 +0200 Subject: [PATCH 53/96] add get for border back --- ultraplot/axes/cartesian.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py index 86a726e99..0678a2a8e 100644 --- a/ultraplot/axes/cartesian.py +++ b/ultraplot/axes/cartesian.py @@ -386,6 +386,7 @@ def _apply_axis_sharing(self): # bottommost or to the *right* of the leftmost panel. But the sharing level # used for the leftmost and bottommost is the *figure* sharing level. + border_axes = self.figure._get_border_axes() # Apply X axis sharing self._apply_axis_sharing_for_axis("x", border_axes) From 582c52f127f84a582ac1b6d75558c5eabc0879fc Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 11:05:20 +0200 Subject: [PATCH 54/96] remote debug --- ultraplot/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 92fc57831..9e413d283 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1048,7 +1048,6 @@ def is_border( # Internal edge or plot reached if cell != self.ax: - print(x, y, direction, self.ax, cell) return self._check_ranges(direction, other=cell) return self.is_border((x + dx, y + dy), direction) From a29061f2c9fa7fa9ec5ef7bc6a647b72bfba1b47 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 11:49:06 +0200 Subject: [PATCH 55/96] make mpl 3.9 compat --- ultraplot/figure.py | 43 ++++++++++++++++++++++++-------- ultraplot/tests/test_subplots.py | 1 + 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 53dec12fb..07ec70c1b 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -6,6 +6,7 @@ import inspect import os from numbers import Integral +from packaging import version try: from typing import List @@ -856,6 +857,25 @@ def _share_ticklabels(self, *, axis: str) -> None: # Check if any of the ticks are set to on for @axis subplot_types = set() + + from packaging import version + from .internals import _version_mpl + + mpl_version = version.parse(str(_version_mpl)) + use_new_labels = mpl_version >= version.parse("3.10") + + label_map = { + "labeltop": "labeltop" if use_new_labels else "labelright", + "labelbottom": "labelbottom" if use_new_labels else "labelleft", + "labelleft": "labelleft", + "labelright": "labelright", + } + + labelleft = label_map["labelleft"] + labelright = label_map["labelright"] + labeltop = label_map["labeltop"] + labelbottom = label_map["labelbottom"] + for axi in self._iter_axes(panels=True, hidden=False): if not type(axi) in ( paxes.CartesianAxes, @@ -871,10 +891,10 @@ def _share_ticklabels(self, *, axis: str) -> None: # Handle x case "x" if isinstance(axi, paxes.CartesianAxes): tmp = axi.xaxis.get_tick_params() - if tmp.get("labeltop"): - tick_params["labeltop"] = tmp["labeltop"] - if tmp.get("labelbottom"): - tick_params["labelbottom"] = tmp["labelbottom"] + if tmp.get(labeltop): + tick_params[labeltop] = tmp[labeltop] + if tmp.get(labelbottom): + tick_params[labelbottom] = tmp[labelbottom] case "x" if isinstance(axi, paxes.GeoAxes): if axi._is_ticklabel_on("labeltop"): @@ -885,10 +905,10 @@ def _share_ticklabels(self, *, axis: str) -> None: # Handle y case "y" if isinstance(axi, paxes.CartesianAxes): tmp = axi.yaxis.get_tick_params() - if tmp.get("labelleft"): - tick_params["labelleft"] = tmp["labelleft"] - if tmp.get("labelright"): - tick_params["labelright"] = tmp["labelright"] + if tmp.get(labelleft): + tick_params[labelleft] = tmp[labelleft] + if tmp.get(labelright): + tick_params[labelright] = tmp[labelright] case "y" if isinstance(axi, paxes.GeoAxes): if axi._is_ticklabel_on("labelleft"): @@ -908,6 +928,9 @@ def _share_ticklabels(self, *, axis: str) -> None: # can leave the ticks as found for side in sides: label = f"label{side}" + if isinstance(axi, paxes.CartesianAxes): + # Ignore for geo as it internally converts + label = label_map[label] if axi not in outer_axes[side]: tmp[label] = False @@ -930,8 +953,8 @@ def _share_ticklabels(self, *, axis: str) -> None: # Deal with backends as tick_params is still a # function axi._toggle_gridliner_labels(**tmp) - else: - axi.tick_params(**tmp) + elif tmp: + getattr(axi, f"{axis}axis").set_tick_params(**tmp) self.stale = True def _context_adjusting(self, cache=True): diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index f698ee420..e84b8acbb 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -291,6 +291,7 @@ def test_panel_sharing_top_right(layout): pax = ax[0].panel(dir) fig.canvas.draw() # force redraw tick labels border_axes = fig._get_border_axes() + uplt.show(block=1) for axi in fig._iter_axes(panels=True): assert ( From c520e0f0dd5a78e628cedde083197e0a411e4fe3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 11:49:32 +0200 Subject: [PATCH 56/96] rm debug --- ultraplot/tests/test_subplots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index e84b8acbb..f698ee420 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -291,7 +291,6 @@ def test_panel_sharing_top_right(layout): pax = ax[0].panel(dir) fig.canvas.draw() # force redraw tick labels border_axes = fig._get_border_axes() - uplt.show(block=1) for axi in fig._iter_axes(panels=True): assert ( From 2ba95c3abf25742d95d946d091d3e5046f7cbde0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 17:58:28 +0200 Subject: [PATCH 57/96] don't use hidden axes --- ultraplot/figure.py | 2 +- ultraplot/utils.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 07ec70c1b..0edd7362c 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1106,7 +1106,7 @@ def _get_border_axes( grid_axis_type = np.zeros(shape, dtype=int) seen_axis_type = dict() ax_type_mapping = dict() - for axi in self._iter_axes(panels=True, hidden=True): + for axi in self._iter_axes(panels=True): gs = axi.get_subplotspec() x, y = np.unravel_index(gs.num1, shape) span = gs._get_rows_columns() diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 9e413d283..773998fd6 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1042,6 +1042,10 @@ def is_border( if cell is None: return self.is_border((x + dx, y + dy), direction) + # If legend or colorbar we should ignore + # if cell._is_legend() or cell._is_colorbar(): + # return self.is_border((x + dx, y + dy), direction) + if self.grid_axis_type[x, y] != self.axis_type: if getattr(cell, "_panel_side", None) is None: return True From fdce0fde9a681a4179c54ccb6d9538e224c251e7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 18:10:10 +0200 Subject: [PATCH 58/96] don't use hidden axes --- ultraplot/figure.py | 2 +- ultraplot/utils.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 0edd7362c..07ec70c1b 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1106,7 +1106,7 @@ def _get_border_axes( grid_axis_type = np.zeros(shape, dtype=int) seen_axis_type = dict() ax_type_mapping = dict() - for axi in self._iter_axes(panels=True): + for axi in self._iter_axes(panels=True, hidden=True): gs = axi.get_subplotspec() x, y = np.unravel_index(gs.num1, shape) span = gs._get_rows_columns() diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 773998fd6..a9831d18d 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1042,9 +1042,8 @@ def is_border( if cell is None: return self.is_border((x + dx, y + dy), direction) - # If legend or colorbar we should ignore - # if cell._is_legend() or cell._is_colorbar(): - # return self.is_border((x + dx, y + dy), direction) + if hasattr(cell, "_panel_hidden"): + return self.is_border((x + dx, y + dy), direction) if self.grid_axis_type[x, y] != self.axis_type: if getattr(cell, "_panel_side", None) is None: From e82d6759f38c3f3fff7a570fa33f873d4808e9f7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 18:16:56 +0200 Subject: [PATCH 59/96] don't use hidden axes --- ultraplot/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index a9831d18d..e31c8d2d0 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1042,7 +1042,7 @@ def is_border( if cell is None: return self.is_border((x + dx, y + dy), direction) - if hasattr(cell, "_panel_hidden"): + if hasattr(cell, "_panel_hidden") and cell._panel_hidden: return self.is_border((x + dx, y + dy), direction) if self.grid_axis_type[x, y] != self.axis_type: From 5f145c07072486ccf1538f48fc84b5b3d716b414 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 18:43:28 +0200 Subject: [PATCH 60/96] rm dead code --- ultraplot/tests/test_2dplots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_2dplots.py b/ultraplot/tests/test_2dplots.py index 13f084c64..8a4282e6e 100644 --- a/ultraplot/tests/test_2dplots.py +++ b/ultraplot/tests/test_2dplots.py @@ -30,7 +30,6 @@ def test_auto_diverging1(rng): """ # Test with basic data fig = uplt.figure() - # fig.format(collabels=('Auto sequential', 'Auto diverging'), suptitle='Default') ax = fig.subplot(121) ax.pcolor(rng.random((10, 10)) * 5, colorbar="b") ax = fig.subplot(122) From 92af811e1d11bc798de926fc29c85db5ad249900 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 3 Oct 2025 19:15:00 +0200 Subject: [PATCH 61/96] debug autodiverging -- locally passing --- ultraplot/tests/test_2dplots.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ultraplot/tests/test_2dplots.py b/ultraplot/tests/test_2dplots.py index 8a4282e6e..a2b75319d 100644 --- a/ultraplot/tests/test_2dplots.py +++ b/ultraplot/tests/test_2dplots.py @@ -35,6 +35,7 @@ def test_auto_diverging1(rng): ax = fig.subplot(122) ax.pcolor(rng.random((10, 10)) * 5 - 3.5, colorbar="b") fig.format(toplabels=("Sequential", "Diverging")) + fig.canvas.draw() return fig From 35f5bc1cbbadd5c0c166cbd34d12b0e614a2fe26 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 08:29:11 +0200 Subject: [PATCH 62/96] fix grid indexing --- ultraplot/axes/geo.py | 30 ++++++++++++++-------- ultraplot/figure.py | 3 --- ultraplot/gridspec.py | 58 ++++++++++++++++++------------------------- ultraplot/utils.py | 2 +- 4 files changed, 44 insertions(+), 49 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 558292c20..15c5f9a43 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -792,15 +792,18 @@ def _to_label_array(arg, lon=True): array[4] = True # possibly toggle geo spine labels elif not any(isinstance(_, str) for _ in array): if len(array) == 1: - array.append(False) # default is to label bottom or left + array.append(None) if len(array) == 2: - array = [False, False, *array] if lon else [*array, False, False] + array = [None, None, *array] if lon else [*array, None, None] if len(array) == 4: - b = any(array) if rc["grid.geolabels"] else False - array.append(b) # possibly toggle geo spine labels + b = ( + any(a for a in array if a is not None) + if rc["grid.geolabels"] + else None + ) + array.append(b) if len(array) != 5: raise ValueError(f"Invald boolean label array length {len(array)}.") - array = list(map(bool, array)) else: raise ValueError(f"Invalid {which}label spec: {arg}.") return array @@ -921,9 +924,13 @@ def format( # NOTE: Cartopy 0.18 and 0.19 inline labels require any of # top, bottom, left, or right to be toggled then ignores them. # Later versions of cartopy permit both or neither labels. - labels = _not_none(labels, rc.find("grid.labels", context=True)) - lonlabels = _not_none(lonlabels, labels) - latlabels = _not_none(latlabels, labels) + if lonlabels is None and latlabels is None: + labels = _not_none(labels, rc.find("grid.labels", context=True)) + lonlabels = labels + latlabels = labels + else: + lonlabels = _not_none(lonlabels, labels) + latlabels = _not_none(latlabels, labels) # Set the ticks self._toggle_ticks(lonlabels, "x") self._toggle_ticks(latlabels, "y") @@ -1452,10 +1459,10 @@ def _toggle_gridliner_labels( side_labels = _CartopyAxes._get_side_labels() togglers = (labelleft, labelright, labelbottom, labeltop) gl = self.gridlines_major + for toggle, side in zip(togglers, side_labels): - if toggle is None: - continue - setattr(gl, side, toggle) + if toggle is not None: + setattr(gl, side, toggle) if geo is not None: # only cartopy 0.20 supported but harmless setattr(gl, "geo_labels", geo) @@ -1749,6 +1756,7 @@ def _update_major_gridlines( for side, lon, lat in zip( "labelleft labelright labelbottom labeltop geo".split(), lonarray, latarray ): + sides[side] = None if lon and lat: sides[side] = True elif lon: diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 07ec70c1b..4cec362c2 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -2094,9 +2094,6 @@ def format( } ax.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_figure=True, **kw, **kwargs) ax.number = store_old_number - # When we apply formatting to all axes, we need - # to potentially adjust the labels. - # Warn unused keyword argument(s) kw = { key: value diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 3183aa1d3..faf29bcd2 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1537,42 +1537,32 @@ def __getitem__(self, key): >>> axs[1, 2] # the subplot in the second row, third column >>> axs[:, 0] # a SubplotGrid containing the subplots in the first column """ - if isinstance(key, tuple) and len(key) == 1: - key = key[0] - # List-style indexing - if isinstance(key, (Integral, slice)): - slices = isinstance(key, slice) - objs = list.__getitem__(self, key) - # Gridspec-style indexing - elif ( - isinstance(key, tuple) - and len(key) == 2 - and all(isinstance(ikey, (Integral, slice)) for ikey in key) - ): - # WARNING: Permit no-op slicing of empty grids here - slices = any(isinstance(ikey, slice) for ikey in key) - objs = [] - if self: - gs = self.gridspec - ss_key = gs._make_subplot_spec(key) # obfuscates panels - row1_key, col1_key = divmod(ss_key.num1, gs.ncols) - row2_key, col2_key = divmod(ss_key.num2, gs.ncols) - for ax in self: - ss = ax._get_topmost_axes().get_subplotspec().get_topmost_subplotspec() - row1, col1 = divmod(ss.num1, gs.ncols) - row2, col2 = divmod(ss.num2, gs.ncols) - inrow = row1_key <= row1 <= row2_key or row1_key <= row2 <= row2_key - incol = col1_key <= col1 <= col2_key or col1_key <= col2 <= col2_key - if inrow and incol: - objs.append(ax) - if not slices and len(objs) == 1: # accounts for overlapping subplots - objs = objs[0] + # Allow 1D list-like indexing + if isinstance(key, int): + return list.__getitem__(self, key) + elif isinstance(key, slice): + return SubplotGrid(list.__getitem__(self, key)) + + # Allow 2D array-like indexing + # NOTE: We assume this is a 2D array of subplots, because this is + # how it is generated in the first place by ultraplot.figure(). + # But it is possible to append subplots manually. + gs = self.gridspec + if gs is None: + raise IndexError( + f"{self.__class__.__name__} has no gridspec, cannot index with {key!r}." + ) + nrows, ncols = gs.get_geometry() + axs = np.array(self, dtype=object).reshape(nrows, ncols) + objs = axs[key] + if hasattr(objs, "flat"): + objs = list(objs.flat) + elif not isinstance(objs, list): + objs = [objs] + if len(objs) == 1: + return objs[0] else: - raise IndexError(f"Invalid index {key!r}.") - if isinstance(objs, list): return SubplotGrid(objs) - else: - return objs def __setitem__(self, key, value): """ diff --git a/ultraplot/utils.py b/ultraplot/utils.py index e31c8d2d0..d5cc5db7f 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1018,7 +1018,7 @@ def find_edge_for( is_border = False for xl, yl in product(xs, ys): - pos = (x, y) + pos = (xl, yl) if self.is_border(pos, d): is_border = True break From 4deb49c414eccf73d5a06c0c9edb08a3f045e4c3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 08:29:11 +0200 Subject: [PATCH 63/96] fix grid indexing --- ultraplot/axes/geo.py | 27 +++++++++++++------- ultraplot/gridspec.py | 58 ++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index ab95a661e..896bc0a6d 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -805,15 +805,18 @@ def _to_label_array(arg, lon=True): array[4] = True # possibly toggle geo spine labels elif not any(isinstance(_, str) for _ in array): if len(array) == 1: - array.append(False) # default is to label bottom or left + array.append(None) if len(array) == 2: - array = [False, False, *array] if lon else [*array, False, False] + array = [None, None, *array] if lon else [*array, None, None] if len(array) == 4: - b = any(array) if rc["grid.geolabels"] else False - array.append(b) # possibly toggle geo spine labels + b = ( + any(a for a in array if a is not None) + if rc["grid.geolabels"] + else None + ) + array.append(b) if len(array) != 5: raise ValueError(f"Invald boolean label array length {len(array)}.") - array = list(map(bool, array)) else: raise ValueError(f"Invalid {which}label spec: {arg}.") return array @@ -934,9 +937,13 @@ def format( # NOTE: Cartopy 0.18 and 0.19 inline labels require any of # top, bottom, left, or right to be toggled then ignores them. # Later versions of cartopy permit both or neither labels. - labels = _not_none(labels, rc.find("grid.labels", context=True)) - lonlabels = _not_none(lonlabels, labels) - latlabels = _not_none(latlabels, labels) + if lonlabels is None and latlabels is None: + labels = _not_none(labels, rc.find("grid.labels", context=True)) + lonlabels = labels + latlabels = labels + else: + lonlabels = _not_none(lonlabels, labels) + latlabels = _not_none(latlabels, labels) # Set the ticks self._toggle_ticks(lonlabels, "x") self._toggle_ticks(latlabels, "y") @@ -1464,8 +1471,9 @@ def _toggle_gridliner_labels( side_labels = _CartopyAxes._get_side_labels() togglers = (labelleft, labelright, labelbottom, labeltop) gl = self.gridlines_major + for toggle, side in zip(togglers, side_labels): - if getattr(gl, side) != toggle: + if toggle is not None: setattr(gl, side, toggle) if geo is not None: # only cartopy 0.20 supported but harmless setattr(gl, "geo_labels", geo) @@ -1760,6 +1768,7 @@ def _update_major_gridlines( for side, lon, lat in zip( "labelleft labelright labelbottom labeltop geo".split(), lonarray, latarray ): + sides[side] = None if lon and lat: sides[side] = True elif lon: diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 0d642505e..9f023ec38 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1536,42 +1536,32 @@ def __getitem__(self, key): >>> axs[1, 2] # the subplot in the second row, third column >>> axs[:, 0] # a SubplotGrid containing the subplots in the first column """ - if isinstance(key, tuple) and len(key) == 1: - key = key[0] - # List-style indexing - if isinstance(key, (Integral, slice)): - slices = isinstance(key, slice) - objs = list.__getitem__(self, key) - # Gridspec-style indexing - elif ( - isinstance(key, tuple) - and len(key) == 2 - and all(isinstance(ikey, (Integral, slice)) for ikey in key) - ): - # WARNING: Permit no-op slicing of empty grids here - slices = any(isinstance(ikey, slice) for ikey in key) - objs = [] - if self: - gs = self.gridspec - ss_key = gs._make_subplot_spec(key) # obfuscates panels - row1_key, col1_key = divmod(ss_key.num1, gs.ncols) - row2_key, col2_key = divmod(ss_key.num2, gs.ncols) - for ax in self: - ss = ax._get_topmost_axes().get_subplotspec().get_topmost_subplotspec() - row1, col1 = divmod(ss.num1, gs.ncols) - row2, col2 = divmod(ss.num2, gs.ncols) - inrow = row1_key <= row1 <= row2_key or row1_key <= row2 <= row2_key - incol = col1_key <= col1 <= col2_key or col1_key <= col2 <= col2_key - if inrow and incol: - objs.append(ax) - if not slices and len(objs) == 1: # accounts for overlapping subplots - objs = objs[0] + # Allow 1D list-like indexing + if isinstance(key, int): + return list.__getitem__(self, key) + elif isinstance(key, slice): + return SubplotGrid(list.__getitem__(self, key)) + + # Allow 2D array-like indexing + # NOTE: We assume this is a 2D array of subplots, because this is + # how it is generated in the first place by ultraplot.figure(). + # But it is possible to append subplots manually. + gs = self.gridspec + if gs is None: + raise IndexError( + f"{self.__class__.__name__} has no gridspec, cannot index with {key!r}." + ) + nrows, ncols = gs.get_geometry() + axs = np.array(self, dtype=object).reshape(nrows, ncols) + objs = axs[key] + if hasattr(objs, "flat"): + objs = list(objs.flat) + elif not isinstance(objs, list): + objs = [objs] + if len(objs) == 1: + return objs[0] else: - raise IndexError(f"Invalid index {key!r}.") - if isinstance(objs, list): return SubplotGrid(objs) - else: - return objs def __setitem__(self, key, value): """ From cd55ccc488a611bd8a51603c5e759c444275be9f Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 08:34:40 +0200 Subject: [PATCH 64/96] add unittest --- ultraplot/tests/test_geographic.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 4b95a9384..72efaf3d5 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -895,3 +895,26 @@ def test_imshow_with_and_without_transform(rng): ax[2].imshow(data, transform=uplt.axes.geo.ccrs.PlateCarree()) ax.format(title=["LCC", "No transform", "PlateCarree"]) return fig + + +@pytest.mark.mpl_image_compare +def test_grid_indexing_formatting(rng): + """ + Check if subplotgrid is correctly selecting + the subplots based on non-shared axis formatting + """ + # See https://github.com/Ultraplot/UltraPlot/issues/356 + lon = np.arange(0, 360, 10) + lat = np.arange(-60, 60 + 1, 10) + data = rng.rnadom((len(lat), len(lon))) + + fig, axs = uplt.subplots(nrows=3, ncols=2, proj="cyl", share=0) + axs.format(coast=True) + + for ax in axs: + m = ax.pcolor(lon, lat, data) + ax.colorbar(m) + + axs[-1, :].format(lonlabels=True) + axs[:, 0].format(latlabels=True) + return fig From 7555bb38eda8020db3566743bad4bfadf00ee03a Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Sat, 4 Oct 2025 08:40:07 +0200 Subject: [PATCH 65/96] Update ultraplot/tests/test_geographic.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultraplot/tests/test_geographic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 72efaf3d5..dd6156e94 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -906,7 +906,7 @@ def test_grid_indexing_formatting(rng): # See https://github.com/Ultraplot/UltraPlot/issues/356 lon = np.arange(0, 360, 10) lat = np.arange(-60, 60 + 1, 10) - data = rng.rnadom((len(lat), len(lon))) + data = rng.random((len(lat), len(lon))) fig, axs = uplt.subplots(nrows=3, ncols=2, proj="cyl", share=0) axs.format(coast=True) From 6dda98053e9a13e1add5b8869f8ae93097bbcc7d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 08:54:52 +0200 Subject: [PATCH 66/96] update tests to reflect changes --- ultraplot/tests/test_geographic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index dd6156e94..35789a54d 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -314,8 +314,8 @@ def test_toggle_gridliner_labels(): gl = ax[0].gridlines_major assert gl.left_labels == False - assert gl.right_labels == None # initially these are none - assert gl.top_labels == None + assert gl.right_labels == False + assert gl.top_labels == False assert gl.bottom_labels == False ax[0]._toggle_gridliner_labels(labeltop=True) assert gl.top_labels == True @@ -617,7 +617,7 @@ def test_cartesian_and_geo(rng): ax[0].pcolormesh(rng.random((10, 10))) ax[1].scatter(*rng.random((2, 100))) ax[0]._apply_axis_sharing() - assert mocked.call_count == 1 + assert mocked.call_count == 2 return fig From 1a72ba1d44b5fa04179d503e7f7daecee352c744 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 12:55:45 +0200 Subject: [PATCH 67/96] fix indexing --- ultraplot/gridspec.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 9f023ec38..5f97781c3 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1552,7 +1552,20 @@ def __getitem__(self, key): f"{self.__class__.__name__} has no gridspec, cannot index with {key!r}." ) nrows, ncols = gs.get_geometry() - axs = np.array(self, dtype=object).reshape(nrows, ncols) + + # Build grid with None for empty slots + grid = np.full((nrows, ncols), None, dtype=object) + for ax in self: + spec = ax.get_subplotspec() + spans = spec._get_grid_span() + rowspan = spans[:2] + colspan = spans[-2:] + + grid[ + slice(*rowspan), + slice(*colspan), + ] = ax + axs = np.array(grid, dtype=object) objs = axs[key] if hasattr(objs, "flat"): objs = list(objs.flat) From c94a3c5ac0a38c0baf102f03d8351dd6664aa369 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 12:59:46 +0200 Subject: [PATCH 68/96] add unittest that made docs fail --- ultraplot/tests/test_subplots.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index dead27f3b..4e7a5d2eb 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -314,3 +314,16 @@ def test_panel_sharing_top_right(layout): # The sharing axis is not showing any ticks assert ax[0]._is_ticklabel_on(dir) == False return fig + + +@pytest.mark.mpl_image_compare +def test_uneven_span_subplots(rng): + fig = uplt.figure(refwidth=1, refnum=5, span=False) + axs = fig.subplots([[1, 1, 2], [3, 4, 2], [3, 4, 5]], hratios=[2.2, 1, 1]) + axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Complex SubplotGrid") + axs[0].format(ec="black", fc="gray1", lw=1.4) + axs[1, 1:].format(fc="blush") + axs[1, :1].format(fc="sky blue") + axs[-1, -1].format(fc="gray4", grid=False) + axs[0].plot((rng.random(50, 10) - 0.5).cumsum(axis=0), cycle="Grays_r", lw=2) + return fig From 5fb9cd9bbfc95026e291f652530be7004d89eeff Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 13:39:48 +0200 Subject: [PATCH 69/96] restore indexing --- ultraplot/gridspec.py | 72 +++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 5f97781c3..233413107 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1536,45 +1536,43 @@ def __getitem__(self, key): >>> axs[1, 2] # the subplot in the second row, third column >>> axs[:, 0] # a SubplotGrid containing the subplots in the first column """ - # Allow 1D list-like indexing - if isinstance(key, int): - return list.__getitem__(self, key) - elif isinstance(key, slice): - return SubplotGrid(list.__getitem__(self, key)) - - # Allow 2D array-like indexing - # NOTE: We assume this is a 2D array of subplots, because this is - # how it is generated in the first place by ultraplot.figure(). - # But it is possible to append subplots manually. - gs = self.gridspec - if gs is None: - raise IndexError( - f"{self.__class__.__name__} has no gridspec, cannot index with {key!r}." - ) - nrows, ncols = gs.get_geometry() - - # Build grid with None for empty slots - grid = np.full((nrows, ncols), None, dtype=object) - for ax in self: - spec = ax.get_subplotspec() - spans = spec._get_grid_span() - rowspan = spans[:2] - colspan = spans[-2:] - - grid[ - slice(*rowspan), - slice(*colspan), - ] = ax - axs = np.array(grid, dtype=object) - objs = axs[key] - if hasattr(objs, "flat"): - objs = list(objs.flat) - elif not isinstance(objs, list): - objs = [objs] - if len(objs) == 1: - return objs[0] + if isinstance(key, tuple) and len(key) == 1: + key = key[0] + # List-style indexing + if isinstance(key, (Integral, slice)): + slices = isinstance(key, slice) + objs = list.__getitem__(self, key) + # Gridspec-style indexing + elif ( + isinstance(key, tuple) + and len(key) == 2 + and all(isinstance(ikey, (Integral, slice)) for ikey in key) + ): + # WARNING: Permit no-op slicing of empty grids here + slices = any(isinstance(ikey, slice) for ikey in key) + objs = [] + if self: + gs = self.gridspec + ss_key = gs._make_subplot_spec(key) # obfuscates panels + row1_key, col1_key = divmod(ss_key.num1, gs.ncols) + row2_key, col2_key = divmod(ss_key.num2, gs.ncols) + for ax in self: + ss = ax._get_topmost_axes().get_subplotspec().get_topmost_subplotspec() + row1, col1 = divmod(ss.num1, gs.ncols) + row2, col2 = divmod(ss.num2, gs.ncols) + inrow = row1_key <= row1 <= row2_key or row1_key <= row2 <= row2_key + incol = col1_key <= col1 <= col2_key or col1_key <= col2 <= col2_key + if inrow and incol: + objs.append(ax) + + if not slices and len(objs) == 1: # accounts for overlapping subplots + objs = objs[0] else: + raise IndexError(f"Invalid index {key!r}.") + if isinstance(objs, list): return SubplotGrid(objs) + else: + return objs def __setitem__(self, key, value): """ From 98555a886fc4746b0f7d9b6f1e1d06eeddf23b94 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 15:06:08 +0200 Subject: [PATCH 70/96] fix indexing --- ultraplot/gridspec.py | 66 ++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 233413107..9757179b6 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1536,43 +1536,39 @@ def __getitem__(self, key): >>> axs[1, 2] # the subplot in the second row, third column >>> axs[:, 0] # a SubplotGrid containing the subplots in the first column """ - if isinstance(key, tuple) and len(key) == 1: - key = key[0] - # List-style indexing - if isinstance(key, (Integral, slice)): - slices = isinstance(key, slice) - objs = list.__getitem__(self, key) - # Gridspec-style indexing - elif ( - isinstance(key, tuple) - and len(key) == 2 - and all(isinstance(ikey, (Integral, slice)) for ikey in key) - ): - # WARNING: Permit no-op slicing of empty grids here - slices = any(isinstance(ikey, slice) for ikey in key) - objs = [] - if self: - gs = self.gridspec - ss_key = gs._make_subplot_spec(key) # obfuscates panels - row1_key, col1_key = divmod(ss_key.num1, gs.ncols) - row2_key, col2_key = divmod(ss_key.num2, gs.ncols) - for ax in self: - ss = ax._get_topmost_axes().get_subplotspec().get_topmost_subplotspec() - row1, col1 = divmod(ss.num1, gs.ncols) - row2, col2 = divmod(ss.num2, gs.ncols) - inrow = row1_key <= row1 <= row2_key or row1_key <= row2 <= row2_key - incol = col1_key <= col1 <= col2_key or col1_key <= col2 <= col2_key - if inrow and incol: - objs.append(ax) - - if not slices and len(objs) == 1: # accounts for overlapping subplots - objs = objs[0] + # Allow 1D list-like indexing + if isinstance(key, int): + return list.__getitem__(self, key) + elif isinstance(key, slice): + return SubplotGrid(list.__getitem__(self, key)) + + # Allow 2D array-like indexing + # NOTE: We assume this is a 2D array of subplots, because this is + # how it is generated in the first place by ultraplot.figure(). + # But it is possible to append subplots manually. + gs = self.gridspec + if gs is None: + raise IndexError( + f"{self.__class__.__name__} has no gridspec, cannot index with {key!r}." + ) + nrows, ncols = gs.get_geometry() + + # Build grid with None for empty slots + grid = np.full((gs.nrows_total, gs.ncols_total), None, dtype=object) + for ax in self: + spec = ax.get_subplotspec() + x1, x2, y1, y2 = spec._get_rows_columns(ncols=gs.ncols_total) + grid[x1 : x2 + 1, y1 : y2 + 1] = ax + objs = grid[key] + if hasattr(objs, "flat"): + objs = list(objs.flat) + elif not isinstance(objs, list): + objs = [objs] + if len(objs) == 1: + return objs[0] else: - raise IndexError(f"Invalid index {key!r}.") - if isinstance(objs, list): + objs = [obj for obj in objs if obj is not None] return SubplotGrid(objs) - else: - return objs def __setitem__(self, key, value): """ From 711ca150d10a505aeb4e24930ec5b63e074c0246 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 15:06:28 +0200 Subject: [PATCH 71/96] rm dead code --- ultraplot/gridspec.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index 9757179b6..ff4469527 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1551,8 +1551,6 @@ def __getitem__(self, key): raise IndexError( f"{self.__class__.__name__} has no gridspec, cannot index with {key!r}." ) - nrows, ncols = gs.get_geometry() - # Build grid with None for empty slots grid = np.full((gs.nrows_total, gs.ncols_total), None, dtype=object) for ax in self: From dd6ff2aa86de00cdd158b5262758d266a05bfea8 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 16:07:16 +0200 Subject: [PATCH 72/96] handle index error --- ultraplot/gridspec.py | 21 ++++++++++++++++----- ultraplot/tests/test_subplots.py | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ultraplot/gridspec.py b/ultraplot/gridspec.py index ff4469527..159cac2c5 100644 --- a/ultraplot/gridspec.py +++ b/ultraplot/gridspec.py @@ -1557,16 +1557,27 @@ def __getitem__(self, key): spec = ax.get_subplotspec() x1, x2, y1, y2 = spec._get_rows_columns(ncols=gs.ncols_total) grid[x1 : x2 + 1, y1 : y2 + 1] = ax - objs = grid[key] + + new_key = [] + for which, keyi in zip("hw", key): + try: + encoded_keyi = gs._encode_indices(keyi, which=which) + except: + raise IndexError( + f"Attempted to access {key=} for gridspec {grid.shape=}" + ) + new_key.append(encoded_keyi) + xs, ys = new_key + objs = grid[xs, ys] if hasattr(objs, "flat"): - objs = list(objs.flat) + objs = [obj for obj in objs.flat if obj is not None] elif not isinstance(objs, list): objs = [objs] + if len(objs) == 1: return objs[0] - else: - objs = [obj for obj in objs if obj is not None] - return SubplotGrid(objs) + objs = [obj for obj in objs if obj is not None] + return SubplotGrid(objs) def __setitem__(self, key, value): """ diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index 4e7a5d2eb..e215a90ee 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -325,5 +325,5 @@ def test_uneven_span_subplots(rng): axs[1, 1:].format(fc="blush") axs[1, :1].format(fc="sky blue") axs[-1, -1].format(fc="gray4", grid=False) - axs[0].plot((rng.random(50, 10) - 0.5).cumsum(axis=0), cycle="Grays_r", lw=2) + axs[0].plot((rng.random((50, 10)) - 0.5).cumsum(axis=0), cycle="Grays_r", lw=2) return fig From a138bbafe0d2bc45def8045b7e2066871775919d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 4 Oct 2025 18:23:50 +0200 Subject: [PATCH 73/96] adjust default panel ticks --- ultraplot/axes/base.py | 6 ------ ultraplot/figure.py | 18 ++++++++++++++++++ ultraplot/tests/test_geographic.py | 5 ----- ultraplot/utils.py | 5 +++++ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index db5aab3c9..22e489d18 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -1518,12 +1518,6 @@ def _apply_title_above(self): for name in names: labels._transfer_label(self._title_dict[name], pax._title_dict[name]) - def _apply_axis_sharing(self): - """ - Should be implemented by subclasses but silently pass if not, e.g. for polar axes - """ - raise ImplementationError("Axis sharing not implemented for this axes type.") - def _apply_auto_share(self): """ Automatically configure axis sharing based on the horizontal and diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 4cec362c2..d3e093b18 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1142,6 +1142,7 @@ def _get_border_axes( grid_axis_type=grid_axis_type, ) for direction, is_border in crawler.find_edges(): + # print(">>", is_border, direction, axi.number) if is_border and axi not in border_axes[direction]: border_axes[direction].append(axi) self._cached_border_axes = border_axes @@ -1273,6 +1274,23 @@ def _add_axes_panel(self, ax, side=None, **kwargs): pax = self.add_subplot(ss, **kwargs) pax._panel_side = side pax._panel_share = share + if share: + # When we are sharing we remove the ticks by default + # as we "push" the labels out. See Figure._share_ticklabels. + # If we add the labels here it is more difficult to control + # for some ticks being on. + from packaging import version + from .internals import _version_mpl + + params = {} + if version.parse(str(_version_mpl)) < version.parse("3.10"): + params = dict(labelleft=False, labelright=False) + pax.xaxis.set_tick_params(**params) + pax.yaxis.set_tick_params(**params) + else: + pax.xaxis.set_tick_params(labelbottom=False, labeltop=False) + pax.yaxis.set_tick_params(labelleft=False, labelright=False) + pax._panel_parent = ax ax._panel_dict[side].append(pax) ax._apply_auto_share() diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index c69fb1026..9d4101d45 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -613,15 +613,10 @@ def test_cartesian_and_geo(rng): ax.format(land=True, lonlim=(-10, 10), latlim=(-10, 10)) ax[0].pcolormesh(rng.random((10, 10))) ax[1].scatter(*rng.random((2, 100))) -<<<<<<< HEAD fig.canvas.draw() assert ( mocked.call_count == 2 ) # needs to be called at least twice; one for each axis -======= - ax[0]._apply_axis_sharing() - assert mocked.call_count == 2 ->>>>>>> hotfix-grid-index return fig diff --git a/ultraplot/utils.py b/ultraplot/utils.py index d5cc5db7f..2e74e725f 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1097,6 +1097,11 @@ def _check_ranges( other_start, other_stop = other_rowspan if this_start == other_start and this_stop == other_stop: + if other._panel_parent is not None: + if dx == 0 and not other._panel_sharex_group: + return True + elif dy == 0 and not other._panel_sharey_group: + return True return False # internal border return True From be2f46b7f4495fcbf3098ce7154724ec8d0cc9ec Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 9 Oct 2025 09:25:14 +0200 Subject: [PATCH 74/96] bump --- ultraplot/figure.py | 14 +++++++++----- ultraplot/utils.py | 43 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d3e093b18..27062dddc 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -886,7 +886,8 @@ def _share_ticklabels(self, *, axis: str) -> None: f"Tick label sharing not implemented for {type(axi)} subplots." ) return - subplot_types.add(type(axi)) + if not axi._panel_side: + subplot_types.add(type(axi)) match axis: # Handle x case "x" if isinstance(axi, paxes.CartesianAxes): @@ -940,18 +941,21 @@ def _share_ticklabels(self, *, axis: str) -> None: # For panels if hasattr(axi, "_panel_sharey_group") and axi._panel_sharey_group: level = 3 + elif axi._panel_side and axi._sharey: + level = 3 else: # x-axis # For panels if hasattr(axi, "_panel_sharex_group") and axi._panel_sharex_group: level = 3 + elif axi._panel_side and axi._sharex: + level = 3 - # Don't update when we are not sharing axis ticks - if level <= 2: + if level < 3: continue if isinstance(axi, paxes.GeoAxes): # TODO: move this to tick_params? - # Deal with backends as tick_params is still a - # function + # Tick_params is independent of gridliner objects + # Depending on the backend tick params is useful or not axi._toggle_gridliner_labels(**tmp) elif tmp: getattr(axi, f"{axis}axis").set_tick_params(**tmp) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 2e74e725f..3ecd9597a 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1046,8 +1046,8 @@ def is_border( return self.is_border((x + dx, y + dy), direction) if self.grid_axis_type[x, y] != self.axis_type: - if getattr(cell, "_panel_side", None) is None: - return True + if cell in self.ax._panel_dict.get(cell._panel_side, []): + return self.is_border((x + dx, y + dy), direction) # Internal edge or plot reached if cell != self.ax: @@ -1097,12 +1097,43 @@ def _check_ranges( other_start, other_stop = other_rowspan if this_start == other_start and this_stop == other_stop: - if other._panel_parent is not None: - if dx == 0 and not other._panel_sharex_group: + # We may hit an internal border if we are at + # the interface with a panel that is not sharing + dmap = { + (-1, 0): "bottom", + (1, 0): "top", + (0, -1): "left", + (0, 1): "right", + } + side = dmap[direction] + if self.ax.number is None: # panel + parent = self.ax._panel_parent + + panels = parent._panel_dict.get(side, []) + # If we are a panel at the end we are a border + # only if we are not sharing axes + if side in ("left", "right"): + if self.ax._sharey is None: + return True + elif not self.ax._panel_sharey_group: + return True + elif side in ("top", "bottom"): + if self.ax._sharex is None: + return True + elif not self.ax._panel_sharex_group: + return True + + # Only consider when we are interfacing with a panel + # axes on the outside will also not share when they are in top + # or left + elif side in ("left", "right") and self.ax._sharey is None: + if other.number is None: return True - elif dy == 0 and not other._panel_sharey_group: + elif side in ("bottom", "top") and self.ax._sharex is None: + if other.number is None: return True - return False # internal border + + return False return True From a5a4145d203704a43b565ba1af636d1db7cdaace Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 9 Oct 2025 09:34:13 +0200 Subject: [PATCH 75/96] bump test --- ultraplot/tests/test_geographic.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 9d4101d45..6eef28fd3 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -615,7 +615,7 @@ def test_cartesian_and_geo(rng): ax[1].scatter(*rng.random((2, 100))) fig.canvas.draw() assert ( - mocked.call_count == 2 + mocked.call_count > 2 ) # needs to be called at least twice; one for each axis return fig @@ -677,19 +677,9 @@ def test_panels_geo(): ax.format(labels=True) for dir in "top bottom right left".split(): pax = ax.panel_axes(dir) - match dir: - case "top": - assert len(pax.get_xticklabels()) > 0 - assert len(pax.get_yticklabels()) > 0 - case "bottom": - assert len(pax.get_xticklabels()) > 0 - assert len(pax.get_yticklabels()) > 0 - case "left": - assert len(pax.get_xticklabels()) > 0 - assert len(pax.get_yticklabels()) > 0 - case "right": - assert len(pax.get_xticklabels()) > 0 - assert len(pax.get_yticklabels()) > 0 + fig.canvas.draw() # need this to update the ticks + assert len(pax.get_xticklabels()) > 0 + assert len(pax.get_yticklabels()) > 0 @pytest.mark.mpl_image_compare From 0d5cf03d6847360f892c7d3e2049f85ab9449e36 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 13 Oct 2025 15:31:14 +0200 Subject: [PATCH 76/96] refactor sharing --- ultraplot/figure.py | 235 ++++++++++++++++++++++++-------------------- 1 file changed, 126 insertions(+), 109 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 27062dddc..73a57bd57 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -836,28 +836,19 @@ def draw(self, renderer): def _share_ticklabels(self, *, axis: str) -> None: """ - Tick label sharing is determined at the figure level. While + Tick label sharing is determined at the figure level. While each subplot controls the limits, we are dealing with the ticklabels - here as the complexity is easiier to deal with. + here as the complexity is easier to deal with. axis: str 'x' or 'y', row or columns to update """ if not self.stale: return outer_axes = self._get_border_axes() - true_outer = {} sides = ("top", "bottom") if axis == "x" else ("left", "right") - # for panels - other_axis = "x" if axis == "y" else "y" - other_sides = ("left", "right") if axis == "x" else ("top", "bottom") - # Outer_axes contains the main grid but we need - # to add the panels that are on these axes potentially - tick_params = {} - - # Check if any of the ticks are set to on for @axis - subplot_types = set() + # Version-dependent label name mapping for reading back params from packaging import version from .internals import _version_mpl @@ -876,89 +867,127 @@ def _share_ticklabels(self, *, axis: str) -> None: labeltop = label_map["labeltop"] labelbottom = label_map["labelbottom"] - for axi in self._iter_axes(panels=True, hidden=False): - if not type(axi) in ( - paxes.CartesianAxes, - paxes._CartopyAxes, - paxes._BasemapAxes, - ): + # Group axes by row (for x) or column (for y) + def _group_key(ax): + ss = ax.get_subplotspec() + return ss.rowspan.start if axis == "x" else ss.colspan.start + + axes = list(self._iter_axes(panels=True, hidden=False)) + groups = {} + for axi in axes: + try: + key = _group_key(axi) + except Exception: + # If we can't get a subplotspec, skip grouping for this axes + continue + groups.setdefault(key, []).append(axi) + + # Process each group independently + for key, group_axes in groups.items(): + # Build baseline from MAIN axes only (exclude panels) + tick_params_group = {} + subplot_types_group = set() + unsupported_found = False + + for axi in group_axes: + # Only main axes "vote" for baseline + if getattr(axi, "_panel_side", None): + continue + # Supported axes types + if not isinstance( + axi, (paxes.CartesianAxes, paxes._CartopyAxes, paxes._BasemapAxes) + ): + warnings._warn_ultraplot( + f"Tick label sharing not implemented for {type(axi)} subplots." + ) + unsupported_found = True + break + subplot_types_group.add(type(axi)) + match axis: + # Handle x + case "x" if isinstance(axi, paxes.CartesianAxes): + tmp = axi.xaxis.get_tick_params() + if tmp.get(labeltop): + tick_params_group[labeltop] = tmp[labeltop] + if tmp.get(labelbottom): + tick_params_group[labelbottom] = tmp[labelbottom] + case "x" if isinstance(axi, paxes.GeoAxes): + if axi._is_ticklabel_on("labeltop"): + tick_params_group["labeltop"] = axi._is_ticklabel_on( + "labeltop" + ) + if axi._is_ticklabel_on("labelbottom"): + tick_params_group["labelbottom"] = axi._is_ticklabel_on( + "labelbottom" + ) + + # Handle y + case "y" if isinstance(axi, paxes.CartesianAxes): + tmp = axi.yaxis.get_tick_params() + if tmp.get(labelleft): + tick_params_group[labelleft] = tmp[labelleft] + if tmp.get(labelright): + tick_params_group[labelright] = tmp[labelright] + case "y" if isinstance(axi, paxes.GeoAxes): + if axi._is_ticklabel_on("labelleft"): + tick_params_group["labelleft"] = axi._is_ticklabel_on( + "labelleft" + ) + if axi._is_ticklabel_on("labelright"): + tick_params_group["labelright"] = axi._is_ticklabel_on( + "labelright" + ) + + # Skip group if unsupported axes were found + if unsupported_found: + continue + + # We cannot mix types (yet) within a group + if len(subplot_types_group) > 1: warnings._warn_ultraplot( - f"Tick label sharing not implemented for {type(axi)} subplots." + "Tick label sharing not implemented for mixed subplot types." ) - return - if not axi._panel_side: - subplot_types.add(type(axi)) - match axis: - # Handle x - case "x" if isinstance(axi, paxes.CartesianAxes): - tmp = axi.xaxis.get_tick_params() - if tmp.get(labeltop): - tick_params[labeltop] = tmp[labeltop] - if tmp.get(labelbottom): - tick_params[labelbottom] = tmp[labelbottom] - - case "x" if isinstance(axi, paxes.GeoAxes): - if axi._is_ticklabel_on("labeltop"): - tick_params["labeltop"] = axi._is_ticklabel_on("labeltop") - if axi._is_ticklabel_on("labelbottom"): - tick_params["labelbottom"] = axi._is_ticklabel_on("labelbottom") - - # Handle y - case "y" if isinstance(axi, paxes.CartesianAxes): - tmp = axi.yaxis.get_tick_params() - if tmp.get(labelleft): - tick_params[labelleft] = tmp[labelleft] - if tmp.get(labelright): - tick_params[labelright] = tmp[labelright] - - case "y" if isinstance(axi, paxes.GeoAxes): - if axi._is_ticklabel_on("labelleft"): - tick_params["labelleft"] = axi._is_ticklabel_on("labelleft") - if axi._is_ticklabel_on("labelright"): - tick_params["labelright"] = axi._is_ticklabel_on("labelright") - - # We cannot mix types (yet) - if len(subplot_types) > 1: - warnings._warn_ultraplot( - "Tick label sharing not implemented for mixed subplot types." - ) - return - for axi in self._iter_axes(panels=True, hidden=False): - tmp = tick_params.copy() - # For sharing limits and or axis labels we - # can leave the ticks as found - for side in sides: - label = f"label{side}" - if isinstance(axi, paxes.CartesianAxes): - # Ignore for geo as it internally converts - label = label_map[label] - if axi not in outer_axes[side]: - tmp[label] = False - - # Determine sharing level - level = getattr(self, f"_share{axis}") - if axis == "y": - # For panels - if hasattr(axi, "_panel_sharey_group") and axi._panel_sharey_group: - level = 3 - elif axi._panel_side and axi._sharey: - level = 3 - else: # x-axis - # For panels - if hasattr(axi, "_panel_sharex_group") and axi._panel_sharex_group: - level = 3 - elif axi._panel_side and axi._sharex: - level = 3 - - if level < 3: continue - if isinstance(axi, paxes.GeoAxes): - # TODO: move this to tick_params? - # Tick_params is independent of gridliner objects - # Depending on the backend tick params is useful or not - axi._toggle_gridliner_labels(**tmp) - elif tmp: - getattr(axi, f"{axis}axis").set_tick_params(**tmp) + + # Apply baseline to all axes in the group (including panels) + for axi in group_axes: + tmp = tick_params_group.copy() + + # Respect figure border sides: only keep labels on true borders + for side in sides: + label = f"label{side}" + if isinstance(axi, paxes.CartesianAxes): + # For cartesian, use version-mapped key when reading/writing + label = label_map[label] + if axi not in outer_axes[side]: + tmp[label] = False + + # Determine sharing level for this axes + level = getattr(self, f"_share{axis}") + if axis == "y": + if hasattr(axi, "_panel_sharey_group") and axi._panel_sharey_group: + level = 3 + elif getattr(axi, "_panel_side", None) and getattr( + axi, "_sharey", None + ): + level = 3 + else: # x-axis + if hasattr(axi, "_panel_sharex_group") and axi._panel_sharex_group: + level = 3 + elif getattr(axi, "_panel_side", None) and getattr( + axi, "_sharex", None + ): + level = 3 + + if level < 3: + continue + + # Apply to geo/cartesian appropriately + if isinstance(axi, paxes.GeoAxes): + axi._toggle_gridliner_labels(**tmp) + elif tmp: + getattr(axi, f"{axis}axis").set_tick_params(**tmp) + self.stale = True def _context_adjusting(self, cache=True): @@ -1278,29 +1307,17 @@ def _add_axes_panel(self, ax, side=None, **kwargs): pax = self.add_subplot(ss, **kwargs) pax._panel_side = side pax._panel_share = share - if share: - # When we are sharing we remove the ticks by default - # as we "push" the labels out. See Figure._share_ticklabels. - # If we add the labels here it is more difficult to control - # for some ticks being on. - from packaging import version - from .internals import _version_mpl - - params = {} - if version.parse(str(_version_mpl)) < version.parse("3.10"): - params = dict(labelleft=False, labelright=False) - pax.xaxis.set_tick_params(**params) - pax.yaxis.set_tick_params(**params) - else: - pax.xaxis.set_tick_params(labelbottom=False, labeltop=False) - pax.yaxis.set_tick_params(labelleft=False, labelright=False) - pax._panel_parent = ax ax._panel_dict[side].append(pax) ax._apply_auto_share() axis = pax.yaxis if side in ("left", "right") else pax.xaxis getattr(axis, "tick_" + side)() # set tick and tick label position axis.set_label_position(side) # set label position + # Turn off top/right panel tick labels by default; sharing will push them later + if side == "top": + pax.xaxis.set_tick_params(labeltop=False) + elif side == "right": + pax.yaxis.set_tick_params(labelright=False) return pax @_clear_border_cache From 2c1f7af7aa59724f831a1517c712375afe1ad38f Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 13:23:56 +0200 Subject: [PATCH 77/96] looking good --- ultraplot/figure.py | 23 ++++++++++++++++++++--- ultraplot/utils.py | 31 ++++++++++++++++++------------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 73a57bd57..9769e9916 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -961,6 +961,10 @@ def _group_key(ax): label = label_map[label] if axi not in outer_axes[side]: tmp[label] = False + from .axes.cartesian import OPPOSITE_SIDE + + if axi._panel_side and OPPOSITE_SIDE[axi._panel_side] == side: + tmp[label] = False # Determine sharing level for this axes level = getattr(self, f"_share{axis}") @@ -1313,11 +1317,24 @@ def _add_axes_panel(self, ax, side=None, **kwargs): axis = pax.yaxis if side in ("left", "right") else pax.xaxis getattr(axis, "tick_" + side)() # set tick and tick label position axis.set_label_position(side) # set label position - # Turn off top/right panel tick labels by default; sharing will push them later + # Prefer outside tick labels for non-sharing top/right panels; otherwise defer to sharing if side == "top": - pax.xaxis.set_tick_params(labeltop=False) + if not share: + pax.xaxis.set_tick_params(labeltop=True) + pax.yaxis.set_tick_params(labelbottom=False) + else: + pax.xaxis.set_tick_params(labeltop=False) + pax.yaxis.set_tick_params(labelbottom=False) + ax.xaxis.set_tick_params(labeltop=False) elif side == "right": - pax.yaxis.set_tick_params(labelright=False) + if not share: + pax.yaxis.set_tick_params(labelright=True) + pax.yaxis.set_tick_params(labelleft=False) + else: + pax.yaxis.set_tick_params(labelright=False) + pax.yaxis.set_tick_params(labelleft=False) + ax.yaxis.set_tick_params(labelright=False) + return pax @_clear_border_cache diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 3ecd9597a..8d6570140 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1107,21 +1107,26 @@ def _check_ranges( } side = dmap[direction] if self.ax.number is None: # panel - parent = self.ax._panel_parent - - panels = parent._panel_dict.get(side, []) - # If we are a panel at the end we are a border - # only if we are not sharing axes + # For panels we need to check if we are apart + # of a group that is sharing its axes + # For top andight the pattern should be + # reversed as the sharing axis is the left-most + # or bottom-most plot. + # Note that for panels to left or right, it will + # always turn the x ticks of whether sharing is set or not + # not sure if this is a bug or a feature. if side in ("left", "right"): - if self.ax._sharey is None: - return True - elif not self.ax._panel_sharey_group: - return True + if self.ax.figure._sharey >= 3: + if self.ax._panel_sharey_group is True and side == "right": + return True + elif self.ax._panel_sharey_group is False and side == "left": + return True elif side in ("top", "bottom"): - if self.ax._sharex is None: - return True - elif not self.ax._panel_sharex_group: - return True + if self.ax.figure._sharex >= 3: + if side == "bottom" and not self.ax._panel_sharex_group: + return True + elif side == "top" and self.ax._panel_sharex_group: + return True # Only consider when we are interfacing with a panel # axes on the outside will also not share when they are in top From bac26d2f70872f604a493d201d69a380850701a7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 13:37:28 +0200 Subject: [PATCH 78/96] fix panel sharing --- pyproject.toml | 3 +++ ultraplot/figure.py | 1 + ultraplot/tests/test_subplots.py | 10 ++++++++++ ultraplot/utils.py | 8 ++++---- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2e0aee22b..1ad8b9c37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,3 +50,6 @@ include-package-data = true [tool.setuptools_scm] write_to = "ultraplot/_version.py" write_to_template = "__version__ = '{version}'\n" + +[tool.pytest.ini_options] +addopts = "--ignore=dbt_packages" diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 9769e9916..dd5fce001 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -961,6 +961,7 @@ def _group_key(ax): label = label_map[label] if axi not in outer_axes[side]: tmp[label] = False + from .axes.cartesian import OPPOSITE_SIDE if axi._panel_side and OPPOSITE_SIDE[axi._panel_side] == side: diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index 067c0ee1f..19030dfb3 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -340,3 +340,13 @@ def test_uneven_span_subplots(rng): axs[-1, -1].format(fc="gray4", grid=False) axs[0].plot((rng.random((50, 10)) - 0.5).cumsum(axis=0), cycle="Grays_r", lw=2) return fig + + +@pytest.mark.parametrize("share_panels", [True, False]) +@pytest.mark.mpl_image_compare +def test_sharing_panels(share_panels): + fig, ax = uplt.subplots(nrows=2) + ax.panel("r", share=share_panels) + ax.format(ytickloc="right") + uplt.show(block=1) + return fig diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 8d6570140..dd5fcaaaf 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1131,11 +1131,11 @@ def _check_ranges( # Only consider when we are interfacing with a panel # axes on the outside will also not share when they are in top # or left - elif side in ("left", "right") and self.ax._sharey is None: - if other.number is None: + elif side in ("left", "right"): + if other.number is None and not other._panel_sharey_group: return True - elif side in ("bottom", "top") and self.ax._sharex is None: - if other.number is None: + elif side in ("bottom", "top"): + if other.number is None and not other._panel_sharex_group: return True return False From b7d37d0c09c70f00b8842fb044373b0673770ffb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 14:30:12 +0200 Subject: [PATCH 79/96] add unittests and fix some logic --- ultraplot/figure.py | 1 - ultraplot/tests/test_inset.py | 1 + ultraplot/tests/test_subplots.py | 55 ++++++++++++++++++++++++++++---- ultraplot/utils.py | 43 ++++++++----------------- 4 files changed, 62 insertions(+), 38 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index dd5fce001..9769e9916 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -961,7 +961,6 @@ def _group_key(ax): label = label_map[label] if axi not in outer_axes[side]: tmp[label] = False - from .axes.cartesian import OPPOSITE_SIDE if axi._panel_side and OPPOSITE_SIDE[axi._panel_side] == side: diff --git a/ultraplot/tests/test_inset.py b/ultraplot/tests/test_inset.py index ea1bf76af..9a1dfc611 100644 --- a/ultraplot/tests/test_inset.py +++ b/ultraplot/tests/test_inset.py @@ -7,6 +7,7 @@ def test_inset_basic(): # spacing, aspect ratios, and axis sharing gs = uplt.GridSpec(nrows=2, ncols=2) fig = uplt.figure(refwidth=1.5, share=False) + fig.canvas.draw() for ss, side in zip(gs, "tlbr"): ax = fig.add_subplot(ss) px = ax.panel_axes(side, width="3em") diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index 19030dfb3..f6cb3a392 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -343,10 +343,51 @@ def test_uneven_span_subplots(rng): @pytest.mark.parametrize("share_panels", [True, False]) -@pytest.mark.mpl_image_compare -def test_sharing_panels(share_panels): - fig, ax = uplt.subplots(nrows=2) - ax.panel("r", share=share_panels) - ax.format(ytickloc="right") - uplt.show(block=1) - return fig +def test_panel_ticklabels_all_sides_share_and_no_share(share_panels): + # 2x2 grid; add panels on all sides of the first axes + fig, ax = uplt.subplots(nrows=2, ncols=2) + axi = ax[0] + + # Create panels on all sides with configurable sharing + pax_left = axi.panel("left", share=share_panels) + pax_right = axi.panel("right", share=share_panels) + pax_top = axi.panel("top", share=share_panels) + pax_bottom = axi.panel("bottom", share=share_panels) + + # Force draw so ticklabel state is resolved + fig.canvas.draw() + + def assert_panel(axi_panel, side, share_flag): + on_left = axi_panel._is_ticklabel_on("labelleft") + on_right = axi_panel._is_ticklabel_on("labelright") + on_top = axi_panel._is_ticklabel_on("labeltop") + on_bottom = axi_panel._is_ticklabel_on("labelbottom") + + # Inside (toward the main) must be off in all cases + if side == "left": + # Inside is right + assert not on_right + elif side == "right": + # Inside is left + assert not on_left + elif side == "top": + # Inside is bottom + assert not on_bottom + elif side == "bottom": + # Inside is top + assert not on_top + + if not share_flag: + # For non-sharing panels, prefer outside labels on for top/right + if side == "right": + assert on_right + if side == "top": + assert on_top + # For left/bottom non-sharing, we don't enforce outside on here + # (baseline may keep left/bottom on the main) + + # Check each panel side + assert_panel(pax_left, "left", share_panels) + assert_panel(pax_right, "right", share_panels) + assert_panel(pax_top, "top", share_panels) + assert_panel(pax_bottom, "bottom", share_panels) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index dd5fcaaaf..4f6b88685 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1107,36 +1107,19 @@ def _check_ranges( } side = dmap[direction] if self.ax.number is None: # panel - # For panels we need to check if we are apart - # of a group that is sharing its axes - # For top andight the pattern should be - # reversed as the sharing axis is the left-most - # or bottom-most plot. - # Note that for panels to left or right, it will - # always turn the x ticks of whether sharing is set or not - # not sure if this is a bug or a feature. - if side in ("left", "right"): - if self.ax.figure._sharey >= 3: - if self.ax._panel_sharey_group is True and side == "right": - return True - elif self.ax._panel_sharey_group is False and side == "left": - return True - elif side in ("top", "bottom"): - if self.ax.figure._sharex >= 3: - if side == "bottom" and not self.ax._panel_sharex_group: - return True - elif side == "top" and self.ax._panel_sharex_group: - return True - - # Only consider when we are interfacing with a panel - # axes on the outside will also not share when they are in top - # or left - elif side in ("left", "right"): - if other.number is None and not other._panel_sharey_group: - return True - elif side in ("bottom", "top"): - if other.number is None and not other._panel_sharex_group: - return True + panel_side = getattr(self.ax, "_panel_side", None) + # Non-sharing panels: treat as border only on their outward side + if not getattr(self.ax, "_panel_share", False): + return side == panel_side + # Sharing panels: do not treat interfaces as borders; rely on OOB + # detection for true figure borders + return False + + else: # main axis + # When a main axes interfaces with a panel, the border lies beyond + # the outer-most panel, not at the interface with the main. + if getattr(other, "number", None) is None: + return False return False return True From 4dcd5debfcc9c4a53a4373b7e7125081adbb3954 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 14:36:00 +0200 Subject: [PATCH 80/96] iterative fix --- ultraplot/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 9769e9916..2b8b15aa5 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1324,8 +1324,8 @@ def _add_axes_panel(self, ax, side=None, **kwargs): pax.yaxis.set_tick_params(labelbottom=False) else: pax.xaxis.set_tick_params(labeltop=False) - pax.yaxis.set_tick_params(labelbottom=False) ax.xaxis.set_tick_params(labeltop=False) + pax.yaxis.set_tick_params(labelleft=True) elif side == "right": if not share: pax.yaxis.set_tick_params(labelright=True) From 553cdee167d2bdbfae771426b04ef81a72f862ff Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 15:06:20 +0200 Subject: [PATCH 81/96] update test --- ultraplot/tests/test_subplots.py | 65 +++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index f6cb3a392..4147a0204 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -292,27 +292,50 @@ def test_panel_sharing_top_right(layout): fig.canvas.draw() # force redraw tick labels border_axes = fig._get_border_axes() - for axi in fig._iter_axes(panels=True): - assert ( - axi._is_ticklabel_on("labelleft") - if axi in border_axes["left"] - else not axi._is_ticklabel_on("labelleft") - ) - assert ( - axi._is_ticklabel_on("labeltop") - if axi in border_axes["top"] - else not axi._is_ticklabel_on("labeltop") - ) - assert ( - axi._is_ticklabel_on("labelright") - if axi in border_axes["right"] - else not axi._is_ticklabel_on("labelright") - ) - assert ( - axi._is_ticklabel_on("labelbottom") - if axi in border_axes["bottom"] - else not axi._is_ticklabel_on("labelbottom") - ) + assert not ax[0]._is_tick_label_on("labelleft") + assert not ax[0]._is_tick_label_on("labelright") + assert not ax[0]._is_tick_label_on("labeltop") + assert not ax[0]._is_tick_label_on("labelbottom") + + panel = ax[0]._panel_dict["left"][-1] + assert panel._is_tick_label_on("labelleft") + assert panel._is_tick_label_on("labelbottom") + assert not panel._is_tick_label_on("labelright") + assert not panel._is_tick_label_on("labeltop") + + panel = ax[0]._panel_dict["top"][-1] + assert panel._is_tick_label_on("labelleft") + assert not panel._is_tick_label_on("labelbottom") + assert not panel._is_tick_label_on("labelright") + assert not panel._is_tick_label_on("labeltop") + + panel = ax[0]._panel_dict["right"][-1] + assert not panel._is_tick_label_on("labelleft") + assert panel._is_tick_label_on("labelbottom") + assert not panel._is_tick_label_on("labelright") + assert not panel._is_tick_label_on("labeltop") + + panel = ax[0]._panel_dict["bottom"][-1] + assert panel._is_tick_label_on("labelleft") + assert not panel._is_tick_label_on("labelbottom") + assert not panel._is_tick_label_on("labelright") + assert not panel._is_tick_label_on("labeltop") + + assert not ax[1]._is_tick_label_on("labelleft") + assert not ax[1]._is_tick_label_on("labelright") + assert not ax[1]._is_tick_label_on("labeltop") + assert not ax[1]._is_tick_label_on("labelbottom") + + assert ax[2]._is_tick_label_on("labelleft") + assert not ax[2]._is_tick_label_on("labelright") + assert not ax[2]._is_tick_label_on("labeltop") + assert ax[2]._is_tick_label_on("labelbottom") + + assert not ax[2]._is_tick_label_on("labelleft") + assert not ax[2]._is_tick_label_on("labelright") + assert not ax[2]._is_tick_label_on("labeltop") + assert ax[2]._is_tick_label_on("labelbottom") + return fig From 504f6d274eb7de3c4ef998627b1a81ecc031823a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 15:10:31 +0200 Subject: [PATCH 82/96] update test --- ultraplot/tests/test_subplots.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index 4147a0204..154246c10 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -291,17 +291,16 @@ def test_panel_sharing_top_right(layout): pax = ax[0].panel(dir) fig.canvas.draw() # force redraw tick labels border_axes = fig._get_border_axes() - - assert not ax[0]._is_tick_label_on("labelleft") - assert not ax[0]._is_tick_label_on("labelright") - assert not ax[0]._is_tick_label_on("labeltop") - assert not ax[0]._is_tick_label_on("labelbottom") + assert not ax[0]._is_ticklabel_on("labelleft") + assert not ax[0]._is_ticklabel_on("labelright") + assert not ax[0]._is_ticklabel_on("labeltop") + assert not ax[0]._is_ticklabel_on("labelbottom") panel = ax[0]._panel_dict["left"][-1] - assert panel._is_tick_label_on("labelleft") - assert panel._is_tick_label_on("labelbottom") - assert not panel._is_tick_label_on("labelright") - assert not panel._is_tick_label_on("labeltop") + assert panel._is_ticklabel_on("labelleft") + assert panel._is_ticklabel_on("labelbottom") + assert not panel._is_ticklabel_on("labelright") + assert not panel._is_ticklabel_on("labeltop") panel = ax[0]._panel_dict["top"][-1] assert panel._is_tick_label_on("labelleft") From faafe0e1e996672ad0b6ed2071bd2f755acd9b69 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 15:10:42 +0200 Subject: [PATCH 83/96] update test --- ultraplot/tests/test_subplots.py | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index 154246c10..80808ffa7 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -303,37 +303,37 @@ def test_panel_sharing_top_right(layout): assert not panel._is_ticklabel_on("labeltop") panel = ax[0]._panel_dict["top"][-1] - assert panel._is_tick_label_on("labelleft") - assert not panel._is_tick_label_on("labelbottom") - assert not panel._is_tick_label_on("labelright") - assert not panel._is_tick_label_on("labeltop") + assert panel._is_ticklabel_on("labelleft") + assert not panel._is_ticklabel_on("labelbottom") + assert not panel._is_ticklabel_on("labelright") + assert not panel._is_ticklabel_on("labeltop") panel = ax[0]._panel_dict["right"][-1] - assert not panel._is_tick_label_on("labelleft") - assert panel._is_tick_label_on("labelbottom") - assert not panel._is_tick_label_on("labelright") - assert not panel._is_tick_label_on("labeltop") + assert not panel._is_ticklabel_on("labelleft") + assert panel._is_ticklabel_on("labelbottom") + assert not panel._is_ticklabel_on("labelright") + assert not panel._is_ticklabel_on("labeltop") panel = ax[0]._panel_dict["bottom"][-1] - assert panel._is_tick_label_on("labelleft") - assert not panel._is_tick_label_on("labelbottom") - assert not panel._is_tick_label_on("labelright") - assert not panel._is_tick_label_on("labeltop") - - assert not ax[1]._is_tick_label_on("labelleft") - assert not ax[1]._is_tick_label_on("labelright") - assert not ax[1]._is_tick_label_on("labeltop") - assert not ax[1]._is_tick_label_on("labelbottom") - - assert ax[2]._is_tick_label_on("labelleft") - assert not ax[2]._is_tick_label_on("labelright") - assert not ax[2]._is_tick_label_on("labeltop") - assert ax[2]._is_tick_label_on("labelbottom") - - assert not ax[2]._is_tick_label_on("labelleft") - assert not ax[2]._is_tick_label_on("labelright") - assert not ax[2]._is_tick_label_on("labeltop") - assert ax[2]._is_tick_label_on("labelbottom") + assert panel._is_ticklabel_on("labelleft") + assert not panel._is_ticklabel_on("labelbottom") + assert not panel._is_ticklabel_on("labelright") + assert not panel._is_ticklabel_on("labeltop") + + assert not ax[1]._is_ticklabel_on("labelleft") + assert not ax[1]._is_ticklabel_on("labelright") + assert not ax[1]._is_ticklabel_on("labeltop") + assert not ax[1]._is_ticklabel_on("labelbottom") + + assert ax[2]._is_ticklabel_on("labelleft") + assert not ax[2]._is_ticklabel_on("labelright") + assert not ax[2]._is_ticklabel_on("labeltop") + assert ax[2]._is_ticklabel_on("labelbottom") + + assert not ax[3]._is_ticklabel_on("labelleft") + assert not ax[3]._is_ticklabel_on("labelright") + assert not ax[3]._is_ticklabel_on("labeltop") + assert ax[3]._is_ticklabel_on("labelbottom") return fig From dca00ca5166dc61badd3916d45eb81bd7636b402 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 15:13:46 +0200 Subject: [PATCH 84/96] copy the formatter for geo --- ultraplot/figure.py | 30 ++++++++++++++++++++++++++++-- ultraplot/utils.py | 6 +++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 2b8b15aa5..098244699 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1317,19 +1317,45 @@ def _add_axes_panel(self, ax, side=None, **kwargs): axis = pax.yaxis if side in ("left", "right") else pax.xaxis getattr(axis, "tick_" + side)() # set tick and tick label position axis.set_label_position(side) # set label position + # Sync limits and formatters with parent when sharing to ensure consistent ticks + if share: + # Copy limits for the shared axis + if side in ("left", "right"): + try: + pax.set_ylim(ax.get_ylim()) + except Exception: + pass + else: + try: + pax.set_xlim(ax.get_xlim()) + except Exception: + pass + # Align with backend: for GeoAxes, use lon/lat degree formatters on panels. + # Otherwise, copy the parent's axis formatters. + if isinstance(ax, paxes.GeoAxes): + fmt_key = "deglat" if side in ("left", "right") else "deglon" + axis.set_major_formatter(constructor.Formatter(fmt_key)) + else: + paxis = ax.yaxis if side in ("left", "right") else ax.xaxis + axis.set_major_formatter(paxis.get_major_formatter()) + axis.set_minor_formatter(paxis.get_minor_formatter()) # Prefer outside tick labels for non-sharing top/right panels; otherwise defer to sharing if side == "top": + # Ensure main votes for top labels so baseline includes them + ax.xaxis.set_tick_params(labeltop=True) if not share: pax.xaxis.set_tick_params(labeltop=True) - pax.yaxis.set_tick_params(labelbottom=False) + ax.xaxis.set_tick_params(labelbottom=False) else: pax.xaxis.set_tick_params(labeltop=False) ax.xaxis.set_tick_params(labeltop=False) pax.yaxis.set_tick_params(labelleft=True) elif side == "right": + # Ensure main votes for right labels so baseline includes them + ax.yaxis.set_tick_params(labelright=True) if not share: pax.yaxis.set_tick_params(labelright=True) - pax.yaxis.set_tick_params(labelleft=False) + ax.yaxis.set_tick_params(labelleft=False) else: pax.yaxis.set_tick_params(labelright=False) pax.yaxis.set_tick_params(labelleft=False) diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 4f6b88685..dddc6b93a 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1046,7 +1046,11 @@ def is_border( return self.is_border((x + dx, y + dy), direction) if self.grid_axis_type[x, y] != self.axis_type: - if cell in self.ax._panel_dict.get(cell._panel_side, []): + # Allow traversing across the parent<->panel interface even when types differ + # e.g., GeoAxes main with cartesian panel or vice versa + if getattr(self.ax, "_panel_parent", None) is cell: + return self.is_border((x + dx, y + dy), direction) + if getattr(cell, "_panel_parent", None) is self.ax: return self.is_border((x + dx, y + dy), direction) # Internal edge or plot reached From b098fca45c51fe21ac1ada03c1b6839af474f724 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 15:55:58 +0200 Subject: [PATCH 85/96] bump --- ultraplot/tests/test_geographic.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 6eef28fd3..79697451a 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -675,11 +675,13 @@ def test_check_tricontourf(): def test_panels_geo(): fig, ax = uplt.subplots(proj="cyl") ax.format(labels=True) + fig.canvas.draw() for dir in "top bottom right left".split(): pax = ax.panel_axes(dir) fig.canvas.draw() # need this to update the ticks - assert len(pax.get_xticklabels()) > 0 - assert len(pax.get_yticklabels()) > 0 + # assert len(pax.get_xticklabels()) > 0 + # assert len(pax.get_yticklabels()) > 0 + uplt.show(block=1) @pytest.mark.mpl_image_compare From 88606940697c4027d985a9da6441ebfc3bda9c1c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 16:07:55 +0200 Subject: [PATCH 86/96] simplify test --- ultraplot/figure.py | 35 ++++++++++++++++++++---------- ultraplot/tests/test_geographic.py | 33 +++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 098244699..929435c72 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1339,26 +1339,37 @@ def _add_axes_panel(self, ax, side=None, **kwargs): paxis = ax.yaxis if side in ("left", "right") else ax.xaxis axis.set_major_formatter(paxis.get_major_formatter()) axis.set_minor_formatter(paxis.get_minor_formatter()) - # Prefer outside tick labels for non-sharing top/right panels; otherwise defer to sharing + # Push main axes tick labels to the outside relative to the added panel + if isinstance(ax, paxes.GeoAxes): + if side == "top": + ax._toggle_gridliner_labels(labeltop=False) + elif side == "bottom": + ax._toggle_gridliner_labels(labelbottom=False) + elif side == "left": + ax._toggle_gridliner_labels(labelleft=False) + elif side == "right": + ax._toggle_gridliner_labels(labelright=False) + else: + if side == "top": + ax.xaxis.set_tick_params(abeltop=False) + elif side == "bottom": + ax.xaxis.set_tick_params(labelbottom=False) + elif side == "left": + ax.yaxis.set_tick_params(labelleft=False) + elif side == "right": + ax.yaxis.set_tick_params(labelright=False) + + # Panel labels: prefer outside only for non-sharing top/right; otherwise keep off if side == "top": - # Ensure main votes for top labels so baseline includes them - ax.xaxis.set_tick_params(labeltop=True) if not share: - pax.xaxis.set_tick_params(labeltop=True) - ax.xaxis.set_tick_params(labelbottom=False) + pax.xaxis.set_tick_params(labeltop=True, labelbottom=False) else: pax.xaxis.set_tick_params(labeltop=False) - ax.xaxis.set_tick_params(labeltop=False) - pax.yaxis.set_tick_params(labelleft=True) elif side == "right": - # Ensure main votes for right labels so baseline includes them - ax.yaxis.set_tick_params(labelright=True) if not share: - pax.yaxis.set_tick_params(labelright=True) - ax.yaxis.set_tick_params(labelleft=False) + pax.yaxis.set_tick_params(labelright=True, labelleft=False) else: pax.yaxis.set_tick_params(labelright=False) - pax.yaxis.set_tick_params(labelleft=False) ax.yaxis.set_tick_params(labelright=False) return pax diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 79697451a..803bceb9f 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -675,13 +675,34 @@ def test_check_tricontourf(): def test_panels_geo(): fig, ax = uplt.subplots(proj="cyl") ax.format(labels=True) - fig.canvas.draw() - for dir in "top bottom right left".split(): + dirs = "top bottom right left".split() + for dir in dirs: pax = ax.panel_axes(dir) - fig.canvas.draw() # need this to update the ticks - # assert len(pax.get_xticklabels()) > 0 - # assert len(pax.get_yticklabels()) > 0 - uplt.show(block=1) + fig.canvas.draw() + pax = ax[0]._panel_dict["left"][-1] + assert pax._is_ticklabel_on("labelleft") # should not error + assert not pax._is_ticklabel_on("labelright") + assert not pax._is_ticklabel_on("labeltop") + assert pax._is_ticklabel_on("labelbottom") + + pax = ax[0]._panel_dict["top"][-1] + assert pax._is_ticklabel_on("labelleft") # should not error + assert not pax._is_ticklabel_on("labelright") + assert not pax._is_ticklabel_on("labeltop") + assert not pax._is_ticklabel_on("labelbottom") + + pax = ax[0]._panel_dict["bottom"][-1] + assert pax._is_ticklabel_on("labelleft") # should not error + assert not pax._is_ticklabel_on("labelright") + assert not pax._is_ticklabel_on("labeltop") + assert pax._is_ticklabel_on("labelbottom") + + pax = ax[0]._panel_dict["right"][-1] + assert not pax._is_ticklabel_on("labelleft") # should not error + assert not pax._is_ticklabel_on("labelright") + assert not pax._is_ticklabel_on("labeltop") + assert pax._is_ticklabel_on("labelbottom") + return fig @pytest.mark.mpl_image_compare From 30a5254e95d283e6d9b3a552cda6b0c1dc0d16f6 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 16:08:37 +0200 Subject: [PATCH 87/96] add sanity check test --- ultraplot/tests/test_geographic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 803bceb9f..c94b0adf9 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -702,6 +702,10 @@ def test_panels_geo(): assert not pax._is_ticklabel_on("labelright") assert not pax._is_ticklabel_on("labeltop") assert pax._is_ticklabel_on("labelbottom") + + for dir in dirs: + not ax[0]._is_ticklabel_on(f"label{dir}") + return fig From 438637ca3e129edd4a70a129ced2ca23bb54a879 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 16:15:25 +0200 Subject: [PATCH 88/96] fix typo --- ultraplot/figure.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 929435c72..7d379de1a 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1342,22 +1342,22 @@ def _add_axes_panel(self, ax, side=None, **kwargs): # Push main axes tick labels to the outside relative to the added panel if isinstance(ax, paxes.GeoAxes): if side == "top": - ax._toggle_gridliner_labels(labeltop=False) + ax._toggle_gridliner_labels(labeltop=False, labelbottom=True) elif side == "bottom": - ax._toggle_gridliner_labels(labelbottom=False) + ax._toggle_gridliner_labels(labelbottom=False, labeltop=True) elif side == "left": - ax._toggle_gridliner_labels(labelleft=False) + ax._toggle_gridliner_labels(labelleft=False, labelright=True) elif side == "right": - ax._toggle_gridliner_labels(labelright=False) + ax._toggle_gridliner_labels(labelright=False, labelleft=True) else: if side == "top": - ax.xaxis.set_tick_params(abeltop=False) + ax.xaxis.set_tick_params(labeltop=False, labelbottom=True) elif side == "bottom": - ax.xaxis.set_tick_params(labelbottom=False) + ax.xaxis.set_tick_params(labelbottom=False, labeltop=True) elif side == "left": - ax.yaxis.set_tick_params(labelleft=False) + ax.yaxis.set_tick_params(labelleft=False, labelright=True) elif side == "right": - ax.yaxis.set_tick_params(labelright=False) + ax.yaxis.set_tick_params(labelright=False, labelleft=True) # Panel labels: prefer outside only for non-sharing top/right; otherwise keep off if side == "top": From e68ce23965f26470102e40df5edddf0f3884ad33 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 16:38:45 +0200 Subject: [PATCH 89/96] fix error --- ultraplot/axes/base.py | 4 ++++ ultraplot/figure.py | 16 ++++++++-------- ultraplot/tests/test_subplots.py | 4 +++- ultraplot/utils.py | 10 +++++++--- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 22e489d18..4a6e5b135 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3274,7 +3274,11 @@ def _is_ticklabel_on(self, side: str) -> bool: label = "label1" if side in ["labelright", "labeltop"]: label = "label2" + + print(self, self._panel_side, axis.get_tick_params()) + return axis.get_tick_params().get(side, False) for tick in axis.get_major_ticks(): + print(tick, getattr(tick, label).get_visible()) if getattr(tick, label).get_visible(): return True return False diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 7d379de1a..6089a1cbe 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1342,22 +1342,22 @@ def _add_axes_panel(self, ax, side=None, **kwargs): # Push main axes tick labels to the outside relative to the added panel if isinstance(ax, paxes.GeoAxes): if side == "top": - ax._toggle_gridliner_labels(labeltop=False, labelbottom=True) + ax._toggle_gridliner_labels(labeltop=False) elif side == "bottom": - ax._toggle_gridliner_labels(labelbottom=False, labeltop=True) + ax._toggle_gridliner_labels(labelbottom=False) elif side == "left": - ax._toggle_gridliner_labels(labelleft=False, labelright=True) + ax._toggle_gridliner_labels(labelleft=False) elif side == "right": - ax._toggle_gridliner_labels(labelright=False, labelleft=True) + ax._toggle_gridliner_labels(labelright=False) else: if side == "top": - ax.xaxis.set_tick_params(labeltop=False, labelbottom=True) + ax.xaxis.set_tick_params(labeltop=False) elif side == "bottom": - ax.xaxis.set_tick_params(labelbottom=False, labeltop=True) + ax.xaxis.set_tick_params(labelbottom=False) elif side == "left": - ax.yaxis.set_tick_params(labelleft=False, labelright=True) + ax.yaxis.set_tick_params(labelleft=False) elif side == "right": - ax.yaxis.set_tick_params(labelright=False, labelleft=True) + ax.yaxis.set_tick_params(labelright=False) # Panel labels: prefer outside only for non-sharing top/right; otherwise keep off if side == "top": diff --git a/ultraplot/tests/test_subplots.py b/ultraplot/tests/test_subplots.py index 80808ffa7..d2379ad73 100644 --- a/ultraplot/tests/test_subplots.py +++ b/ultraplot/tests/test_subplots.py @@ -290,12 +290,14 @@ def test_panel_sharing_top_right(layout): for dir in "left right top bottom".split(): pax = ax[0].panel(dir) fig.canvas.draw() # force redraw tick labels - border_axes = fig._get_border_axes() + + # Main panel: ticks are off assert not ax[0]._is_ticklabel_on("labelleft") assert not ax[0]._is_ticklabel_on("labelright") assert not ax[0]._is_ticklabel_on("labeltop") assert not ax[0]._is_ticklabel_on("labelbottom") + # For panels the inside ticks are off panel = ax[0]._panel_dict["left"][-1] assert panel._is_ticklabel_on("labelleft") assert panel._is_ticklabel_on("labelbottom") diff --git a/ultraplot/utils.py b/ultraplot/utils.py index dddc6b93a..4177bd7f9 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1112,11 +1112,15 @@ def _check_ranges( side = dmap[direction] if self.ax.number is None: # panel panel_side = getattr(self.ax, "_panel_side", None) - # Non-sharing panels: treat as border only on their outward side + # Non-sharing panels: border only on their outward side if not getattr(self.ax, "_panel_share", False): return side == panel_side - # Sharing panels: do not treat interfaces as borders; rely on OOB - # detection for true figure borders + # Sharing panels: border only if this is the outward side and this + # panel is the outer-most panel for that side relative to its parent. + parent = self.ax._panel_parent + panels = parent._panel_dict.get(panel_side, []) + if side == panel_side and panels and panels[-1] is self.ax: + return True return False else: # main axis From 3083427767baaf8e61a0e52bfb320791e312318d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 16:49:32 +0200 Subject: [PATCH 90/96] adjust logic to ignore colorbar --- ultraplot/figure.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 6089a1cbe..1dbef9fc0 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -1340,24 +1340,26 @@ def _add_axes_panel(self, ax, side=None, **kwargs): axis.set_major_formatter(paxis.get_major_formatter()) axis.set_minor_formatter(paxis.get_minor_formatter()) # Push main axes tick labels to the outside relative to the added panel - if isinstance(ax, paxes.GeoAxes): - if side == "top": - ax._toggle_gridliner_labels(labeltop=False) - elif side == "bottom": - ax._toggle_gridliner_labels(labelbottom=False) - elif side == "left": - ax._toggle_gridliner_labels(labelleft=False) - elif side == "right": - ax._toggle_gridliner_labels(labelright=False) - else: - if side == "top": - ax.xaxis.set_tick_params(labeltop=False) - elif side == "bottom": - ax.xaxis.set_tick_params(labelbottom=False) - elif side == "left": - ax.yaxis.set_tick_params(labelleft=False) - elif side == "right": - ax.yaxis.set_tick_params(labelright=False) + # Skip this for filled panels (colorbars/legends) + if not kw.get("filled", False): + if isinstance(ax, paxes.GeoAxes): + if side == "top": + ax._toggle_gridliner_labels(labeltop=False) + elif side == "bottom": + ax._toggle_gridliner_labels(labelbottom=False) + elif side == "left": + ax._toggle_gridliner_labels(labelleft=False) + elif side == "right": + ax._toggle_gridliner_labels(labelright=False) + else: + if side == "top": + ax.xaxis.set_tick_params(labeltop=False) + elif side == "bottom": + ax.xaxis.set_tick_params(labelbottom=False) + elif side == "left": + ax.yaxis.set_tick_params(labelleft=False) + elif side == "right": + ax.yaxis.set_tick_params(labelright=False) # Panel labels: prefer outside only for non-sharing top/right; otherwise keep off if side == "top": From be0660f9c39165b152b819408b9f165dab0bc798 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 16:59:40 +0200 Subject: [PATCH 91/96] adjust logic per version --- ultraplot/axes/base.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 4a6e5b135..6c38cc5b4 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3275,8 +3275,19 @@ def _is_ticklabel_on(self, side: str) -> bool: if side in ["labelright", "labeltop"]: label = "label2" - print(self, self._panel_side, axis.get_tick_params()) - return axis.get_tick_params().get(side, False) + + from packaging import version + from .internals import _version_mpl + mpl_version = version.parse(str(_version_mpl)) + use_new_labels = mpl_version >= version.parse("3.10") + + label_map = { + "labeltop": "labeltop" if use_new_labels else "labelright", + "labelbottom": "labelbottom" if use_new_labels else "labelleft", + "labelleft": "labelleft", + "labelright": "labelright", + } + return axis.get_tick_params().get(label_map[side], False) for tick in axis.get_major_ticks(): print(tick, getattr(tick, label).get_visible()) if getattr(tick, label).get_visible(): From 3ff98c227b801d92ca73cfb24af9a9ee7b108592 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 17:09:41 +0200 Subject: [PATCH 92/96] mv label translation to private function --- ultraplot/axes/base.py | 38 ++++++++++++++++++++------------------ ultraplot/figure.py | 28 +++++++++++----------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 6c38cc5b4..41817955a 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3261,6 +3261,25 @@ def _is_panel_group_member(self, other: "Axes") -> bool: # Not in the same panel group return False + def _label_key(self, side: str) -> str: + """ + Map requested side name to the correct tick_params key across mpl versions. + + This accounts for the API change around Matplotlib 3.10 where labeltop/labelbottom + became first-class tick parameter keys. For older versions, these map to + labelright/labelleft respectively. + """ + from packaging import version + from .internals import _version_mpl + + use_new = version.parse(str(_version_mpl)) >= version.parse("3.10") + if side == "labeltop": + return "labeltop" if use_new else "labelright" + if side == "labelbottom": + return "labelbottom" if use_new else "labelleft" + # "labelleft" and "labelright" are stable across versions + return side + def _is_ticklabel_on(self, side: str) -> bool: """ Check if tick labels are on for the specified sides. @@ -3275,24 +3294,7 @@ def _is_ticklabel_on(self, side: str) -> bool: if side in ["labelright", "labeltop"]: label = "label2" - - from packaging import version - from .internals import _version_mpl - mpl_version = version.parse(str(_version_mpl)) - use_new_labels = mpl_version >= version.parse("3.10") - - label_map = { - "labeltop": "labeltop" if use_new_labels else "labelright", - "labelbottom": "labelbottom" if use_new_labels else "labelleft", - "labelleft": "labelleft", - "labelright": "labelright", - } - return axis.get_tick_params().get(label_map[side], False) - for tick in axis.get_major_ticks(): - print(tick, getattr(tick, label).get_visible()) - if getattr(tick, label).get_visible(): - return True - return False + return axis.get_tick_params().get(self._label_key(side), False) @docstring._snippet_manager def inset(self, *args, **kwargs): diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 1dbef9fc0..d0b39cb1b 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -849,23 +849,17 @@ def _share_ticklabels(self, *, axis: str) -> None: sides = ("top", "bottom") if axis == "x" else ("left", "right") # Version-dependent label name mapping for reading back params - from packaging import version - from .internals import _version_mpl - - mpl_version = version.parse(str(_version_mpl)) - use_new_labels = mpl_version >= version.parse("3.10") - - label_map = { - "labeltop": "labeltop" if use_new_labels else "labelright", - "labelbottom": "labelbottom" if use_new_labels else "labelleft", - "labelleft": "labelleft", - "labelright": "labelright", - } - - labelleft = label_map["labelleft"] - labelright = label_map["labelright"] - labeltop = label_map["labeltop"] - labelbottom = label_map["labelbottom"] + first_axi = next(self._iter_axes(panels=True), None) + if first_axi is None: + labelleft = "labelleft" + labelright = "labelright" + labeltop = "labeltop" + labelbottom = "labelbottom" + else: + labelleft = first_axi._label_key("labelleft") + labelright = first_axi._label_key("labelright") + labeltop = first_axi._label_key("labeltop") + labelbottom = first_axi._label_key("labelbottom") # Group axes by row (for x) or column (for y) def _group_key(ax): From e697da9aa054fee313b0715772b2fc055c5f1d07 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 17:10:09 +0200 Subject: [PATCH 93/96] fix import --- ultraplot/axes/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 41817955a..14bb62b16 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3270,7 +3270,7 @@ def _label_key(self, side: str) -> str: labelright/labelleft respectively. """ from packaging import version - from .internals import _version_mpl + from ..internals import _version_mpl use_new = version.parse(str(_version_mpl)) >= version.parse("3.10") if side == "labeltop": From d2a1b8727aeaebd6fe2808050a47c593eccb74f0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 17:12:00 +0200 Subject: [PATCH 94/96] replace func cal --- ultraplot/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d0b39cb1b..127c90215 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -952,7 +952,7 @@ def _group_key(ax): label = f"label{side}" if isinstance(axi, paxes.CartesianAxes): # For cartesian, use version-mapped key when reading/writing - label = label_map[label] + label = axi._label_key[label] if axi not in outer_axes[side]: tmp[label] = False from .axes.cartesian import OPPOSITE_SIDE From 7190c93f0230161df395a61cd9227fc4ce6cba50 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 17:12:15 +0200 Subject: [PATCH 95/96] replace func cal --- ultraplot/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 127c90215..7a9410c73 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -952,7 +952,7 @@ def _group_key(ax): label = f"label{side}" if isinstance(axi, paxes.CartesianAxes): # For cartesian, use version-mapped key when reading/writing - label = axi._label_key[label] + label = axi._label_key(label) if axi not in outer_axes[side]: tmp[label] = False from .axes.cartesian import OPPOSITE_SIDE From ba2f4e4dc820b376bbbd8b8f30676ee3524cb363 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 17 Oct 2025 17:55:11 +0200 Subject: [PATCH 96/96] rm dead code --- ultraplot/axes/base.py | 1 + ultraplot/utils.py | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 14bb62b16..c140b0161 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3271,6 +3271,7 @@ def _label_key(self, side: str) -> str: """ from packaging import version from ..internals import _version_mpl + #TODO: internal deprecation warning when we drop 3.9, we need to remove this use_new = version.parse(str(_version_mpl)) >= version.parse("3.10") if side == "labeltop": diff --git a/ultraplot/utils.py b/ultraplot/utils.py index 4177bd7f9..621127982 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -1122,13 +1122,6 @@ def _check_ranges( if side == panel_side and panels and panels[-1] is self.ax: return True return False - - else: # main axis - # When a main axes interfaces with a panel, the border lies beyond - # the outer-most panel, not at the interface with the main. - if getattr(other, "number", None) is None: - return False - return False return True