Skip to content

Commit 8d33af7

Browse files
authored
Merge pull request #17266 from efiring/set_ticklabels
FIX: Keep explicit ticklabels in sync with ticks from FixedLocator
2 parents b5d4a6c + 3eb471d commit 8d33af7

File tree

7 files changed

+80
-14
lines changed

7 files changed

+80
-14
lines changed

doc/api/api_changes_3.3/behaviour.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ did nothing, when passed an unsupported value. It now raises a ``ValueError``.
3737
`.pyplot.tick_params`) used to accept any value for ``which`` and silently
3838
did nothing, when passed an unsupported value. It now raises a ``ValueError``.
3939

40+
``Axis.set_ticklabels()`` must match ``FixedLocator.locs``
41+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
42+
If an axis is using a `.ticker.FixedLocator`, typically set by a call to
43+
`.Axis.set_ticks`, then the number of ticklabels supplied must match the
44+
number of locations available (``FixedFormattor.locs``). If not, a
45+
``ValueError`` is raised.
46+
4047
``backend_pgf.LatexManager.latex``
4148
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4249
``backend_pgf.LatexManager.latex`` is now created with ``encoding="utf-8"``, so

doc/api/axis_api.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,20 @@ Other
228228
Discouraged
229229
-----------
230230

231-
These methods implicitly use `~matplotlib.ticker.FixedLocator` and
232-
`~matplotlib.ticker.FixedFormatter`. They can be convenient, but if
233-
not used together may de-couple your tick labels from your data.
231+
These methods should be used together with care, calling ``set_ticks``
232+
to specify the desired tick locations **before** calling ``set_ticklabels`` to
233+
specify a matching series of labels. Calling ``set_ticks`` makes a
234+
`~matplotlib.ticker.FixedLocator`; it's list of locations is then used by
235+
``set_ticklabels`` to make an appropriate
236+
`~matplotlib.ticker.FuncFormatter`.
234237

235238
.. autosummary::
236239
:toctree: _as_gen
237240
:template: autosummary.rst
238241
:nosignatures:
239242

240-
Axis.set_ticklabels
241243
Axis.set_ticks
244+
Axis.set_ticklabels
242245

243246

244247

