diff --git a/doc/api/next_api_changes/behavior/28429-AL.rst b/doc/api/next_api_changes/behavior/28429-AL.rst new file mode 100644 index 000000000000..1df04be6328c --- /dev/null +++ b/doc/api/next_api_changes/behavior/28429-AL.rst @@ -0,0 +1,9 @@ +Tick locating code now ensure that the locator's axis is correctly set +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Tick locators maintain a reference to the Axis on which they are used (so that +they can e.g. check the size of the Axis to know how many ticks will fit), but +is problematic if the same locator object is used for multiple Axis. From now +on, before calling a locator to determine tick locations, Matplotlib will +ensure that the locator's axis is correctly (temporarily) set to the correct +Axis. This fix can cause a change in behavior if a locator's axis had been +intentionally set to an artificial value. diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 921de9271be8..8d594c31f31f 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1527,14 +1527,14 @@ def get_ticklines(self, minor=False): def get_majorticklocs(self): """Return this Axis' major tick locations in data coordinates.""" - return self.major.locator() + return self.major.locator._call_with_axis(self) def get_minorticklocs(self): """Return this Axis' minor tick locations in data coordinates.""" # Remove minor ticks duplicating major ticks. - minor_locs = np.asarray(self.minor.locator()) + minor_locs = np.asarray(self.minor.locator._call_with_axis(self)) if self.remove_overlapping_locs: - major_locs = self.major.locator() + major_locs = self.major.locator._call_with_axis(self) transform = self._scale.get_transform() tr_minor_locs = transform.transform(minor_locs) tr_major_locs = transform.transform(major_locs) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index ac68a5d90b14..9b6c881a7a4b 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1873,3 +1873,16 @@ def test_minorticks_on_multi_fig(): assert ax.get_xgridlines() assert isinstance(ax.xaxis.get_minor_locator(), mpl.ticker.AutoMinorLocator) + + +def test_locator_reuse(): + fig = plt.figure() + ax = fig.add_subplot(xlim=(.6, .8)) + loc = mticker.AutoLocator() + ax.xaxis.set_major_locator(loc) + ax.yaxis.set_major_locator(loc) + fig.draw_without_rendering() + xticklabels = [l.get_text() for l in ax.get_xticklabels()] + yticklabels = [l.get_text() for l in ax.get_yticklabels()] + assert xticklabels == ["0.60", "0.65", "0.70", "0.75", "0.80"] + assert yticklabels == ["0.0", "0.2", "0.4", "0.6", "0.8", "1.0"] diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 2b00937f9e29..b008a8447baf 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1629,11 +1629,28 @@ def set_params(self, **kwargs): str(type(self))) def __call__(self): - """Return the locations of the ticks.""" + """ + Return the locations of the ticks. + + The returned locations depend on the locator's axis. If a locator + is used across multiple axises, make sure to (temporarily) set + ``locator.axis`` to the correct axis before getting the tick locations. + """ # note: some locators return data limits, other return view limits, # hence there is no *one* interface to call self.tick_values. raise NotImplementedError('Derived must override') + def _call_with_axis(self, axis): + """ + Get the tick locations while the locator's axis is temporarily set to *axis*. + """ + current = axis + try: + self.set_axis(axis) + return self() + finally: + self.set_axis(current) + def raise_if_exceeds(self, locs): """ Log at WARNING level if *locs* is longer than `Locator.MAXTICKS`. diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index 1d695c129ae2..03febd5980dd 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -185,11 +185,11 @@ def get_tick_iterators(self, axes): angle_normal, angle_tangent = {0: (90, 0), 1: (0, 90)}[self.nth_coord] major = self.axis.major - major_locs = major.locator() + major_locs = major.locator._call_with_axis(self.axis) major_labels = major.formatter.format_ticks(major_locs) minor = self.axis.minor - minor_locs = minor.locator() + minor_locs = minor.locator._call_with_axis(self.axis) minor_labels = minor.formatter.format_ticks(minor_locs) tick_to_axes = self.get_tick_transform(axes) - axes.transAxes @@ -246,11 +246,11 @@ def get_tick_iterators(self, axes): angle_normal, angle_tangent = {0: (90, 0), 1: (0, 90)}[self.nth_coord] major = self.axis.major - major_locs = major.locator() + major_locs = major.locator._call_with_axis(self.axis) major_labels = major.formatter.format_ticks(major_locs) minor = self.axis.minor - minor_locs = minor.locator() + minor_locs = minor.locator._call_with_axis(self.axis) minor_labels = minor.formatter.format_ticks(minor_locs) data_to_axes = axes.transData - axes.transAxes @@ -351,18 +351,22 @@ def get_gridlines(self, which="major", axis="both"): locs = [] y1, y2 = self.axes.get_ylim() if which in ("both", "major"): - locs.extend(self.axes.xaxis.major.locator()) + locs.extend( + self.axes.xaxis.major.locator._call_with_axis(self.axes.xaxis)) if which in ("both", "minor"): - locs.extend(self.axes.xaxis.minor.locator()) + locs.extend( + self.axes.xaxis.minor.locator._call_with_axis(self.axes.xaxis)) gridlines.extend([[x, x], [y1, y2]] for x in locs) if axis in ("both", "y"): x1, x2 = self.axes.get_xlim() locs = [] if self.axes.yaxis._major_tick_kw["gridOn"]: - locs.extend(self.axes.yaxis.major.locator()) + locs.extend( + self.axes.yaxis.major.locator._call_with_axis(self.axes.yaxis)) if self.axes.yaxis._minor_tick_kw["gridOn"]: - locs.extend(self.axes.yaxis.minor.locator()) + locs.extend( + self.axes.yaxis.minor.locator._call_with_axis(self.axes.yaxis)) gridlines.extend([[x1, x2], [y, y]] for y in locs) return gridlines