Skip to content

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

timhoffm
Copy link
Member

@timhoffm timhoffm commented Apr 1, 2025

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 (#29849)

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

@rcomer
Copy link
Member

rcomer commented Apr 2, 2025

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 😕

@timhoffm timhoffm marked this pull request as draft April 2, 2025 11:34
@anntzer
Copy link
Contributor

anntzer commented May 1, 2025

This would also close #19956.

timhoffm added 3 commits July 27, 2025 22:20
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)
When destroying a manager, replace the figure's canvas by a
figure canvas base.
@timhoffm
Copy link
Member Author

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 to FigureManagerBase.destroy(). However, that is currently empty and not called from any of the specific FigureManagerX.destroy(), so we'd need to inject super().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 of ENH: 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 the fig.canvas is a FigureCanvasBase again.

@timhoffm
Copy link
Member Author

timhoffm commented Jul 27, 2025

Note: the failing tests are in test_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')
assert result.getvalue() == result_after.getvalue()

This is now no longer the case, as closing the figure resets the canvas to FigureCanvasBase, 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 a FigureCanvasBase (which imlies it can be saved with savefig, but we don't have to test the contents? Or are there other approaches?

@anntzer
Copy link
Contributor

anntzer commented Jul 27, 2025

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
Member Author

timhoffm commented Jul 27, 2025

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.

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

Successfully merging this pull request may close these issues.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy