Skip to content

Commit 8d85e26

Browse files
committed
Simplified the creation of a WebAgg html canvas.
1 parent ca476df commit 8d85e26

File tree

5 files changed

+259
-158
lines changed

5 files changed

+259
-158
lines changed

examples/animation/double_pendulum_animated.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Double pendulum formula translated from the C code at
22
# http://www.physics.usyd.edu.au/~wheat/dpend_html/solve_dpend.c
33

4+
import matplotlib
5+
matplotlib.use('webagg')
6+
47
from numpy import sin, cos, pi, array
58
import numpy as np
69
import matplotlib.pyplot as plt
@@ -60,7 +63,7 @@ def derivs(state, t):
6063
x2 = L2*sin(y[:,2]) + x1
6164
y2 = -L2*cos(y[:,2]) + y1
6265

63-
fig = plt.figure()
66+
fig = plt.figure(figsize=[12, 5])
6467
ax = fig.add_subplot(111, autoscale_on=False, xlim=(-2, 2), ylim=(-2, 2))
6568
ax.grid()
6669

lib/matplotlib/backends/backend_webagg.py

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ def get_diff_image(self):
161161
# The buffer is created as type uint32 so that entire
162162
# pixels can be compared in one numpy call, rather than
163163
# needing to compare each plane separately.
164-
buffer = np.frombuffer(
164+
buff = np.frombuffer(
165165
self._renderer.buffer_rgba(), dtype=np.uint32)
166-
buffer.shape = (
166+
buff.shape = (
167167
self._renderer.height, self._renderer.width)
168168

169169
if not self._force_full:
@@ -172,10 +172,10 @@ def get_diff_image(self):
172172
last_buffer.shape = (
173173
self._renderer.height, self._renderer.width)
174174

175-
diff = buffer != last_buffer
176-
output = np.where(diff, buffer, 0)
175+
diff = buff != last_buffer
176+
output = np.where(diff, buff, 0)
177177
else:
178-
output = buffer
178+
output = buff
179179

180180
# Clear out the PNG data buffer rather than recreating it
181181
# each time. This reduces the number of memory
@@ -198,27 +198,30 @@ def get_diff_image(self):
198198
return self._png_buffer.getvalue()
199199

200200
def get_renderer(self):
201-
l, b, w, h = self.figure.bbox.bounds
201+
# Mirrors super.get_renderer, but caches the old one
202+
# so that we can do things such as prodce a diff image
203+
# in get_diff_image
204+
_, _, w, h = self.figure.bbox.bounds
202205
key = w, h, self.figure.dpi
203206
try:
204207
self._lastKey, self._renderer
205208
except AttributeError:
206209
need_new_renderer = True
207210
else:
208211
need_new_renderer = (self._lastKey != key)
209-
212+
210213
if need_new_renderer:
211214
self._renderer = backend_agg.RendererAgg(
212215
w, h, self.figure.dpi)
213216
self._last_renderer = backend_agg.RendererAgg(
214217
w, h, self.figure.dpi)
215218
self._lastKey = key
216-
219+
217220
return self._renderer
218221

219222
def handle_event(self, event):
220-
type = event['type']
221-
if type in ('button_press', 'button_release', 'motion_notify'):
223+
e_type = event['type']
224+
if e_type in ('button_press', 'button_release', 'motion_notify'):
222225
x = event['x']
223226
y = event['y']
224227
y = self.get_renderer().height - y
@@ -234,23 +237,24 @@ def handle_event(self, event):
234237
if button == 2:
235238
button = 3
236239

237-
if type == 'button_press':
240+
if e_type == 'button_press':
238241
self.button_press_event(x, y, button)
239-
elif type == 'button_release':
242+
elif e_type == 'button_release':
240243
self.button_release_event(x, y, button)
241-
elif type == 'motion_notify':
244+
elif e_type == 'motion_notify':
242245
self.motion_notify_event(x, y)
243-
elif type in ('key_press', 'key_release'):
246+
elif e_type in ('key_press', 'key_release'):
244247
key = event['key']
245248

246-
if type == 'key_press':
249+
if e_type == 'key_press':
247250
self.key_press_event(key)
248-
elif type == 'key_release':
251+
elif e_type == 'key_release':
249252
self.key_release_event(key)
250-
elif type == 'toolbar_button':
253+
elif e_type == 'toolbar_button':
254+
print('Toolbar button pressed: ', event['name'])
251255
# TODO: Be more suspicious of the input
252256
getattr(self.toolbar, event['name'])()
253-
elif type == 'refresh':
257+
elif e_type == 'refresh':
254258
self._force_full = True
255259
self.draw_idle()
256260

@@ -306,24 +310,23 @@ def resize(self, w, h):
306310

307311

308312
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
309-
toolitems = list(backend_bases.NavigationToolbar2.toolitems[:6]) + [
310-
('Download', 'Download plot', 'filesave', 'download')
311-
]
313+
_jquery_icon_classes = {'home': 'ui-icon ui-icon-home',
314+
'back': 'ui-icon ui-icon-circle-arrow-w',
315+
'forward': 'ui-icon ui-icon-circle-arrow-e',
316+
'zoom_to_rect': 'ui-icon ui-icon-search',
317+
'move': 'ui-icon ui-icon-arrow-4',
318+
'filesave': 'ui-icon ui-icon-disk',
319+
None: None
320+
}
312321

313322
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])
323+
NavigationToolbar2WebAgg.toolitems = tuple(
324+
(text, tooltip_text,
325+
self._jquery_icon_classes[image_file], name_of_method)
326+
for text, tooltip_text, image_file, name_of_method
327+
in backend_bases.NavigationToolbar2.toolitems
328+
if image_file in self._jquery_icon_classes)
329+
327330
self.message = ''
328331
self.cursor = 0
329332

