Skip to content

ENH: offset parameter for MultipleLocator #25542

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

Merged
merged 1 commit into from
Jun 3, 2023
Merged
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
17 changes: 17 additions & 0 deletions doc/users/next_whats_new/multiplelocator_offset.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
``offset`` parameter for MultipleLocator
----------------------------------------

An *offset* may now be specified to shift all the ticks by the given value.

.. plot::
:include-source: true

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

_, ax = plt.subplots()
ax.plot(range(10))
locator = mticker.MultipleLocator(base=3, offset=0.3)
ax.xaxis.set_major_locator(locator)

plt.show()
6 changes: 3 additions & 3 deletions galleries/examples/ticks/tick-locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def setup(ax, title):
axs[0].xaxis.set_minor_locator(ticker.NullLocator())

# Multiple Locator
setup(axs[1], title="MultipleLocator(0.5)")
axs[1].xaxis.set_major_locator(ticker.MultipleLocator(0.5))
setup(axs[1], title="MultipleLocator(0.5, offset=0.2)")
axs[1].xaxis.set_major_locator(ticker.MultipleLocator(0.5, offset=0.2))
axs[1].xaxis.set_minor_locator(ticker.MultipleLocator(0.1))

# Fixed Locator
Expand All @@ -53,7 +53,7 @@ def setup(ax, title):

# Index Locator
setup(axs[4], title="IndexLocator(base=0.5, offset=0.25)")
axs[4].plot(range(0, 5), [0]*5, color='white')
axs[4].plot([0]*5, color='white')
Copy link
Member Author

@rcomer rcomer Mar 31, 2023

Choose a reason for hiding this comment

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

It's not completely obvious from looking at this example what the difference is between MultipleLocator and IndexLocator. But from the API docs it seems to me that IndexLocator makes most sense when the axis does not have numbers associated with it. So I took out the x-values.

I clearly should have done more homework before the Thursday call, then I would have known that offset is already used as a locator param 🤦‍♀️

axs[4].xaxis.set_major_locator(ticker.IndexLocator(base=0.5, offset=0.25))

# Auto Locator
Expand Down
17 changes: 17 additions & 0 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ def test_basic(self):
9.441, 12.588])
assert_almost_equal(loc.tick_values(-7, 10), test_value)

def test_basic_with_offset(self):
loc = mticker.MultipleLocator(base=3.147, offset=1.2)
test_value = np.array([-8.241, -5.094, -1.947, 1.2, 4.347, 7.494,
10.641])
assert_almost_equal(loc.tick_values(-7, 10), test_value)

def test_view_limits(self):
"""
Test basic behavior of view limits.
Expand All @@ -80,6 +86,15 @@ def test_view_limits_round_numbers(self):
loc = mticker.MultipleLocator(base=3.147)
assert_almost_equal(loc.view_limits(-4, 4), (-6.294, 6.294))

def test_view_limits_round_numbers_with_offset(self):
"""
Test that everything works properly with 'round_numbers' for auto
limit.
"""
with mpl.rc_context({'axes.autolimit_mode': 'round_numbers'}):
loc = mticker.MultipleLocator(base=3.147, offset=1.3)
assert_almost_equal(loc.view_limits(-4, 4), (-4.994, 4.447))

def test_set_params(self):
"""
Create multiple locator with 0.7 base, and change it to something else.
Expand All @@ -88,6 +103,8 @@ def test_set_params(self):
mult = mticker.MultipleLocator(base=0.7)
mult.set_params(base=1.7)
assert mult._edge.step == 1.7
mult.set_params(offset=3)
assert mult._offset == 3


class TestAutoMinorLocator:
Expand Down
45 changes: 35 additions & 10 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1831,17 +1831,41 @@ def view_limits(self, vmin, vmax):

class MultipleLocator(Locator):
"""
Set a tick on each integer multiple of the *base* within the view
interval.
Set a tick on each integer multiple of the *base* plus an *offset* within
the view interval.
"""

def __init__(self, base=1.0):
def __init__(self, base=1.0, offset=0.0):
"""
Parameters
----------
base : float > 0
Interval between ticks.
offset : float
Value added to each multiple of *base*.

.. versionadded:: 3.8
"""
self._edge = _Edge_integer(base, 0)
self._offset = offset

def set_params(self, base):
"""Set parameters within this locator."""
def set_params(self, base=None, offset=None):
"""
Set parameters within this locator.

Parameters
----------
base : float > 0
Interval between ticks.
offset : float
Value added to each multiple of *base*.

.. versionadded:: 3.8
"""
if base is not None:
self._edge = _Edge_integer(base, 0)
if offset is not None:
self._offset = offset

def __call__(self):
"""Return the locations of the ticks."""
Expand All @@ -1852,19 +1876,20 @@ def tick_values(self, vmin, vmax):
if vmax < vmin:
vmin, vmax = vmax, vmin
step = self._edge.step
vmin -= self._offset
vmax -= self._offset
vmin = self._edge.ge(vmin) * step
n = (vmax - vmin + 0.001 * step) // step
locs = vmin - step + np.arange(n + 3) * step
locs = vmin - step + np.arange(n + 3) * step + self._offset
return self.raise_if_exceeds(locs)

def view_limits(self, dmin, dmax):
"""
Set the view limits to the nearest multiples of *base* that
contain the data.
Set the view limits to the nearest tick values that contain the data.
"""
if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
vmin = self._edge.le(dmin) * self._edge.step
vmax = self._edge.ge(dmax) * self._edge.step
vmin = self._edge.le(dmin - self._offset) * self._edge.step + self._offset
vmax = self._edge.ge(dmax - self._offset) * self._edge.step + self._offset
if vmin == vmax:
vmin -= 1
vmax += 1
Expand Down
5 changes: 2 additions & 3 deletions lib/matplotlib/ticker.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,8 @@ class LinearLocator(Locator):
) -> None: ...

class MultipleLocator(Locator):
def __init__(self, base: float = ...) -> None: ...
# Makes set_params `base` argument mandatory
def set_params(self, base: float | None) -> None: ... # type: ignore[override]
def __init__(self, base: float = ..., offset: float = ...) -> None: ...
def set_params(self, base: float | None = ..., offset: float | None = ...) -> None: ...
def view_limits(self, dmin: float, dmax: float) -> tuple[float, float]: ...

class _Edge_integer:
Expand Down