diff --git a/doc/users/next_whats_new/multiplelocator_offset.rst b/doc/users/next_whats_new/multiplelocator_offset.rst new file mode 100644 index 000000000000..863fdb3c4d7e --- /dev/null +++ b/doc/users/next_whats_new/multiplelocator_offset.rst @@ -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() diff --git a/galleries/examples/ticks/tick-locators.py b/galleries/examples/ticks/tick-locators.py index e634e022173c..6cf4afaf22d7 100644 --- a/galleries/examples/ticks/tick-locators.py +++ b/galleries/examples/ticks/tick-locators.py @@ -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 @@ -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') axs[4].xaxis.set_major_locator(ticker.IndexLocator(base=0.5, offset=0.25)) # Auto Locator diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 3d38df575f09..53224d373f80 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -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. @@ -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. @@ -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: diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index bf059567f5fe..0877e58c5fe7 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -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.""" @@ -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 diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index e53a665432e2..1f4239ef2718 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -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: