Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
tacaswell merged 22 commits intomatplotlib:mainfromtimhoffm:pyplot-register-figure
Sep 16, 2025

Conversation

@timhoffm
Copy link
Member

@timhoffmtimhoffm commentedApr 1, 2025
edited
Loading

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 Figureimport matplotlib.pyplot as pltfig = Figure()fig.subplots().plot([1, 3, 2])plt.figure(fig)  # fig is now tracked in pyplotplt.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 (#29849)

Anybody, feel free to play around with this and try to break it.

anntzer, story645, rcomer, and tacaswell reacted with heart emoji
@rcomer
Copy link
Member

rcomer commentedApr 2, 2025
edited
Loading

I was hoping I could modify the figure and show again, but that does not seem to be the case

importmatplotlib.pyplotaspltfrommatplotlib.figureimportFigurefig=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 😕

timhoffm reacted with eyes emoji

@timhoffmtimhoffm marked this pull request as draftApril 2, 2025 11:34
@anntzer
Copy link
Contributor

This would also close#19956.

It may be fundamentally nice not to have to create the figurethough pyplot to be able to use it in pyplot afterwards. You can now do```from matplotlib.figure import Figureimport matplotlib.pyplot as pltfig = Figure()fig.subplots().plot([1, 3, 2])plt.figure(fig)  # fig is now tracked in pyplotplt.show()```This also opens up the possibility to more dynamically trackand untrack figures in pyplot, which opens up the road tooptimized figure tracking in pyplot (matplotlib#29849)
@timhoffm
Copy link
MemberAuthor

Showing again now works properly. When destroying a figure manager, the figure's canvas is reset to a FigureCanvasBase.

Technical questions:

  • I've added resetting the canvas to the specific manager's destroy method (currently only implemented for Qt). This means, I have to add it to all gui-specific managers. But I feel it belongs there: The gui-specific managers replace the FigureCanvasBase when they are created, so they should undo this when they are destroyed. - One could in theory add it toFigureManagerBase.destroy(). However, that is currently empty and not called from any of the specific FigureManagerX.destroy(), so we'd need to injectsuper().destroy()` calls and then I think I'd rather keep it explicit. - Feedback welcome.
  • I've also not a good idea how to test this, i.e. the equivalent ofENH: Allow to register standalone figures with pyplot #29855 (comment). I suspect, I need to use a proper GUI backend with a real window and then somehow get the window to close. - Can this be done in tests? - After that, I could check that thefig.canvas is aFigureCanvasBase again.

@timhoffm
Copy link
MemberAuthor

timhoffm commentedJul 27, 2025
edited
Loading

Note: the failing tests are intest_interactive_backend, which saves the figure to file, closes it, and then saves it to file again, expecting exactly the same output.

result=io.BytesIO()
fig.savefig(result,format='png')
plt.show()
# Ensure that the window is really closed.
plt.pause(0.5)
# Test that saving works after interactive window is closed, but the figure
# is not deleted.
result_after=io.BytesIO()
fig.savefig(result_after,format='png')
assertresult.getvalue()==result_after.getvalue()

This is now no longer the case, as closing the figure resets the canvas toFigureCanvasBase, i.e. the images are genereated through different canvases -FigureCanvasBase and the backend-specific canvas, which do not necessarily have pixel-identical output. An example diff looks like this:

after-failed-diff

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 aFigureCanvasBase (which imlies it can be saved withsavefig, but we don't have to test the contents? Or are there other approaches?

@anntzer
Copy link
Contributor

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.

@timhoffm
Copy link
MemberAuthor

timhoffm commentedJul 27, 2025
edited
Loading

Failing tests are

FAILED lib/matplotlib/tests/test_backends_interactive.py::test_interactive_backend[toolbar2-MPLBACKEND=qtagg-QT_API=PyQt5-BACKEND_DEPS=PyQt5]FAILED lib/matplotlib/tests/test_backends_interactive.py::test_interactive_backend[toolbar2-MPLBACKEND=qtcairo-QT_API=PyQt6-BACKEND_DEPS=PyQt6,cairocffi]FAILED lib/matplotlib/tests/test_backends_interactive.py::test_interactive_backend[toolbar2-MPLBACKEND=qtcairo-QT_API=PySide6-BACKEND_DEPS=PySide6,cairocffi]FAILED lib/matplotlib/tests/test_backends_interactive.py::test_interactive_backend[toolbar2-MPLBACKEND=qtcairo-QT_API=PyQt5-BACKEND_DEPS=PyQt5,cairocffi]FAILED lib/matplotlib/tests/test_backends_interactive.py::test_interactive_backend[toolbar2-MPLBACKEND=qtcairo-QT_API=PySide2-BACKEND_DEPS=PySide2,cairocffi]

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.

@anntzer
Copy link
Contributor

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?
No idea about the qt5agg test, though.

@anntzer
Copy link
Contributor

  • I've added resetting the canvas to the specific manager's destroy method (currently only implemented for Qt). This means, I have to add it to all gui-specific managers. But I feel it belongs there: The gui-specific managers replace the FigureCanvasBase when they are created, so they should undo this when they are destroyed. - One could in theory add it toFigureManagerBase.destroy(). However, that is currently empty and not called from any of the specific FigureManagerX.destroy(), so we'd need to injectsuper().destroy()` calls and then I think I'd rather keep it explicit. - Feedback welcome.

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).

@timhoffm
Copy link
MemberAuthor

My thought was that the specific FigureManager* classes override the canvas in their__init__ and hence they should undo what they did in theirdestroy(). But one could also argue that resetting to the base canvas is a universal property of destroying any manager.

When destroying a manager, replace the figure's canvas by afigure canvas base.
There in now machinery for all of the public API that takes arenderer is input to get it from the current canvas on the rootfigure.  Use that machinery instead.
@tacaswell
Copy link
Member

I took the liberty of fixing one of the tests. We probably should purge all of thecanvas.get_renderer() calls from the tests, but that is a later problem.

@timhoffmtimhoffmforce-pushed thepyplot-register-figure branch 3 times, most recently fromd420613 to6dcd489CompareSeptember 8, 2025 19:48
import must be local
src/_macosx.m Outdated
{
[self->windowclose];
self->window =NULL;
[superdestroy];
Copy link
MemberAuthor

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 ofsuper.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'

Copy link
Contributor

@greglucasgreglucasSep 8, 2025
edited
Loading

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])

