1212to have smooth integration with the GUI event loop as with pyplot.
1313
1414"""
15- import logging
15+ from collections import Counter
1616import functools
17- from itertools import count
17+ import logging
18+ import warnings
19+ import weakref
1820
1921from matplotlib .backend_bases import FigureCanvasBase as _FigureCanvasBase
2022
@@ -68,7 +70,7 @@ def show(figs, *, block=None, timeout=0):
6870 if fig .canvas .manager is not None :
6971 managers .append (fig .canvas .manager )
7072 else :
71- managers .append (promote_figure (fig ))
73+ managers .append (promote_figure (fig , num = None ))
7274
7375 if block is None :
7476 block = not is_interactive ()
@@ -115,32 +117,41 @@ def __init__(self, *, block=None, timeout=0, prefix="Figure "):
115117 # settings stashed to set defaults on show
116118 self ._timeout = timeout
117119 self ._block = block
118- # Settings / state to control the default figure label
119- self ._count = count ()
120- self ._prefix = prefix
121120 # 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
123122 # 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 )
125130
126131 def _register_fig (self , fig ):
127132 # if the user closes the figure by any other mechanism, drop our
128133 # reference to it. This is important for getting a "pyplot" like user
129134 # 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 ))
136144 # Make sure we give the figure a quasi-unique label. We will never set
137145 # the same label twice, but will not over-ride any user label (but
138146 # empty string) on a Figure so if they provide duplicate labels, change
139147 # the labels under us, or provide a label that will be shadowed in the
140148 # future it will be what it is.
141- fignum = next (self ._count )
149+ fignum = max (self ._fig_to_number . values (), default = - 1 ) + 1
142150 if fig .get_label () == "" :
143151 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 )
144155 return fig
145156
146157 @property
@@ -150,7 +161,27 @@ def by_label(self):
150161
151162 If there are duplicate labels, newer figures will take precedence.
152163 """
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 }
154185
155186 @functools .wraps (figure )
156187 def figure (self , * args , ** kwargs ):
@@ -167,6 +198,11 @@ def subplot_mosaic(self, *args, **kwargs):
167198 fig , axd = subplot_mosaic (* args , ** kwargs )
168199 return self ._register_fig (fig ), axd
169200
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+
170206 def show_all (self , * , block = None , timeout = None ):
171207 """
172208 Show all of the Figures that the FigureRegistry knows about.
@@ -198,7 +234,7 @@ def show_all(self, *, block=None, timeout=None):
198234
199235 if timeout is None :
200236 timeout = self ._timeout
201-
237+ self . _ensure_all_figures_promoted ()
202238 show (self .figures , block = self ._block , timeout = self ._timeout )
203239
204240 # alias to easy pyplot compatibility
@@ -219,20 +255,62 @@ def close_all(self):
219255 passing it to `show`.
220256
221257 """
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 ()
225308 # disconnect figure from canvas
226309 fig .canvas .figure = None
227310 # disconnect canvas from figure
228311 _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 )
236314
237315
238316class FigureContext (FigureRegistry ):
0 commit comments