8000 Merge pull request #1878 from pelson/webagg_changes · matplotlib/matplotlib@9e477b3 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9e477b3

Browse files
committed
Merge pull request #1878 from pelson/webagg_changes
Webagg changes
2 parents 1ccf29d + 786a6b4 commit 9e477b3

File tree

7 files changed

+521
-257
lines changed

7 files changed

+521
-257
lines changed

lib/matplotlib/_pylab_helpers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import sys, gc
77

88
import atexit
9-
import traceback
109

1110

1211
def error_msg(msg):

lib/matplotlib/backends/backend_webagg.py

Lines changed: 160 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ def draw_if_interactive():
4444
class 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

308314
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+
}
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):
356365
class 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

Comments
 (0)
0