5
5
import io
6
6
7
7
from IPython .display import display , HTML
8
+ from IPython import get_ipython
9
+ from IPython import version_info as ipython_version_info
8
10
9
11
from ipywidgets import DOMWidget , widget_serialization
10
12
from traitlets import (
11
- Unicode , Bool , CInt , Float , List , Instance , CaselessStrEnum , Enum ,
13
+ Unicode , Bool , CInt , List , Instance , CaselessStrEnum , Enum ,
12
14
default
13
15
)
14
16
15
17
import matplotlib
16
- from matplotlib import rcParams
17
- from matplotlib import is_interactive
18
+ from matplotlib import rcParams , is_interactive
18
19
from matplotlib .backends .backend_webagg_core import (FigureManagerWebAgg ,
19
20
FigureCanvasWebAggCore ,
20
21
NavigationToolbar2WebAgg ,
@@ -40,7 +41,6 @@ def connection_info():
40
41
use.
41
42
42
43
"""
43
- from matplotlib ._pylab_helpers import Gcf
44
44
result = []
45
45
for manager in Gcf .get_all_fig_managers ():
46
46
fig = manager .canvas .figure
@@ -83,16 +83,8 @@ def __init__(self, canvas, *args, **kwargs):
83
83
def export (self ):
84
84
buf = io .BytesIO ()
85
85
self .canvas .figure .savefig (buf , format = 'png' , dpi = 'figure' )
86
- # Figure width in pixels
87
- pwidth = (self .canvas .figure .get_figwidth () *
88
- self .canvas .figure .get_dpi ())
89
- # Scale size to match widget on HiDPI monitors.
90
- if hasattr (self .canvas , 'device_pixel_ratio' ): # Matplotlib 3.5+
91
- width = pwidth / self .canvas .device_pixel_ratio
92
- else :
93
- width = pwidth / self .canvas ._dpi_ratio
94
- data = "<img src='data:image/png;base64,{0}' width={1}/>"
95
- data = data .format (b64encode (buf .getvalue ()).decode ('utf-8' ), width )
86
+ data = "<img src='data:image/png;base64,{0}'/>"
87
+ data = data .format (b64encode (buf .getvalue ()).decode ('utf-8' ))
96
88
display (HTML (data ))
97
89
98
90
@default ('toolitems' )
@@ -160,7 +152,9 @@ class Canvas(DOMWidget, FigureCanvasWebAggCore):
160
152
_png_is_old = Bool ()
161
153
_force_full = Bool ()
162
154
_current_image_mode = Unicode ()
163
- _dpi_ratio = Float (1.0 )
155
+
156
+ # Static as it should be the same for all canvases
157
+ current_dpi_ratio = 1.0
164
158
165
159
def __init__ (self , figure , * args , ** kwargs ):
166
160
DOMWidget .__init__ (self , * args , ** kwargs )
@@ -172,9 +166,15 @@ def _handle_message(self, object, content, buffers):
172
166
# Every content has a "type".
173
167
if content ['type' ] == 'closing' :
174
168
self ._closed = True
169
+
175
170
elif content ['type' ] == 'initialized' :
176
171
_ , _ , w , h = self .figure .bbox .bounds
177
172
self .manager .resize (w , h )
173
+
174
+ elif content ['type' ] == 'set_dpi_ratio' :
175
+ Canvas .current_dpi_ratio = content ['dpi_ratio' ]
176
+ self .manager .handle_json (content )
177
+
178
178
else :
179
179
self .manager .handle_json (content )
180
180
@@ -208,6 +208,41 @@ def send_binary(self, data):
208
208
def new_timer (self , * args , ** kwargs ):
209
209
return TimerTornado (* args , ** kwargs )
210
210
211
+ def _repr_mimebundle_ (self , ** kwargs ):
212
+ # now happens before the actual display call.
213
+ if hasattr (self , '_handle_displayed' ):
214
+ self ._handle_displayed (** kwargs )
215
+ plaintext = repr (self )
216
+ if len (plaintext ) > 110 :
217
+ plaintext = plaintext [:110 ] + '…'
218
+
219
+ buf = io .BytesIO ()
220
+ self .figure .savefig (buf , format = 'png' , dpi = 'figure' )
221
+ data_url = b64encode (buf .getvalue ()).decode ('utf-8' )
222
+
223
+ data = {
224
+ 'text/plain' : plaintext ,
225
+ 'image/png' : data_url ,
226
+ 'application/vnd.jupyter.widget-view+json' : {
227
+ 'version_major' : 2 ,
228
+ 'version_minor' : 0 ,
229
+ 'model_id' : self ._model_id
230
+ }
231
+ }
232
+
233
+ return data
234
+
235
+ def _ipython_display_ (self , ** kwargs ):
236
+ """Called when `IPython.display.display` is called on a widget.
237
+ Note: if we are in IPython 6.1 or later, we return NotImplemented so
238
+ that _repr_mimebundle_ is used directly.
239
+ """
240
+ if ipython_version_info >= (6 , 1 ):
241
+ raise NotImplementedError
242
+
243
+ data = self ._repr_mimebundle_ (** kwargs )
244
+ display (data , raw = True )
245
+
211
246
if matplotlib .__version__ < '3.4' :
212
247
# backport the Python side changes to match the js changes
213
248
def _handle_key (self , event ):
@@ -294,14 +329,18 @@ class _Backend_ipympl(_Backend):
294
329
FigureCanvas = Canvas
295
330
FigureManager = FigureManager
296
331
332
+ _to_show = []
333
+ _draw_called = False
334
+
297
335
@staticmethod
298
336
def new_figure_manager_given_figure (num , figure ):
299
337
canvas = Canvas (figure )
300
338
if 'nbagg.transparent' in rcParams and rcParams ['nbagg.transparent' ]:
301
339
figure .patch .set_alpha (0 )
302
340
manager = FigureManager (canvas , num )
341
+
303
342
if is_interactive ():
304
- manager . show ( )
343
+ _Backend_ipympl . _to_show . append ( figure )
305
344
figure .canvas .draw_idle ()
306
345
307
346
def destroy (event ):
@@ -312,17 +351,17 @@ def destroy(event):
312
351
return manager
313
352
314
353
@staticmethod
315
- def show (block = None ):
316
- # TODO: something to do when keyword block==False ?
354
+ def show (close = None , block = None ):
355
+ # # TODO: something to do when keyword block==False ?
356
+ interactive = is_interactive ()
317
357
318
- managers = Gcf .get_all_fig_managers ()
319
- if not managers :
358
+ manager = Gcf .get_active ()
359
+ if manager is None :
320
360
return
321
361
322
- interactive = is_interactive ()
323
-
324
- for manager in managers :
325
- manager .show ()
362
+ try :
363
+ display (manager .canvas )
364
+ # metadata=_fetch_figure_metadata(manager.canvas.figure)
326
365
327
366
# plt.figure adds an event which makes the figure in focus the
328
367
# active one. Disable this behaviour, as it results in
@@ -333,3 +372,57 @@ def show(block=None):
333
372
334
373
if not interactive :
335
374
Gcf .figs .pop (manager .num , None )
375
+ finally :
376
+ if manager .canvas .figure in _Backend_ipympl ._to_show :
377
+ _Backend_ipympl ._to_show .remove (manager .canvas .figure )
378
+
379
+ @staticmethod
380
+ def draw_if_interactive ():
381
+ # If matplotlib was manually set to non-interactive mode, this function
382
+ # should be a no-op (otherwise we'll generate duplicate plots, since a
383
+ # user who set ioff() manually expects to make separate draw/show
384
+ # calls).
385
+ if not is_interactive ():
386
+ return
387
+
388
+ manager = Gcf .get_active ()
389
+ if manager is None :
390
+ return
391
+ fig = manager .canvas .figure
392
+
393
+ # ensure current figure will be drawn, and each subsequent call
394
+ # of draw_if_interactive() moves the active figure to ensure it is
395
+ # drawn last
396
+ try :
397
+ _Backend_ipympl ._to_show .remove (fig )
398
+ except ValueError :
399
+ # ensure it only appears in the draw list once
400
+ pass
401
+ # Queue up the figure for drawing in next show() call
402
+ _Backend_ipympl ._to_show .append (fig )
403
+ _Backend_ipympl ._draw_called = True
404
+
405
+
406
+ def flush_figures ():
407
+ if rcParams ['backend' ] == 'module://ipympl.backend_nbagg' :
408
+ if not _Backend_ipympl ._draw_called :
409
+ return
410
+
411
+ try :
412
+ # exclude any figures that were closed:
413
+ active = set ([
414
+ fm .canvas .figure for fm in Gcf .get_all_fig_managers ()
415
+ ])
416
+
417
+ for fig in [
418
+ fig for fig in _Backend_ipympl ._to_show if fig in active ]:
419
+ # display(fig.canvas, metadata=_fetch_figure_metadata(fig))
420
+ display (fig .canvas )
421
+ finally :
422
+ # clear flags for next round
423
+ _Backend_ipympl ._to_show = []
424
+ _Backend_ipympl ._draw_called = False
425
+
426
+
427
+ ip = get_ipython ()
428
+ ip .events .register ('post_execute' , flush_figures )
0 commit comments