33# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
44# that changes made maintain expected behaviour.
55
6+ import datetime
67from base64 import b64encode
78import json
89import io
9- from tempfile import mkdtemp
10- import shutil
1110import os
1211import six
1312from uuid import uuid4 as uuid
1413
15- from IPython .display import display , HTML
16- from IPython import version_info
14+ import tornado .ioloop
15+
16+ from IPython .display import display , Javascript , HTML
1717try :
1818 # Jupyter/IPython 4.x or later
19- from ipywidgets import DOMWidget
20- from traitlets import Unicode , Bool , Float , List , Any
21- from notebook .nbextensions import install_nbextension , check_nbextension
19+ from ipykernel .comm import Comm
2220except ImportError :
2321 # Jupyter/IPython 3.x or earlier
24- from IPython .html .widgets import DOMWidget
25- from IPython .utils .traitlets import Unicode , Bool , Float , List , Any
26- from IPython .html .nbextensions import install_nbextension
22+ from IPython .kernel .comm import Comm
2723
2824from matplotlib import rcParams , is_interactive
2925from matplotlib ._pylab_helpers import Gcf
3329from matplotlib .backend_bases import (
3430 _Backend , FigureCanvasBase , NavigationToolbar2 )
3531from matplotlib .figure import Figure
32+ from matplotlib import is_interactive
33+ from matplotlib .backends .backend_webagg_core import (FigureManagerWebAgg ,
34+ FigureCanvasWebAggCore ,
35+ NavigationToolbar2WebAgg ,
36+ TimerTornado )
37+ from matplotlib .backend_bases import (ShowBase , NavigationToolbar2 ,
38+ FigureCanvasBase )
3639
3740
3841def connection_info ():
@@ -65,7 +68,6 @@ def connection_info():
6568 'zoom_to_rect' : 'fa fa-square-o icon-check-empty' ,
6669 'move' : 'fa fa-arrows icon-move' ,
6770 'download' : 'fa fa-floppy-o icon-save' ,
68- 'export' : 'fa fa-file-picture-o icon-picture' ,
6971 None : None
7072}
7173
@@ -77,154 +79,161 @@ class NavigationIPy(NavigationToolbar2WebAgg):
7779 _FONT_AWESOME_CLASSES [image_file ], name_of_method )
7880 for text , tooltip_text , image_file , name_of_method
7981 in (NavigationToolbar2 .toolitems +
80- (('Download' , 'Download plot' , 'download' , 'download' ),
81- ('Export' , 'Export plot' , 'export' , 'export' )))
82+ (('Download' , 'Download plot' , 'download' , 'download' ),))
8283 if image_file in _FONT_AWESOME_CLASSES ]
8384
84- def export (self ):
85- buf = io .BytesIO ()
86- self .canvas .figure .savefig (buf , format = 'png' , dpi = 'figure' )
87- # Figure width in pixels
88- pwidth = self .canvas .figure .get_figwidth ()* self .canvas .figure .get_dpi ()
89- # Scale size to match widget on HiPD monitors
90- width = pwidth / self .canvas ._dpi_ratio
91- data = "<img src='data:image/png;base64,{0}' width={1}/>"
92- data = data .format (b64encode (buf .getvalue ()).decode ('utf-8' ), width )
93- display (HTML (data ))
94-
95-
96- class FigureCanvasNbAgg (DOMWidget , FigureCanvasWebAggCore ):
97- _view_module = Unicode ("matplotlib" , sync = True )
98- _view_name = Unicode ('MPLCanvasView' , sync = True )
99- _toolbar_items = List (sync = True )
100- _closed = Bool (True )
101- _id = Unicode ('' , sync = True )
102-
103- # Must declare the superclass private members.
104- _png_is_old = Bool ()
105- _force_full = Bool ()
106- _current_image_mode = Unicode ()
107- _dpi_ratio = Float (1.0 )
108- _is_idle_drawing = Bool ()
109- _is_saving = Bool ()
110- _button = Any ()
111- _key = Any ()
112- _lastx = Any ()
113- _lasty = Any ()
114- _is_idle_drawing = Bool ()
115-
116- def __init__ (self , figure , * args , ** kwargs ):
117- super (FigureCanvasWebAggCore , self ).__init__ (figure , * args , ** kwargs )
118- super (DOMWidget , self ).__init__ (* args , ** kwargs )
119- self ._uid = uuid ().hex
120- self .on_msg (self ._handle_message )
121-
122- def _handle_message (self , object , message , buffers ):
123- # The 'supports_binary' message is relevant to the
124- # websocket itself. The other messages get passed along
125- # to matplotlib as-is.
126-
127- # Every message has a "type" and a "figure_id".
128- message = json .loads (message )
129- if message ['type' ] == 'closing' :
130- self ._closed = True
131- elif message ['type' ] == 'supports_binary' :
132- self .supports_binary = message ['value' ]
133- elif message ['type' ] == 'initialized' :
134- _ , _ , w , h = self .figure .bbox .bounds
135- self .manager .resize (w , h )
136- self .send_json ('refresh' )
137- else :
138- self .manager .handle_json (message )
139-
140- def send_json (self , content ):
141- self .send ({'data' : json .dumps (content )})
142-
143- def send_binary (self , blob ):
144- # The comm is ascii, so we always send the image in base64
145- # encoded data URL form.
146- data = b64encode (blob )
147- if six .PY3 :
148- data = data .decode ('ascii' )
149- data_uri = "data:image/png;base64,{0}" .format (data )
150- self .send ({'data' : data_uri })
151-
152- def new_timer (self , * args , ** kwargs ):
153- return TimerTornado (* args , ** kwargs )
154-
155- def start_event_loop (self , timeout ):
156- FigureCanvasBase .start_event_loop_default (self , timeout )
157-
158- def stop_event_loop (self ):
159- FigureCanvasBase .stop_event_loop_default (self )
160-
16185
16286class FigureManagerNbAgg (FigureManagerWebAgg ):
16387 ToolbarCls = NavigationIPy
16488
16589 def __init__ (self , canvas , num ):
90+ self ._shown = False
16691 FigureManagerWebAgg .__init__ (self , canvas , num )
167- toolitems = []
168- for name , tooltip , image , method in self .ToolbarCls .toolitems :
169- if name is None :
170- toolitems .append (['' , '' , '' , '' ])
171- else :
172- toolitems .append ([name , tooltip , image , method ])
173- canvas ._toolbar_items = toolitems
174- self .web_sockets = [self .canvas ]
92+
93+ def display_js (self ):
94+ # XXX How to do this just once? It has to deal with multiple
95+ # browser instances using the same kernel (require.js - but the
96+ # file isn't static?).
97+ display (Javascript (FigureManagerNbAgg .get_javascript ()))
17598
17699 def show (self ):
177- if self .canvas . _closed :
178- self .canvas . _closed = False
179- display ( self .canvas )
100+ if not self ._shown :
101+ self .display_js ()
102+ self ._create_comm ( )
180103 else :
181104 self .canvas .draw_idle ()
105+ self ._shown = True
106+
107+ def reshow (self ):
108+ """
109+ A special method to re-show the figure in the notebook.
110+
111+ """
112+ self ._shown = False
113+ self .show ()
114+
115+ @property
116+ def connected (self ):
117+ return bool (self .web_sockets )
118+
119+ @classmethod
120+ def get_javascript (cls , stream = None ):
121+ if stream is None :
122+ output = io .StringIO ()
123+ else :
124+ output = stream
125+ super (FigureManagerNbAgg , cls ).get_javascript (stream = output )
126+ with io .open (os .path .join (
127+ os .path .dirname (__file__ ),
128+ "web_backend" ,
129+ "nbagg_mpl.js" ), encoding = 'utf8' ) as fd :
130+ output .write (fd .read ())
131+ if stream is None :
132+ return output .getvalue ()
133+
134+ def _create_comm (self ):
135+ comm = CommSocket (self )
136+ self .add_web_socket (comm )
137+ return comm
182138
183139 def destroy (self ):
184140 self ._send_event ('close' )
141+ # need to copy comms as callbacks will modify this list
142+ for comm in list (self .web_sockets ):
143+ comm .on_close ()
144+ self .clearup_closed ()
145+
146+ def clearup_closed (self ):
147+ """Clear up any closed Comms."""
148+ self .web_sockets = set ([socket for socket in self .web_sockets
149+ if socket .is_open ()])
150+
151+ if len (self .web_sockets ) == 0 :
152+ self .canvas .close_event ()
153+
154+ def remove_comm (self , comm_id ):
155+ self .web_sockets = set ([socket for socket in self .web_sockets
156+ if not socket .comm .comm_id == comm_id ])
157+
158+
159+ class FigureCanvasNbAgg (FigureCanvasWebAggCore ):
160+ def new_timer (self , * args , ** kwargs ):
161+ return TimerTornado (* args , ** kwargs )
185162
186163
187- def nbinstall ( overwrite = False , user = True ):
164+ class CommSocket ( object ):
188165 """
189- Copies javascript dependencies to the '/nbextensions' folder in
190- your IPython directory.
191-
192- Parameters
193- ----------
194-
195- overwrite : bool
196- If True, always install the files, regardless of what may already be
197- installed. Defaults to False.
198- user : bool
199- Whether to install to the user's .ipython/nbextensions directory.
200- Otherwise do a system-wide install
201- (e.g. /usr/local/share/jupyter/nbextensions). Defaults to False.
166+ Manages the Comm connection between IPython and the browser (client).
167+
168+ Comms are 2 way, with the CommSocket being able to publish a message
169+ via the send_json method, and handle a message with on_message. On the
170+ JS side figure.send_message and figure.ws.onmessage do the sending and
171+ receiving respectively.
172+
202173 """
203- if (check_nbextension ('matplotlib' ) or
204- check_nbextension ('matplotlib' , True )):
205- return
206-
207- # Make a temporary directory so we can wrap mpl.js in a requirejs define().
208- tempdir = mkdtemp ()
209- path = os .path .join (os .path .dirname (__file__ ), "web_backend" )
210- shutil .copy2 (os .path .join (path , "nbagg_mpl.js" ), tempdir )
211-
212- with open (os .path .join (path , 'mpl.js' )) as fid :
213- contents = fid .read ()
214-
215- with open (os .path .join (tempdir , 'mpl.js' ), 'w' ) as fid :
216- fid .write ('define(["jquery"], function($) {\n ' )
217- fid .write (contents )
218- fid .write ('\n return mpl;\n });' )
219-
220- install_nbextension (
221- tempdir ,
222- overwrite = overwrite ,
223- symlink = False ,
224- destination = 'matplotlib' ,
225- verbose = 0 ,
226- ** ({'user' : user } if version_info >= (3 , 0 , 0 , '' ) else {})
227- )
174+ def __init__ (self , manager ):
175+ self .supports_binary = None
176+ self .manager = manager
177+ self .uuid = str (uuid ())
178+ # Publish an output area with a unique ID. The javascript can then
179+ # hook into this area.
180+ display (HTML ("<div id=%r></div>" % self .uuid ))
181+ try :
182+ self .comm = Comm ('matplotlib' , data = {'id' : self .uuid })
183+ except AttributeError :
184+ raise RuntimeError ('Unable to create an IPython notebook Comm '
185+ 'instance. Are you in the IPython notebook?' )
186+ self .comm .on_msg (self .on_message )
187+
188+ manager = self .manager
189+ self ._ext_close = False
190+
191+ def _on_close (close_message ):
192+ self ._ext_close = True
193+ manager .remove_comm (close_message ['content' ]['comm_id' ])
194+ manager .clearup_closed ()
195+
196+ self .comm .on_close (_on_close )
197+
198+ def is_open (self ):
199+ return not (self ._ext_close or self .comm ._closed )
200+
201+ def on_close (self ):
202+ # When the socket is closed, deregister the websocket with
203+ # the FigureManager.
204+ if self .is_open ():
205+ try :
206+ self .comm .close ()
207+ except KeyError :
208+ # apparently already cleaned it up?
209+ pass
210+
211+ def send_json (self , content ):
212+ self .comm .send ({'data' : json .dumps (content )})
213+
214+ def send_binary (self , blob ):
215+ # The comm is ascii, so we always send the image in base64
216+ # encoded data URL form.
217+ data = b64encode (blob )
218+ if six .PY3 :
219+ data = data .decode ('ascii' )
220+ data_uri = "data:image/png;base64,{0}" .format (data )
221+ self .comm .send ({'data' : data_uri })
222+
223+ def on_message (self , message ):
224+ # The 'supports_binary' message is relevant to the
225+ # websocket itself. The other messages get passed along
226+ # to matplotlib as-is.
227+
228+ # Every message has a "type" and a "figure_id".
229+ message = json .loads (message ['content' ]['data' ])
230+ if message ['type' ] == 'closing' :
231+ self .on_close ()
232+ self .manager .clearup_closed ()
233+ elif message ['type' ] == 'supports_binary' :
234+ self .supports_binary = message ['value' ]
235+ else :
236+ self .manager .handle_json (message )
228237
229238
230239@_Backend .export
0 commit comments