@@ -44,14 +44,16 @@ def draw_if_interactive():
44
44
class Show (backend_bases .ShowBase ):
45
45
def mainloop (self ):
46
46
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 ))
55
57
56
58
WebAggApplication .start ()
57
59
@@ -161,9 +163,9 @@ def get_diff_image(self):
161
163
# The buffer is created as type uint32 so that entire
162
164
# pixels can be compared in one numpy call, rather than
163
165
# needing to compare each plane separately.
164
- buffer = np .frombuffer (
166
+ buff = np .frombuffer (
165
167
self ._renderer .buffer_rgba (), dtype = np .uint32 )
166
- buffer .shape = (
168
+ buff .shape = (
167
169
self ._renderer .height , self ._renderer .width )
168
170
169
171
if not self ._force_full :
@@ -172,10 +174,10 @@ def get_diff_image(self):
172
174
last_buffer .shape = (
173
175
self ._renderer .height , self ._renderer .width )
174
176
175
- diff = buffer != last_buffer
176
- output = np .where (diff , buffer , 0 )
177
+ diff = buff != last_buffer
178
+ output = np .where (diff , buff , 0 )
177
179
else :
178
- output = buffer
180
+ output = buff
179
181
180
182
# Clear out the PNG data buffer rather than recreating it
181
183
# each time. This reduces the number of memory
@@ -198,27 +200,30 @@ def get_diff_image(self):
198
200
return self ._png_buffer .getvalue ()
199
201
200
202
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
202
207
key = w , h , self .figure .dpi
203
208
try :
204
209
self ._lastKey , self ._renderer
205
210
except AttributeError :
206
211
need_new_renderer = True
207
212
else :
208
213
need_new_renderer = (self ._lastKey != key )
209
-
214
+
210
215
if need_new_renderer :
211
216
self ._renderer = backend_agg .RendererAgg (
212
217
w , h , self .figure .dpi )
213
218
self ._last_renderer = backend_agg .RendererAgg (
214
219
w , h , self .figure .dpi )
215
220
self ._lastKey = key
216
-
221
+
217
222
return self ._renderer
218
223
219
224
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' ):
222
227
x = event ['x' ]
223
228
y = event ['y' ]
224
229
y = self .get_renderer ().height - y
@@ -234,23 +239,24 @@ def handle_event(self, event):
234
239
if button == 2 :
235
240
button = 3
236
241
237
- if type == 'button_press' :
242
+ if e_type == 'button_press' :
238
243
self .button_press_event (x , y , button )
239
- elif type == 'button_release' :
244
+ elif e_type == 'button_release' :
240
245
self .button_release_event (x , y , button )
241
- elif type == 'motion_notify' :
246
+ elif e_type == 'motion_notify' :
242
247
self .motion_notify_event (x , y )
243
- elif type in ('key_press' , 'key_release' ):
248
+ elif e_type in ('key_press' , 'key_release' ):
244
249
key = event ['key' ]
245
250
246
- if type == 'key_press' :
251
+ if e_type == 'key_press' :
247
252
self .key_press_event (key )
248
- elif type == 'key_release' :
253
+ elif e_type == 'key_release' :
249
254
self .key_release_event (key )
250
- elif type == 'toolbar_button' :
255
+ elif e_type == 'toolbar_button' :
256
+ print ('Toolbar button pressed: ' , event ['name' ])
251
257
# TODO: Be more suspicious of the input
252
258
getattr (self .toolbar , event ['name' ])()
253
- elif type == 'refresh' :
259
+ elif e_type == 'refresh' :
254
260
self ._force_full = True
255
261
self .draw_idle ()
256
262
@@ -306,24 +312,27 @@ def resize(self, w, h):
306
312
307
313
308
314
class 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
+ }
312
323
313
324
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
+
327
336
self .message = ''
328
337
self .cursor = 0
329
338
@@ -356,20 +365,71 @@ def release_zoom(self, event):
356
365
class WebAggApplication (tornado .web .Application ):
357
366
initialized = False
358
367
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' )}
359
375
360
376
class FavIcon (tornado .web .RequestHandler ):
361
377
def get (self ):
362
378
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 :
366
381
self .write (fd .read ())
367
382
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 ):
369
430
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 :
373
433
tpl = fd .read ()
374
434
375
435
fignum = int (fignum )
@@ -381,7 +441,7 @@ def get(self, fignum):
381
441
canvas = manager .canvas ))
382
442
383
443
class Download (tornado .web .RequestHandler ):
384
- def get (self , fignum , format ):
444
+ def get (self , fignum , fmt ):
385
445
self .fignum = int (fignum )
386
446
manager = Gcf .get_fig_manager (self .fignum )
387
447
@@ -397,11 +457,11 @@ def get(self, fignum, format):
397
457
'emf' : 'application/emf'
398
458
}
399
459
400
- self .set_header ('Content-Type' , mimetypes .get (format , 'binary' ))
460
+ self .set_header ('Content-Type' , mimetypes .get (fmt , 'binary' ))
401
461
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 ())
405
465
406
466
class WebSocket (tornado .websocket .WebSocketHandler ):
407
467
supports_binary = True
@@ -410,7 +470,7 @@ def open(self, fignum):
410
470
self .fignum = int (fignum )
411
471
manager = Gcf .get_fig_manager (self .fignum )
412
472
manager .add_web_socket (self )
413
- l , b , w , h = manager .canvas .figure .bbox .bounds
473
+ _ , _ , w , h = manager .canvas .figure .bbox .bounds
414
474
manager .resize (w , h )
415
475
self .on_message ('{"type":"refresh"}' )
416
476
@@ -443,52 +503,69 @@ def send_image(self):
443
503
diff .encode ('base64' ).replace ('\n ' , '' ))
444
504
self .write_message (data_uri )
445
505
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
+
447
511
super (WebAggApplication , self ).__init__ ([
448
512
# Static files for the CSS and JS
449
- (r'/static /(.*)' ,
513
+ (url_prefix + r'/_static /(.*)' ,
450
514
tornado .web .StaticFileHandler ,
451
- {'path' :
452
- os . path . join ( os . path . dirname ( __file__ ), 'web_backend' )}),
515
+ {'path' : self . _mpl_dirs [ 'web_backend' ]}),
516
+
453
517
# Static images for toolbar buttons
454
- (r' /images/(.*)' ,
518
+ (url_prefix + r'/_static /images/(.*)' ,
455
519
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/(.*)' ,
459
523
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/(.*)' ,
464
528
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
+
475
539
# An MPL favicon
476
- (r'/favicon.ico' , self .FavIcon ),
540
+ (url_prefix + r'/favicon.ico' , self .FavIcon ),
541
+
477
542
# 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
+
479
548
# Sends images and events to the browser, and receives
480
549
# events from the browser
481
- (r'/([0-9]+)/ws' , self .WebSocket ),
550
+ (url_prefix + r'/([0-9]+)/ws' , self .WebSocket ),
551
+
482
552
# 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 }),
484
558
])
485
559
486
560
@classmethod
487
- def initialize (cls ):
561
+ def initialize (cls , url_prefix = '' ):
488
562
if cls .initialized :
489
563
return
490
564
491
- app = cls ()
565
+ # Create the class instance
566
+ app = cls (url_prefix = url_prefix )
567
+
568
+ cls .url_prefix = url_prefix
492
569
493
570
# This port selection algorithm is borrowed, more or less
494
571
# verbatim, from IPython.
0 commit comments