@@ -356,20 +359,39 @@ def release_zoom(self, event):
356359
class WebAggApplication(tornado.web.Application):
357360
initialized = False
358361
started = False
362+
363+
_mpl_data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
364+
'mpl-data')
365+
_mpl_dirs = {'mpl-data': _mpl_data_path,
366+
'images': os.path.join(_mpl_data_path, 'images'),
367+
'web_backend': os.path.join(os.path.dirname(__file__),
368+
'web_backend')}
359369

360370
class FavIcon(tornado.web.RequestHandler):
361371
def get(self):
362372
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:
373+
with open(os.path.join(self._mpl_dirs['images'],
374+
'matplotlib.png')) as fd:
366375
self.write(fd.read())
367376

368-
class IndexPage(tornado.web.RequestHandler):
377+
class FigurePage(tornado.web.RequestHandler):
378+
def get(self, fignum):
379+
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
380+
'index.html')) as fd:
381+
tpl = fd.read()
382+
383+
fignum = int(fignum)
384+
manager = Gcf.get_fig_manager(fignum)
385+
386+
t = tornado.template.Template(tpl)
387+
self.write(t.generate(
388+
toolitems=NavigationToolbar2WebAgg.toolitems,
389+
canvas=manager.canvas))
390+
391+
class MPLInterfaceJS(tornado.web.RequestHandler):
369392
def get(self, fignum):
370-
with open(os.path.join(
371-
os.path.dirname(__file__),
372-
'web_backend', 'index.html')) as fd:
393+
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
394+
'mpl_interface.js')) as fd:
373395
tpl = fd.read()
374396

375397
fignum = int(fignum)
@@ -381,7 +403,7 @@ def get(self, fignum):
381403
canvas=manager.canvas))
382404

383405
class Download(tornado.web.RequestHandler):
384-
def get(self, fignum, format):
406+
def get(self, fignum, fmt):
385407
self.fignum = int(fignum)
386408
manager = Gcf.get_fig_manager(self.fignum)
387409

@@ -397,11 +419,11 @@ def get(self, fignum, format):
397419
'emf': 'application/emf'
398420
}
399421

