1919import io
2020import json
2121from base64 import b64encode
22+ from threading import Lock
2223
2324try :
2425 from collections .abc import Iterable
6768 cursors .WAIT : 'wait' ,
6869}
6970
71+ # threading.Lock to prevent multiple threads from accessing globals such as Gcf
72+ _lock = Lock ()
73+
74+
7075
7176def connection_info ():
7277 """
@@ -75,18 +80,19 @@ def connection_info():
7580 use.
7681
7782 """
78- result = []
79- for manager in Gcf .get_all_fig_managers ():
80- fig = manager .canvas .figure
81- result .append (
82- '{} - {}' .format (
83- (fig .get_label () or f"Figure { manager .num } " ),
84- manager .web_sockets ,
83+ with _lock :
84+ result = []
85+ for manager in Gcf .get_all_fig_managers ():
86+ fig = manager .canvas .figure
87+ result .append (
88+ '{} - {}' .format (
89+ (fig .get_label () or f"Figure { manager .num } " ),
90+ manager .web_sockets ,
91+ )
8592 )
86- )
87- if not is_interactive ():
88- result .append (f'Figures pending show: { len (Gcf ._activeQue )} ' )
89- return '\n ' .join (result )
93+ if not is_interactive ():
94+ result .append (f'Figures pending show: { len (Gcf ._activeQue )} ' )
95+ return '\n ' .join (result )
9096
9197
9298class Toolbar (DOMWidget , NavigationToolbar2WebAgg ):
@@ -134,7 +140,8 @@ def export(self):
134140 width = pwidth / self .canvas ._dpi_ratio
135141 data = "<img src='data:image/png;base64,{0}' width={1}/>"
136142 data = data .format (b64encode (buf .getvalue ()).decode ('utf-8' ), width )
137- display (HTML (data ))
143+ with _lock :
144+ display (HTML (data ))
138145
139146 @default ('toolitems' )
140147 def _default_toolitems (self ):
@@ -397,7 +404,8 @@ def __init__(self, canvas, num):
397404 def show (self ):
398405 if self .canvas ._closed :
399406 self .canvas ._closed = False
400- display (self .canvas )
407+ with _lock :
408+ display (self .canvas )
401409 else :
402410 self .canvas .draw_idle ()
403411
@@ -415,83 +423,86 @@ class _Backend_ipympl(_Backend):
415423
416424 @staticmethod
417425 def new_figure_manager_given_figure (num , figure ):
418- canvas = Canvas (figure )
419- if 'nbagg.transparent' in rcParams and rcParams ['nbagg.transparent' ]:
420- figure .patch .set_alpha (0 )
421- manager = FigureManager (canvas , num )
422-
423- if is_interactive ():
424- _Backend_ipympl ._to_show .append (figure )
425- figure .canvas .draw_idle ()
426-
427- def destroy (event ):
428- canvas .mpl_disconnect (cid )
429- Gcf .destroy (manager )
430-
431- cid = canvas .mpl_connect ('close_event' , destroy )
432-
433- # Only register figure for showing when in interactive mode (otherwise
434- # we'll generate duplicate plots, since a user who set ioff() manually
435- # expects to make separate draw/show calls).
436- if is_interactive ():
437- # ensure current figure will be drawn.
438- try :
439- _Backend_ipympl ._to_show .remove (figure )
440- except ValueError :
441- # ensure it only appears in the draw list once
442- pass
443- # Queue up the figure for drawing in next show() call
444- _Backend_ipympl ._to_show .append (figure )
445- _Backend_ipympl ._draw_called = True
446-
447- return manager
426+ with _lock :
427+ canvas = Canvas (figure )
428+ if 'nbagg.transparent' in rcParams and rcParams ['nbagg.transparent' ]:
429+ figure .patch .set_alpha (0 )
430+ manager = FigureManager (canvas , num )
431+
432+ if is_interactive ():
433+ _Backend_ipympl ._to_show .append (figure )
434+ figure .canvas .draw_idle ()
435+
436+ def destroy (event ):
437+ canvas .mpl_disconnect (cid )
438+ Gcf .destroy (manager )
439+
440+ cid = canvas .mpl_connect ('close_event' , destroy )
441+
442+ # Only register figure for showing when in interactive mode (otherwise
443+ # we'll generate duplicate plots, since a user who set ioff() manually
444+ # expects to make separate draw/show calls).
445+ if is_interactive ():
446+ # ensure current figure will be drawn.
447+ try :
448+ _Backend_ipympl ._to_show .remove (figure )
449+ except ValueError :
450+ # ensure it only appears in the draw list once
451+ pass
452+ # Queue up the figure for drawing in next show() call
453+ _Backend_ipympl ._to_show .append (figure )
454+ _Backend_ipympl ._draw_called = True
455+
456+ return manager
448457
449458 @staticmethod
450459 def show (block = None ):
451- # # TODO: something to do when keyword block==False ?
452- interactive = is_interactive ()
460+ with _lock :
461+ # # TODO: something to do when keyword block==False ?
462+ interactive = is_interactive ()
453463
454- manager = Gcf .get_active ()
455- if manager is None :
456- return
464+ manager = Gcf .get_active ()
465+ if manager is None :
466+ return
457467
458- try :
459- display (manager .canvas )
460- # metadata=_fetch_figure_metadata(manager.canvas.figure)
468+ try :
469+ display (manager .canvas )
470+ # metadata=_fetch_figure_metadata(manager.canvas.figure)
461471
462- # plt.figure adds an event which makes the figure in focus the
463- # active one. Disable this behaviour, as it results in
464- # figures being put as the active figure after they have been
465- # shown, even in non-interactive mode.
466- if hasattr (manager , '_cidgcf' ):
467- manager .canvas .mpl_disconnect (manager ._cidgcf )
472+ # plt.figure adds an event which makes the figure in focus the
473+ # active one. Disable this behaviour, as it results in
474+ # figures being put as the active figure after they have been
475+ # shown, even in non-interactive mode.
476+ if hasattr (manager , '_cidgcf' ):
477+ manager .canvas .mpl_disconnect (manager ._cidgcf )
468478
469- if not interactive :
470- Gcf .figs .pop (manager .num , None )
471- finally :
472- if manager .canvas .figure in _Backend_ipympl ._to_show :
473- _Backend_ipympl ._to_show .remove (manager .canvas .figure )
479+ if not interactive :
480+ Gcf .figs .pop (manager .num , None )
481+ finally :
482+ if manager .canvas .figure in _Backend_ipympl ._to_show :
483+ _Backend_ipympl ._to_show .remove (manager .canvas .figure )
474484
475485
476486def flush_figures ():
477- backend = matplotlib .get_backend ()
478- if backend in ('widget' , 'ipympl' , 'module://ipympl.backend_nbagg' ):
479- if not _Backend_ipympl ._draw_called :
480- return
481-
482- try :
483- # exclude any figures that were closed:
484- active = {fm .canvas .figure for fm in Gcf .get_all_fig_managers ()}
485-
486- for fig in [fig for fig in _Backend_ipympl ._to_show if fig in active ]:
487- # display(fig.canvas, metadata=_fetch_figure_metadata(fig))
488- display (fig .canvas )
489- finally :
490- # clear flags for next round
491- _Backend_ipympl ._to_show = []
492- _Backend_ipympl ._draw_called = False
493-
494-
495- ip = get_ipython ()
496- if ip is not None :
497- ip .events .register ('post_execute' , flush_figures )
487+ with _lock :
488+ backend = matplotlib .get_backend ()
489+ if backend in ('widget' , 'ipympl' , 'module://ipympl.backend_nbagg' ):
490+ if not _Backend_ipympl ._draw_called :
491+ return
492+
493+ try :
494+ # exclude any figures that were closed:
495+ active = {fm .canvas .figure for fm in Gcf .get_all_fig_managers ()}
496+ for fig in [fig for fig in _Backend_ipympl ._to_show if fig in active ]:
497+ # display(fig.canvas, metadata=_fetch_figure_metadata(fig))
498+ display (fig .canvas )
499+ finally :
500+ # clear flags for next round
501+ _Backend_ipympl ._to_show = []
502+ _Backend_ipympl ._draw_called = False
503+
504+
505+ with _lock :
506+ ip = get_ipython ()
507+ if ip is not None :
508+ ip .events .register ('post_execute' , flush_figures )
0 commit comments