3
3
# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
4
4
# that changes made maintain expected behaviour.
5
5
6
+ import datetime
6
7
from base64 import b64encode
7
8
import json
8
9
import io
9
- from tempfile import mkdtemp
10
- import shutil
11
10
import os
12
11
import six
13
12
from uuid import uuid4 as uuid
14
13
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
17
17
try :
18
18
# 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
22
20
except ImportError :
23
21
# 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
27
23
28
24
from matplotlib import rcParams , is_interactive
29
25
from matplotlib ._pylab_helpers import Gcf
33
29
from matplotlib .backend_bases import (
34
30
_Backend , FigureCanvasBase , NavigationToolbar2 )
35
31
from 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 )
36
39
37
40
38
41
def connection_info ():
@@ -65,7 +68,6 @@ def connection_info():
65
68
'zoom_to_rect' : 'fa fa-square-o icon-check-empty' ,
66
69
'move' : 'fa fa-arrows icon-move' ,
67
70
'download' : 'fa fa-floppy-o icon-save' ,
68
- 'export' : 'fa fa-file-picture-o icon-picture' ,
69
71
None : None
70
72
}
71
73
@@ -77,154 +79,161 @@ class NavigationIPy(NavigationToolbar2WebAgg):
77
79
_FONT_AWESOME_CLASSES [image_file ], name_of_method )
78
80
for text , tooltip_text , image_file , name_of_method
79
81
in (NavigationToolbar2 .toolitems +
80
- (('Download' , 'Download plot' , 'download' , 'download' ),
81
- ('Export' , 'Export plot' , 'export' , 'export' )))
82
+ (('Download' , 'Download plot' , 'download' , 'download' ),))
82
83
if image_file in _FONT_AWESOME_CLASSES ]
83
84
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
-
161
85
162
86
class FigureManagerNbAgg (FigureManagerWebAgg ):
163
87
ToolbarCls = NavigationIPy
164
88
165
89
def __init__ (self , canvas , num ):
90
+ self ._shown = False
166
91
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 ()))
175
98
176
99
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 ( )
180
103
else :
181
104
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
182
138
183
139
def destroy (self ):
184
140
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 )
185
162
186
163
187
- def nbinstall ( overwrite = False , user = True ):
164
+ class CommSocket ( object ):
188
165
"""
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
+
202
173
"""
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 )
228
237
229
238
230
239
@_Backend .export
0 commit comments