Skip to content

Allow sharing locators across axises. #28429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/api/next_api_changes/behavior/28429-AL.rst
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
19 changes: 18 additions & 1 deletion lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1629,11 +1629,28 @@
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should get a docstring. If I understand it correctly, we always want to call this method instead of __call__, which then makes the explicit self.axis unused because it is always temporarily overwritten with the passed parameter.

We might choose to make this public as the new API, so people can use it directly (users wanting to evaluate, and locator authors implementing their logic directly here). We could then later deprecate __call__ and self.axis/set_axis.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring added. Agreed that this should be the future public API, but I didn't want to commit yet to anything.

"""
Get the tick locations while the locator's axis is temporarily set to *axis*.
"""
current = axis
try:
self.set_axis(axis)
return self()
finally:

Check warning on line 1651 in lib/matplotlib/ticker.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/ticker.py#L1651

Added line #L1651 was not covered by tests
self.set_axis(current)

def raise_if_exceeds(self, locs):
"""
Log at WARNING level if *locs* is longer than `Locator.MAXTICKS`.
Expand Down
20 changes: 12 additions & 8 deletions lib/mpl_toolkits/axisartist/axislines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading