-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix cla colorbar #20330
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
Fix cla colorbar #20330
Conversation
co-author: Greg Lucas <greg.m.lucas@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a better fix 👍. Could you also add the remove()
method on the axes too? I think that is the one I was running into actually.
del self.inner_ax | ||
del self.outer_ax | ||
|
||
self.xaxis.set_visible(True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here self is back to an Axes now, so can you call super().cla()
to get the defaults for you?
We can probably make that work. I think it's a little trickier than what you had in the other PR. What is the goal of removing the axes from the axes versus calling cb.remove()? |
Actually, I'm a little confused what you want |
The issue I was running into was that matplotlib/lib/matplotlib/colorbar.py Line 260 in 08f4629
I assumed that was because the remove() method wasn't on CbarAxes, but now that I think about it I'm not sure why that method wouldn't be inherited from the base Axes in the first place for something to be there already. It may be that the cla() method here is enough to clean up that use case, which I think is shown by your test not failing...
|
Here is my reproducing example on master currently. import matplotlib.pyplot as plt
import numpy as np
X, Y = np.meshgrid(np.linspace(0, 6, 30), np.linspace(0, 6, 30))
arr = np.sin(X) * np.cos(Y) + 1j*(np.sin(3*Y) * np.cos(Y/2.))
fig, axarr = plt.subplots(figsize=(12, 9), nrows=2, ncols=2)
im1 = axarr[0, 0].imshow(arr.real, cmap='nipy_spectral')
im2 = axarr[1, 0].imshow(arr.imag, cmap='hot')
im3 = axarr[0, 1].imshow(np.abs(arr), cmap='jet')
im4 = axarr[1, 1].imshow(np.arctan2(arr.imag, arr.real), cmap='hsv')
cb1 = plt.colorbar(im1, ax=axarr[:, 0])
cb2 = plt.colorbar(im2, ax=axarr[:, 1])
cb1.ax.cla()
cb3 = plt.colorbar(im3, cax=cb1.ax)
plt.show() |
Sure, that works fine on this PR. |
lib/matplotlib/colorbar.py
Outdated
try: | ||
self.draw_all() | ||
except AttributeError: | ||
# update_normal sometimes is called when it shouldn't be.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does update_normal
get called when it shouldn't be? Presumably it has something to do with the new cla()
addition you added. I wonder if you need to remove the callbacks_cid
from callbacksSM
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh more lovely callbacks ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fundamental problem is that the colorbar axes is cla
, but the colorbar
object is not removed. Then Colorbar__init__
calls mappable.autoscale_None()
which triggers a draw of the orphaned colorbar. The orphaned colorbar tries to draw itself, but of course all its axes have been destroyed and we get an AttributeError
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that for the callbacks, each mappable only maps to one colorbar, but in the following the mappable is used for two colorbars:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
pc = ax.imshow(np.arange(100).reshape(10, 10))
cb = fig.colorbar(pc, extend='both')
cb2 = fig.colorbar(pc)
# Clear and re-use the same colorbar axes
print('HERE1:', pc, pc.callbacksSM.callbacks)
cb.ax.cla()
print('HERE2:', pc, pc.callbacksSM.callbacks)
cb2.ax.cla()
print('HERE3:', pc, pc.callbacksSM.callbacks)
cb = fig.colorbar(pc, cax=cb.ax)
cb2 = fig.colorbar(pc, cax=cb2.ax)
which results in:
HERE1: AxesImage(80,52.8;317.44x369.6) {'changed': {0: <weakref at 0x1085d69e0; to 'Colorbar' at 0x1096758e0>, 1: <weakref at 0x109709120; to 'Colorbar' at 0x109748130>}}
id 1
HERE2: AxesImage(80,52.8;317.44x369.6) {'changed': {0: <weakref at 0x1085d69e0; to 'Colorbar' at 0x1096758e0>}}
id 1
HERE3: AxesImage(80,52.8;317.44x369.6) {'changed': {0: <weakref at 0x1085d69e0; to 'Colorbar' at 0x1096758e0>}}
For HERE3
there should be no callback, but it never gets dicsonnected when we do cb2.ax.cla()
because pc
doesn't know about cb2
.
The culprit is here:
matplotlib/lib/matplotlib/colorbar.py
Lines 1181 to 1183 in d0137e9
mappable.colorbar = self | |
mappable.colorbar_cid = mappable.callbacksSM.connect( | |
'changed', self.update_normal) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course this doesn't work at all on master? So maybe this test is just too stringent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is an idea that would help this situation (and the pan/zoom on colorbars too which would help me). What about making the Colorbar
object itself inherit from CbarAxes
/become an Axes object? Then you would only call methods on the Colorbar object, not cbar.ax
/cax
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if that helps in particular.
The issue here is that the scalar mappable is only storing a reference to a single colorbar, whereas it stores two callbacks. So the line self.colorbar.mappable.callbacksSM.disconnect(self.colorbar.mappable.colorbar_cid)
only knows about one colorbar id, and loses track of the other colorbar. That leads to the stray draw.
The newest commit promotes colorbar_cid
to a dictionary if there is more than one and .colorbar
to a list. Thats a bit of a breaking change for what is stored on this attribute, but makes sure we don't lose track of what colorbars are on the scalar mappable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, this opened a can of worms ;)
I don't think changing colorbar_cid
to a dictionary is a great idea here, that seems like a lot more logic/work going on under the hood to take care of a niche case. As it so happens, it looks like there is some discussion about multiple callbacks in #20210 as well.
For this specific use-case, the reason you're getting multiple callbacks is because when you call cbar.ax.cla()
that ax.cla()
has no idea it should also clear a callback on the cbar
. But, if we move that up a level to make it just cbar.cla()
(i.e. Colorbar is now an Axes object), that clear call would know to get rid of the callback (and also clear the inner/outer axes as you already have implemented).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not at all fundamentally against that, but it seems a pretty hefty change. I think what you would do is lock out cb.ax and then just reproduce all the axes methods (which has been done already, but only partially). But it'd take some work to do it and not have a bunch of breaking API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surprisingly, it doesn't seem that bad... See this commit:
greglucas@569fbc1
Passes all test_colorbar.py
tests locally. Just pointing self.ax = self
after initialization would help and we could probably stick in a deprecation warning pass-through on that too. Maybe this is getting a little side-tracked for this thread and I should open up a separate issue for discussion?
Pretty sure this is obsoleted by #20501 |
PR Summary
Alternative to #20323.
This allows calling
cb.ax.cla
and reusing the same axes to swap in a new colorbar. If the colorbar is not filled in, an empty axes with the default facecolor is left where the old one was.Co-authored with @greglucas since I stole his test (though he is welcome to disavow the PR ;-))
PR Checklist
pytest
passes).flake8
on changed files to check).flake8-docstrings
and runflake8 --docstring-convention=all
).doc/users/next_whats_new/
(follow instructions in README.rst there).doc/api/next_api_changes/
(follow instructions in README.rst there).