@@ -44,14 +44,16 @@ def draw_if_interactive():
4444class Show (backend_bases .ShowBase ):
4545 def mainloop (self ):
4646 WebAggApplication .initialize ()
47- for manager in Gcf .get_all_fig_managers ():
48- url = "http://127.0.0.1:{0}/{1}/" .format (
49- WebAggApplication .port , manager .num )
50- if rcParams ['webagg.open_in_browser' ]:
51- import webbrowser
52- webbrowser .open (url )
53- else :
54- print ("To view figure, visit {0}" .format (url ))
47+
48+ url = "http://127.0.0.1:{port}{prefix}" .format (
49+ port = WebAggApplication .port ,
50+ prefix = WebAggApplication .url_prefix )
51+
52+ if rcParams ['webagg.open_in_browser' ]:
53+ import webbrowser
54+ webbrowser .open (url )
55+ else :
56+ print ("To view figure, visit {0}" .format (url ))
5557
5658 WebAggApplication .start ()
5759
@@ -161,9 +163,9 @@ def get_diff_image(self):
161163 # The buffer is created as type uint32 so that entire
162164 # pixels can be compared in one numpy call, rather than
163165 # needing to compare each plane separately.
164- buffer = np .frombuffer (
166+ buff = np .frombuffer (
165167 self ._renderer .buffer_rgba (), dtype = np .uint32 )
166- buffer .shape = (
168+ buff .shape = (
167169 self ._renderer .height , self ._renderer .width )
168170
169171 if not self ._force_full :
@@ -172,10 +174,10 @@ def get_diff_image(self):
172174 last_buffer .shape = (
173175 self ._renderer .height , self ._renderer .width )
174176
175- diff = buffer != last_buffer
176- output = np .where (diff , buffer , 0 )
177+ diff = buff != last_buffer
178+ output = np .where (diff , buff , 0 )
177179 else :
178- output = buffer
180+ output = buff
179181
180182 # Clear out the PNG data buffer rather than recreating it
181183 # each time. This reduces the number of memory
@@ -198,27 +200,30 @@ def get_diff_image(self):
198200 return self ._png_buffer .getvalue ()
199201
200202 def get_renderer (self ):
201- l , b , w , h = self .figure .bbox .bounds
203+ # Mirrors super.get_renderer, but caches the old one
204+ # so that we can do things such as prodce a diff image
205+ # in get_diff_image
206+ _ , _ , w , h = self .figure .bbox .bounds
202207 key = w , h , self .figure .dpi
203208 try :
204209 self ._lastKey , self ._renderer
205210 except AttributeError :
206211 need_new_renderer = True
207212 else :
208213 need_new_renderer = (self ._lastKey != key )
209-
214+
210215 if need_new_renderer :
211216 self ._renderer = backend_agg .RendererAgg (
212217 w , h , self .figure .dpi )
213218 self ._last_renderer = backend_agg .RendererAgg (
214219 w , h , self .figure .dpi )
215220 self ._lastKey = key
216-
221+
217222 return self ._renderer
218223
219224 def handle_event (self , event ):
220- type = event ['type' ]
221- if type in ('button_press' , 'button_release' , 'motion_notify' ):
225+ e_type = event ['type' ]
226+ if e_type in ('button_press' , 'button_release' , 'motion_notify' ):
222227 x = event ['x' ]
223228 y = event ['y' ]
224229 y = self .get_renderer ().height - y
@@ -234,23 +239,24 @@ def handle_event(self, event):
234239 if button == 2 :
235240 button = 3
236241
237- if type == 'button_press' :
242+ if e_type == 'button_press' :
238243 self .button_press_event (x , y , button )
239- elif type == 'button_release' :
244+ elif e_type == 'button_release' :
240245 self .button_release_event (x , y , button )
241- elif type == 'motion_notify' :
246+ elif e_type == 'motion_notify' :
242247 self .motion_notify_event (x , y )
243- elif type in ('key_press' , 'key_release' ):
248+ elif e_type in ('key_press' , 'key_release' ):
244249 key = event ['key' ]
245250
246- if type == 'key_press' :
251+ if e_type == 'key_press' :
247252 self .key_press_event (key )
248- elif type == 'key_release' :
253+ elif e_type == 'key_release' :
249254 self .key_release_event (key )
250- elif type == 'toolbar_button' :
255+ elif e_type == 'toolbar_button' :
256+ print ('Toolbar button pressed: ' , event ['name' ])
251257 # TODO: Be more suspicious of the input
252258 getattr (self .toolbar , event ['name' ])()
253- elif type == 'refresh' :
259+ elif e_type == 'refresh' :
254260 self ._force_full = True
255261 self .draw_idle ()
256262
@@ -306,24 +312,27 @@ def resize(self, w, h):
306312
307313
308314class NavigationToolbar2WebAgg (backend_bases .NavigationToolbar2 ):
309- toolitems = list (backend_bases .NavigationToolbar2 .toolitems [:6 ]) + [
310- ('Download' , 'Download plot' , 'filesave' , 'download' )
311- ]
315+ _jquery_icon_classes = {'home' : 'ui-icon ui-icon-home' ,
316+ 'back' : 'ui-icon ui-icon-circle-arrow-w' ,
317+ 'forward' : 'ui-icon ui-icon-circle-arrow-e' ,
318+ 'zoom_to_rect' : 'ui-icon ui-icon-search' ,
319+ 'move' : 'ui-icon ui-icon-arrow-4' ,
320+ 'download' : 'ui-icon ui-icon-disk' ,
321+ None : None
322+ }
312323
313324 def _init_toolbar (self ):
314- jqueryui_icons = [
315- 'ui-icon ui-icon-home' ,
316- 'ui-icon ui-icon-circle-arrow-w' ,
317- 'ui-icon ui-icon-circle-arrow-e' ,
318- None ,
319- 'ui-icon ui-icon-arrow-4' ,
320- 'ui-icon ui-icon-search' ,
321- 'ui-icon ui-icon-disk'
322- ]
323- for index , item in enumerate (self .toolitems ):
324- if item [0 ] is not None :
325- self .toolitems [index ] = (
326- item [0 ], item [1 ], jqueryui_icons [index ], item [3 ])
325+ # Use the standard toolbar items + download button
326+ toolitems = (backend_bases .NavigationToolbar2 .toolitems +
327+ (('Download' , 'Download plot' , 'download' , 'download' ),))
328+
329+ NavigationToolbar2WebAgg .toolitems = \
330+ tuple (
331+ (text , tooltip_text , self ._jquery_icon_classes [image_file ],
332+ name_of_method )
333+ for text , tooltip_text , image_file , name_of_method
334+ in toolitems if image_file in self ._jquery_icon_classes )
335+
327336 self .message = ''
328337 self .cursor = 0
329338
@@ -356,20 +365,71 @@ def release_zoom(self, event):
356365class WebAggApplication (tornado .web .Application ):
357366 initialized = False
358367 started = False
368+
369+ _mpl_data_path = os .path .join (os .path .dirname (os .path .dirname (__file__ )),
370+ 'mpl-data' )
371+ _mpl_dirs = {'mpl-data' : _mpl_data_path ,
372+ 'images' : os .path .join (_mpl_data_path , 'images' ),
373+ 'web_backend' : os .path .join (os .path .dirname (__file__ ),
374+ 'web_backend' )}
359375
360376 class FavIcon (tornado .web .RequestHandler ):
361377 def get (self ):
362378 self .set_header ('Content-Type' , 'image/png' )
363- with open (os .path .join (
364- os .path .dirname (__file__ ),
365- '../mpl-data/images/matplotlib.png' )) as fd :
379+ with open (os .path .join (WebAggApplication ._mpl_dirs ['images' ],
380+ 'matplotlib.png' )) as fd :
366381 self .write (fd .read ())
367382
368- class IndexPage (tornado .web .RequestHandler ):
383+ class SingleFigurePage (tornado .web .RequestHandler ):
384+ def __init__ (self , application , request , ** kwargs ):
385+ self .url_prefix = kwargs .pop ('url_prefix' , '' )
386+ return tornado .web .RequestHandler .__init__ (self , application ,
387+ request , ** kwargs )
388+
389+ def get (self , fignum ):
390+ with open (os .path .join (WebAggApplication ._mpl_dirs ['web_backend' ],
391+ 'single_figure.html' )) as fd :
392+ tpl = fd .read ()
393+
394+ fignum = int (fignum )
395+ manager = Gcf .get_fig_manager (fignum )
396+
397+ ws_uri = 'ws://{req.host}{prefix}/' .format (req = self .request ,
398+ prefix = self .url_prefix )
399+ t = tornado .template .Template (tpl )
400+ self .write (t .generate (
401+ prefix = self .url_prefix ,
402+ ws_uri = ws_uri ,
403+ fig_id = fignum ,
404+ toolitems = NavigationToolbar2WebAgg .toolitems ,
405+ canvas = manager .canvas ))
406+
407+ class AllFiguresPage (tornado .web .RequestHandler ):
408+ def __init__ (self , application , request , ** kwargs ):
409+ self .url_prefix = kwargs .pop ('url_prefix' , '' )
410+ return tornado .web .RequestHandler .__init__ (self , application ,
411+ request , ** kwargs )
412+
413+ def get (self ):
414+ with open (os .path .join (WebAggApplication ._mpl_dirs ['web_backend' ],
415+ 'all_figures.html' )) as fd :
416+ tpl = fd .read ()
417+
418+ ws_uri = 'ws://{req.host}{prefix}/' .format (req = self .request ,
419+ prefix = self .url_prefix )
420+ t = tornado .template .Template (tpl )
421+
422+ self .write (t .generate (
423+ prefix = self .url_prefix ,
424+ ws_uri = ws_uri ,
425+ figures = sorted (list (Gcf .figs .items ()), key = lambda item : item [0 ]),
426+ toolitems = NavigationToolbar2WebAgg .toolitems ))
427+
428+
429+ class MPLInterfaceJS (tornado .web .RequestHandler ):
369430 def get (self , fignum ):
370- with open (os .path .join (
371- os .path .dirname (__file__ ),
372- 'web_backend' , 'index.html' )) as fd :
431+ with open (os .path .join (WebAggApplication ._mpl_dirs ['web_backend' ],
432+ 'mpl_interface.js' )) as fd :
373433 tpl = fd .read ()
374434
375435 fignum = int (fignum )
@@ -381,7 +441,7 @@ def get(self, fignum):
381441 canvas = manager .canvas ))
382442
383443 class Download (tornado .web .RequestHandler ):
384- def get (self , fignum , format ):
444+ def get (self , fignum , fmt ):
385445 self .fignum = int (fignum )
386446 manager = Gcf .get_fig_manager (self .fignum )
387447
@@ -397,11 +457,11 @@ def get(self, fignum, format):
397457 'emf' : 'application/emf'
398458 }
399459
400- self .set_header ('Content-Type' , mimetypes .get (format , 'binary' ))
460+ self .set_header ('Content-Type' , mimetypes .get (fmt , 'binary' ))
401461
402- buffer = io .BytesIO ()
403- manager .canvas .print_figure (buffer , format = format )
404- self .write (buffer .getvalue ())
462+ buff = io .BytesIO ()
463+ manager .canvas .print_figure (buff , format = fmt )
464+ self .write (buff .getvalue ())
405465
406466 class WebSocket (tornado .websocket .WebSocketHandler ):
407467 supports_binary = True
@@ -410,7 +470,7 @@ def open(self, fignum):
410470 self .fignum = int (fignum )
411471 manager = Gcf .get_fig_manager (self .fignum )
412472 manager .add_web_socket (self )
413- l , b , w , h = manager .canvas .figure .bbox .bounds
473+ _ , _ , w , h = manager .canvas .figure .bbox .bounds
414474 manager .resize (w , h )
415475 self .on_message ('{"type":"refresh"}' )
416476
@@ -443,52 +503,69 @@ def send_image(self):
443503 diff .encode ('base64' ).replace ('\n ' , '' ))
444504 self .write_message (data_uri )
445505
446- def __init__ (self ):
506+ def __init__ (self , url_prefix = '' ):
507+ if url_prefix :
508+ assert url_prefix [0 ] == '/' and url_prefix [- 1 ] != '/' , \
509+ 'url_prefix must start with a "/" and not end with one.'
510+
447511 super (WebAggApplication , self ).__init__ ([
448512 # Static files for the CSS and JS
449- (r'/static /(.*)' ,
513+ (url_prefix + r'/_static /(.*)' ,
450514 tornado .web .StaticFileHandler ,
451- {'path' :
452- os . path . join ( os . path . dirname ( __file__ ), 'web_backend' )}),
515+ {'path' : self . _mpl_dirs [ 'web_backend' ]}),
516+
453517 # Static images for toolbar buttons
454- (r' /images/(.*)' ,
518+ (url_prefix + r'/_static /images/(.*)' ,
455519 tornado .web .StaticFileHandler ,
456- {'path' :
457- os . path . join ( os . path . dirname ( __file__ ), '../mpl-data/images' )}),
458- (r'/static /jquery/css/themes/base/(.*)' ,
520+ {'path' : self . _mpl_dirs [ 'images' ]}),
521+
522+ (url_prefix + r'/_static /jquery/css/themes/base/(.*)' ,
459523 tornado .web .StaticFileHandler ,
460- {'path' :
461- os . path . join ( os . path . dirname ( __file__ ),
462- 'web_backend/jquery/css/themes/base' )}),
463- (r'/static /jquery/css/themes/base/images/(.*)' ,
524+ {'path' : os . path . join ( self . _mpl_dirs [ 'web_backend' ], 'jquery' ,
525+ 'css' , 'themes' , 'base' )} ),
526+
527+ (url_prefix + r'/_static /jquery/css/themes/base/images/(.*)' ,
464528 tornado .web .StaticFileHandler ,
465- {'path' :
466- os . path . join ( os . path . dirname ( __file__ ),
467- 'web_backend/jquery/css/themes/base/images' )}),
468- (r'/static /jquery/js/(.*)' , tornado .web .StaticFileHandler ,
469- {'path' :
470- os . path . join ( os . path . dirname ( __file__ ),
471- 'web_backend/jquery/js' )}),
472- (r'/static /css/(.*)' , tornado .web .StaticFileHandler ,
473- {'path' :
474- os . path . join ( os . path . dirname ( __file__ ), 'web_backend/css' )}),
529+ {'path' : os . path . join ( self . _mpl_dirs [ 'web_backend' ], 'jquery' ,
530+ 'css' , 'themes' , 'base' , 'images' )} ),
531+
532+ (url_prefix + r'/_static /jquery/js/(.*)' , tornado .web .StaticFileHandler ,
533+ {'path' : os . path . join ( self . _mpl_dirs [ 'web_backend' ],
534+ 'jquery' , 'js' )} ),
535+
536+ (url_prefix + r'/_static /css/(.*)' , tornado .web .StaticFileHandler ,
537+ {'path' : os . path . join ( self . _mpl_dirs [ 'web_backend' ], 'css' )}),
538+
475539 # An MPL favicon
476- (r'/favicon.ico' , self .FavIcon ),
540+ (url_prefix + r'/favicon.ico' , self .FavIcon ),
541+
477542 # The page that contains all of the pieces
478- (r'/([0-9]+)/' , self .IndexPage ),
543+ (url_prefix + r'/([0-9]+)' , self .SingleFigurePage ,
544+ {'url_prefix' : url_prefix }),
545+
546+ (url_prefix + r'/([0-9]+)/mpl_interface.js' , self .MPLInterfaceJS ),
547+
479548 # Sends images and events to the browser, and receives
480549 # events from the browser
481- (r'/([0-9]+)/ws' , self .WebSocket ),
550+ (url_prefix + r'/([0-9]+)/ws' , self .WebSocket ),
551+
482552 # Handles the downloading (i.e., saving) of static images
483- (r'/([0-9]+)/download.([a-z]+)' , self .Download )
553+ (url_prefix + r'/([0-9]+)/download.([a-z]+)' , self .Download ),
554+
555+ # The page that contains all of the figures
556+ (url_prefix + r'/?' , self .AllFiguresPage ,
557+ {'url_prefix' : url_prefix }),
484558 ])
485559
486560 @classmethod
487- def initialize (cls ):
561+ def initialize (cls , url_prefix = '' ):
488562 if cls .initialized :
489563 return
490564
491- app = cls ()
565+ # Create the class instance
566+ app = cls (url_prefix = url_prefix )
567+
568+ cls .url_prefix = url_prefix
492569
493570 # This port selection algorithm is borrowed, more or less
494571 # verbatim, from IPython.
0 commit comments