From 3f42311a3ee255ba809dae9d8a4f299642c54734 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 26 Dec 2015 17:13:05 -0600 Subject: [PATCH 1/4] nbagg widget refactor nbagg widget refactor wip nbagg widget refactor wip ipython widget Display the static figure when closed Revert changes to uat notebook Add support for the toolbar Fix embed logic and show() logic Better handling of class variables Clean up imports and add timer support Add an nbinstall function Implement resize on start. Implement explicit export IPython 3 compat Remove debug in nbinstall Simplify define() Progress on distributing the js files Reorganize javascript files Update paths --- lib/matplotlib/__init__.py | 11 + lib/matplotlib/backend_bases.py | 3 +- lib/matplotlib/backends/backend_nbagg.py | 287 +++++++++--------- .../backends/backend_webagg_core.py | 5 +- .../backends/web_backend/{ => js}/mpl.js | 0 .../web_backend/{ => js}/mpl_tornado.js | 0 .../backends/web_backend/js/nbagg_mpl.js | 166 ++++++++++ .../backends/web_backend/nbagg_mpl.js | 211 ------------- setupext.py | 1 + 9 files changed, 322 insertions(+), 362 deletions(-) rename lib/matplotlib/backends/web_backend/{ => js}/mpl.js (100%) rename lib/matplotlib/backends/web_backend/{ => js}/mpl_tornado.js (100%) create mode 100644 lib/matplotlib/backends/web_backend/js/nbagg_mpl.js delete mode 100644 lib/matplotlib/backends/web_backend/nbagg_mpl.js diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index d306b950f1ee..977cfd73bcb0 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1454,6 +1454,17 @@ def tk_window_focus(): except (KeyError, ValueError): pass + +# Jupyter extension paths +def _jupyter_nbextension_paths(): + return [{ + 'section': 'notebook', + 'src': 'backends/web_backend/js', + 'dest': 'matplotlib', + 'require': 'matplotlib/mpl' + }] + + default_test_modules = [ 'matplotlib.tests.test_agg', 'matplotlib.tests.test_animation', diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 6c72f16020ae..fe7310155336 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2161,7 +2161,8 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None, origfacecolor = self.figure.get_facecolor() origedgecolor = self.figure.get_edgecolor() - self.figure.dpi = dpi + if dpi != 'figure': + self.figure.dpi = dpi self.figure.set_facecolor(facecolor) self.figure.set_edgecolor(edgecolor) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 3fcca314124d..7792262620aa 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -3,23 +3,27 @@ # lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify # that changes made maintain expected behaviour. -import datetime from base64 import b64encode import json import io +from tempfile import mkdtemp +import shutil import os from matplotlib.externals import six from uuid import uuid4 as uuid -import tornado.ioloop - -from IPython.display import display, Javascript, HTML +from IPython.display import display, HTML +from IPython import version_info try: # Jupyter/IPython 4.x or later - from ipykernel.comm import Comm + from ipywidgets import DOMWidget + from traitlets import Unicode, Bool, Float, List, Any + from notebook.nbextensions import install_nbextension, check_nbextension except ImportError: # Jupyter/IPython 3.x or earlier - from IPython.kernel.comm import Comm + from IPython.html.widgets import DOMWidget + from IPython.utils.traitlets import Unicode, Bool, Float, List, Any + from IPython.html.nbextensions import install_nbextension from matplotlib import rcParams from matplotlib.figure import Figure @@ -33,6 +37,7 @@ class Show(ShowBase): + def __call__(self, block=None): from matplotlib._pylab_helpers import Gcf @@ -98,6 +103,7 @@ def connection_info(): 'zoom_to_rect': 'fa fa-square-o icon-check-empty', 'move': 'fa fa-arrows icon-move', 'download': 'fa fa-floppy-o icon-save', + 'export': 'fa fa-file-picture-o icon-picture', None: None } @@ -109,84 +115,74 @@ class NavigationIPy(NavigationToolbar2WebAgg): _FONT_AWESOME_CLASSES[image_file], name_of_method) for text, tooltip_text, image_file, name_of_method in (NavigationToolbar2.toolitems + - (('Download', 'Download plot', 'download', 'download'),)) + (('Download', 'Download plot', 'download', 'download'), + ('Export', 'Export plot', 'export', 'export'))) if image_file in _FONT_AWESOME_CLASSES] + def export(self): + buf = io.BytesIO() + self.canvas.figure.savefig(buf, format='png', dpi='figure') + data = "" + data = data.format(b64encode(buf.getvalue()).decode('utf-8')) + display(HTML(data)) + + +class FigureCanvasNbAgg(DOMWidget, FigureCanvasWebAggCore): + _view_module = Unicode("nbextensions/matplotlib/nbagg_mpl", sync=True) + _view_name = Unicode('MPLCanvasView', sync=True) + _toolbar_items = List(sync=True) + _closed = Bool(True) + _id = Unicode('', sync=True) + + # Must declare the superclass private members. + _png_is_old = Bool() + _force_full = Bool() + _current_image_mode = Unicode() + _dpi_ratio = Float(1.0) + _is_idle_drawing = Bool() + _is_saving = Bool() + _button = Any() + _key = Any() + _lastx = Any() + _lasty = Any() + _is_idle_drawing = Bool() + + def __init__(self, figure, *args, **kwargs): + super(FigureCanvasWebAggCore, self).__init__(figure, *args, **kwargs) + super(DOMWidget, self).__init__(*args, **kwargs) + self._uid = uuid().hex + self.on_msg(self._handle_message) + + def _handle_message(self, object, message, buffers): + # The 'supports_binary' message is relevant to the + # websocket itself. The other messages get passed along + # to matplotlib as-is. -class FigureManagerNbAgg(FigureManagerWebAgg): - ToolbarCls = NavigationIPy - - def __init__(self, canvas, num): - self._shown = False - FigureManagerWebAgg.__init__(self, canvas, num) - - def display_js(self): - # XXX How to do this just once? It has to deal with multiple - # browser instances using the same kernel (require.js - but the - # file isn't static?). - display(Javascript(FigureManagerNbAgg.get_javascript())) - - def show(self): - if not self._shown: - self.display_js() - self._create_comm() - else: - self.canvas.draw_idle() - self._shown = True - - def reshow(self): - """ - A special method to re-show the figure in the notebook. - - """ - 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() + # Every message has a "type" and a "figure_id". + message = json.loads(message) + if message['type'] == 'closing': + self._closed = True + elif message['type'] == 'supports_binary': + self.supports_binary = message['value'] + elif message['type'] == 'initialized': + _, _, w, h = self.figure.bbox.bounds + self.manager.resize(w, h) + self.send_json('refresh') 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') - # need to copy comms as callbacks will modify this list - for comm in list(self.web_sockets): - comm.on_close() - self.clearup_closed() - - def clearup_closed(self): - """Clear up any closed Comms.""" - self.web_sockets = set([socket for socket in self.web_sockets - if socket.is_open()]) - - if len(self.web_sockets) == 0: - self.canvas.close_event() + self.manager.handle_json(message) - def remove_comm(self, comm_id): - self.web_sockets = set([socket for socket in self.web_sockets - if not socket.comm.comm_id == comm_id]) + def send_json(self, content): + self.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 = b64encode(blob) + if six.PY3: + data = data.decode('ascii') + data_uri = "data:image/png;base64,{0}".format(data) + self.send({'data': data_uri}) -class FigureCanvasNbAgg(FigureCanvasWebAggCore): def new_timer(self, *args, **kwargs): return TimerTornado(*args, **kwargs) @@ -197,6 +193,31 @@ def stop_event_loop(self): FigureCanvasBase.stop_event_loop_default(self) +class FigureManagerNbAgg(FigureManagerWebAgg): + ToolbarCls = NavigationIPy + + def __init__(self, canvas, num): + FigureManagerWebAgg.__init__(self, canvas, num) + toolitems = [] + for name, tooltip, image, method in self.ToolbarCls.toolitems: + if name is None: + toolitems.append(['', '', '', '']) + else: + toolitems.append([name, tooltip, image, method]) + canvas._toolbar_items = toolitems + self.web_sockets = [self.canvas] + + def show(self): + if self.canvas._closed: + self.canvas._closed = False + display(self.canvas) + else: + self.canvas.draw_idle() + + def destroy(self): + self._send_event('close') + + def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance @@ -229,76 +250,46 @@ def closer(event): return manager -class CommSocket(object): +def nbinstall(overwrite=False, user=True): """ - 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. - + Copies javascript dependencies to the '/nbextensions' folder in + your IPython directory. + + Parameters + ---------- + + overwrite : bool + If True, always install the files, regardless of what mayƓ already be + installed. Defaults to False. + user : bool + Whether to install to the user's .ipython/nbextensions directory. + Otherwise do a system-wide install + (e.g. /usr/local/share/jupyter/nbextensions). Defaults to False. """ - def __init__(self, manager): - self.supports_binary = None - self.manager = manager - self.uuid = str(uuid()) - # Publish an output area with a unique ID. The javascript can then - # hook into this area. - 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) - - manager = self.manager - self._ext_close = False - - def _on_close(close_message): - self._ext_close = True - manager.remove_comm(close_message['content']['comm_id']) - manager.clearup_closed() - - self.comm.on_close(_on_close) - - def is_open(self): - return not (self._ext_close or self.comm._closed) - - def on_close(self): - # When the socket is closed, deregister the websocket with - # the FigureManager. - if self.is_open(): - try: - self.comm.close() - except KeyError: - # apparently already cleaned it up? - pass - - 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 = b64encode(blob) - if six.PY3: - data = data.decode('ascii') - data_uri = "data:image/png;base64,{0}".format(data) - 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() - self.manager.clearup_closed() - elif message['type'] == 'supports_binary': - self.supports_binary = message['value'] - else: - self.manager.handle_json(message) + if (check_nbextension('matplotlib') or + check_nbextension('matplotlib', True)): + return + + # Make a temporary directory so we can wrap mpl.js in a requirejs define(). + tempdir = mkdtemp() + path = os.path.join(os.path.dirname(__file__), "web_backend") + shutil.copy2(os.path.join(path, "nbagg_mpl.js"), tempdir) + + with open(os.path.join(path, 'mpl.js')) as fid: + contents = fid.read() + + with open(os.path.join(tempdir, 'mpl.js'), 'w') as fid: + fid.write('define(["jquery"], function($) {\n') + fid.write(contents) + fid.write('\nreturn mpl;\n});') + + install_nbextension( + tempdir, + overwrite=overwrite, + symlink=False, + destination='matplotlib', + verbose=0, + **({'user': user} if version_info >= (3, 0, 0, '') else {}) + ) + +#nbinstall() diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 8dba7c3e8647..9f93b2bb1bc8 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -18,7 +18,7 @@ import io import json import os -import time +import datetime import warnings import numpy as np @@ -501,6 +501,7 @@ def get_javascript(cls, stream=None): with io.open(os.path.join( os.path.dirname(__file__), "web_backend", + "js", "mpl.js"), encoding='utf8') as fd: output.write(fd.read()) @@ -530,7 +531,7 @@ def get_javascript(cls, stream=None): @classmethod def get_static_file_path(cls): - return os.path.join(os.path.dirname(__file__), 'web_backend') + return os.path.join(os.path.dirname(__file__), 'web_backend', 'js') def _send_event(self, event_type, **kwargs): payload = {'type': event_type} diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js similarity index 100% rename from lib/matplotlib/backends/web_backend/mpl.js rename to lib/matplotlib/backends/web_backend/js/mpl.js diff --git a/lib/matplotlib/backends/web_backend/mpl_tornado.js b/lib/matplotlib/backends/web_backend/js/mpl_tornado.js similarity index 100% rename from lib/matplotlib/backends/web_backend/mpl_tornado.js rename to lib/matplotlib/backends/web_backend/js/mpl_tornado.js diff --git a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js new file mode 100644 index 000000000000..c5341b2208d8 --- /dev/null +++ b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js @@ -0,0 +1,166 @@ + +define(['jupyter-js-widgets', './mpl.js'], function(widgets, mpl) { + + var MPLCanvasView = widgets.WidgetView.extend({ + + render: function() { + var that = this; + + var id = this.model.get('_id'); + + var element = this.$el; + + this.ws_proxy = this.comm_websocket_adapter(this.model.comm); + + function ondownload(figure, format) { + window.open(figure.imageObj.src); + } + + mpl.toolbar_items = this.model.get('_toolbar_items') + + var fig = new mpl.figure(id, this.ws_proxy, + ondownload, + 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. + this.ws_proxy.onopen(); + + fig.parent_element = element.get(0); + + // subscribe to incoming messages from the MPLCanvasWidget + this.model.on('msg:custom', this.ws_proxy.onmessage, this); + + this.send(JSON.stringify({ type: 'initialized' })); + }, + + 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 = {}; + var that = this; + + ws.close = function() { + comm.close() + }; + ws.send = function(m) { + that.send(m); + }; + return ws; + } + + }); + + mpl.figure.prototype.handle_close = function(fig, msg) { + var width = fig.canvas.width/mpl.ratio + fig.root.unbind('remove') + + // Re-enable the keyboard manager in IPython - without this line, in FF, + // the notebook keyboard shortcuts fail. + IPython.keyboard_manager.enable() + fig.close_ws(fig, msg); + } + + mpl.figure.prototype.close_ws = function(fig, msg){ + fig.send_message('closing', msg); + // fig.ws.close() + } + + mpl.figure.prototype.updated_canvas_event = function() { + // Tell IPython that the notebook contents must change. + IPython.notebook.set_dirty(true); + this.send_message("ack", {}); + } + + mpl.figure.prototype._init_toolbar = function() { + var fig = this; + + var nav_element = $('
') + nav_element.attr('style', 'width: 100%'); + this.root.append(nav_element); + + // 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']); + } + + for(var toolbar_ind in mpl.toolbar_items){ + var name = mpl.toolbar_items[toolbar_ind][0]; + var tooltip = mpl.toolbar_items[toolbar_ind][1]; + var image = mpl.toolbar_items[toolbar_ind][2]; + var method_name = mpl.toolbar_items[toolbar_ind][3]; + + if (!name) { continue; }; + + var button = $(''); + button.click(method_name, toolbar_event); + button.mouseover(tooltip, toolbar_mouse_event); + nav_element.append(button); + } + + // Add the status bar. + var status_bar = $(''); + nav_element.append(status_bar); + this.message = status_bar[0]; + + // Add the close button to the window. + var buttongrp = $('
'); + var button = $(''); + button.click(function (evt) { fig.handle_close(fig, {}); } ); + button.mouseover('Stop Interaction', toolbar_mouse_event); + buttongrp.append(button); + var titlebar = this.root.find($('.ui-dialog-titlebar')); + titlebar.prepend(buttongrp); + } + + mpl.figure.prototype._root_extra_style = function(el){ + var fig = this + el.on("remove", function(){ + fig.close_ws(fig, {}); + }); + } + + mpl.figure.prototype._canvas_extra_style = function(el){ + // this is important to make the div 'focusable + el.attr('tabindex', 0) + // reach out to IPython and tell the keyboard manager to turn it's self + // off when our div gets focus + + // location in version 3 + if (IPython.notebook.keyboard_manager) { + IPython.notebook.keyboard_manager.register_events(el); + } + else { + // location in version 2 + IPython.keyboard_manager.register_events(el); + } + + } + + mpl.figure.prototype._key_event_extra = function(event, name) { + var manager = IPython.notebook.keyboard_manager; + if (!manager) + manager = IPython.keyboard_manager; + + // Check for shift+enter + if (event.shiftKey && event.which == 13) { + this.canvas_div.blur(); + event.shiftKey = false; + // Send a "J" for go to next cell + event.which = 74; + event.keyCode = 74; + manager.command_mode(); + manager.handle_keydown(event); + } + } + + mpl.figure.prototype.handle_save = function(fig, msg) { + fig.ondownload(fig, null); + } + + return {MPLCanvasView: MPLCanvasView} +}); diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js deleted file mode 100644 index 9471f5340d51..000000000000 --- a/lib/matplotlib/backends/web_backend/nbagg_mpl.js +++ /dev/null @@ -1,211 +0,0 @@ -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; - // Get hold of the div created by the display call when the Comm - // socket was opened in Python. - var element = $("#" + id); - var ws_proxy = comm_websocket_adapter(comm) - - function ondownload(figure, format) { - window.open(figure.imageObj.src); - } - - var fig = new mpl.figure(id, ws_proxy, - ondownload, - 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("
"); - if (!fig.cell_info) { - console.error("Failed to find cell for figure", id, fig); - return; - } - - var output_index = fig.cell_info[2] - var cell = fig.cell_info[0]; - -}; - -mpl.figure.prototype.handle_close = function(fig, msg) { - var width = fig.canvas.width/mpl.ratio - fig.root.unbind('remove') - - // Update the output cell to use the data from the current canvas. - fig.push_to_output(); - var dataURL = fig.canvas.toDataURL(); - // Re-enable the keyboard manager in IPython - without this line, in FF, - // the notebook keyboard shortcuts fail. - IPython.keyboard_manager.enable() - $(fig.parent_element).html(''); - fig.close_ws(fig, msg); -} - -mpl.figure.prototype.close_ws = function(fig, msg){ - fig.send_message('closing', msg); - // fig.ws.close() -} - -mpl.figure.prototype.push_to_output = function(remove_interactive) { - // Turn the data on the canvas into data in the output cell. - var width = this.canvas.width/mpl.ratio - var dataURL = this.canvas.toDataURL(); - this.cell_info[1]['text/html'] = ''; -} - -mpl.figure.prototype.updated_canvas_event = function() { - // Tell IPython that the notebook contents must change. - IPython.notebook.set_dirty(true); - this.send_message("ack", {}); - var fig = this; - // Wait a second, then push the new image to the DOM so - // that it is saved nicely (might be nice to debounce this). - setTimeout(function () { fig.push_to_output() }, 1000); -} - -mpl.figure.prototype._init_toolbar = function() { - var fig = this; - - var nav_element = $('
') - nav_element.attr('style', 'width: 100%'); - this.root.append(nav_element); - - // 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']); - } - - for(var toolbar_ind in mpl.toolbar_items){ - var name = mpl.toolbar_items[toolbar_ind][0]; - var tooltip = mpl.toolbar_items[toolbar_ind][1]; - var image = mpl.toolbar_items[toolbar_ind][2]; - var method_name = mpl.toolbar_items[toolbar_ind][3]; - - if (!name) { continue; }; - - var button = $(''); - button.click(method_name, toolbar_event); - button.mouseover(tooltip, toolbar_mouse_event); - nav_element.append(button); - } - - // Add the status bar. - var status_bar = $(''); - nav_element.append(status_bar); - this.message = status_bar[0]; - - // Add the close button to the window. - var buttongrp = $('
'); - var button = $(''); - button.click(function (evt) { fig.handle_close(fig, {}); } ); - button.mouseover('Stop Interaction', toolbar_mouse_event); - buttongrp.append(button); - var titlebar = this.root.find($('.ui-dialog-titlebar')); - titlebar.prepend(buttongrp); -} - -mpl.figure.prototype._root_extra_style = function(el){ - var fig = this - el.on("remove", function(){ - fig.close_ws(fig, {}); - }); -} - -mpl.figure.prototype._canvas_extra_style = function(el){ - // this is important to make the div 'focusable - el.attr('tabindex', 0) - // reach out to IPython and tell the keyboard manager to turn it's self - // off when our div gets focus - - // location in version 3 - if (IPython.notebook.keyboard_manager) { - IPython.notebook.keyboard_manager.register_events(el); - } - else { - // location in version 2 - IPython.keyboard_manager.register_events(el); - } - -} - -mpl.figure.prototype._key_event_extra = function(event, name) { - var manager = IPython.notebook.keyboard_manager; - if (!manager) - manager = IPython.keyboard_manager; - - // Check for shift+enter - if (event.shiftKey && event.which == 13) { - this.canvas_div.blur(); - event.shiftKey = false; - // Send a "J" for go to next cell - event.which = 74; - event.keyCode = 74; - manager.command_mode(); - manager.handle_keydown(event); - } -} - -mpl.figure.prototype.handle_save = function(fig, msg) { - fig.ondownload(fig, null); -} - - -mpl.find_output_cell = function(html_output) { - // Return the cell and output element which can be found *uniquely* in the notebook. - // Note - this is a bit hacky, but it is done because the "notebook_saving.Notebook" - // IPython event is triggered only after the cells have been serialised, which for - // our purposes (turning an active figure into a static one), is too late. - var cells = IPython.notebook.get_cells(); - var ncells = cells.length; - for (var i=0; i= 3 moved mimebundle to data attribute of output - data = data.data; - } - if (data['text/html'] == html_output) { - return [cell, data, j]; - } - } - } - } -} - -// Register the function which deals with the matplotlib target/channel. -// The kernel may be null if the page has been refreshed. -if (IPython.notebook.kernel != null) { - IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm); -} diff --git a/setupext.py b/setupext.py index 6e6705e8bd33..4cf2548e3157 100755 --- a/setupext.py +++ b/setupext.py @@ -676,6 +676,7 @@ def get_package_data(self): 'backends/web_backend/jquery/css/themes/base/*.min.css', 'backends/web_backend/jquery/css/themes/base/images/*', 'backends/web_backend/css/*.*', + 'backends/web_backend/nbextension/*.js', 'backends/Matplotlib.nib/*', 'mpl-data/stylelib/*.mplstyle', ]} From 566e97a3372949d15951bcd4646ecde748cdb280 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 2 May 2016 10:37:55 -0500 Subject: [PATCH 2/4] Fix path and make mpl.js a UMD file --- lib/matplotlib/backends/web_backend/js/mpl.js | 26 +++++++++++++++---- .../backends/web_backend/js/nbagg_mpl.js | 2 +- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index cecebd8e0201..d9949453ea5c 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -1,4 +1,16 @@ /* Put everything inside the global mpl namespace */ + +// Universal Module Definition +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else { + // Browser globals (root is window) + root.returnExports = factory(root.jQuery); + } +}(this, function ($) { + window.mpl = {}; @@ -133,11 +145,11 @@ mpl.figure.prototype._init_canvas = function() { this.context = canvas[0].getContext("2d"); var backingStore = this.context.backingStorePixelRatio || - this.context.webkitBackingStorePixelRatio || - this.context.mozBackingStorePixelRatio || - this.context.msBackingStorePixelRatio || - this.context.oBackingStorePixelRatio || - this.context.backingStorePixelRatio || 1; + this.context.webkitBackingStorePixelRatio || + this.context.mozBackingStorePixelRatio || + this.context.msBackingStorePixelRatio || + this.context.oBackingStorePixelRatio || + this.context.backingStorePixelRatio || 1; mpl.ratio = (window.devicePixelRatio || 1) / backingStore; @@ -552,3 +564,7 @@ mpl.figure.prototype.toolbar_button_onclick = function(name) { mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) { this.message.textContent = tooltip; }; + +return mpl; + +})); diff --git a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js index c5341b2208d8..196c63cb75eb 100644 --- a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js @@ -1,5 +1,5 @@ -define(['jupyter-js-widgets', './mpl.js'], function(widgets, mpl) { +define(['jupyter-js-widgets', '/nbextensions/matplotlib/mpl.js'], function(widgets, mpl) { var MPLCanvasView = widgets.WidgetView.extend({ From de652b71feb55201696116d8c7d10ac9a1812177 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 2 May 2016 11:52:05 -0500 Subject: [PATCH 3/4] Clean up the includes --- lib/matplotlib/__init__.py | 2 +- lib/matplotlib/backends/backend_nbagg.py | 2 +- .../backends/web_backend/js/extension.js | 18 ++++++++++++++++++ .../backends/web_backend/js/nbagg_mpl.js | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 lib/matplotlib/backends/web_backend/js/extension.js diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 977cfd73bcb0..63698180788a 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1461,7 +1461,7 @@ def _jupyter_nbextension_paths(): 'section': 'notebook', 'src': 'backends/web_backend/js', 'dest': 'matplotlib', - 'require': 'matplotlib/mpl' + 'require': 'matplotlib/extension' }] diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 7792262620aa..972bc8929975 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -128,7 +128,7 @@ def export(self): class FigureCanvasNbAgg(DOMWidget, FigureCanvasWebAggCore): - _view_module = Unicode("nbextensions/matplotlib/nbagg_mpl", sync=True) + _view_module = Unicode("matplotlib", sync=True) _view_name = Unicode('MPLCanvasView', sync=True) _toolbar_items = List(sync=True) _closed = Bool(True) diff --git a/lib/matplotlib/backends/web_backend/js/extension.js b/lib/matplotlib/backends/web_backend/js/extension.js new file mode 100644 index 000000000000..be7ea701550c --- /dev/null +++ b/lib/matplotlib/backends/web_backend/js/extension.js @@ -0,0 +1,18 @@ + +define([], function() { + if (window.require) { + window.require.config({ + map: { + "*" : { + "matplotlib": "nbextensions/matplotlib/nbagg_mpl", + "jupyter-js-widgets": "nbextensions/jupyter-js-widgets/extension" + } + } + }); + } + + // Export the required load_ipython_extention + return { + load_ipython_extension: function() {} + }; +}); diff --git a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js index 196c63cb75eb..0718434c75e8 100644 --- a/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/js/nbagg_mpl.js @@ -1,4 +1,5 @@ + define(['jupyter-js-widgets', '/nbextensions/matplotlib/mpl.js'], function(widgets, mpl) { var MPLCanvasView = widgets.WidgetView.extend({ From 1ec611a7a3ff1edd7e2358d1e0b7d0880b7bf83a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 2 May 2016 12:23:53 -0500 Subject: [PATCH 4/4] Update the package data --- setup.py | 8 ++++++++ setupext.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2570f157152b..754639ad5ec5 100644 --- a/setup.py +++ b/setup.py @@ -290,6 +290,14 @@ def run(self): ext_modules=ext_modules, package_dir=package_dir, package_data=package_data, + include_package_data=True, + data_files=[ + ('share/jupyter/nbextensions/matplotlib', [ + 'lib/matplotlib/backends/web_backend/js/extension.js', + 'lib/matplotlib/backends/web_backend/js/nbagg_mpl.js', + 'lib/matplotlib/backends/web_backend/js/mpl.js', + ]), + ], classifiers=classifiers, download_url="http://matplotlib.org/users/installing.html", diff --git a/setupext.py b/setupext.py index 4cf2548e3157..95644c79bb28 100755 --- a/setupext.py +++ b/setupext.py @@ -676,7 +676,7 @@ def get_package_data(self): 'backends/web_backend/jquery/css/themes/base/*.min.css', 'backends/web_backend/jquery/css/themes/base/images/*', 'backends/web_backend/css/*.*', - 'backends/web_backend/nbextension/*.js', + 'backends/web_backend/js/*.js', 'backends/Matplotlib.nib/*', 'mpl-data/stylelib/*.mplstyle', ]}