examples/images_contours_and_fields/image_annotated_heatmap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def annotate_heatmap(im, data=None, valfmt="{x:.2f}",
288288
# the diagonal elements (which are all 1) by using a
289289
# `matplotlib.ticker.FuncFormatter`.
290290

291-
corr_matrix = np.corrcoef(np.random.rand(6, 5))
291+
corr_matrix = np.corrcoef(harvest)
292292
im, _ = heatmap(corr_matrix, vegetables, vegetables, ax=ax4,
293293
cmap="PuOr", vmin=-1, vmax=1,
294294
cbarlabel="correlation coeff.")

lib/matplotlib/axes/_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3010,6 +3010,7 @@ def locator_params(self, axis='both', tight=None, **kwargs):
30103010
self.yaxis.get_major_locator().set_params(**kwargs)
30113011
self._request_autoscale_view(tight=tight,
30123012
scalex=update_x, scaley=update_y)
3013+
self.stale = True
30133014

30143015
def tick_params(self, axis='both', **kwargs):
30153016
"""

lib/matplotlib/axis.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import datetime
6+
import functools
67
import logging
78

89
import numpy as np
@@ -1646,6 +1647,11 @@ def set_pickradius(self, pickradius):
16461647
"""
16471648
self.pickradius = pickradius
16481649

1650+
# Helper for set_ticklabels. Defining it here makes it pickleable.
1651+
@staticmethod
1652+
def _format_with_dict(tickd, x, pos):
1653+
return tickd.get(x, "")
1654+
16491655
def set_ticklabels(self, ticklabels, *, minor=False, **kwargs):
16501656
r"""
16511657
Set the text values of the tick labels.
@@ -1658,8 +1664,9 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs):
16581664
Parameters
16591665
----------
16601666
ticklabels : sequence of str or of `.Text`\s
1661-
List of texts for tick labels; must include values for non-visible
1662-
labels.
1667+
Texts for labeling each tick location in the sequence set by
1668+
`.Axis.set_ticks`; the number of labels must match the number of
1669+
locations.
16631670
minor : bool
16641671
If True, set minor ticks instead of major ticks.
16651672
**kwargs
@@ -1673,14 +1680,34 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs):
16731680
"""
16741681
ticklabels = [t.get_text() if hasattr(t, 'get_text') else t
16751682
for t in ticklabels]
1683+
locator = (self.get_minor_locator() if minor
1684+
else self.get_major_locator())
1685+
if isinstance(locator, mticker.FixedLocator):
1686+
if len(locator.locs) != len(ticklabels):
1687+
raise ValueError(
1688+
"The number of FixedLocator locations"
1689+
f" ({len(locator.locs)}), usually from a call to"
1690+
" set_ticks, does not match"
1691+
f" the number of ticklabels ({len(ticklabels)}).")
1692+
tickd = {loc: lab for loc, lab in zip(locator.locs, ticklabels)}
1693+
func = functools.partial(self._format_with_dict, tickd)
1694+
formatter = mticker.FuncFormatter(func)
1695+
else:
1696+
formatter = mticker.FixedFormatter(ticklabels)
1697+
16761698
if minor:
1677-
self.set_minor_formatter(mticker.FixedFormatter(ticklabels))
1678-
ticks = self.get_minor_ticks()
1699+
self.set_minor_formatter(formatter)
1700+
locs = self.get_minorticklocs()
1701+
ticks = self.get_minor_ticks(len(locs))
16791702
else:
1680-
self.set_major_formatter(mticker.FixedFormatter(ticklabels))
1681-
ticks = self.get_major_ticks()
1703+
self.set_major_formatter(formatter)
1704+
locs = self.get_majorticklocs()
1705+
ticks = self.get_major_ticks(len(locs))
1706+
16821707
ret = []
1683-
for tick_label, tick in zip(ticklabels, ticks):
1708+
for pos, (loc, tick) in enumerate(zip(locs, ticks)):
1709+
tick.update_position(loc)
1710+
tick_label = formatter(loc, pos)
16841711
# deal with label1
16851712
tick.label1.set_text(tick_label)
16861713
tick.label1.update(kwargs)

lib/matplotlib/tests/test_axes.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4497,8 +4497,8 @@ def test_set_get_ticklabels():
44974497
# set ticklabel to 1 plot in normal way
44984498
ax[0].set_xticks(range(10))
44994499
ax[0].set_yticks(range(10))
4500-
ax[0].set_xticklabels(['a', 'b', 'c', 'd'])
4501-
ax[0].set_yticklabels(['11', '12', '13', '14'])
4500+
ax[0].set_xticklabels(['a', 'b', 'c', 'd'] + 6 * [''])
4501+
ax[0].set_yticklabels(['11', '12', '13', '14'] + 6 * [''])
45024502

45034503
# set ticklabel to the other plot, expect the 2 plots have same label
45044504
# setting pass get_ticklabels return value as ticklabels argument
@@ -4508,6 +4508,26 @@ def test_set_get_ticklabels():
45084508
ax[1].set_yticklabels(ax[0].get_yticklabels())
45094509

45104510

4511+
def test_subsampled_ticklabels():
4512+
# test issue 11937
4513+
fig, ax = plt.subplots()
4514+
ax.plot(np.arange(10))
4515+
ax.xaxis.set_ticks(np.arange(10) + 0.1)
4516+
ax.locator_params(nbins=5)
4517+
ax.xaxis.set_ticklabels([c for c in "bcdefghijk"])
4518+
plt.draw()
4519+
labels = [t.get_text() for t in ax.xaxis.get_ticklabels()]
4520+
assert labels == ['b', 'd', 'f', 'h', 'j']
4521+
4522+
4523+
def test_mismatched_ticklabels():
4524+
fig, ax = plt.subplots()
4525+
ax.plot(np.arange(10))
4526+
ax.xaxis.set_ticks([1.5, 2.5])
4527+
with pytest.raises(ValueError):
4528+
ax.xaxis.set_ticklabels(['a', 'b', 'c'])
4529+
4530+
45114531
@image_comparison(['retain_tick_visibility.png'])
45124532
def test_retain_tick_visibility():
45134533
fig, ax = plt.subplots()

lib/matplotlib/ticker.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,10 @@ class FuncFormatter(Formatter):
387387
position ``pos``), and return a string containing the corresponding
388388
tick label.
389389
"""
390+
390391
def __init__(self, func):
391392
self.func = func
393+
self.offset_string = ""
392394

393395
def __call__(self, x, pos=None):
394396
"""
@@ -398,6 +400,12 @@ def __call__(self, x, pos=None):
398400
"""
399401
return self.func(x, pos)
400402

403+
def get_offset(self):
404+
return self.offset_string
405+
406+
def set_offset_string(self, ofs):
407+
self.offset_string = ofs
408+
401409

402410
class FormatStrFormatter(Formatter):
403411
"""

0 commit comments

Comments
 (0)