diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 164a20efe78d..aa1b76635643 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2514,7 +2514,7 @@ class NonGuiException(Exception): pass -class FigureManagerBase: +class FigureManagerBase(object): """ Helper class for pyplot mode, wraps everything up into a neat bundle diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py new file mode 100644 index 000000000000..df2bc84bf264 --- /dev/null +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -0,0 +1,206 @@ +"""Interactive figures in the IPython notebook""" +from base64 import b64encode +import json +import io +import os +from uuid import uuid4 as uuid + +from IPython.display import display,Javascript,HTML +from IPython.kernel.comm import Comm + +from matplotlib.figure import Figure +from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg, + FigureCanvasWebAggCore, + NavigationToolbar2WebAgg) +from matplotlib.backend_bases import ShowBase, NavigationToolbar2 + + +class Show(ShowBase): + def display_js(self): + # XXX How to do this just once? It has to deal with multiple + # browser instances using the same kernel. + display(Javascript(FigureManagerNbAgg.get_javascript())) + + def __call__(self, block=None): + from matplotlib import is_interactive + import matplotlib._pylab_helpers as pylab_helpers + + queue = pylab_helpers.Gcf._activeQue + for manager in queue[:]: + if not manager.shown: + self.display_js() + + manager.show() + # If we are not interactive, disable the figure from + # the active queue, but don't destroy it. + if not is_interactive(): + queue.remove(manager) + manager.canvas.draw_idle() + + +show = Show() + + +def draw_if_interactive(): + from matplotlib import is_interactive + import matplotlib._pylab_helpers as pylab_helpers + + if is_interactive(): + manager = pylab_helpers.Gcf.get_active() + if manager is not None: + if not manager.shown: + manager.show() + manager.canvas.draw_idle() + + +def connection_info(): + """ + Return a string showing the figure and connection status for + the backend. + + """ + # TODO: Make this useful! + import matplotlib._pylab_helpers as pylab_helpers + result = [] + for manager in pylab_helpers.Gcf.get_all_fig_managers(): + fig = manager.canvas.figure + result.append('{} - {}'.format(fig.get_label() or "Figure {0}".format(manager.num), + manager.web_sockets)) + result.append('Figures pending show: ' + str(len(pylab_helpers.Gcf._activeQue))) + return '\n'.join(result) + + +class NavigationIPy(NavigationToolbar2WebAgg): + # Note: Version 3.2 icons, not the later 4.0 ones. + # http://fontawesome.io/3.2.1/icons/ + _font_awesome_classes = { + 'home': 'icon-home', + 'back': 'icon-arrow-left', + 'forward': 'icon-arrow-right', + 'zoom_to_rect': 'icon-check-empty', + 'move': 'icon-move', + None: None + } + + # Use the standard toolbar items + download button + toolitems = [(text, tooltip_text, _font_awesome_classes[image_file], name_of_method) + for text, tooltip_text, image_file, name_of_method + in NavigationToolbar2.toolitems + if image_file in _font_awesome_classes] + + +class FigureManagerNbAgg(FigureManagerWebAgg): + ToolbarCls = NavigationIPy + + def __init__(self, canvas, num): + self.shown = False + FigureManagerWebAgg.__init__(self, canvas, num) + + def show(self): + if not self.shown: + self._create_comm() + self.shown = True + + def reshow(self): + self.shown = False + self.show() + + @property + def connected(self): + return bool(self.web_sockets) + + @classmethod + def get_javascript(cls, stream=None): + if stream is None: + output = io.StringIO() + else: + output = stream + super(FigureManagerNbAgg, cls).get_javascript(stream=output) + with io.open(os.path.join( + os.path.dirname(__file__), + "web_backend", + "nbagg_mpl.js"), encoding='utf8') as fd: + output.write(fd.read()) + if stream is None: + return output.getvalue() + + def _create_comm(self): + comm = CommSocket(self) + self.add_web_socket(comm) + return comm + + def destroy(self): + self._send_event('close') + for comm in self.web_sockets.copy(): + comm.on_close() + + +def new_figure_manager(num, *args, **kwargs): + """ + Create a new figure manager instance + """ + FigureClass = kwargs.pop('FigureClass', Figure) + thisFig = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasWebAggCore(figure) + manager = FigureManagerNbAgg(canvas, num) + return manager + + +class CommSocket(object): + """ + Manages the Comm connection between IPython and the browser (client). + + Comms are 2 way, with the CommSocket being able to publish a message + via the send_json method, and handle a message with on_message. On the + JS side figure.send_message and figure.ws.onmessage do the sending and + receiving respectively. + + """ + def __init__(self, manager): + self.supports_binary = None + self.manager = manager + self.uuid = str(uuid()) + display(HTML("
" % self.uuid)) + try: + self.comm = Comm('matplotlib', data={'id': self.uuid}) + except AttributeError: + raise RuntimeError('Unable to create an IPython notebook Comm ' + 'instance. Are you in the IPython notebook?') + self.comm.on_msg(self.on_message) + + def on_close(self): + # When the socket is closed, deregister the websocket with + # the FigureManager. + if self.comm in self.manager.web_sockets: + self.manager.remove_web_socket(self) + self.comm.close() + + def send_json(self, content): + self.comm.send({'data': json.dumps(content)}) + + def send_binary(self, blob): + # The comm is ascii, so we always send the image in base64 + # encoded data URL form. + data_uri = "data:image/png;base64,{0}".format(b64encode(blob)) + self.comm.send({'data': data_uri}) + + def on_message(self, message): + # The 'supports_binary' message is relevant to the + # websocket itself. The other messages get passed along + # to matplotlib as-is. + + # Every message has a "type" and a "figure_id". + message = json.loads(message['content']['data']) + if message['type'] == 'closing': + self.on_close() + elif message['type'] == 'supports_binary': + self.supports_binary = message['value'] + else: + self.manager.handle_json(message) diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index df3117a86fd2..05e0e1cb23c6 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -230,15 +230,13 @@ class WebSocket(tornado.websocket.WebSocketHandler): def open(self, fignum): self.fignum = int(fignum) - manager = Gcf.get_fig_manager(self.fignum) - manager.add_web_socket(self) + self.manager = Gcf.get_fig_manager(self.fignum) + self.manager.add_web_socket(self) if hasattr(self, 'set_nodelay'): self.set_nodelay(True) def on_close(self): - manager = Gcf.get_fig_manager(self.fignum) - if manager is not None: - manager.remove_web_socket(self) + self.manager.remove_web_socket(self) def on_message(self, message): message = json.loads(message) diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index c7a50b420a4d..1b5f5b3e6464 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -92,7 +92,7 @@ def get_diff_image(self): # pixels can be compared in one numpy call, rather than # needing to compare each plane separately. buff = np.frombuffer( - self._renderer.buffer_rgba(), dtype=np.uint32) + self.get_renderer().buffer_rgba(), dtype=np.uint32) buff.shape = ( self._renderer.height, self._renderer.width) @@ -129,7 +129,7 @@ def get_diff_image(self): def get_renderer(self, cleared=None): # Mirrors super.get_renderer, but caches the old one - # so that we can do things such as prodce a diff image + # so that we can do things such as produce a diff image # in get_diff_image _, _, w, h = self.figure.bbox.bounds key = w, h, self.figure.dpi @@ -200,6 +200,26 @@ def handle_event(self, event): self.send_event('figure_label', label=figure_label) self._force_full = True self.draw_idle() + else: + handler = getattr(self, 'handle_{}'.format(e_type), None) + if handler is None: + import warnings + warnings.warn('Unhandled message type {}. {}'.format(e_type, event)) + else: + return handler(event) + + def handle_resize(self, event): + x, y = event.get('width', 800), event.get('height', 800) + x, y = int(x), int(y) + fig = self.figure + # An attempt at approximating the figure size in pixels. + fig.set_size_inches(x / fig.dpi, y / fig.dpi) + + _, _, w, h = self.figure.bbox.bounds + # Acknowledge the resize, and force the viewer to update the canvas size to the + # figure's new size (which is hopefully identical or within a pixel or so). + self._png_is_old = True + self.manager.resize(w, h) def send_event(self, event_type, **kwargs): self.manager._send_event(event_type, **kwargs) @@ -216,7 +236,54 @@ def stop_event_loop(self): backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__ +class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2): + _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, + } + + # Use the standard toolbar items + download button + toolitems = [(text, tooltip_text, _jquery_icon_classes[image_file], name_of_method) + for text, tooltip_text, image_file, name_of_method + in (backend_bases.NavigationToolbar2.toolitems + + (('Download', 'Download plot', 'download', 'download'),)) + if image_file in _jquery_icon_classes] + + def _init_toolbar(self): + self.message = '' + self.cursor = 0 + + def set_message(self, message): + if message != self.message: + self.canvas.send_event("message", message=message) + self.message = message + + def set_cursor(self, cursor): + if cursor != self.cursor: + self.canvas.send_event("cursor", cursor=cursor) + self.cursor = cursor + + def dynamic_update(self): + self.canvas.draw_idle() + + def draw_rubberband(self, event, x0, y0, x1, y1): + self.canvas.send_event( + "rubberband", x0=x0, y0=y0, x1=x1, y1=y1) + + def release_zoom(self, event): + super(NavigationToolbar2WebAgg, self).release_zoom(event) + self.canvas.send_event( + "rubberband", x0=-1, y0=-1, x1=-1, y1=-1) + + class FigureManagerWebAgg(backend_bases.FigureManagerBase): + ToolbarCls = NavigationToolbar2WebAgg + def __init__(self, canvas, num): backend_bases.FigureManagerBase.__init__(self, canvas, num) @@ -228,7 +295,7 @@ def show(self): pass def _get_toolbar(self, canvas): - toolbar = NavigationToolbar2WebAgg(canvas) + toolbar = self.ToolbarCls(canvas) return toolbar def resize(self, w, h): @@ -275,7 +342,7 @@ def get_javascript(cls, stream=None): output.write(fd.read()) toolitems = [] - for name, tooltip, image, method in NavigationToolbar2WebAgg.toolitems: + for name, tooltip, image, method in cls.ToolbarCls.toolitems: if name is None: toolitems.append(['', '', '', '']) else: @@ -306,57 +373,3 @@ def _send_event(self, event_type, **kwargs): payload.update(kwargs) for s in self.web_sockets: s.send_json(payload) - - -class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2): - _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): - # 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 - - def set_message(self, message): - if message != self.message: - self.canvas.send_event("message", message=message) - self.message = message - - def set_cursor(self, cursor): - if cursor != self.cursor: - self.canvas.send_event("cursor", cursor=cursor) - self.cursor = cursor - - def dynamic_update(self): - self.canvas.draw_idle() - - def draw_rubberband(self, event, x0, y0, x1, y1): - self.canvas.send_event( - "rubberband", x0=x0, y0=y0, x1=x1, y1=y1) - - def release_zoom(self, event): - super(NavigationToolbar2WebAgg, self).release_zoom(event) - self.canvas.send_event( - "rubberband", x0=-1, y0=-1, x1=-1, y1=-1) - -FigureCanvas = FigureCanvasWebAggCore -FigureManager = FigureManagerWebAgg diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js index 626c11f76261..fef98e852595 100644 --- a/lib/matplotlib/backends/web_backend/mpl.js +++ b/lib/matplotlib/backends/web_backend/mpl.js @@ -1,6 +1,5 @@ -/* Put everything inside the mpl namespace */ -var mpl = {}; - +/* Put everything inside the global mpl namespace */ +window.mpl = {}; mpl.get_websocket_type = function() { if (typeof(WebSocket) !== 'undefined') { @@ -15,7 +14,6 @@ mpl.get_websocket_type = function() { }; } - mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.id = figure_id; @@ -56,21 +54,15 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.waiting = false; - onopen_creator = function(fig) { - return function () { + this.ws.onopen = function () { fig.send_message("supports_binary", {value: fig.supports_binary}); fig.send_message("refresh", {}); } - }; - this.ws.onopen = onopen_creator(fig); - onload_creator = function(fig) { - return function() { + this.imageObj.onload = function() { fig.context.drawImage(fig.imageObj, 0, 0); fig.waiting = false; }; - }; - this.imageObj.onload = onload_creator(fig); this.imageObj.onunload = function() { this.ws.close(); @@ -81,62 +73,78 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { this.ondownload = ondownload; } - mpl.figure.prototype._init_header = function() { var titlebar = $( ''); var titletext = $( ''); + 'text-align: center; padding: 3px;"/>'); titlebar.append(titletext) this.root.append(titlebar); this.header = titletext[0]; } - mpl.figure.prototype._init_canvas = function() { var fig = this; var canvas_div = $(''); + canvas_div.resizable({ resize: mpl.debounce_resize( + function(event, ui) { fig.request_resize(ui.size.width, ui.size.height); } + , 50)}); + canvas_div.attr('style', 'position: relative; clear: both;'); this.root.append(canvas_div); var 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_div.keydown('key_press', canvas_keyboard_event); - canvas_div.keyup('key_release', canvas_keyboard_event); - - canvas_div.append(canvas); this.canvas = canvas[0]; this.context = canvas[0].getContext("2d"); - // create a second canvas which floats on top of the first. var rubberband = $(''); 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); + // Throttle sequential mouse events to 1 every 20ms. rubberband.mousemove('motion_notify', mouse_event_fn); + + canvas_div.append(canvas); canvas_div.append(rubberband); + canvas_div.keydown('key_press', canvas_keyboard_event); + canvas_div.keydown('key_release', canvas_keyboard_event); + + this.rubberband = rubberband; this.rubberband_canvas = rubberband[0]; this.rubberband_context = rubberband[0].getContext("2d"); this.rubberband_context.strokeStyle = "#000000"; -} + this._resize_canvas = function(width, height) { + // Keep the size of the canvas, canvas container, and rubber band + // canvas in synch. + canvas_div.css('width', width) + canvas_div.css('height', height) + + canvas.attr('width', width); + canvas.attr('height', height); + + rubberband.attr('width', width); + rubberband.attr('height', height); + } + + // Set the figure to an initial 600x600px, this will subsequently be updated + // upon first draw. + this._resize_canvas(600, 600); +} mpl.figure.prototype._init_toolbar = function() { var fig = this; @@ -163,7 +171,6 @@ mpl.figure.prototype._init_toolbar = function() { // put a spacer in here. continue; } - var button = $(''); button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' + 'ui-button-icon-only'); @@ -213,6 +220,11 @@ mpl.figure.prototype._init_toolbar = function() { this.message = status_bar[0]; } +mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) { + // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client, + // which will in turn request a refresh of the image. + this.send_message('resize', {'width': x_pixels, 'height': y_pixels}); +} mpl.figure.prototype.send_message = function(type, properties) { properties['type'] = type; @@ -220,7 +232,6 @@ mpl.figure.prototype.send_message = function(type, properties) { this.ws.send(JSON.stringify(properties)); } - mpl.figure.prototype.send_draw_message = function() { if (!this.waiting) { this.waiting = true; @@ -228,106 +239,123 @@ mpl.figure.prototype.send_draw_message = function() { } } +mpl.figure.prototype.handle_resize = function(fig, msg) { + var size = msg['size']; + if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) { + fig._resize_canvas(size[0], size[1]); + fig.send_message("refresh", {}); + }; +} + +mpl.figure.prototype.handle_rubberband = function(fig, msg) { + var x0 = msg['x0']; + var y0 = fig.canvas.height - msg['y0']; + var x1 = msg['x1']; + 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; + y1 = Math.floor(y1) + 0.5; + var min_x = Math.min(x0, x1); + var min_y = Math.min(y0, y1); + var width = Math.abs(x1 - x0); + var height = Math.abs(y1 - y0); + + fig.rubberband_context.clearRect( + 0, 0, fig.canvas.width, fig.canvas.height); + + fig.rubberband_context.strokeRect(min_x, min_y, width, height); +} + +mpl.figure.prototype.handle_figure_label = function(fig, msg) { + // Updates the figure title. + fig.header.textContent = msg['label']; +} + +mpl.figure.prototype.handle_cursor = function(fig, msg) { + var cursor = msg['cursor']; + switch(cursor) + { + case 0: + cursor = 'pointer'; + break; + case 1: + cursor = 'default'; + break; + case 2: + cursor = 'crosshair'; + break; + case 3: + cursor = 'move'; + break; + } + fig.rubberband_canvas.style.cursor = cursor; +} + +mpl.figure.prototype.handle_message = function(fig, msg) { + fig.message.textContent = msg['message']; +} + +mpl.figure.prototype.handle_draw = function(fig, msg) { + // Request the server to send over a new figure. + fig.send_draw_message(); +} +mpl.figure.prototype.updated_canvas_event = function() { + // Called whenever the canvas gets updated. + this.send_message("ack", {}); +} + +// A function to construct a web socket function for onmessage handling. +// Called in the figure constructor. mpl.figure.prototype._make_on_message_function = function(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 (fig.imageObj.src) { - (window.URL || window.webkitURL).revokeObjectURL( - fig.imageObj.src); - } - fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL( - evt.data); - fig.send_message("ack", {}); - return; - } - } else { - if (evt.data.slice(0, 21) == "data:image/png;base64") { - fig.imageObj.src = evt.data; - fig.send_message("ack", {}); - return; + 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 (fig.imageObj.src) { + (window.URL || window.webkitURL).revokeObjectURL( + fig.imageObj.src); } + fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL( + evt.data); + fig.updated_canvas_event(); + return; + } + else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == "data:image/png;base64") { + fig.imageObj.src = evt.data; + fig.updated_canvas_event(); + return; } var msg = JSON.parse(evt.data); + var msg_type = msg['type']; + + // Call the "handle_{type}" callback, which takes + // the figure and JSON message as its only arguments. + try { + var callback = fig["handle_" + msg_type]; + } catch (e) { + console.log("No handler for the '" + msg_type + "' message type: ", msg); + return; + } - switch(msg['type']) { - case 'draw': - fig.send_draw_message(); - break; - - case 'message': - fig.message.textContent = msg['message']; - break; - - case 'cursor': - var cursor = msg['cursor']; - switch(cursor) - { - case 0: - cursor = 'pointer'; - break; - case 1: - cursor = 'default'; - break; - case 2: - cursor = 'crosshair'; - break; - case 3: - cursor = 'move'; - break; - } - fig.canvas.style.cursor = cursor; - break; - - case 'resize': - var size = msg['size']; - 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.send_message("refresh", {}); - fig.send_message("supports_binary", {value: fig.supports_binary}); + if (callback) { + try { + callback(fig, msg); + } catch (e) { + console.log("Exception inside the 'handler_" + msg_type + "' callback:", e, e.stack, msg); } - break; - - case 'rubberband': - var x0 = msg['x0']; - var y0 = fig.canvas.height - msg['y0']; - var x1 = msg['x1']; - 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; - y1 = Math.floor(y1) + 0.5; - var min_x = Math.min(x0, x1); - var min_y = Math.min(y0, y1); - var width = Math.abs(x1 - x0); - var height = Math.abs(y1 - y0); - - fig.rubberband_context.clearRect( - 0, 0, fig.canvas.width, fig.canvas.height); - fig.rubberband_context.strokeRect(min_x, min_y, width, height); - break; - - case 'figure_label': - fig.header.textContent = msg['label']; - break; } }; } // from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas - mpl.findpos = function(e) { //this section is from http://www.quirksmode.org/js/events_properties.html var targ; @@ -349,7 +377,6 @@ mpl.findpos = function(e) { return {"x": x, "y": y}; }; - mpl.figure.prototype.mouse_event = function(event, name) { var canvas_pos = mpl.findpos(event) @@ -371,7 +398,6 @@ mpl.figure.prototype.mouse_event = function(event, name) { return false; } - mpl.figure.prototype.key_event = function(event, name) { /* Don't fire events just when a modifier is changed. Modifiers are sent along with other keys. */ @@ -391,7 +417,6 @@ mpl.figure.prototype.key_event = function(event, name) { this.send_message(name, {key: value}); } - mpl.figure.prototype.toolbar_button_onclick = function(name) { if (name == 'download') { var format_dropdown = this.format_dropdown; @@ -402,7 +427,22 @@ mpl.figure.prototype.toolbar_button_onclick = function(name) { } }; - mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) { this.message.textContent = tooltip; }; + +mpl.debounce_event = function(func, time){ + var timer; + return function(event){ + clearTimeout(timer); + timer = setTimeout(function(){ func(event); }, time); + }; +} + +mpl.debounce_resize = function(func, time){ + var timer; + return function(event, ui){ + clearTimeout(timer); + timer = setTimeout(function(){ func(event, ui); }, time); + }; +} diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js new file mode 100644 index 000000000000..caac0c17656d --- /dev/null +++ b/lib/matplotlib/backends/web_backend/nbagg_mpl.js @@ -0,0 +1,145 @@ +var comm_websocket_adapter = function(comm) { + // Create a "websocket"-like object which calls the given IPython comm + // object with the appropriate methods. Currently this is a non binary + // socket, so there is still some room for performance tuning. + var ws = {}; + + ws.close = function() { + comm.close() + }; + ws.send = function(m) { + //console.log('sending', m); + comm.send(m); + }; + // Register the callback with on_msg. + comm.on_msg(function(msg) { + //console.log('receiving', msg['content']['data'], msg); + // Pass the mpl event to the overriden (by mpl) onmessage function. + ws.onmessage(msg['content']['data']) + }); + return ws; +} + +mpl.mpl_figure_comm = function(comm, msg) { + // This is the function which gets called when the mpl process + // starts-up an IPython Comm through the "matplotlib" channel. + + var id = msg.content.data.id; + var element = $("#" + id); + var ws_proxy = comm_websocket_adapter(comm) + + var fig = new mpl.figure(id, ws_proxy, + function() { }, + element.get(0)); + + // Call onopen now - mpl needs it, as it is assuming we've passed it a real + // web socket which is closed, not our websocket->open comm proxy. + ws_proxy.onopen(); + + fig.parent_element = element.get(0); + fig.cell_info = mpl.find_output_cell(""); + + var output_index = fig.cell_info[2] + var cell = fig.cell_info[0]; + + // Disable right mouse context menu. + $(fig.rubberband_canvas).bind("contextmenu",function(e){ + return false; + }); + +}; + +mpl.figure.prototype.handle_close = function(fig, msg) { + // Update the output cell to use the data from the current canvas. + fig.push_to_output(); + var dataURL = fig.canvas.toDataURL(); + $(fig.parent_element).html('