Skip to content

Instantly share code, notes, and snippets.

@mdboom
Created October 11, 2012 19:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mdboom/3874762 to your computer and use it in GitHub Desktop.
Save mdboom/3874762 to your computer and use it in GitHub Desktop.
Proof of concept code for serving interactive matplotlib figures to the webbrowser
import json
import tornado.web
import tornado.ioloop
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib import _png
from matplotlib import backend_bases
html = """
<html>
<head>
<script>
var last_id = -1;
function GUID ()
{
var S4 = function ()
{
return Math.floor(
Math.random() * 0x10000 /* 65536 */
).toString(16);
};
return (
S4() + S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + S4() + S4()
);
};
var get_image_scheduled = false;
function schedule_get_image() {
if (!get_image_scheduled) {
get_image_scheduled = true;
setTimeout("get_image()", 50);
}
}
function get_image() {
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
var imageObj = new Image();
imageObj.onload = function() {
context.drawImage(imageObj, 0, 0);
last_id = id;
get_image_scheduled = false;
};
id = GUID();
imageObj.src = "image.png?id=" + id + "&last_id=" + last_id;
return imageObj;
}
window.onload = function() {
get_image();
setTimeout('poll()', 1000);
};
function poll() {
xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if(xmlhttp.readyState == 4) {
json = eval(xmlhttp.responseText);
if (json[1]) {
schedule_get_image();
}
setTimeout('poll()', 500);
}
};
xmlhttp.open(
"GET",
"event?type=poll");
xmlhttp.send();
}
function mouse_event(event, name) {
xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if(xmlhttp.readyState == 4) {
var message = document.getElementById("message");
json = eval(xmlhttp.responseText);
// The response is:
// [message (str), needs_draw (bool)]
message.textContent = json[0];
if (json[1]) {
schedule_get_image();
}
}
};
xmlhttp.open(
"GET",
"event?type=" + name +
"&x=" + event.clientX +
"&y=" + event.clientY +
"&button=" + event.button);
xmlhttp.send();
}
</script>
<body>
<canvas id="myCanvas" width="800" height="600"
onmousedown="mouse_event(event, 'button_press')"
onmouseup="mouse_event(event, 'button_release')"
onmousemove="mouse_event(event, 'motion_notify')">
</canvas>
<div id="message">MESSAGE</div>
</body>
</html>
"""
class IndexPage(tornado.web.RequestHandler):
def get(self):
self.write(html)
def serve_figure(fig, port=8888):
# The panning and zooming is handled by the toolbar, (strange enough),
# so we need to create a dummy one.
class Toolbar(backend_bases.NavigationToolbar2):
def _init_toolbar(self):
self.message = ''
self.needs_draw = True
def set_message(self, message):
self.message = message
def dynamic_update(self):
if self.needs_draw is False:
Image.image_number += 1
self.needs_draw = True
toolbar = Toolbar(fig.canvas)
# Set pan mode -- it's the most interesting one
toolbar.pan()
class Image(tornado.web.RequestHandler):
last_buffer = None
last_id = None
image_number = 0
def get(self):
self.set_header("Content-Type", "image/png")
self.set_header("Cache-Control", "no-store")
id = self.get_argument("id")
last_id = self.get_argument("last_id")
if fig.canvas.toolbar.needs_draw:
fig.canvas.draw()
fig.canvas.toolbar.needs_draw = False
renderer = fig.canvas.get_renderer()
buffer = np.array(
np.frombuffer(renderer.buffer_rgba(), dtype=np.uint32),
copy=True)
buffer = buffer.reshape((renderer.height, renderer.width))
last_buffer = self.last_buffer
if last_buffer is not None and last_id == self.last_id:
diff = buffer != last_buffer
if not np.any(diff):
output = np.zeros((1, 1))
else:
output = np.where(diff, buffer, 0)
else:
output = buffer
_png.write_png(output.tostring(),
output.shape[1], output.shape[0],
self)
self.__class__.last_buffer = buffer
self.__class__.last_id = id
class Event(tornado.web.RequestHandler):
def get(self):
type = self.get_argument('type')
if type != 'poll':
x = int(self.get_argument('x'))
y = int(self.get_argument('y'))
y = fig.canvas.get_renderer().height - y
# Javascript button numbers and matplotlib button numbers are
# off by 1
button = int(self.get_argument('button')) + 1
# The right mouse button pops up a context menu, which doesn't
# work very well, so use the middle mouse button instead
if button == 2:
button = 3
if type == 'button_press':
fig.canvas.button_press_event(x, y, button)
elif type == 'button_release':
fig.canvas.button_release_event(x, y, button)
elif type == 'motion_notify':
fig.canvas.motion_notify_event(x, y)
# The response is:
# [message (str), needs_draw (bool) ]
self.write(
json.dumps(
[fig.canvas.toolbar.message,
fig.canvas.toolbar.needs_draw]))
application = tornado.web.Application([
(r"/", IndexPage),
(r"/image.png", Image),
(r"/event", Event)
])
application.listen(port)
tornado.ioloop.IOLoop.instance().start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment