diff --git a/doc/api/api_changes_3.3/behaviour.rst b/doc/api/api_changes_3.3/behaviour.rst index 70a02d81b9d4..483f93448b7c 100644 --- a/doc/api/api_changes_3.3/behaviour.rst +++ b/doc/api/api_changes_3.3/behaviour.rst @@ -37,6 +37,13 @@ did nothing, when passed an unsupported value. It now raises a ``ValueError``. `.pyplot.tick_params`) used to accept any value for ``which`` and silently did nothing, when passed an unsupported value. It now raises a ``ValueError``. +``Axis.set_ticklabels()`` must match ``FixedLocator.locs`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If an axis is using a `.ticker.FixedLocator`, typically set by a call to +`.Axis.set_ticks`, then the number of ticklabels supplied must match the +number of locations available (``FixedFormattor.locs``). If not, a +``ValueError`` is raised. + ``backend_pgf.LatexManager.latex`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``backend_pgf.LatexManager.latex`` is now created with ``encoding="utf-8"``, so diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index beff96184ec9..23874127b062 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -226,17 +226,20 @@ Other Discouraged ----------- -These methods implicitly use `~matplotlib.ticker.FixedLocator` and -`~matplotlib.ticker.FixedFormatter`. They can be convenient, but if -not used together may de-couple your tick labels from your data. +These methods should be used together with care, calling ``set_ticks`` +to specify the desired tick locations **before** calling ``set_ticklabels`` to +specify a matching series of labels. Calling ``set_ticks`` makes a +`~matplotlib.ticker.FixedLocator`; it's list of locations is then used by +``set_ticklabels`` to make an appropriate +`~matplotlib.ticker.FuncFormatter`. .. autosummary:: :toctree: _as_gen :template: autosummary.rst :nosignatures: - Axis.set_ticklabels Axis.set_ticks + Axis.set_ticklabels diff --git a/examples/images_contours_and_fields/image_annotated_heatmap.py b/examples/images_contours_and_fields/image_annotated_heatmap.py index 3aac185f0e82..fd912dffa755 100644 --- a/examples/images_contours_and_fields/image_annotated_heatmap.py +++ b/examples/images_contours_and_fields/image_annotated_heatmap.py @@ -288,7 +288,7 @@ def annotate_heatmap(im, data=None, valfmt="{x:.2f}", # the diagonal elements (which are all 1) by using a # `matplotlib.ticker.FuncFormatter`. -corr_matrix = np.corrcoef(np.random.rand(6, 5)) +corr_matrix = np.corrcoef(harvest) im, _ = heatmap(corr_matrix, vegetables, vegetables, ax=ax4, cmap="PuOr", vmin=-1, vmax=1, cbarlabel="correlation coeff.") diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 5b105e13aeeb..eb0d365fa0c2 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3016,6 +3016,7 @@ def locator_params(self, axis='both', tight=None, **kwargs): self.yaxis.get_major_locator().set_params(**kwargs) self._request_autoscale_view(tight=tight, scalex=update_x, scaley=update_y) + self.stale = True def tick_params(self, axis='both', **kwargs): """ diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 359999c73720..6566af285278 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -3,6 +3,7 @@ """ import datetime +import functools import logging import numpy as np @@ -1599,6 +1600,11 @@ def set_pickradius(self, pickradius): """ self.pickradius = pickradius + # Helper for set_ticklabels. Defining it here makes it pickleable. + @staticmethod + def _format_with_dict(tickd, x, pos): + return tickd.get(x, "") + def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): r""" Set the text values of the tick labels. @@ -1611,8 +1617,9 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): Parameters ---------- ticklabels : sequence of str or of `.Text`\s - List of texts for tick labels; must include values for non-visible - labels. + Texts for labeling each tick location in the sequence set by + `.Axis.set_ticks`; the number of labels must match the number of + locations. minor : bool If True, set minor ticks instead of major ticks. **kwargs @@ -1626,14 +1633,34 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): """ ticklabels = [t.get_text() if hasattr(t, 'get_text') else t for t in ticklabels] + locator = (self.get_minor_locator() if minor + else self.get_major_locator()) + if isinstance(locator, mticker.FixedLocator): + if len(locator.locs) != len(ticklabels): + raise ValueError( + "The number of FixedLocator locations" + f" ({len(locator.locs)}), usually from a call to" + " set_ticks, does not match" + f" the number of ticklabels ({len(ticklabels)}).") + tickd = {loc: lab for loc, lab in zip(locator.locs, ticklabels)} + func = functools.partial(self._format_with_dict, tickd) + formatter = mticker.FuncFormatter(func) + else: + formatter = mticker.FixedFormatter(ticklabels) + if minor: - self.set_minor_formatter(mticker.FixedFormatter(ticklabels)) - ticks = self.get_minor_ticks() + self.set_minor_formatter(formatter) + locs = self.get_minorticklocs() + ticks = self.get_minor_ticks(len(locs)) else: - self.set_major_formatter(mticker.FixedFormatter(ticklabels)) - ticks = self.get_major_ticks() + self.set_major_formatter(formatter) + locs = self.get_majorticklocs() + ticks = self.get_major_ticks(len(locs)) + ret = [] - for tick_label, tick in zip(ticklabels, ticks): + for pos, (loc, tick) in enumerate(zip(locs, ticks)): + tick.update_position(loc) + tick_label = formatter(loc, pos) # deal with label1 tick.label1.set_text(tick_label) tick.label1.update(kwargs) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index faf024a02512..7635336b78e7 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4441,8 +4441,8 @@ def test_set_get_ticklabels(): # set ticklabel to 1 plot in normal way ax[0].set_xticks(range(10)) ax[0].set_yticks(range(10)) - ax[0].set_xticklabels(['a', 'b', 'c', 'd']) - ax[0].set_yticklabels(['11', '12', '13', '14']) + ax[0].set_xticklabels(['a', 'b', 'c', 'd'] + 6 * ['']) + ax[0].set_yticklabels(['11', '12', '13', '14'] + 6 * ['']) # set ticklabel to the other plot, expect the 2 plots have same label # setting pass get_ticklabels return value as ticklabels argument @@ -4452,6 +4452,26 @@ def test_set_get_ticklabels(): ax[1].set_yticklabels(ax[0].get_yticklabels()) +def test_subsampled_ticklabels(): + # test issue 11937 + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + ax.xaxis.set_ticks(np.arange(10) + 0.1) + ax.locator_params(nbins=5) + ax.xaxis.set_ticklabels([c for c in "bcdefghijk"]) + plt.draw() + labels = [t.get_text() for t in ax.xaxis.get_ticklabels()] + assert labels == ['b', 'd', 'f', 'h', 'j'] + + +def test_mismatched_ticklabels(): + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + ax.xaxis.set_ticks([1.5, 2.5]) + with pytest.raises(ValueError): + ax.xaxis.set_ticklabels(['a', 'b', 'c']) + + @image_comparison(['retain_tick_visibility.png']) def test_retain_tick_visibility(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 277f55a1afab..689db7fb6b6d 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -381,8 +381,10 @@ class FuncFormatter(Formatter): position ``pos``), and return a string containing the corresponding tick label. """ + def __init__(self, func): self.func = func + self.offset_string = "" def __call__(self, x, pos=None): """ @@ -392,6 +394,12 @@ def __call__(self, x, pos=None): """ return self.func(x, pos) + def get_offset(self): + return self.offset_string + + def set_offset_string(self, ofs): + self.offset_string = ofs + class FormatStrFormatter(Formatter): """