From c593c2ca276b9568daa614c9eb17de402db9237c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 17 Apr 2021 17:18:22 +0200 Subject: [PATCH] Fix removal of shared polar axes. There's really two separate fixes here: - Move isDefault_{maj,min}{loc,fmt} tracking to the Ticker instances, where they logically belong (note the previous need to additionally track them manually on axes removal, when that info was tracked on the Axis). This has the side effect of fixing removal of sharex'd polar axes, as ThetaLocators rely on _AxisWrappers which don't have that isDefault attribute. (Note that the patch would have resulted in a net decrease of lines of code if it didn't need to maintain backcompat on isDefault_foos). - Ensure that RadialLocator correctly propagates Axis information to the linear locator it wraps (consistently with ThetaLocator), so that when an axes is removed the wrapped linear locator doesn't stay pointing at an obsolete axes. This, together with the first patch, fixes removal of sharey'd polar axes. --- lib/matplotlib/axis.py | 34 +++++++++++++++++++++++++++++ lib/matplotlib/figure.py | 31 ++++---------------------- lib/matplotlib/projections/polar.py | 3 +++ lib/matplotlib/tests/test_polar.py | 15 +++++++++++++ 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 66443a42e82c..82c56470b4be 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -552,6 +552,8 @@ class Ticker: def __init__(self): self._locator = None self._formatter = None + self._locator_is_default = True + self._formatter_is_default = True @property def locator(self): @@ -689,6 +691,38 @@ def __init__(self, axes, pickradius=15): self.clear() self._set_scale('linear') + @property + def isDefault_majloc(self): + return self.major._locator_is_default + + @isDefault_majloc.setter + def isDefault_majloc(self, value): + self.major._locator_is_default = value + + @property + def isDefault_majfmt(self): + return self.major._formatter_is_default + + @isDefault_majfmt.setter + def isDefault_majfmt(self, value): + self.major._formatter_is_default = value + + @property + def isDefault_minloc(self): + return self.minor._locator_is_default + + @isDefault_minloc.setter + def isDefault_minloc(self, value): + self.minor._locator_is_default = value + + @property + def isDefault_minfmt(self): + return self.minor._formatter_is_default + + @isDefault_minfmt.setter + def isDefault_minfmt(self, value): + self.minor._formatter_is_default = value + # During initialization, Axis objects often create ticks that are later # unused; this turns out to be a very slow step. Instead, use a custom # descriptor to make the tick lists lazy and instantiate them as needed. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index a0c7c4e44fe7..9e6b6a601b5e 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -916,33 +916,10 @@ def _reset_locators_and_formatters(axis): # Set the formatters and locators to be associated with axis # (where previously they may have been associated with another # Axis instance) - # - # Because set_major_formatter() etc. force isDefault_* to be False, - # we have to manually check if the original formatter was a - # default and manually set isDefault_* if that was the case. - majfmt = axis.get_major_formatter() - isDefault = majfmt.axis.isDefault_majfmt - axis.set_major_formatter(majfmt) - if isDefault: - majfmt.axis.isDefault_majfmt = True - - majloc = axis.get_major_locator() - isDefault = majloc.axis.isDefault_majloc - axis.set_major_locator(majloc) - if isDefault: - majloc.axis.isDefault_majloc = True - - minfmt = axis.get_minor_formatter() - isDefault = majloc.axis.isDefault_minfmt - axis.set_minor_formatter(minfmt) - if isDefault: - minfmt.axis.isDefault_minfmt = True - - minloc = axis.get_minor_locator() - isDefault = majloc.axis.isDefault_minloc - axis.set_minor_locator(minloc) - if isDefault: - minloc.axis.isDefault_minloc = True + axis.get_major_formatter().set_axis(axis) + axis.get_major_locator().set_axis(axis) + axis.get_minor_formatter().set_axis(axis) + axis.get_minor_locator().set_axis(axis) def _break_share_link(ax, grouper): siblings = grouper.get_siblings(ax) diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 4ee8c6bcc020..31a970f60eca 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -425,6 +425,9 @@ def __init__(self, base, axes=None): self.base = base self._axes = axes + def set_axis(self, axis): + self.base.set_axis(axis) + def __call__(self): # Ensure previous behaviour with full circle non-annular views. if self._axes: diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 1c8f15cc0894..73eea36e46a9 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -379,3 +379,18 @@ def test_axvspan(): ax = plt.subplot(projection="polar") span = plt.axvspan(0, np.pi/4) assert span.get_path()._interpolation_steps > 1 + + +@check_figures_equal(extensions=["png"]) +def test_remove_shared_polar(fig_ref, fig_test): + # Removing shared polar axes used to crash. Test removing them, keeping in + # both cases just the lower left axes of a grid to avoid running into a + # separate issue (now being fixed) of ticklabel visibility for shared axes. + axs = fig_ref.subplots( + 2, 2, sharex=True, subplot_kw={"projection": "polar"}) + for i in [0, 1, 3]: + axs.flat[i].remove() + axs = fig_test.subplots( + 2, 2, sharey=True, subplot_kw={"projection": "polar"}) + for i in [0, 1, 3]: + axs.flat[i].remove()