33# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
44# that changes made maintain expected behaviour.
55
6- import datetime
76from base64 import b64encode
87import json
98import io
9+ from tempfile import mkdtemp
10+ import shutil
1011import os
1112from matplotlib .externals import six
1213from uuid import uuid4 as uuid
1314
14- import tornado .ioloop
15-
16- from IPython .display import display , Javascript , HTML
15+ from IPython .display import display , HTML
16+ from IPython import version_info
1717try :
1818 # Jupyter/IPython 4.x or later
19- from ipykernel .comm import Comm
19+ from ipywidgets import DOMWidget
20+ from traitlets import Unicode , Bool , Float , List , Any
21+ from notebook .nbextensions import install_nbextension , check_nbextension
2022except ImportError :
2123 # Jupyter/IPython 3.x or earlier
22- from IPython .kernel .comm import Comm
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
2327
2428from matplotlib import rcParams
2529from matplotlib .figure import Figure
3337
3438
3539class Show (ShowBase ):
40+
3641 def __call__ (self , block = None ):
3742 from matplotlib ._pylab_helpers import Gcf
3843
@@ -98,6 +103,7 @@ def connection_info():
98103 'zoom_to_rect' : 'fa fa-square-o icon-check-empty' ,
99104 'move' : 'fa fa-arrows icon-move' ,
100105 'download' : 'fa fa-floppy-o icon-save' ,
106+ 'export' : 'fa fa-file-picture-o icon-picture' ,
101107 None : None
102108}
103109
@@ -109,84 +115,74 @@ class NavigationIPy(NavigationToolbar2WebAgg):
109115 _FONT_AWESOME_CLASSES [image_file ], name_of_method )
110116 for text , tooltip_text , image_file , name_of_method
111117 in (NavigationToolbar2 .toolitems +
112- (('Download' , 'Download plot' , 'download' , 'download' ),))
118+ (('Download' , 'Download plot' , 'download' , 'download' ),
119+ ('Export' , 'Export plot' , 'export' , 'export' )))
113120 if image_file in _FONT_AWESOME_CLASSES ]
114121
122+ def export (self ):
123+ buf = io .BytesIO ()
124+ self .canvas .figure .savefig (buf , format = 'png' , dpi = 'figure' )
125+ data = "<img src='data:image/png;base64,{0}'/>"
126+ data = data .format (b64encode (buf .getvalue ()).decode ('utf-8' ))
127+ display (HTML (data ))
128+
129+
130+ class FigureCanvasNbAgg (DOMWidget , FigureCanvasWebAggCore ):
131+ _view_module = Unicode ("nbextensions/matplotlib/nbagg_mpl" , sync = True )
132+ _view_name = Unicode ('MPLCanvasView' , sync = True )
133+ _toolbar_items = List (sync = True )
134+ _closed = Bool (True )
135+ _id = Unicode ('' , sync = True )
136+
137+ # Must declare the superclass private members.
138+ _png_is_old = Bool ()
139+ _force_full = Bool ()
140+ _current_image_mode = Unicode ()
141+ _dpi_ratio = Float (1.0 )
142+ _is_idle_drawing = Bool ()
143+ _is_saving = Bool ()
144+ _button = Any ()
145+ _key = Any ()
146+ _lastx = Any ()
147+ _lasty = Any ()
148+ _is_idle_drawing = Bool ()
149+
150+ def __init__ (self , figure , * args , ** kwargs ):
151+ super (FigureCanvasWebAggCore , self ).__init__ (figure , * args , ** kwargs )
152+ super (DOMWidget , self ).__init__ (* args , ** kwargs )
153+ self ._uid = uuid ().hex
154+ self .on_msg (self ._handle_message )
155+
156+ def _handle_message (self , object , message , buffers ):
157+ # The 'supports_binary' message is relevant to the
158+ # websocket itself. The other messages get passed along
159+ # to matplotlib as-is.
115160
116- class FigureManagerNbAgg (FigureManagerWebAgg ):
117- ToolbarCls = NavigationIPy
118-
119- def __init__ (self , canvas , num ):
120- self ._shown = False
121- FigureManagerWebAgg .__init__ (self , canvas , num )
122-
123- def display_js (self ):
124- # XXX How to do this just once? It has to deal with multiple
125- # browser instances using the same kernel (require.js - but the
126- # file isn't static?).
127- display (Javascript (FigureManagerNbAgg .get_javascript ()))
128-
129- def show (self ):
130- if not self ._shown :
131- self .display_js ()
132- self ._create_comm ()
133- else :
134- self .canvas .draw_idle ()
135- self ._shown = True
136-
137- def reshow (self ):
138- """
139- A special method to re-show the figure in the notebook.
140-
141- """
142- self ._shown = False
143- self .show ()
144-
145- @property
146- def connected (self ):
147- return bool (self .web_sockets )
148-
149- @classmethod
150- def get_javascript (cls , stream = None ):
151- if stream is None :
152- output = io .StringIO ()
161+ # Every message has a "type" and a "figure_id".
162+ message = json .loads (message )
163+ if message ['type' ] == 'closing' :
164+ self ._closed = True
165+ elif message ['type' ] == 'supports_binary' :
166+ self .supports_binary = message ['value' ]
167+ elif message ['type' ] == 'initialized' :
168+ _ , _ , w , h = self .figure .bbox .bounds
169+ self .manager .resize (w , h )
170+ self .send_json ('refresh' )
153171 else :
154- output = stream
155- super (FigureManagerNbAgg , cls ).get_javascript (stream = output )
156- with io .open (os .path .join (
157- os .path .dirname (__file__ ),
158- "web_backend" ,
159- "nbagg_mpl.js" ), encoding = 'utf8' ) as fd :
160- output .write (fd .read ())
161- if stream is None :
162- return output .getvalue ()
163-
164- def _create_comm (self ):
165- comm = CommSocket (self )
166- self .add_web_socket (comm )
167- return comm
168-
169- def destroy (self ):
170- self ._send_event ('close' )
171- # need to copy comms as callbacks will modify this list
172- for comm in list (self .web_sockets ):
173- comm .on_close ()
174- self .clearup_closed ()
175-
176- def clearup_closed (self ):
177- """Clear up any closed Comms."""
178- self .web_sockets = set ([socket for socket in self .web_sockets
179- if socket .is_open ()])
180-
181- if len (self .web_sockets ) == 0 :
182- self .canvas .close_event ()
172+ self .manager .handle_json (message )
183173
184- def remove_comm (self , comm_id ):
185- self .web_sockets = set ([socket for socket in self .web_sockets
186- if not socket .comm .comm_id == comm_id ])
174+ def send_json (self , content ):
175+ self .send ({'data' : json .dumps (content )})
187176
177+ def send_binary (self , blob ):
178+ # The comm is ascii, so we always send the image in base64
179+ # encoded data URL form.
180+ data = b64encode (blob )
181+ if six .PY3 :
182+ data = data .decode ('ascii' )
183+ data_uri = "data:image/png;base64,{0}" .format (data )
184+ self .send ({'data' : data_uri })
188185
189- class FigureCanvasNbAgg (FigureCanvasWebAggCore ):
190186 def new_timer (self , * args , ** kwargs ):
191187 return TimerTornado (* args , ** kwargs )
192188
@@ -197,6 +193,31 @@ def stop_event_loop(self):
197193 FigureCanvasBase .stop_event_loop_default (self )
198194
199195
196+ class FigureManagerNbAgg (FigureManagerWebAgg ):
197+ ToolbarCls = NavigationIPy
198+
199+ def __init__ (self , canvas , num ):
200+ FigureManagerWebAgg .__init__ (self , canvas , num )
201+ toolitems = []
202+ for name , tooltip , image , method in self .ToolbarCls .toolitems :
203+ if name is None :
204+ toolitems .append (['' , '' , '' , '' ])
205+ else :
206+ toolitems .append ([name , tooltip , image , method ])
207+ canvas ._toolbar_items = toolitems
208+ self .web_sockets = [self .canvas ]
209+
210+ def show (self ):
211+ if self .canvas ._closed :
212+ self .canvas ._closed = False
213+ display (self .canvas )
214+ else :
215+ self .canvas .draw_idle ()
216+
217+ def destroy (self ):
218+ self ._send_event ('close' )
219+
220+
200221def new_figure_manager (num , * args , ** kwargs ):
201222 """
202223 Create a new figure manager instance
@@ -229,76 +250,46 @@ def closer(event):
229250 return manager
230251
231252
232- class CommSocket ( object ):
253+ def nbinstall ( overwrite = False , user = True ):
233254 """
234- Manages the Comm connection between IPython and the browser (client).
235-
236- Comms are 2 way, with the CommSocket being able to publish a message
237- via the send_json method, and handle a message with on_message. On the
238- JS side figure.send_message and figure.ws.onmessage do the sending and
239- receiving respectively.
240-
255+ Copies javascript dependencies to the '/nbextensions' folder in
256+ your IPython directory.
257+
258+ Parameters
259+ ----------
260+
261+ overwrite : bool
262+ If True, always install the files, regardless of what mayœ already be
263+ installed. Defaults to False.
264+ user : bool
265+ Whether to install to the user's .ipython/nbextensions directory.
266+ Otherwise do a system-wide install
267+ (e.g. /usr/local/share/jupyter/nbextensions). Defaults to False.
241268 """
242- def __init__ (self , manager ):
243- self .supports_binary = None
244- self .manager = manager
245- self .uuid = str (uuid ())
246- # Publish an output area with a unique ID. The javascript can then
247- # hook into this area.
248- display (HTML ("<div id=%r></div>" % self .uuid ))
249- try :
250- self .comm = Comm ('matplotlib' , data = {'id' : self .uuid })
251- except AttributeError :
252- raise RuntimeError ('Unable to create an IPython notebook Comm '
253- 'instance. Are you in the IPython notebook?' )
254- self .comm .on_msg (self .on_message )
255-
256- manager = self .manager
257- self ._ext_close = False
258-
259- def _on_close (close_message ):
260- self ._ext_close = True
261- manager .remove_comm (close_message ['content' ]['comm_id' ])
262- manager .clearup_closed ()
263-
264- self .comm .on_close (_on_close )
265-
266- def is_open (self ):
267- return not (self ._ext_close or self .comm ._closed )
268-
269- def on_close (self ):
270- # When the socket is closed, deregister the websocket with
271- # the FigureManager.
272- if self .is_open ():
273- try :
274- self .comm .close ()
275- except KeyError :
276- # apparently already cleaned it up?
277- pass
278-
279- def send_json (self , content ):
280- self .comm .send ({'data' : json .dumps (content )})
281-
282- def send_binary (self , blob ):
283- # The comm is ascii, so we always send the image in base64
284- # encoded data URL form.
285- data = b64encode (blob )
286- if six .PY3 :
287- data = data .decode ('ascii' )
288- data_uri = "data:image/png;base64,{0}" .format (data )
289- self .comm .send ({'data' : data_uri })
290-
291- def on_message (self , message ):
292- # The 'supports_binary' message is relevant to the
293- # websocket itself. The other messages get passed along
294- # to matplotlib as-is.
295-
296- # Every message has a "type" and a "figure_id".
297- message = json .loads (message ['content' ]['data' ])
298- if message ['type' ] == 'closing' :
299- self .on_close ()
300- self .manager .clearup_closed ()
301- elif message ['type' ] == 'supports_binary' :
302- self .supports_binary = message ['value' ]
303- else :
304- self .manager .handle_json (message )
269+ if (check_nbextension ('matplotlib' ) or
270+ check_nbextension ('matplotlib' , True )):
271+ return
272+
273+ # Make a temporary directory so we can wrap mpl.js in a requirejs define().
274+ tempdir = mkdtemp ()
275+ path = os .path .join (os .path .dirname (__file__ ), "web_backend" )
276+ shutil .copy2 (os .path .join (path , "nbagg_mpl.js" ), tempdir )
277+
278+ with open (os .path .join (path , 'mpl.js' )) as fid :
279+ contents = fid .read ()
280+
281+ with open (os .path .join (tempdir , 'mpl.js' ), 'w' ) as fid :
282+ fid .write ('define(["jquery"], function($) {\n ' )
283+ fid .write (contents )
284+ fid .write ('\n return mpl;\n });' )
285+
286+ install_nbextension (
287+ tempdir ,
288+ overwrite = overwrite ,
289+ symlink = False ,
290+ destination = 'matplotlib' ,
291+ verbose = 0 ,
292+ ** ({'user' : user } if version_info >= (3 , 0 , 0 , '' ) else {})
293+ )
294+
295+ #nbinstall()
0 commit comments