400-
self.set_header('Content-Type', mimetypes.get(format, 'binary'))
422+
self.set_header('Content-Type', mimetypes.get(fmt, 'binary'))
401423

402-
buffer = io.BytesIO()
403-
manager.canvas.print_figure(buffer, format=format)
404-
self.write(buffer.getvalue())
424+
buff = io.BytesIO()
425+
manager.canvas.print_figure(buff, format=fmt)
426+
self.write(buff.getvalue())
405427

406428
class WebSocket(tornado.websocket.WebSocketHandler):
407429
supports_binary = True
@@ -410,7 +432,7 @@ def open(self, fignum):
410432
self.fignum = int(fignum)
411433
manager = Gcf.get_fig_manager(self.fignum)
412434
manager.add_web_socket(self)
413-
l, b, w, h = manager.canvas.figure.bbox.bounds
435+
_, _, w, h = manager.canvas.figure.bbox.bounds
414436
manager.resize(w, h)
415437
self.on_message('{"type":"refresh"}')
416438

@@ -448,37 +470,42 @@ def __init__(self):
448470
# Static files for the CSS and JS
449471
(r'/static/(.*)',
450472
tornado.web.StaticFileHandler,
451-
{'path':
452-
os.path.join(os.path.dirname(__file__), 'web_backend')}),
473+
{'path': self._mpl_dirs['web_backend']}),
474+
453475
# Static images for toolbar buttons
454476
(r'/images/(.*)',
455477
tornado.web.StaticFileHandler,
456-
{'path':
457-
os.path.join(os.path.dirname(__file__), '../mpl-data/images')}),
478+
{'path': self._mpl_dirs['images']}),
479+
458480
(r'/static/jquery/css/themes/base/(.*)',
459481
tornado.web.StaticFileHandler,
460-
{'path':
461-
os.path.join(os.path.dirname(__file__),
462-
'web_backend/jquery/css/themes/base')}),
482+
{'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
483+
'css', 'themes', 'base')}),
484+
463485
(r'/static/jquery/css/themes/base/images/(.*)',
464486
tornado.web.StaticFileHandler,
465-
{'path':
466-
os.path.join(os.path.dirname(__file__),
467-
'web_backend/jquery/css/themes/base/images')}),
487+
{'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
488+
'css', 'themes', 'base', 'images')}),
489+
468490
(r'/static/jquery/js/(.*)', tornado.web.StaticFileHandler,
469-
{'path':
470-
os.path.join(os.path.dirname(__file__),
471-
'web_backend/jquery/js')}),
491+
{'path': os.path.join(self._mpl_dirs['web_backend'],
492+
'jquery', 'js')}),
493+
472494
(r'/static/css/(.*)', tornado.web.StaticFileHandler,
473-
{'path':
474-
os.path.join(os.path.dirname(__file__), 'web_backend/css')}),
495+
{'path': os.path.join(self._mpl_dirs['web_backend'], 'css')}),
496+
475497
# An MPL favicon
476498
(r'/favicon.ico', self.FavIcon),
499+
477500
# The page that contains all of the pieces
478-
(r'/([0-9]+)/', self.IndexPage),
501+
(r'/([0-9]+)/?', self.FigurePage),
502+
503+
(r'/([0-9]+)/mpl_interface.js', self.MPLInterfaceJS),
504+
479505
# Sends images and events to the browser, and receives
480506
# events from the browser
481507
(r'/([0-9]+)/ws', self.WebSocket),
508+
482509
# Handles the downloading (i.e., saving) of static images
483510
(r'/([0-9]+)/download.([a-z]+)', self.Download)
484511
])

lib/matplotlib/backends/web_backend/index.html