defdestroy(self):
# We need to clear any pending timers that never fired, otherwise
# we get a memory leak from the timer callbacks holding a reference
whileself.canvas._timers:
timer=self.canvas._timers.pop()
timer.stop()
super().destroy()

Copy link
MemberAuthor

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.

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@timhoffmtimhoffmforce-pushed thepyplot-register-figure branch 2 times, most recently froma3e97f8 to232e562CompareSeptember 9, 2025 20:59
src/_macosx.m Outdated
Comment on lines 692 to 707
// call super().destroy()
PyObject *super_obj =PyObject_CallFunctionObjArgs(
(PyObject *)&PySuper_Type,
(PyObject *)&FigureManagerType,
self,
NULL
);
if (super_obj ==NULL) {
returnNULL;// error
}
PyObject *result =PyObject_CallMethod(super_obj,"destroy",NULL);
Py_DECREF(super_obj);
if (result ==NULL) {
returnNULL;// error
}
Py_DECREF(result);
Copy link
MemberAuthor

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.

Copy link
Member

@QuLogicQuLogic left a 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.

@tacaswell
Copy link
Member

I spot-checked this on OSX and it seems work as expected:

  • showing figure
  • re-showing figure
  • switching the canvas back toFigureCanvasBase
  • withplt.ioff() andplt.ion()
  • mouse/keyboard events work with reshown figures

@tacaswelltacaswell merged commit3db41bd intomatplotlib:mainSep 16, 2025
40 of 41 checks passed
@tacaswell
Copy link
Member

tacaswell commentedSep 16, 2025
edited
Loading

Thank you@timhoffm !

I'm super stoked we landed this as this has been a low-key goal of mine for a while now. I'm also very happy that the actual implementation was way simpler and less invasive than the direction I was going.

timhoffm reacted with heart emoji

@rcomer
Copy link
Member

🎉😀🍾🥂

timhoffm reacted with hooray emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@anntzeranntzeranntzer approved these changes

@rcomerrcomerrcomer left review comments

@greglucasgreglucasgreglucas left review comments

@tacaswelltacaswelltacaswell approved these changes

@QuLogicQuLogicQuLogic approved these changes

Assignees

No one assigned

Projects

None yet

Milestone

v3.11.0

Development

Successfully merging this pull request may close these issues.

6 participants

@timhoffm@rcomer@anntzer@tacaswell@QuLogic@greglucas

[8]ページ先頭

©2009-2025 Movatter.jp