Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Bug]: regression with setting ticklabels for colorbars in matplotlib 3.5.0b1 #20989

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

Closed
neutrinoceros opened this issue Sep 3, 2021 · 12 comments

Comments

@neutrinoceros
Copy link
Contributor

neutrinoceros commented Sep 3, 2021

Bug summary

matplotlib 3.5.0b1 breaks the following code, which runs fine on 3.4.3
The crash happens while running the last line:

cbar.ax.set_xticklabels(cbar.ax.get_xticklabels())

Admittedly this line seems like it should not do anything and certainly not produce an error.
In practice, my actual application contains the following:

cbar.ax.set_xticklabels(cbar.ax.get_xticklabels(), rotation=45)

which looks more like a hack than a supported usage of matplotlib's api, so I'd be happy to change it but I was not yet able to find a cleaner way to perform that rotation.
Still, no matter how untidy my application is, I'm fairly convinced that breaking it is a bug.

Code for reproduction

import matplotlib.pyplot as plt
import numpy as np

x, y = np.mgrid[1:100, 1:100]
z = np.random.random_sample(x.shape)

fig, ax = plt.subplots()

im = ax.contourf(x, y, z, levels=50)
cbar = fig.colorbar(im, orientation='horizontal')
cbar.ax.set_xticklabels(cbar.ax.get_xticklabels())

Actual outcome