Lines changed: 21 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,84 +7,30 @@
77
<script src="/static/jquery/js/jquery-1.7.1.min.js"></script>
88
<script src="/static/jquery/js/jquery-ui.min.js"></script>
99
<script src="/static/mpl.js"></script>
10+
<script src="/1/mpl_interface.js"></script>
1011
<script>
11-
$(function(){
12-
// Hover states on the static widgets
13-
$( ".ui-button" ).hover(
14-
function() {
15-
$( this ).addClass( "ui-state-hover" );
16-
},
17-
function() {
18-
$( this ).removeClass( "ui-state-hover" );
19-
}
20-
);
21-
});
12+
13+
$(document).ready(
14+
function() {
15+
init_mpl_toolbar('toolbar');
16+
17+
var statusbar_prefix = 'wibble';
18+
var status_id = init_mpl_statusbar('toolbar', statusbar_prefix);
19+
20+
var canvas_prefix = 'mpl-foo';
21+
init_mpl_canvas('my-canvas-div', canvas_prefix);
22+
23+
finalize_mpl(canvas_prefix, statusbar_prefix);
24+
}
25+
);
26+
2227
</script>
2328
</head>
24-
<body
25-
onkeydown="key_event(event, 'key_press')"
26-
onkeyup="key_event(event, 'key_release')">
27-
<div id="mpl-div"
28-
style="margin-left: auto ; margin-right: auto ; width: 800px;">
29-
<div id="mpl-warnings" class="mpl-warnings">
30-
</div>
31-
32-
<div id="mpl-canvas-div"
33-
style="position: relative;">
34-
<canvas id="mpl-canvas"
35-
class="mpl-canvas"
36-
width="800" height="600"
37-
style="position: absolute; left: 0; top: 0; z-index: 0">
38-
</canvas>
39-
40-
<canvas id="mpl-rubberband-canvas"
41-
width="800" height="600"
42-
onmousedown="mouse_event(event, 'button_press')"
43-
onmouseup="mouse_event(event, 'button_release')"
44-
onmousemove="mouse_event(event, 'motion_notify')"
45-
style="position: absolute; left: 0; top: 0; z-index: 1">
46-
</canvas>
47-
</div>
48-
49-
<div id="toolbar" class="ui-widget ui-widget-content"
50-
style="border-top-style: none; border-left-style: none;
51-
border-right-style: none; border-bottom-style: none;">
52-
{% for name, tooltip, image, method in toolitems %}
53-
{% if name is None %}
54-
<span style='width: 0.1em'></span>
55-
{% else %}
56-
<button id="{{ name }}"
57-
onclick="toolbar_button_onclick('{{ method }}');"
58-
role="button"
59-
aria-disabled="false"
60-
class="ui-button ui-widget ui-state-default ui-corner-all
61-
ui-button-icon-only">
62-
<span
63-
class="ui-button-icon-primary ui-icon {{ image }}
64-
ui-corner-all">
65-
</span>
66-
<span class="ui-button-text">
67-
{{ tooltip }}
68-
</span>
69-
</button>
70-
{% end %}
71-
{% end %}
72-
<span>
73-
<select id="mpl-format" class="mpl-toolbar-option ui-widget ui-widget-content" style="margin-top: 0px;">
74-
{% for filetype, extensions in sorted(canvas.get_supported_filetypes_grouped().items()) %}
75-
<option value="{{ extensions[0] }}"
76-
{% if extensions[0] == canvas.get_default_filetype() %}
77-
selected
78-
{% end %}
79-
>{{filetype}} ({{", ".join(extensions)}})
80-
</option>
81-
{% end %}
82-
</select>
83-
</span>
84-
</div>
85-
<div>
86-
<span id="mpl-message" class="mpl-message"/>
87-
</div>
29+
<body onkeydown="key_event(event, 'key_press')" onkeyup="key_event(event, 'key_release')">
30+
<div style="margin: 10px 100px;">
31+
<div id="mpl-warnings" class="mpl-warnings"></div>
32+
<div id="my-canvas-div" style='height: 1000px;'></div>
33+
<div id="toolbar" style="width: 500px;"></div>
8834
</div>
8935
</body>
9036
</html>

0 commit comments

Comments
 (0)