-
-
Notifications
You must be signed in to change notification settings - Fork 8k
ENH: Allow to register standalone figures with pyplot #29855
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
base: main
Are you sure you want to change the base?
Conversation
0e13c82
to
56f4ee1
Compare
56f4ee1
to
f97c262
Compare
I was hoping I could modify the figure and show again, but that does not seem to be the case import matplotlib.pyplot as plt
from matplotlib.figure import Figure
fig = Figure()
ax = fig.subplots()
ax.plot([0, 2])
plt.figure(fig)
plt.show()
ax.set_title('A cool line')
plt.figure(fig)
plt.show() No title is shown 😕 |
This would also close #19956. |
It may be fundamentally nice not to have to create the figure though pyplot to be able to use it in pyplot afterwards. You can now do ``` from matplotlib.figure import Figure import matplotlib.pyplot as plt fig = Figure() fig.subplots().plot([1, 3, 2]) plt.figure(fig) # fig is now tracked in pyplot plt.show() ``` This also opens up the possibility to more dynamically track and untrack figures in pyplot, which opens up the road to optimized figure tracking in pyplot (matplotlib#29849)
f97c262
to
d0958e4
Compare
Showing again now works properly. When destroying a figure manager, the figure's canvas is reset to a FigureCanvasBase. Technical questions:
|
Note: the failing tests are in matplotlib/lib/matplotlib/tests/test_backends_interactive.py Lines 222 to 235 in 011d12f
This is now no longer the case, as closing the figure resets the canvas to ![]() How should we handle this? I'm inclined to say that the previous expectation is no longer justified, and it makes sense that the canvas is reset. Should we try to fix this with tolerances? Or don't we need this image test at all anymore and instead it is sufficient to test that the figure has again a |
It's a bit strange that the end png result is not the same, as both should ultimately go through Agg for rasterizing... It would be nice to figure out what is going wrong, but I agree this isn't a blocker. |
Failing tests are
and the same again for toolmanager, which I left out for simplicity. So all cairocffi tests fail and addtionally the qtagg-PyQt5 test. But the agg-based tests for PyQt6, PySide2 and PySide6 pass. Interestingly, the qtagg-PyQt5 test passes on my local machine. The above diff image was from qtcairo-PyQt5. |
At least for the cairo tests this is understandable: if you reset to FigureCanvasBase, then they will now rasterize using agg, whereas they rasterized with cairo prior to that. I guess that's probably fine? |
I prefer adding doing this via super() calls: for the benefit of third-parties, it is relatively easier to document "one needs to call super().destroy() in subclasses" rather than "one needs to do this and that" (restore the default canvas, but who knows what else can change in the future). |
My thought was that the specific FigureManager* classes override the canvas in their |
When destroying a manager, replace the figure's canvas by a figure canvas base.
d0958e4
to
a38f9bb
Compare
There in now machinery for all of the public API that takes a renderer is input to get it from the current canvas on the root figure. Use that machinery instead.
I took the liberty of fixing one of the tests. We probably should purge all of the |
"power-cycled" to re-trigger pinning away from the known-bad pytest-rerunner. |
general, you should not store a reference to the canvas, but rather always | ||
obtain it from the figure with ``fig.canvas``. This will return the current | ||
canvas, which is either the original `.FigureCanvasBase` or a backend-dependent | ||
subclass, depending on whether the figure is registered with pyplot or not. |
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.
Maybe add a comment that there are currently known limitations wrt. blitting/widgets (#30503)? We can always edit that out once that issue is fixed.
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.
Added
Additionally, the swapping of the canvas currently does not play well with
blitting of matplotlib widgets; in such cases either deactivate blitting or do not
continue to use the figure (e.g. saving it after closing the window).
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.
Modulo addressing the comments.
6ac7b60
to
3cb0681
Compare
d420613
to
6dcd489
Compare
import must be local
6dcd489
to
6b1797b
Compare
src/_macosx.m
Outdated
@@ -686,6 +686,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) | |||
{ | |||
[self->window close]; | |||
self->window = NULL; | |||
[super destroy]; |
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 could need a bit of help here from someone with more Objective-C / Cpython knowledge. This is missing the equivalent of super.destroy()
. The change here was a rather wild guess from pattern-matching other similar cases in the module, but is apparently not correct, because this now fails the build with
../../src/_macosx.m:689:6: error: use of undeclared identifier 'super'
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.
Do you need to go into the objective C layer or can you stay at the Python layer (have not looked at the updates here in detail). But, we do have some multiple inheritance going on here, so you may have to explicitly call which super you want to interact with at this place. (I think the super().destroy() currently on these lines goes to the objective C implementation, and you want to go the FigureManagerBase implementation [we want to travel to both it sounds like])
matplotlib/lib/matplotlib/backends/backend_macosx.py
Lines 168 to 174 in 352b419
def destroy(self): | |
# We need to clear any pending timers that never fired, otherwise | |
# we get a memory leak from the timer callbacks holding a reference | |
while self.canvas._timers: | |
timer = self.canvas._timers.pop() | |
timer.stop() | |
super().destroy() |
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.
super() goes to the next class in MRO, and that covers the whole inheritance tree also in case of multiple inheritance if all classes in the hierarchy call it without explicitly stating the parent. That‘s what I want. I do not want to mess with calling any specific parent.
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.
My quick suggestion would be to avoid trying to call super() from the C-layer. To avoid the multiple inheritance issue, you can rename the (PyCFunction)FigureManager_destroy
to (PyCFunction)FigureManager__destroy
or something else (its in a private module) and then call that method explicitly from the Python destroy() side. You can keep the super().destroy() already there which will still travel the tree as you want I think.
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.
Maybe something along the lines of https://stackoverflow.com/questions/42201881/python-c-api-how-to-call-base-type-method-from-subtype would work?
a3e97f8
to
232e562
Compare
232e562
to
774d02c
Compare
src/_macosx.m
Outdated
// call super().destroy() | ||
PyObject *super_obj = PyObject_CallFunctionObjArgs( | ||
(PyObject *)&PySuper_Type, | ||
(PyObject *)&FigureManagerType, | ||
self, | ||
NULL | ||
); | ||
if (super_obj == NULL) { | ||
return NULL; // error | ||
} | ||
PyObject *result = PyObject_CallMethod(super_obj, "destroy", NULL); | ||
Py_DECREF(super_obj); | ||
if (result == NULL) { | ||
return NULL; // error | ||
} | ||
Py_DECREF(result); |
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.
There are two approvals already, but since this super() call is new and I'm not the most experienced C-API developer, I'd like someone to cross check this bit again.
6f596db
to
ccf1994
Compare
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.
LGTM, but maybe someone on macOS should double-check that part.
It may be fundamentally nice not to have to create the figure though pyplot to be able to use it in pyplot afterwards. You can now do
This also opens up the possibility to more dynamically track and untrack figures in pyplot, which opens up the road to optimized figure tracking in pyplot (#29849)
Anybody, feel free to play around with this and try to break it.