12
12
to have smooth integration with the GUI event loop as with pyplot.
13
13
14
14
"""
15
- import logging
15
+ from collections import Counter
16
16
import functools
17
- from itertools import count
17
+ import logging
18
+ import warnings
19
+ import weakref
18
20
19
21
from matplotlib .backend_bases import FigureCanvasBase as _FigureCanvasBase
20
22
@@ -68,7 +70,7 @@ def show(figs, *, block=None, timeout=0):
68
70
if fig .canvas .manager is not None :
69
71
managers .append (fig .canvas .manager )
70
72
else :
71
- managers .append (promote_figure (fig ))
73
+ managers .append (promote_figure (fig , num = None ))
72
74
73
75
if block is None :
74
76
block = not is_interactive ()
@@ -115,32 +117,41 @@ def __init__(self, *, block=None, timeout=0, prefix="Figure "):
115
117
# settings stashed to set defaults on show
116
118
self ._timeout = timeout
117
119
self ._block = block
118
- # Settings / state to control the default figure label
119
- self ._count = count ()
120
- self ._prefix = prefix
121
120
# the canonical location for storing the Figures this registry owns.
122
- # any additional views must never include a figure not in the list but
121
+ # any additional views must never include a figure that is not a key but
123
122
# may omit figures
124
- self .figures = []
123
+ self ._fig_to_number = dict ()
124
+ # Settings / state to control the default figure label
125
+ self ._prefix = prefix
126
+
127
+ @property
128
+ def figures (self ):
129
+ return tuple (self ._fig_to_number )
125
130
126
131
def _register_fig (self , fig ):
127
132
# if the user closes the figure by any other mechanism, drop our
128
133
# reference to it. This is important for getting a "pyplot" like user
129
134
# experience
130
- fig .canvas .mpl_connect (
131
- "close_event" ,
132
- lambda e : self .figures .remove (fig ) if fig in self .figures else None ,
133
- )
134
- # hold a hard reference to the figure.
135
- self .figures .append (fig )
135
+ def registry_cleanup (fig_wr ):
136
+ fig = fig_wr ()
137
+ if fig is not None :
138
+ if fig .canvas is not None :
139
+ fig .canvas .mpl_disconnect (cid )
140
+ self .close (fig )
141
+
142
+ fig_wr = weakref .ref (fig )
143
+ cid = fig .canvas .mpl_connect ("close_event" , lambda e : registry_cleanup (fig_wr ))
136
144
# Make sure we give the figure a quasi-unique label. We will never set
137
145
# the same label twice, but will not over-ride any user label (but
138
146
# empty string) on a Figure so if they provide duplicate labels, change
139
147
# the labels under us, or provide a label that will be shadowed in the
140
148
# future it will be what it is.
141
- fignum = next (self ._count )
149
+ fignum = max (self ._fig_to_number . values (), default = - 1 ) + 1
142
150
if fig .get_label () == "" :
143
151
fig .set_label (f"{ self ._prefix } { fignum :d} " )
152
+ self ._fig_to_number [fig ] = fignum
153
+ if is_interactive ():
154
+ promote_figure (fig , num = fignum )
144
155
return fig
145
156
146
157
@property
@@ -150,7 +161,27 @@ def by_label(self):
150
161
151
162
If there are duplicate labels, newer figures will take precedence.
152
163
"""
153
- return {fig .get_label (): fig for fig in self .figures }
164
+ mapping = {fig .get_label (): fig for fig in self .figures }
165
+ if len (mapping ) != len (self .figures ):
166
+ counts = Counter (fig .get_label () for fig in self .figures )
167
+ multiples = {k : v for k , v in counts .items () if v > 1 }
168
+ warnings .warn (
169
+ (
170
+ f"There are repeated labels ({ multiples !r} ), but only the newest figure with that label can "
171
+ "be returned. "
172
+ ),
173
+ stacklevel = 2 ,
174
+ )
175
+ return mapping
176
+
177
+ @property
178
+ def by_number (self ):
179
+ """
180
+ Return a dictionary of the current mapping number -> figures.
181
+
182
+ """
183
+ self ._ensure_all_figures_promoted ()
184
+ return {fig .canvas .manager .num : fig for fig in self .figures }
154
185
155
186
@functools .wraps (figure )
156
187
def figure (self , * args , ** kwargs ):
@@ -167,6 +198,11 @@ def subplot_mosaic(self, *args, **kwargs):
167
198
fig , axd = subplot_mosaic (* args , ** kwargs )
168
199
return self ._register_fig (fig ), axd
169
200
201
+ def _ensure_all_figures_promoted (self ):
202
+ for f in self .figures :
203
+ if f .canvas .manager is None :
204
+ promote_figure (f , num = self ._fig_to_number [f ])
205
+
170
206
def show_all (self , * , block = None , timeout = None ):
171
207
"""
172
208
Show all of the Figures that the FigureRegistry knows about.
@@ -198,7 +234,7 @@ def show_all(self, *, block=None, timeout=None):
198
234
199
235
if timeout is None :
200
236
timeout = self ._timeout
201
-
237
+ self . _ensure_all_figures_promoted ()
202
238
show (self .figures , block = self ._block , timeout = self ._timeout )
203
239
204
240
# alias to easy pyplot compatibility
@@ -219,20 +255,62 @@ def close_all(self):
219
255
passing it to `show`.
220
256
221
257
"""
222
- for fig in self .figures :
223
- if fig .canvas .manager is not None :
224
- fig .canvas .manager .destroy ()
258
+ for fig in list (self .figures ):
259
+ self .close (fig )
260
+
261
+ def close (self , val ):
262
+ """
263
+ Close (meaning destroy the UI) and forget a managed Figure.
264
+
265
+ This will do two things:
266
+
267
+ - start the destruction process of an UI (the event loop may need to
268
+ run to complete this process and if the user is holding hard
269
+ references to any of the UI elements they may remain alive).
270
+ - Remove the `Figure` from this Registry.
271
+
272
+ We will no longer have any hard references to the Figure, but if
273
+ the user does the `Figure` (and its components) will not be garbage
274
+ collected. Due to the circular references in Matplotlib these
275
+ objects may not be collected until the full cyclic garbage collection
276
+ runs.
277
+
278
+ If the user still has a reference to the `Figure` they can re-show the
279
+ figure via `show`, but the `FigureRegistry` will not be aware of it.
280
+
281
+ Parameters
282
+ ----------
283
+ val : 'all' or int or str or Figure
284
+
285
+ - The special case of 'all' closes all open Figures
286
+ - If any other string is passed, it is interpreted as a key in
287
+ `by_label` and that Figure is closed
288
+ - If an integer it is interpreted as a key in `by_number` and that
289
+ Figure is closed
290
+ - If it is a `Figure` instance, then that figure is closed
291
+
292
+ """
293
+ if val == "all" :
294
+ return self .close_all ()
295
+ # or do we want to close _all_ of the figures with a given label / number?
296
+ if isinstance (val , str ):
297
+ fig = self .by_label [val ]
298
+ elif isinstance (val , int ):
299
+ fig = self .by_number [val ]
300
+ else :
301
+ fig = val
302
+ if fig not in self .figures :
303
+ raise ValueError (
304
+ "Trying to close a figure not associated with this Registry."
305
+ )
306
+ if fig .canvas .manager is not None :
307
+ fig .canvas .manager .destroy ()
225
308
# disconnect figure from canvas
226
309
fig .canvas .figure = None
227
310
# disconnect canvas from figure
228
311
_FigureCanvasBase (figure = fig )
229
- self .figures .clear ()
230
-
231
- def close (self , val ):
232
- if val != "all" :
233
- # TODO close figures 1 at a time
234
- raise RuntimeError ("can only close them all" )
235
- self .close_all ()
312
+ assert fig .canvas .manager is None
313
+ self ._fig_to_number .pop (fig , None )
236
314
237
315
238
316
class FigureContext (FigureRegistry ):
0 commit comments