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

Skip to content

Commit 3db41bd

Browse files
authored
Merge pull request #29855 from timhoffm/pyplot-register-figure
ENH: Allow to register standalone figures with pyplot
2 parents 5b38f50 + 623fe4d commit 3db41bd

17 files changed

+174
-19
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
Figures can be attached to and removed from pyplot
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Figures can now be attached to and removed from management through pyplot, which in
4+
the background also means a less strict coupling to backends.
5+
6+
In particular, standalone figures (created with the `.Figure` constructor) can now be
7+
registered with the `.pyplot` module by calling ``plt.figure(fig)``. This allows to
8+
show them with ``plt.show()`` as you would do with any figure created with pyplot
9+
factory methods such as ``plt.figure()`` or ``plt.subplots()``.
10+
11+
When closing a shown figure window, the related figure is reset to the standalone
12+
state, i.e. it's not visible to pyplot anymore, but if you still hold a reference
13+
to it, you can continue to work with it (e.g. do ``fig.savefig()``, or re-add it
14+
to pyplot with ``plt.figure(fig)`` and then show it again).
15+
16+
The following is now possible - though the example is exaggerated to show what's
17+
possible. In practice, you'll stick with much simpler versions for better
18+
consistency ::
19+
20+
import matplotlib.pyplot as plt
21+
from matplotlib.figure import Figure
22+
23+
# Create a standalone figure
24+
fig = Figure()
25+
ax = fig.add_subplot()
26+
ax.plot([1, 2, 3], [4, 5, 6])
27+
28+
# Register it with pyplot
29+
plt.figure(fig)
30+
31+
# Modify the figure through pyplot
32+
plt.xlabel("x label")
33+
34+
# Show the figure
35+
plt.show()
36+
37+
# Close the figure window through the GUI
38+
39+
# Continue to work on the figure
40+
fig.savefig("my_figure.png")
41+
ax.set_ylabel("y label")
42+
43+
# Re-register the figure and show it again
44+
plt.figure(fig)
45+
plt.show()
46+
47+
Technical detail: Standalone figures use `.FigureCanvasBase` as canvas. This is
48+
replaced by a backend-dependent subclass when registering with pyplot, and is
49+
reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses
50+
the current canvas to save the figure (if possible). Since `.FigureCanvasBase`
51+
can not render the figure, when saving the figure, it will fallback to a suitable
52+
canvas subclass, e.g. `.FigureCanvasAgg` for raster outputs such as png.
53+
Any Agg-based backend will create the same file output. However, there may be
54+
slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as
55+
interactive backend, ``fig.savefig("file.png")`` may create a slightly different
56+
image depending on whether the figure is registered with pyplot or not. In
57+
general, you should not store a reference to the canvas, but rather always
58+
obtain it from the figure with ``fig.canvas``. This will return the current
59+
canvas, which is either the original `.FigureCanvasBase` or a backend-dependent
60+
subclass, depending on whether the figure is registered with pyplot or not.
61+
Additionally, the swapping of the canvas currently does not play well with
62+
blitting of matplotlib widgets; in such cases either deactivate blitting or do not
63+
continue to use the figure (e.g. saving it after closing the window).

lib/matplotlib/_pylab_helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def destroy(cls, num):
5353
two managers share the same number.
5454
"""
5555
if all(hasattr(num, attr) for attr in ["num", "destroy"]):
56+
# num is a manager-like instance (not necessarily a
57+
# FigureManagerBase subclass)
5658
manager = num
5759
if cls.figs.get(manager.num) is manager:
5860
cls.figs.pop(manager.num)

lib/matplotlib/backend_bases.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2806,7 +2806,9 @@ def show(self):
28062806
f"shown")
28072807

28082808
def destroy(self):
2809-
pass
2809+
# managers may have swapped the canvas to a GUI-framework specific one.
2810+
# restore the base canvas when the manager is destroyed.
2811+
self.canvas.figure._set_base_canvas()
28102812

28112813
def full_screen_toggle(self):
28122814
pass

lib/matplotlib/backends/_backend_gtk.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def destroy(self, *args):
195195
self._destroying = True
196196
self.window.destroy()
197197
self.canvas.destroy()
198+
super().destroy()
198199

199200
@classmethod
200201
def start_main_loop(cls):

lib/matplotlib/backends/_backend_tk.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ def delayed_destroy():
634634
else:
635635
self.window.update()
636636
delayed_destroy()
637+
super().destroy()
637638

638639
def get_window_title(self):
639640
return self.window.wm_title()

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def __init__(self, figure=None):
9696

9797
def destroy(self):
9898
CloseEvent("close_event", self)._process()
99+
super().destroy()
99100

100101
def set_cursor(self, cursor):
101102
# docstring inherited

lib/matplotlib/backends/backend_nbagg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def destroy(self):
142142
for comm in list(self.web_sockets):
143143
comm.on_close()
144144
self.clearup_closed()
145+
super().destroy()
145146

146147
def clearup_closed(self):
147148
"""Clear up any closed Comms."""

lib/matplotlib/backends/backend_qt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ def destroy(self, *args):
674674
if self.toolbar:
675675
self.toolbar.destroy()
676676
self.window.close()
677+
super().destroy()
677678

678679
def get_window_title(self):
679680
return self.window.windowTitle()

lib/matplotlib/backends/backend_wx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,7 @@ def destroy(self, *args):
10121012
# As this can be called from non-GUI thread from plt.close use
10131013
# wx.CallAfter to ensure thread safety.
10141014
wx.CallAfter(frame.Close)
1015+
super().destroy()
10151016

10161017
def full_screen_toggle(self):
10171018
# docstring inherited

lib/matplotlib/figure.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2642,7 +2642,7 @@ def __init__(self,
26422642
self._set_artist_props(self.patch)
26432643
self.patch.set_antialiased(False)
26442644

2645-
FigureCanvasBase(self) # Set self.canvas.
2645+
self._set_base_canvas()
26462646

26472647
if subplotpars is None:
26482648
subplotpars = SubplotParams()
@@ -2996,6 +2996,20 @@ def get_constrained_layout_pads(self, relative=False):
29962996

29972997
return w_pad, h_pad, wspace, hspace
29982998

2999+
def _set_base_canvas(self):
3000+
"""
3001+
Initialize self.canvas with a FigureCanvasBase instance.
3002+
3003+
This is used upon initialization of the Figure, but also
3004+
to reset the canvas when decoupling from pyplot.
3005+
"""
3006+
# check if we have changed the DPI due to hi-dpi screens
3007+
orig_dpi = getattr(self, '_original_dpi', self._dpi)
3008+
FigureCanvasBase(self) # Set self.canvas as a side-effect
3009+
# put it back to what it was
3010+
if orig_dpi != self._dpi:
3011+
self.dpi = orig_dpi
3012+
29993013
def set_canvas(self, canvas):
30003014
"""
30013015
Set the canvas that contains the figure

0 commit comments

Comments
 (0)