Created
October 11, 2012 19:05
-
-
Save mdboom/3874762 to your computer and use it in GitHub Desktop.
Proof of concept code for serving interactive matplotlib figures to the webbrowser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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