Skip to content

Loading…

Webagg changes #1878

Merged
merged 4 commits into from

2 participants

@pelson
Matplotlib Developers member

Some refactoring work on the WebAgg backend.

Key features:

  • you can now run the backend with a url prefix (i.e. it is now possible to run the service under 127.0.0.1/mpl/*, if you so desired)
  • objectified the JS figure (and associated web socket) so that more than one figure can exist on the same page
  • there is an "all figures" landing page which handles keyboard & mouse focus appropriately
  • the creation of the appropriate HTML elements is now easier due to the mpl_interface.js factory functions

I've been testing this out with the following code:

import matplotlib
matplotlib.use('webagg')

import matplotlib.pyplot as plt

plt.figure()
plt.plot(range(10))

plt.figure('wibble')
plt.bar(range(10), range(10))


plt.show()

I still have some other changes I'd like to make to the WebAgg architecture, but I think this is a reasonable first step which adds some nice, self contained, functionality.

@pelson
Matplotlib Developers member

Note: one of the changes I was referring to was to JSLint the javascript - I'd be happy to do that here before anybody sets their eyes on my (probably poor) javascript. Shout if you'd like that to happen now, otherwise, I'll probably defer the linting to my next PR ;-)

@mdboom mdboom commented on the diff
lib/matplotlib/backends/backend_webagg.py
@@ -443,52 +503,69 @@ def send_image(self):
diff.encode('base64').replace('\n', ''))
self.write_message(data_uri)
- def __init__(self):
+ def __init__(self, url_prefix=''):
+ if url_prefix:
+ assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
+ 'url_prefix must start with a "/" and not end with one.'
@mdboom Matplotlib Developers member
mdboom added a note

Should we instead just add the prefix / if it's not there?

@pelson Matplotlib Developers member
pelson added a note

Both are fine, it's something most people will never use (I don't propose making in an RcParam) - its for follow on applications (which I plan to look at in the next couple of weeks) for which I will want to implement a namespace. The one thing in favour of this approach is that there is no ambiguity - if you get it wrong then you will get a decent message about it, but I can also see the other side of the argument too (if you can check that something is wrong, you can fix it programatically for the user...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mdboom
Matplotlib Developers member

This is great. I think the objectification work you've done to allow multiple figures on the same page will come in very handy when this gets integrated with IPython notebook. (I hope to hash out a plan for that at Scipy if I can get all the experts in the room together...)

I am getting this message when I run this (haven't really looked into why):

ERROR:root:Uncaught exception GET /favicon.ico (127.0.0.1)
HTTPRequest(protocol='http', host='127.0.0.1:8988', method='GET', uri='/favicon.ico', version='HTTP/1.1', remote_ip='127.0.0.1', body='', headers={'Accept-Charset': 'UTF-8,*;q=0.5', 'Connection': 'keep-alive', 'Accept-Language': 'en-US,en;q=0.8', 'Accept-Encoding': 'gzip,deflate,sdch', 'Host': '127.0.0.1:8988', 'Accept': '*/*', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31'})
Traceback (most recent call last):
  File "/home/mdboom/python/lib/python2.7/site-packages/tornado-2.4.1-py2.7.egg/tornado/web.py", line 1042, in _execute
    getattr(self, self.request.method.lower())(*args, **kwargs)
  File "/home/mdboom/python/lib/python2.7/site-packages/matplotlib-1.3.x-py2.7-linux-x86_64.egg/matplotlib/backends/backend_webagg.py", line 379, in get
    with open(os.path.join(self._mpl_dirs['images'],
AttributeError: 'FavIcon' object has no attribute '_mpl_dirs'
@pelson
Matplotlib Developers member

Fixed the favicon directory problem in 786a6b4.

@pelson
Matplotlib Developers member

@mdboom - any actions on this? I'm keen to reduce my number of PRs :smile:

@mdboom
Matplotlib Developers member

Now that the favicon thing is fixed, I think this is good to go.

@mdboom mdboom merged commit 9e477b3 into matplotlib:master

1 check passed

Details default The Travis build passed
@pelson
Matplotlib Developers member

Cool. Thanks @mdboom. I'm going to start up a conversation on the ipython-dev mailing list in the next week or so to start thinking about notebook magic. I'd like to be able to use it in a talk at SciPy :wink:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1 lib/matplotlib/_pylab_helpers.py
@@ -6,7 +6,6 @@
import sys, gc
import atexit
-import traceback
def error_msg(msg):
View
243 lib/matplotlib/backends/backend_webagg.py
@@ -44,14 +44,16 @@ def draw_if_interactive():
class Show(backend_bases.ShowBase):
def mainloop(self):
WebAggApplication.initialize()
- for manager in Gcf.get_all_fig_managers():
- url = "http://127.0.0.1:{0}/{1}/".format(
- WebAggApplication.port, manager.num)
- if rcParams['webagg.open_in_browser']:
- import webbrowser
- webbrowser.open(url)
- else:
- print("To view figure, visit {0}".format(url))
+
+ url = "http://127.0.0.1:{port}{prefix}".format(
+ port=WebAggApplication.port,
+ prefix=WebAggApplication.url_prefix)
+
+ if rcParams['webagg.open_in_browser']:
+ import webbrowser
+ webbrowser.open(url)
+ else:
+ print("To view figure, visit {0}".format(url))
WebAggApplication.start()
@@ -161,9 +163,9 @@ def get_diff_image(self):
# The buffer is created as type uint32 so that entire
# pixels can be compared in one numpy call, rather than
# needing to compare each plane separately.
- buffer = np.frombuffer(
+ buff = np.frombuffer(
self._renderer.buffer_rgba(), dtype=np.uint32)
- buffer.shape = (
+ buff.shape = (
self._renderer.height, self._renderer.width)
if not self._force_full:
@@ -172,10 +174,10 @@ def get_diff_image(self):
last_buffer.shape = (
self._renderer.height, self._renderer.width)
- diff = buffer != last_buffer
- output = np.where(diff, buffer, 0)
+ diff = buff != last_buffer
+ output = np.where(diff, buff, 0)
else:
- output = buffer
+ output = buff
# Clear out the PNG data buffer rather than recreating it
# each time. This reduces the number of memory
@@ -198,7 +200,10 @@ def get_diff_image(self):
return self._png_buffer.getvalue()
def get_renderer(self):
- l, b, w, h = self.figure.bbox.bounds
+ # Mirrors super.get_renderer, but caches the old one
+ # so that we can do things such as prodce a diff image
+ # in get_diff_image
+ _, _, w, h = self.figure.bbox.bounds
key = w, h, self.figure.dpi
try:
self._lastKey, self._renderer
@@ -206,19 +211,19 @@ def get_renderer(self):
need_new_renderer = True
else:
need_new_renderer = (self._lastKey != key)
-
+
if need_new_renderer:
self._renderer = backend_agg.RendererAgg(
w, h, self.figure.dpi)
self._last_renderer = backend_agg.RendererAgg(
w, h, self.figure.dpi)
self._lastKey = key
-
+
return self._renderer
def handle_event(self, event):
- type = event['type']
- if type in ('button_press', 'button_release', 'motion_notify'):
+ e_type = event['type']
+ if e_type in ('button_press', 'button_release', 'motion_notify'):
x = event['x']
y = event['y']
y = self.get_renderer().height - y
@@ -234,23 +239,24 @@ def handle_event(self, event):
if button == 2:
button = 3
- if type == 'button_press':
+ if e_type == 'button_press':
self.button_press_event(x, y, button)
- elif type == 'button_release':
+ elif e_type == 'button_release':
self.button_release_event(x, y, button)
- elif type == 'motion_notify':
+ elif e_type == 'motion_notify':
self.motion_notify_event(x, y)
- elif type in ('key_press', 'key_release'):
+ elif e_type in ('key_press', 'key_release'):
key = event['key']
- if type == 'key_press':
+ if e_type == 'key_press':
self.key_press_event(key)
- elif type == 'key_release':
+ elif e_type == 'key_release':
self.key_release_event(key)
- elif type == 'toolbar_button':
+ elif e_type == 'toolbar_button':
+ print('Toolbar button pressed: ', event['name'])
# TODO: Be more suspicious of the input
getattr(self.toolbar, event['name'])()
- elif type == 'refresh':
+ elif e_type == 'refresh':
self._force_full = True
self.draw_idle()
@@ -306,24 +312,27 @@ def resize(self, w, h):
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
- toolitems = list(backend_bases.NavigationToolbar2.toolitems[:6]) + [
- ('Download', 'Download plot', 'filesave', 'download')
- ]
+ _jquery_icon_classes = {'home': 'ui-icon ui-icon-home',
+ 'back': 'ui-icon ui-icon-circle-arrow-w',
+ 'forward': 'ui-icon ui-icon-circle-arrow-e',
+ 'zoom_to_rect': 'ui-icon ui-icon-search',
+ 'move': 'ui-icon ui-icon-arrow-4',
+ 'download': 'ui-icon ui-icon-disk',
+ None: None
+ }
def _init_toolbar(self):
- jqueryui_icons = [
- 'ui-icon ui-icon-home',
- 'ui-icon ui-icon-circle-arrow-w',
- 'ui-icon ui-icon-circle-arrow-e',
- None,
- 'ui-icon ui-icon-arrow-4',
- 'ui-icon ui-icon-search',
- 'ui-icon ui-icon-disk'
- ]
- for index, item in enumerate(self.toolitems):
- if item[0] is not None:
- self.toolitems[index] = (
- item[0], item[1], jqueryui_icons[index], item[3])
+ # Use the standard toolbar items + download button
+ toolitems = (backend_bases.NavigationToolbar2.toolitems +
+ (('Download', 'Download plot', 'download', 'download'),))
+
+ NavigationToolbar2WebAgg.toolitems = \
+ tuple(
+ (text, tooltip_text, self._jquery_icon_classes[image_file],
+ name_of_method)
+ for text, tooltip_text, image_file, name_of_method
+ in toolitems if image_file in self._jquery_icon_classes)
+
self.message = ''
self.cursor = 0
@@ -356,20 +365,71 @@ def release_zoom(self, event):
class WebAggApplication(tornado.web.Application):
initialized = False
started = False
+
+ _mpl_data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ 'mpl-data')
+ _mpl_dirs = {'mpl-data': _mpl_data_path,
+ 'images': os.path.join(_mpl_data_path, 'images'),
+ 'web_backend': os.path.join(os.path.dirname(__file__),
+ 'web_backend')}
class FavIcon(tornado.web.RequestHandler):
def get(self):
self.set_header('Content-Type', 'image/png')
- with open(os.path.join(
- os.path.dirname(__file__),
- '../mpl-data/images/matplotlib.png')) as fd:
+ with open(os.path.join(WebAggApplication._mpl_dirs['images'],
+ 'matplotlib.png')) as fd:
self.write(fd.read())
- class IndexPage(tornado.web.RequestHandler):
+ class SingleFigurePage(tornado.web.RequestHandler):
+ def __init__(self, application, request, **kwargs):
+ self.url_prefix = kwargs.pop('url_prefix', '')
+ return tornado.web.RequestHandler.__init__(self, application,
+ request, **kwargs)
+
+ def get(self, fignum):
+ with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
+ 'single_figure.html')) as fd:
+ tpl = fd.read()
+
+ fignum = int(fignum)
+ manager = Gcf.get_fig_manager(fignum)
+
+ ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
+ prefix=self.url_prefix)
+ t = tornado.template.Template(tpl)
+ self.write(t.generate(
+ prefix=self.url_prefix,
+ ws_uri=ws_uri,
+ fig_id=fignum,
+ toolitems=NavigationToolbar2WebAgg.toolitems,
+ canvas=manager.canvas))
+
+ class AllFiguresPage(tornado.web.RequestHandler):
+ def __init__(self, application, request, **kwargs):
+ self.url_prefix = kwargs.pop('url_prefix', '')
+ return tornado.web.RequestHandler.__init__(self, application,
+ request, **kwargs)
+
+ def get(self):
+ with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
+ 'all_figures.html')) as fd:
+ tpl = fd.read()
+
+ ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
+ prefix=self.url_prefix)
+ t = tornado.template.Template(tpl)
+
+ self.write(t.generate(
+ prefix=self.url_prefix,
+ ws_uri=ws_uri,
+ figures = sorted(list(Gcf.figs.items()), key=lambda item: item[0]),
+ toolitems=NavigationToolbar2WebAgg.toolitems))
+
+
+ class MPLInterfaceJS(tornado.web.RequestHandler):
def get(self, fignum):
- with open(os.path.join(
- os.path.dirname(__file__),
- 'web_backend', 'index.html')) as fd:
+ with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
+ 'mpl_interface.js')) as fd:
tpl = fd.read()
fignum = int(fignum)
@@ -381,7 +441,7 @@ def get(self, fignum):
canvas=manager.canvas))
class Download(tornado.web.RequestHandler):
- def get(self, fignum, format):
+ def get(self, fignum, fmt):
self.fignum = int(fignum)
manager = Gcf.get_fig_manager(self.fignum)
@@ -397,11 +457,11 @@ def get(self, fignum, format):
'emf': 'application/emf'
}
- self.set_header('Content-Type', mimetypes.get(format, 'binary'))
+ self.set_header('Content-Type', mimetypes.get(fmt, 'binary'))
- buffer = io.BytesIO()
- manager.canvas.print_figure(buffer, format=format)
- self.write(buffer.getvalue())
+ buff = io.BytesIO()
+ manager.canvas.print_figure(buff, format=fmt)
+ self.write(buff.getvalue())
class WebSocket(tornado.websocket.WebSocketHandler):
supports_binary = True
@@ -410,7 +470,7 @@ def open(self, fignum):
self.fignum = int(fignum)
manager = Gcf.get_fig_manager(self.fignum)
manager.add_web_socket(self)
- l, b, w, h = manager.canvas.figure.bbox.bounds
+ _, _, w, h = manager.canvas.figure.bbox.bounds
manager.resize(w, h)
self.on_message('{"type":"refresh"}')
@@ -443,52 +503,69 @@ def send_image(self):
diff.encode('base64').replace('\n', ''))
self.write_message(data_uri)
- def __init__(self):
+ def __init__(self, url_prefix=''):
+ if url_prefix:
+ assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
+ 'url_prefix must start with a "/" and not end with one.'
@mdboom Matplotlib Developers member
mdboom added a note