Traceback
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /private/tmp/dustyn/t_mpl.py:11 in <module>                                                      │
│                                                                                                  │
│    8                                                                                             │
│    9 im = ax.contourf(x, y, z, levels=50)                                                        │
│   10 cbar = fig.colorbar(im, orientation='horizontal')                                           │
│ ❱ 11 cbar.ax.set_xticklabels(cbar.ax.get_xticklabels())                                          │
│   12                                                                                             │
│                                                                                                  │
│ /Users/robcleme/.pyenv/versions/3.9.6/envs/mpl35_tmp/lib/python3.9/site-packages/matplotlib/axes │
│ /_base.py:75 in wrapper                                                                          │
│                                                                                                  │
│     72 │   │   get_method = attrgetter(f"{self.attr_name}.{self.method_name}")                   │
│     73 │   │                                                                                     │
│     74 │   │   def wrapper(self, *args, **kwargs):                                               │
│ ❱   75 │   │   │   return get_method(self)(*args, **kwargs)                                      │
│     76 │   │                                                                                     │
│     77 │   │   wrapper.__module__ = owner.__module__                                             │
│     78 │   │   wrapper.__name__ = name                                                           │
│                                                                                                  │
│ /Users/robcleme/.pyenv/versions/3.9.6/envs/mpl35_tmp/lib/python3.9/site-packages/matplotlib/axis │
│ .py:1798 in _set_ticklabels                                                                      │
│                                                                                                  │
│   1795 │   │   """                                                                               │
│   1796 │   │   if fontdict is not None:                                                          │
│   1797 │   │   │   kwargs.update(fontdict)                                                       │
│ ❱ 1798 │   │   return self.set_ticklabels(labels, minor=minor, **kwargs)                         │
│   1799 │                                                                                         │
│   1800 │   def _set_tick_locations(self, ticks, *, minor=False):                                 │
│   1801 │   │   # see docstring of set_ticks                                                      │
│                                                                                                  │
│ /Users/robcleme/.pyenv/versions/3.9.6/envs/mpl35_tmp/lib/python3.9/site-packages/matplotlib/axis │
│ .py:1720 in set_ticklabels                                                                       │
│                                                                                                  │
│   1717 │   │   │   # Passing [] as a list of ticklabels is often used as a way to                │
│   1718 │   │   │   # remove all tick labels, so only error for > 0 ticklabels                    │
│   1719 │   │   │   if len(locator.locs) != len(ticklabels) and len(ticklabels) != 0:             │
│ ❱ 1720 │   │   │   │   raise ValueError(                                                         │
│   1721 │   │   │   │   │   "The number of FixedLocator locations"                                │
│   1722 │   │   │   │   │   f" ({len(locator.locs)}), usually from a call to"                     │
│   1723 │   │   │   │   │   " set_ticks, does not match"                                          │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: The number of FixedLocator locations (51), usually from a call to set_ticks, does not match the number of ticklabels (9).

Expected outcome

a boring image :)

Operating system

OS/X

Matplotlib Version

.5.0b1

Matplotlib Backend

MacOSX

Python version

3.9.6

Jupyter version

N/A

Other libraries

No response

Installation

pip

Conda channel

No response

@dstansby
Copy link
Member

dstansby commented Sep 3, 2021

This bisects to 146856b from #20054

@dstansby dstansby added this to the v3.5.0 milestone Sep 3, 2021
@dstansby
Copy link
Member

dstansby commented Sep 3, 2021

A bit of diving and this is because:

  • Setting levels=50 means the colorbar has the .boundaries attribute set
  • This means (as of Enh better colorbar axes #20054) the colorbar axis has a FixedLocator set (I'm not entirely sure why this is the case, perhaps @jklymak can advise?)
  • Because the FixedLocator has len(locs) = 51 but nbins=10 (nbins=10 is hardcoded in Colorbar):
    • ax.get_xticklabels() returns 10 labels, as that's the number of labels visible
    • ax.set_xticklabels() expects 51 labels to go with all the locations, even if they are not all visible

So there are two things going on here:

  1. What you're trying to do has never worked for a BoundaryNorm where len(locs) < nbins
  2. Colorbars now have a BoundaryNorm in this situation

At this point I'm not sure whether the fix is to change 1. or 2. or neither here, perhaps someone else can chime in with that 😄

@neutrinoceros
Copy link
Contributor Author

Thank you so much for digging into this so promptly ! I have to say I would be very happy to learn a more idiomatic way to rotate ticklabels on colorbar, but I note that this stack overflow post is the first result when I google this again:
https://stackoverflow.com/questions/32050030/rotation-of-colorbar-tick-labels-in-matplotlib

so it's very likely been cargo culted into the module where I discovered this, and I wouldn't be surprised if it was also blindly copy-pasted in other codes downstream.

@QuLogic
Copy link
Member

QuLogic commented Sep 3, 2021

What you want to achieve is much simpler with Axes.tick_params

import matplotlib.pyplot as plt
import numpy as np

x, y = np.mgrid[1:100, 1:100]
z = np.random.random_sample(x.shape)

fig, ax = plt.subplots()

im = ax.contourf(x, y, z, levels=50)
cbar = fig.colorbar(im, orientation='horizontal')
cbar.ax.tick_params(rotation=45)

rotate

@neutrinoceros
Copy link
Contributor Author

Excellent, thanks @QuLogic ! I posted it as an answer in the SO post linked above, in case you'd like to upvote it https://stackoverflow.com/a/69053022/5489622

neutrinoceros added a commit to neutrinoceros/dustyn that referenced this issue Sep 4, 2021
@jklymak
Copy link
Member

jklymak commented Sep 4, 2021

I guess I think this is a bug in get_xticklabels. If we expect ticks and labels to always be in sync, the get should also return what we would give the set.

I don't understand what the locator and formatter are doing here exactly. Just to be clear in #20054 I tried not to change too much, and don't make any claim to understand all the choices. But presumably the same discrepancy is present with a normal x axis.

@neutrinoceros
Copy link
Contributor Author

presumably the same discrepancy is present with a normal x axis.

actually it's different.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.set_xticklabels(ax.get_xticklabels())
fig.savefig("/tmp/20989.png")

output

UserWarning: FixedFormatter should only be used together with FixedLocator
  ax.set_xticklabels(ax.get_xticklabels())

example

note that this behaviour is consistent from matplotlib 3.4.3 to the current latest commit on the main branch (d358cc3), so presumably no one cares. However, this indicates that the problem I reported is indeed exclusive to colorbar axes.

@jklymak
Copy link
Member

jklymak commented Oct 28, 2021

The reproducer on a normal axes is:

fig, ax = plt.subplots()

ax.set_xlim(0, 1)
ax.xaxis.set_major_locator(FixedLocator(np.linspace(0, 1, 51), nbins=10))
ax.set_xticklabels(ax.get_xticklabels())
plt.show()

Which gives:

ValueError: The number of FixedLocator locations (51), usually from a call to set_ticks, does not match the number of ticklabels (9).

If we think the incantation above is acceptable, then this is a bug in set_xticklabels which has special logic for FixedLocator.

@jklymak
Copy link
Member

jklymak commented Oct 28, 2021

... But I think that magical incantation never worked for normal axes because the text is not populated until after a draw. It used to work for colorbars because they manually populated the ticks, but the whole point of #20054 was to make the axes more similar in behaviour.

So, 1) axis.py needs a small change to use nbins instead of levels for the check above 2) get_xticklabels in general needs to work properly before the above works. 1) can be done, but its not going to close this. 2) we've talked about a bunch of times, but not done, and I'm not sure we will get in for 3.5.0.

Overall I don't think this "regression" is going to go away, and given it never worked for normal axises, I'm tempted to accept the breakage here which just happened to work unintentionally for colorbars.

@jklymak
Copy link
Member

jklymak commented Oct 28, 2021

Also please note that set_xticklabels is officially "discouraged" overall.

@neutrinoceros
Copy link
Contributor Author

neutrinoceros commented Oct 28, 2021

Sounds reasonable to me, I wouldn't want this to block the 3.5 release

@jklymak
Copy link
Member

jklymak commented Oct 29, 2021

Lets close as "won't fix" - this does not work on "normal" axes with Fixed locators and nbins set, so it doesn't work here. Appreciated that the user didn't explicitly set a FixedLocator on nbins, but it seems hard to fix this just for colorbars. The way to change tick label properties is given above, and we recommend users use that....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants