diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index d0b7c22d0cc9..8163dcac289c 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -10,6 +10,7 @@ import os import random import socket +import threading import numpy as np @@ -20,7 +21,6 @@ import tornado.web import tornado.ioloop import tornado.websocket -import tornado.template import matplotlib from matplotlib import rcParams @@ -30,6 +30,15 @@ from matplotlib._pylab_helpers import Gcf from matplotlib import _png +# TODO: This should really only be set for the IPython notebook, but +# I'm not sure how to detect that. +try: + __IPYTHON__ +except: + _in_ipython = False +else: + _in_ipython = True + def draw_if_interactive(): """ @@ -46,8 +55,8 @@ def mainloop(self): WebAggApplication.initialize() url = "http://127.0.0.1:{port}{prefix}".format( - port=WebAggApplication.port, - prefix=WebAggApplication.url_prefix) + port=WebAggApplication.port, + prefix=WebAggApplication.url_prefix) if rcParams['webagg.open_in_browser']: import webbrowser @@ -57,7 +66,25 @@ def mainloop(self): WebAggApplication.start() -show = Show() + +if not _in_ipython: + show = Show() +else: + def show(): + from IPython.display import display_html + + result = [] + import matplotlib._pylab_helpers as pylab_helpers + for manager in pylab_helpers.Gcf().get_all_fig_managers(): + result.append(ipython_inline_display(manager.canvas.figure)) + return display_html('\n'.join(result), raw=True) + + +class ServerThread(threading.Thread): + def run(self): + tornado.ioloop.IOLoop.instance().start() + +server_thread = ServerThread() def new_figure_manager(num, *args, **kwargs): @@ -127,6 +154,16 @@ def __init__(self, *args, **kwargs): # messages from piling up. self._pending_draw = None + # TODO: I'd like to dynamically add the _repr_html_ method + # to the figure in the right context, but then IPython doesn't + # use it, for some reason. + + # Add the _repr_html_ member to the figure for IPython inline + # support + # if _in_ipython: + # self.figure._repr_html_ = types.MethodType( + # ipython_inline_display, self.figure, self.figure.__class__) + def show(self): # show the figure window show() @@ -199,7 +236,7 @@ def get_diff_image(self): self._png_is_old = False return self._png_buffer.getvalue() - def get_renderer(self, cleared=False): + 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 # in get_diff_image @@ -269,12 +306,12 @@ def start_event_loop(self, timeout): backend_bases.FigureCanvasBase.start_event_loop_default( self, timeout) start_event_loop.__doc__ = \ - backend_bases.FigureCanvasBase.start_event_loop_default.__doc__ + backend_bases.FigureCanvasBase.start_event_loop_default.__doc__ def stop_event_loop(self): backend_bases.FigureCanvasBase.stop_event_loop_default(self) stop_event_loop.__doc__ = \ - backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__ + backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__ class FigureManagerWebAgg(backend_bases.FigureManagerBase): @@ -313,26 +350,29 @@ def resize(self, w, h): 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 - } + _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'),)) + 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) + (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 @@ -388,22 +428,18 @@ def __init__(self, application, request, **kwargs): 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( + self.render( + "single_figure.html", prefix=self.url_prefix, ws_uri=ws_uri, fig_id=fignum, toolitems=NavigationToolbar2WebAgg.toolitems, - canvas=manager.canvas)) + canvas=manager.canvas) class AllFiguresPage(tornado.web.RequestHandler): def __init__(self, application, request, **kwargs): @@ -412,34 +448,27 @@ def __init__(self, application, request, **kwargs): 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( + self.render( + "all_figures.html", prefix=self.url_prefix, ws_uri=ws_uri, - figures = sorted(list(Gcf.figs.items()), key=lambda item: item[0]), - toolitems=NavigationToolbar2WebAgg.toolitems)) - + 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(WebAggApplication._mpl_dirs['web_backend'], - 'mpl_interface.js')) as fd: - tpl = fd.read() + def get(self): + manager = Gcf.get_fig_manager(1) + canvas = manager.canvas - fignum = int(fignum) - manager = Gcf.get_fig_manager(fignum) + self.set_header('Content-Type', 'application/javascript') - t = tornado.template.Template(tpl) - self.write(t.generate( + self.render( + "mpl_interface.js", toolitems=NavigationToolbar2WebAgg.toolitems, - canvas=manager.canvas)) + canvas=canvas) class Download(tornado.web.RequestHandler): def get(self, fignum, fmt): @@ -516,7 +545,7 @@ def send_diff_image(self, diff): 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.' + 'url_prefix must start with a "/" and not end with one.' super(WebAggApplication, self).__init__([ # Static files for the CSS and JS @@ -539,11 +568,13 @@ def __init__(self, url_prefix=''): {'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery', 'css', 'themes', 'base', 'images')}), - (url_prefix + r'/_static/jquery/js/(.*)', tornado.web.StaticFileHandler, + (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, + (url_prefix + r'/_static/css/(.*)', + tornado.web.StaticFileHandler, {'path': os.path.join(self._mpl_dirs['web_backend'], 'css')}), # An MPL favicon @@ -553,19 +584,20 @@ def __init__(self, url_prefix=''): (url_prefix + r'/([0-9]+)', self.SingleFigurePage, {'url_prefix': url_prefix}), - (url_prefix + r'/([0-9]+)/mpl_interface.js', self.MPLInterfaceJS), + (url_prefix + r'/mpl_interface.js', self.MPLInterfaceJS), # Sends images and events to the browser, and receives # events from the browser (url_prefix + r'/([0-9]+)/ws', self.WebSocket), # Handles the downloading (i.e., saving) of static images - (url_prefix + r'/([0-9]+)/download.([a-z]+)', self.Download), + (url_prefix + r'/([0-9]+)/download.([a-z0-9.]+)', self.Download), # The page that contains all of the figures (url_prefix + r'/?', self.AllFiguresPage, {'url_prefix': url_prefix}), - ]) + ], + template_path=self._mpl_dirs['web_backend']) @classmethod def initialize(cls, url_prefix=''): @@ -623,3 +655,27 @@ def start(cls): print("Server stopped") cls.started = True + + +def ipython_inline_display(figure): + import matplotlib._pylab_helpers as pylab_helpers + import tornado.template + + WebAggApplication.initialize() + if not server_thread.is_alive(): + server_thread.start() + + with open(os.path.join( + WebAggApplication._mpl_dirs['web_backend'], + 'ipython_inline_figure.html')) as fd: + tpl = fd.read() + + fignum = figure.number + + t = tornado.template.Template(tpl) + return t.generate( + prefix=WebAggApplication.url_prefix, + fig_id=fignum, + toolitems=NavigationToolbar2WebAgg.toolitems, + canvas=figure.canvas, + port=WebAggApplication.port) diff --git a/lib/matplotlib/backends/web_backend/all_figures.html b/lib/matplotlib/backends/web_backend/all_figures.html index a501d9379ce5..62ab46adfd58 100644 --- a/lib/matplotlib/backends/web_backend/all_figures.html +++ b/lib/matplotlib/backends/web_backend/all_figures.html @@ -3,65 +3,49 @@ - - - - - - - + + + + + - - MPL | WebAgg current figures + + + MPL | WebAgg current figures - +
- {% 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 %} - -
-

- - {{ fig_label }} - - -

-
-
-
- {% end %} - +
diff --git a/lib/matplotlib/backends/web_backend/ipython_inline_figure.html b/lib/matplotlib/backends/web_backend/ipython_inline_figure.html new file mode 100644 index 000000000000..0ab029d6e4b3 --- /dev/null +++ b/lib/matplotlib/backends/web_backend/ipython_inline_figure.html @@ -0,0 +1,32 @@ + + diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js index f9d9761801cc..24854df42f74 100644 --- a/lib/matplotlib/backends/web_backend/mpl.js +++ b/lib/matplotlib/backends/web_backend/mpl.js @@ -1,6 +1,6 @@ -function figure(fig_id, websocket_url_prefix) { +function figure(fig_id, websocket_url_prefix, parent_element) { this.id = fig_id; - + if (typeof(WebSocket) !== 'undefined') { this.WebSocket = WebSocket; } else if (typeof(MozWebSocket) !== 'undefined') { @@ -11,10 +11,10 @@ function figure(fig_id, websocket_url_prefix) { '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); if (!this.supports_binary) { @@ -24,55 +24,44 @@ function figure(fig_id, websocket_url_prefix) { "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; - -} -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.root = $('
'); + $(parent_element).append(this.root); + + init_mpl_canvas(this); + init_mpl_toolbar(this); + this.ws.onopen = function () { - this.ws.send(JSON.stringify( + this.send(JSON.stringify( {type: 'supports_binary', - value: this.supports_binary})); + value: fig.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);};}; + onload_creator = function(fig) { + return function() { + fig.context.drawImage(fig.imageObj, 0, 0); + }; + }; this.imageObj.onload = onload_creator(fig); - this.imageObj.onunload = function() { this.ws.close(); } this.ws.onmessage = gen_on_msg_fn(this); -}; +} function gen_on_msg_fn(fig) @@ -85,7 +74,7 @@ function gen_on_msg_fn(fig) * 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( @@ -103,14 +92,14 @@ function gen_on_msg_fn(fig) return; } } - + var msg = JSON.parse(evt.data); - + switch(msg['type']) { case 'message': fig.message.textContent = msg['message']; break; - + case 'cursor': var cursor = msg['cursor']; switch(cursor) @@ -130,7 +119,7 @@ function gen_on_msg_fn(fig) } fig.canvas.style.cursor = cursor; break; - + case 'resize': var size = msg['size']; if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) { @@ -144,7 +133,7 @@ function gen_on_msg_fn(fig) value: fig.supports_binary})); } break; - + case 'rubberband': var x0 = msg['x0']; var y0 = fig.canvas.height - msg['y0']; @@ -158,7 +147,7 @@ function gen_on_msg_fn(fig) 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); @@ -167,33 +156,40 @@ function gen_on_msg_fn(fig) }; }; +// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas + +function findPos(e) { + //this section is from http://www.quirksmode.org/js/events_properties.html + var targ; + if (!e) + e = window.event; + if (e.target) + targ = e.target; + else if (e.srcElement) + targ = e.srcElement; + if (targ.nodeType == 3) // defeat Safari bug + targ = targ.parentNode; + + // jQuery normalizes the pageX and pageY + // pageX,Y are the mouse positions relative to the document + // offset() returns the position of the element relative to the document + var x = e.pageX - $(targ).offset().left; + var y = e.pageY - $(targ).offset().top; + + return {"x": x, "y": y}; +}; - -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) - + var canvas_pos = findPos(event) + 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; + + var x = canvas_pos.x; + var y = canvas_pos.y; this.ws.send(JSON.stringify( {type: name, @@ -205,7 +201,7 @@ figure.prototype.mouse_event = function(event, name) { * to control all of the cursor setting manually through the * 'cursor' event from matplotlib */ event.preventDefault(); - return false; + return false; } figure.prototype.key_event = function(event, name) { @@ -241,9 +237,6 @@ figure.prototype.toolbar_button_onclick = function(name) { } }; - figure.prototype.toolbar_button_onmouseover = function(tooltip) { this.message.textContent = tooltip; }; - - diff --git a/lib/matplotlib/backends/web_backend/mpl_interface.js b/lib/matplotlib/backends/web_backend/mpl_interface.js index d6f2c046b89b..06b782aaede0 100644 --- a/lib/matplotlib/backends/web_backend/mpl_interface.js +++ b/lib/matplotlib/backends/web_backend/mpl_interface.js @@ -6,111 +6,113 @@ var extensions = [{% for filetype, extensions in sorted(canvas.get_supported_fil 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)); +function init_mpl_canvas(fig) { + var canvas_div = $('
'); canvas_div.attr('style', 'position: relative; clear: both;'); - - var canvas = $('', {id: id_prefix + '-canvas'}); - canvas.attr('id', id_prefix + '-canvas'); + fig.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.keydown('key_press', canvas_keyboard_event); - canvas.keyup('key_release', canvas_keyboard_event); - + canvas.keydown('key_press', canvas_keyboard_event); + canvas.keyup('key_release', canvas_keyboard_event); + canvas_div.append(canvas); - + + fig.canvas = canvas[0]; + fig.context = canvas[0].getContext("2d"); + // Let the top level document handle key events. + canvas.unbind('keydown'); + canvas.unbind('keyup'); + // create a second canvas which floats on top of the first. - var rubberband = $('', {id: id_prefix + '-rubberband-canvas'}); + 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.mousedown('button_press', mouse_event_fn); rubberband.mouseup('button_release', mouse_event_fn); rubberband.mousemove('motion_notify', mouse_event_fn); canvas_div.append(rubberband); + + fig.rubberband_canvas = rubberband[0]; + fig.rubberband_context = rubberband[0].getContext("2d"); + fig.rubberband_context.strokeStyle = "#000000"; }; -function init_mpl_statusbar(container_id, id_prefix) { - var status_bar = $(''); - 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) { + var nav_element = $('
') + nav_element.attr('style', 'width: 600px'); + fig.root.append(nav_element); -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 = $('