Should we instead just add the prefix / if it's not there?

@pelson Matplotlib Developers member
pelson added a note

Both are fine, it's something most people will never use (I don't propose making in an RcParam) - its for follow on applications (which I plan to look at in the next couple of weeks) for which I will want to implement a namespace. The one thing in favour of this approach is that there is no ambiguity - if you get it wrong then you will get a decent message about it, but I can also see the other side of the argument too (if you can check that something is wrong, you can fix it programatically for the user...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
super(WebAggApplication, self).__init__([
# Static files for the CSS and JS
- (r'/static/(.*)',
+ (url_prefix + r'/_static/(.*)',
tornado.web.StaticFileHandler,
- {'path':
- os.path.join(os.path.dirname(__file__), 'web_backend')}),
+ {'path': self._mpl_dirs['web_backend']}),
+
# Static images for toolbar buttons
- (r'/images/(.*)',
+ (url_prefix + r'/_static/images/(.*)',
tornado.web.StaticFileHandler,
- {'path':
- os.path.join(os.path.dirname(__file__), '../mpl-data/images')}),
- (r'/static/jquery/css/themes/base/(.*)',
+ {'path': self._mpl_dirs['images']}),
+
+ (url_prefix + r'/_static/jquery/css/themes/base/(.*)',
tornado.web.StaticFileHandler,
- {'path':
- os.path.join(os.path.dirname(__file__),
- 'web_backend/jquery/css/themes/base')}),
- (r'/static/jquery/css/themes/base/images/(.*)',
+ {'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
+ 'css', 'themes', 'base')}),
+
+ (url_prefix + r'/_static/jquery/css/themes/base/images/(.*)',
tornado.web.StaticFileHandler,
- {'path':
- os.path.join(os.path.dirname(__file__),
- 'web_backend/jquery/css/themes/base/images')}),
- (r'/static/jquery/js/(.*)', tornado.web.StaticFileHandler,
- {'path':
- os.path.join(os.path.dirname(__file__),
- 'web_backend/jquery/js')}),
- (r'/static/css/(.*)', tornado.web.StaticFileHandler,
- {'path':
- os.path.join(os.path.dirname(__file__), 'web_backend/css')}),
+ {'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
+ 'css', 'themes', 'base', 'images')}),
+
+ (url_prefix + r'/_static/jquery/js/(.*)', tornado.web.StaticFileHandler,
+ {'path': os.path.join(self._mpl_dirs['web_backend'],
+ 'jquery', 'js')}),
+
+ (url_prefix + r'/_static/css/(.*)', tornado.web.StaticFileHandler,
+ {'path': os.path.join(self._mpl_dirs['web_backend'], 'css')}),
+
# An MPL favicon
- (r'/favicon.ico', self.FavIcon),
+ (url_prefix + r'/favicon.ico', self.FavIcon),
+
# The page that contains all of the pieces
- (r'/([0-9]+)/', self.IndexPage),
+ (url_prefix + r'/([0-9]+)', self.SingleFigurePage,
+ {'url_prefix': url_prefix}),
+
+ (url_prefix + r'/([0-9]+)/mpl_interface.js', self.MPLInterfaceJS),
+
# Sends images and events to the browser, and receives
# events from the browser
- (r'/([0-9]+)/ws', self.WebSocket),
+ (url_prefix + r'/([0-9]+)/ws', self.WebSocket),
+
# Handles the downloading (i.e., saving) of static images
- (r'/([0-9]+)/download.([a-z]+)', self.Download)
+ (url_prefix + r'/([0-9]+)/download.([a-z]+)', self.Download),
+
+ # The page that contains all of the figures
+ (url_prefix + r'/?', self.AllFiguresPage,
+ {'url_prefix': url_prefix}),
])
@classmethod
- def initialize(cls):
+ def initialize(cls, url_prefix=''):
if cls.initialized:
return
- app = cls()
+ # Create the class instance
+ app = cls(url_prefix=url_prefix)
+
+ cls.url_prefix = url_prefix
# This port selection algorithm is borrowed, more or less
# verbatim, from IPython.
View
67 lib/matplotlib/backends/web_backend/all_figures.html
@@ -0,0 +1,67 @@
+<html>
+ <head>
+ <link rel="stylesheet" href="{{ prefix }}/_static/css/page.css" type="text/css">
+ <link rel="stylesheet" href="{{ prefix }}/_static/css/boilerplate.css" type="text/css" />
+ <link rel="stylesheet" href="{{ prefix }}/_static/css/fbm.css" type="text/css" />
+ <link rel="stylesheet" href="{{ prefix }}/_static/jquery/css/themes/base/jquery-ui.min.css" >
+ <script src="{{ prefix }}/_static/jquery/js/jquery-1.7.1.min.js"></script>
+ <script src="{{ prefix }}/_static/jquery/js/jquery-ui.min.js"></script>
+ <script src="{{ prefix }}/_static/mpl.js"></script>
+ <script src="{{ prefix }}/{{ str(figures[0][0]) }}/mpl_interface.js"></script>
+
+ <script>
+ var websocket_url_prefix = "{{ ws_uri }}";
+ var figures = new Array();
+
+ {% for (fig_id, _) in figures %}
+ $(document).ready(
+ function() {
+ fig = new figure({{ repr(str(fig_id)) }}, websocket_url_prefix);
+ figures.push(fig);
+
+ fig.focus_on_mouseover = true;
+
+ var toolbar_prefix = '{{ str(fig_id).replace(' ', '') }}-toolbar';
+ init_mpl_toolbar(fig, toolbar_prefix);
+
+ var statusbar_prefix = '{{ str(fig_id).replace(' ', '') }}-statusbar';
+ var status_id = init_mpl_statusbar(toolbar_prefix, statusbar_prefix);
+
+ var canvas_prefix = '{{ str(fig_id).replace(' ', '') }}-canvas';
+ init_mpl_canvas(fig, '{{ str(fig_id).replace(' ', '') }}-canvas-div', canvas_prefix);
+
+ fig.finalize(canvas_prefix, toolbar_prefix, statusbar_prefix);
+
+ $(fig.canvas).attr('tabindex', {{ fig_id }});
+ }
+ );
+
+ {% end %}
+ </script>
+
+ <title>MPL | WebAgg current figures</title>
+
+ </head>
+ <body>
+ <div id="mpl-warnings" class="mpl-warnings"></div>
+ {% for (fig_id, fig_manager) in figures %}
+ {% set fig_label='Figure: {}'.format(fig_manager.canvas.figure.get_label()) %}
+
+ {% if fig_label == 'Figure: ' %}
+ {% set fig_label="Figure {}".format(fig_id) %}
+ {% end %}
+
+ <div style="margin: 25px 100px;">
+ <h2>
+ <a href="{{ prefix }}/{{ str(fig_id) }}">
+ {{ fig_label }}
+
+ </a>
+ </h2>
+ <div id="{{ str(fig_id).replace(' ', '') }}-canvas-div"></div>
+ <div id="{{ str(fig_id).replace(' ', '') }}-toolbar" style="width: 700px;"></div>
+ </div>
+ {% end %}
+
+ </body>
+</html>
View
90 lib/matplotlib/backends/web_backend/index.html
@@ -1,90 +0,0 @@
-<html>
- <head>
- <link rel="stylesheet" href="/static/css/page.css" type="text/css">
- <link rel="stylesheet" href="/static/css/boilerplate.css" type="text/css" />
- <link rel="stylesheet" href="/static/css/fbm.css" type="text/css" />
- <link rel="stylesheet" href="/static/jquery/css/themes/base/jquery-ui.min.css" >
- <script src="/static/jquery/js/jquery-1.7.1.min.js"></script>
- <script src="/static/jquery/js/jquery-ui.min.js"></script>
- <script src="/static/mpl.js"></script>
- <script>
- $(function(){
- // Hover states on the static widgets
- $( ".ui-button" ).hover(
- function() {
- $( this ).addClass( "ui-state-hover" );
- },
- function() {
- $( this ).removeClass( "ui-state-hover" );
- }
- );
- });
- </script>
- </head>
- <body
- onkeydown="key_event(event, 'key_press')"
- onkeyup="key_event(event, 'key_release')">
- <div id="mpl-div"
- style="margin-left: auto ; margin-right: auto ; width: 800px;">
- <div id="mpl-warnings" class="mpl-warnings">
- </div>
-
- <div id="mpl-canvas-div"
- style="position: relative;">
- <canvas id="mpl-canvas"
- class="mpl-canvas"
- width="800" height="600"
- style="position: absolute; left: 0; top: 0; z-index: 0">
- </canvas>
-
- <canvas id="mpl-rubberband-canvas"
- width="800" height="600"
- onmousedown="mouse_event(event, 'button_press')"
- onmouseup="mouse_event(event, 'button_release')"
- onmousemove="mouse_event(event, 'motion_notify')"
- style="position: absolute; left: 0; top: 0; z-index: 1">
- </canvas>
- </div>
-
- <div id="toolbar" class="ui-widget ui-widget-content"
- style="border-top-style: none; border-left-style: none;
- border-right-style: none; border-bottom-style: none;">
- {% for name, tooltip, image, method in toolitems %}
- {% if name is None %}
- <span style='width: 0.1em'></span>
- {% else %}
- <button id="{{ name }}"
- onclick="toolbar_button_onclick('{{ method }}');"
- role="button"
- aria-disabled="false"
- class="ui-button ui-widget ui-state-default ui-corner-all
- ui-button-icon-only">
- <span
- class="ui-button-icon-primary ui-icon {{ image }}
- ui-corner-all">
- </span>
- <span class="ui-button-text">
- {{ tooltip }}
- </span>
- </button>
- {% end %}
- {% end %}
- <span>
- <select id="mpl-format" class="mpl-toolbar-option ui-widget ui-widget-content" style="margin-top: 0px;">
- {% for filetype, extensions in sorted(canvas.get_supported_filetypes_grouped().items()) %}
- <option value="{{ extensions[0] }}"
- {% if extensions[0] == canvas.get_default_filetype() %}
- selected
- {% end %}
- >{{filetype}} ({{", ".join(extensions)}})
- </option>
- {% end %}
- </select>
- </span>
- </div>
- <div>
- <span id="mpl-message" class="mpl-message"/>
- </div>
- </div>
- </body>
-</html>
View
204 lib/matplotlib/backends/web_backend/mpl.js
@@ -1,17 +1,6 @@
-var ws;
-
-function ws_url(path) {
- var loc = window.location
- var new_uri;
-
- new_uri = "ws://" + loc.host;
- new_uri += loc.pathname;
- new_uri += path;
-
- return new_uri;
-}
-
-window.onload = function() {
+function figure(fig_id, websocket_url_prefix) {
+ this.id = fig_id;
+
if (typeof(WebSocket) !== 'undefined') {
this.WebSocket = WebSocket;
} else if (typeof(MozWebSocket) !== 'undefined') {
@@ -22,66 +11,99 @@ window.onload = function() {
'Firefox 4 and 5 are also supported but you ' +
'have to enable WebSockets in about:config.');
};
+
+
+ this.ws = new this.WebSocket(websocket_url_prefix + fig_id + '/ws');
+
+ this.supports_binary = (this.ws.binaryType != undefined);
- var message = document.getElementById("mpl-message");
- var canvas_div = document.getElementById("mpl-canvas-div");
- var canvas = document.getElementById("mpl-canvas");
- var context = canvas.getContext("2d");
- var rubberband_canvas = document.getElementById("mpl-rubberband-canvas");
- var rubberband_context = rubberband_canvas.getContext("2d");
- rubberband_context.strokeStyle = "#000000";
-
- ws = new this.WebSocket(ws_url("ws"));
-
- var supports_binary = (ws.binaryType != undefined);
-
- if (!supports_binary) {
+ if (!this.supports_binary) {
var warnings = document.getElementById("mpl-warnings");
warnings.style.display = 'block';
warnings.textContent = (
"This browser does not support binary websocket messages. " +
"Performance may be slow.");
}
+
+ this.imageObj = new Image();
+
+ this.context = undefined;
+ this.message = undefined;
+ this.canvas = undefined;
+ this.rubberband_canvas = undefined;
+ this.rubberband_context = undefined;
+ this.format_dropdown = undefined;
+
+ this.focus_on_mousover = false;
+
+}
- ws.onopen = function () {
- ws.send(JSON.stringify(
+figure.prototype.finalize = function (canvas_id_prefix, toolbar_id_prefix, message_id_prefix) {
+ // resizing_div_id might be the canvas or a containing div for more control of display
+
+ var canvas_id = canvas_id_prefix + '-canvas';
+ var rubberband_id = canvas_id_prefix + '-rubberband-canvas';
+ var message_id = message_id_prefix + '-message';
+
+ this.message = document.getElementById(message_id);
+ this.canvas = document.getElementById(canvas_id);
+ this.context = this.canvas.getContext("2d");
+ this.rubberband_canvas = document.getElementById(rubberband_id);
+ this.rubberband_context = this.rubberband_canvas.getContext("2d");
+ this.rubberband_context.strokeStyle = "#000000";
+
+ this.format_dropdown = document.getElementById(toolbar_id_prefix + '-format_picker');
+
+ this.ws.onopen = function () {
+ this.ws.send(JSON.stringify(
{type: 'supports_binary',
- value: supports_binary}));
+ value: this.supports_binary}));
}
+
+ // attach the onload function to the image object when an
+ // image has been recieved via onmessage
+ fig = this
+ onload_creator = function(fig) {return function() {fig.context.drawImage(fig.imageObj, 0, 0);};};
+ this.imageObj.onload = onload_creator(fig);
+
+ this.ws.onmessage = gen_on_msg_fn(this);
+};
+
- ws.onmessage = function (evt) {
- if (supports_binary) {
+function gen_on_msg_fn(fig)
+{
+ return function socket_on_message(evt) {
+ if (fig.supports_binary) {
if (evt.data instanceof Blob) {
/* FIXME: We get "Resource interpreted as Image but
* transferred with MIME type text/plain:" errors on
* Chrome. But how to set the MIME type? It doesn't seem
* to be part of the websocket stream */
evt.data.type = "image/png";
-
+
/* Free the memory for the previous frames */
- if (imageObj.src) {
+ if (fig.imageObj.src) {
(window.URL || window.webkitURL).revokeObjectURL(
- imageObj.src);
+ fig.imageObj.src);
}
-
- imageObj.src = (window.URL || window.webkitURL).createObjectURL(
+ fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(
evt.data);
return;
}
} else {
if (evt.data.slice(0, 21) == "data:image/png;base64") {
- imageObj.src = evt.data;
+ fig.imageObj.src = evt.data;
return;
}
}
-
+
var msg = JSON.parse(evt.data);
-
+
switch(msg['type']) {
case 'message':
- message.textContent = msg['message'];
+ fig.message.textContent = msg['message'];
break;
-
+
case 'cursor':
var cursor = msg['cursor'];
switch(cursor)
@@ -99,29 +121,25 @@ window.onload = function() {
cursor = 'move';
break;
}
- canvas.style.cursor = cursor;
+ fig.canvas.style.cursor = cursor;
break;
-
+
case 'resize':
var size = msg['size'];
- if (size[0] != canvas.width || size[1] != canvas.height) {
- var div = document.getElementById("mpl-div");
- canvas.width = size[0];
- canvas.height = size[1];
- rubberband_canvas.width = size[0];
- rubberband_canvas.height = size[1];
- canvas_div.style.width = size[0];
- canvas_div.style.height = size[1];
- div.style.width = size[0];
- ws.send(JSON.stringify({type: 'refresh'}));
+ if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {
+ fig.canvas.width = size[0];
+ fig.canvas.height = size[1];
+ fig.rubberband_canvas.width = size[0];
+ fig.rubberband_canvas.height = size[1];
+ fig.ws.send(JSON.stringify({type: 'refresh'}));
}
break;
-
+
case 'rubberband':
var x0 = msg['x0'];
- var y0 = rubberband_canvas.height - msg['y0'];
+ var y0 = fig.canvas.height - msg['y0'];
var x1 = msg['x1'];
- var y1 = rubberband_canvas.height - msg['y1'];
+ var y1 = fig.canvas.height - msg['y1'];
x0 = Math.floor(x0) + 0.5;
y0 = Math.floor(y0) + 0.5;
x1 = Math.floor(x1) + 0.5;
@@ -130,26 +148,44 @@ window.onload = function() {
var min_y = Math.min(y0, y1);
var width = Math.abs(x1 - x0);
var height = Math.abs(y1 - y0);
-
- rubberband_context.clearRect(
- 0, 0, rubberband_canvas.width, rubberband_canvas.height);
- rubberband_context.strokeRect(min_x, min_y, width, height);
+
+ fig.rubberband_context.clearRect(
+ 0, 0, fig.canvas.width, fig.canvas.height);
+ fig.rubberband_context.strokeRect(min_x, min_y, width, height);
break;
}
};
-
- imageObj = new Image();
- imageObj.onload = function() {
- context.drawImage(imageObj, 0, 0);
- };
};
-function mouse_event(event, name) {
- var canvas_div = document.getElementById("mpl-canvas-div");
- var x = event.pageX - canvas_div.offsetLeft;
- var y = event.pageY - canvas_div.offsetTop;
- ws.send(JSON.stringify(
+
+
+function findPos(obj) {
+ // Find the position of the given HTML node.
+
+ var curleft = 0, curtop = 0;
+ if (obj.offsetParent) {
+ do {
+ curleft += obj.offsetLeft;
+ curtop += obj.offsetTop;
+ } while (obj = obj.offsetParent);
+ return { x: curleft, y: curtop };
+ }
+ return undefined;
+}
+
+figure.prototype.mouse_event = function(event, name) {
+ var canvas_pos = findPos(this.canvas)
+
+ if (this.focus_on_mouseover && name === 'motion_notify')
+ {
+ this.canvas.focus();
+ }
+
+ var x = event.pageX - canvas_pos.x;
+ var y = event.pageY - canvas_pos.y;
+
+ this.ws.send(JSON.stringify(
{type: name,
x: x, y: y,
button: event.button}));
@@ -159,10 +195,10 @@ function mouse_event(event, name) {
* to control all of the cursor setting manually through the
* 'cursor' event from matplotlib */
event.preventDefault();
- return false;
+ return false;
}
-function key_event(event, name) {
+figure.prototype.key_event = function(event, name) {
/* Don't fire events just when a modifier is changed. Modifiers are
sent along with other keys. */
if (event.keyCode >= 16 && event.keyCode <= 20) {
@@ -178,24 +214,26 @@ function key_event(event, name) {
}
value += String.fromCharCode(event.keyCode).toLowerCase();
- ws.send(JSON.stringify(
+ this.ws.send(JSON.stringify(
{type: name,
key: value}));
}
-function toolbar_button_onclick(name) {
+figure.prototype.toolbar_button_onclick = function(name) {
if (name == 'download') {
- var format_dropdown = document.getElementById("mpl-format");
+ var format_dropdown = this.format_dropdown;
var format = format_dropdown.options[format_dropdown.selectedIndex].value;
- window.open('download.' + format, '_blank');
+ window.open(this.id + '/download.' + format, '_blank');
} else {
- ws.send(JSON.stringify(
+ this.ws.send(JSON.stringify(
{type: "toolbar_button",
"name": name}));
}
-}
+};
+
+
+figure.prototype.toolbar_button_onmouseover = function(tooltip) {
+ this.message.textContent = tooltip;
+};
+
-function toolbar_button_onmouseover(name) {
- var message = document.getElementById("mpl-message");
- message.textContent = name;
-}
View
116 lib/matplotlib/backends/web_backend/mpl_interface.js
@@ -0,0 +1,116 @@
+var toolbar_items = [{% for name, tooltip, image, method in toolitems %}
+ [{% if name is None %}'', '', '', ''{% else %}'{{ name }}', '{{ tooltip }}', '{{ image }}', '{{ method }}'{% end %}], {% end %}];
+
+
+var extensions = [{% for filetype, extensions in sorted(canvas.get_supported_filetypes_grouped().items()) %}'{{ extensions[0] }}', {% end %}];
+var default_extension = '{{ canvas.get_default_filetype() }}';
+
+
+function init_mpl_canvas(fig, canvas_div_id, id_prefix) {
+
+ var canvas_div = $(document.getElementById(canvas_div_id));
+ canvas_div.attr('style', 'position: relative; clear: both;');
+
+ var canvas = $('<canvas/>', {id: id_prefix + '-canvas'});
+ canvas.attr('id', id_prefix + '-canvas');
+ canvas.addClass('mpl-canvas');
+ canvas.attr('style', "left: 0; top: 0; z-index: 0;")
+ canvas.attr('width', '800');
+ canvas.attr('height', '800');
+
+ function canvas_keyboard_event(event) { return fig.key_event(event, event['data']); }
+ canvas.keydown('key_press', canvas_keyboard_event);
+ canvas.keyup('key_release', canvas_keyboard_event);
+
+ canvas_div.append(canvas);
+
+ // create a second canvas which floats on top of the first.
+ var rubberband = $('<canvas/>', {id: id_prefix + '-rubberband-canvas'});
+ rubberband.attr('style', "position: absolute; left: 0; top: 0; z-index: 1;")
+ rubberband.attr('width', '800');
+ rubberband.attr('height', '800');
+ function mouse_event_fn(event) {
+ return fig.mouse_event(event, event['data']);
+ }
+ rubberband.mousedown('button_press', mouse_event_fn);
+ rubberband.mouseup('button_release', mouse_event_fn);
+ rubberband.mousemove('motion_notify', mouse_event_fn);
+ canvas_div.append(rubberband);
+};
+
+
+function init_mpl_statusbar(container_id, id_prefix) {
+ var status_bar = $('<span class="mpl-message"/>');
+ var status_id = id_prefix + '-message';
+ status_bar.attr('id', status_id);
+ $(document.getElementById(container_id)).append(status_bar);
+ return status_id
+};
+
+function init_mpl_toolbar(fig, nav_container_id, nav_elem_id_prefix) {
+ // Adds a navigation toolbar to the object found with the given jquery query string
+
+ if (nav_elem_id_prefix === undefined) {
+ nav_elem_id_prefix = nav_container_id;
+ }
+
+ // Define a callback function for later on.
+ function toolbar_event(event) { return fig.toolbar_button_onclick(event['data']); }
+ function toolbar_mouse_event(event) { return fig.toolbar_button_onmouseover(event['data']); }
+
+ var nav_element = $(document.getElementById(nav_container_id));
+
+ for(var toolbar_ind in toolbar_items){
+ var name = toolbar_items[toolbar_ind][0];
+ var tooltip = toolbar_items[toolbar_ind][1];
+ var image = toolbar_items[toolbar_ind][2];
+ var method_name = toolbar_items[toolbar_ind][3];
+
+ if (!name) {
+ // put a spacer in here.
+ continue;
+ }
+
+ var button = $('<button/>');
+ button.attr("id", nav_elem_id_prefix + name);
+ button.addClass('ui-button ui-widget ui-state-default ui-corner-all ui-button-icon-only');
+ button.attr('role', 'button');
+ button.attr('aria-disabled', 'false');
+ button.click(method_name, toolbar_event);
+ button.mouseover(tooltip, toolbar_mouse_event);
+
+ var icon_img = $('<span/>');
+ icon_img.addClass('ui-button-icon-primary ui-icon');
+ icon_img.addClass(image);
+ icon_img.addClass('ui-corner-all');
+
+ var tooltip_span = $('<span/>');
+ tooltip_span.addClass('ui-button-text');
+ tooltip_span.html(tooltip);
+
+ button.append(icon_img);
+ button.append(tooltip_span);
+
+ nav_element.append(button);
+ }
+
+ var fmt_picker_span = $('<span/>');
+
+ var fmt_picker = $('<select/>', {id: nav_elem_id_prefix + '-format_picker'});
+ fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');
+ fmt_picker_span.append(fmt_picker);
+ nav_element.append(fmt_picker_span);
+
+ for (var ind in extensions) {
+ var fmt = extensions[ind];
+ var option = $('<option/>', {selected: fmt === default_extension}).html(fmt);
+ fmt_picker.append(option)
+ }
+
+
+ // Add hover states to the ui-buttons
+ $( ".ui-button" ).hover(
+ function() { $(this).addClass("ui-state-hover");},
+ function() { $(this).removeClass("ui-state-hover");}
+ );
+};
View
57 lib/matplotlib/backends/web_backend/single_figure.html
@@ -0,0 +1,57 @@
+<html>
+ <head>
+ <link rel="stylesheet" href="{{ prefix }}/_static/css/page.css" type="text/css">
+ <link rel="stylesheet" href="{{ prefix }}/_static/css/boilerplate.css" type="text/css" />
+ <link rel="stylesheet" href="{{ prefix }}/_static/css/fbm.css" type="text/css" />
+ <link rel="stylesheet" href="{{ prefix }}/_static/jquery/css/themes/base/jquery-ui.min.css" >
+ <script src="{{ prefix }}/_static/jquery/js/jquery-1.7.1.min.js"></script>
+ <script src="{{ prefix }}/_static/jquery/js/jquery-ui.min.js"></script>
+ <script src="{{ prefix }}/_static/mpl.js"></script>
+ <script src="{{ prefix }}/1/mpl_interface.js"></script>
+
+ <script>
+ var websocket_url_prefix = "{{ ws_uri }}";
+ var fig = new figure({{ repr(str(fig_id)) }}, websocket_url_prefix);
+
+ $(document).ready(
+ function() {
+ var toolbar_prefix = 'fig-toolbar';
+ init_mpl_toolbar(fig, toolbar_prefix);
+
+ var statusbar_prefix = 'statusbar';
+ var status_id = init_mpl_statusbar(toolbar_prefix, statusbar_prefix);
+
+ var canvas_prefix = 'figure';
+ init_mpl_canvas(fig, 'fig-canvas-div', canvas_prefix);
+
+ fig.finalize(canvas_prefix, toolbar_prefix, statusbar_prefix);
+ // fig.canvas.focus();
+ // Let the top level document handle key events.
+ fig.canvas.unbind('keydown');
+ fig.canvas.unbind('keyup');
+ }
+ );
+ </script>
+
+{% set fig_label='Figure: {}'.format(canvas.figure.get_label()) %}
+
+{% if fig_label == 'Figure: ' %}
+{% set fig_label="Figure {}".format(fig_id) %}
+{% end %}
+
+ <title>MPL | {{ fig_label }}</title>
+
+
+ </head>
+ <body onkeydown="fig.key_event(event, 'key_press');" onkeyup="fig.key_event(event, 'key_release');">
+ <div style="margin: 10px 100px;">
+ <h1>
+ {{ fig_label }}
+ </h1>
+ <div id="mpl-warnings" class="mpl-warnings"></div>
+ <div id="fig-canvas-div"></div>
+ <div id="fig-toolbar" style="width: 600px;"></div>
+ </div>
+
+ </body>
+</html>
Something went wrong with that request. Please try again.