From 31f305ed4e146ee01d9fe4cc0b427670745c0cfb Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Fri, 19 Sep 2014 17:33:35 +0100 Subject: [PATCH 1/4] Extended the nbagg backend to support animation. --- lib/matplotlib/backends/backend_nbagg.py | 83 +++- lib/matplotlib/backends/backend_webagg.py | 29 +- .../backends/web_backend/nbagg_mpl.js | 5 +- .../backends/web_backend/nbagg_uat.ipynb | 365 ++++++++++++++++++ 4 files changed, 437 insertions(+), 45 deletions(-) create mode 100644 lib/matplotlib/backends/web_backend/nbagg_uat.ipynb diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index c69c558ea071..907c737419b5 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -5,6 +5,8 @@ import os from uuid import uuid4 as uuid +import tornado.ioloop + from IPython.display import display, Javascript, HTML from IPython.kernel.comm import Comm @@ -12,24 +14,23 @@ from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg, FigureCanvasWebAggCore, NavigationToolbar2WebAgg) -from matplotlib.backend_bases import ShowBase, NavigationToolbar2 +from matplotlib.backend_bases import ShowBase, NavigationToolbar2, TimerBase class Show(ShowBase): def __call__(self, block=None): - import matplotlib._pylab_helpers as pylab_helpers + from matplotlib._pylab_helpers import Gcf from matplotlib import is_interactive - managers = pylab_helpers.Gcf.get_all_fig_managers() + managers = Gcf.get_all_fig_managers() if not managers: return - interactive = is_interactive() - for manager in managers: manager.show() - if not interactive and manager in pylab_helpers.Gcf._activeQue: - pylab_helpers.Gcf._activeQue.remove(manager) + + if not is_interactive() and manager in Gcf._activeQue: + Gcf._activeQue.remove(manager) show = Show() @@ -48,19 +49,18 @@ def draw_if_interactive(): def connection_info(): """ Return a string showing the figure and connection status for - the backend. + the backend. This is intended as a diagnostic tool, and not for general + use. """ - # TODO: Make this useful! - import matplotlib._pylab_helpers as pylab_helpers + from matplotlib._pylab_helpers import Gcf result = [] - for manager in pylab_helpers.Gcf.get_all_fig_managers(): + for manager in 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))) + result.append('Figures pending show: {}'.format(len(Gcf._activeQue))) return '\n'.join(result) @@ -93,7 +93,8 @@ def __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. + # browser instances using the same kernel (require.js - but the + # file isn't static?). display(Javascript(FigureManagerNbAgg.get_javascript())) def show(self): @@ -105,6 +106,10 @@ def show(self): self._shown = True def reshow(self): + """ + A special method to re-show the figure in the notebook. + + """ self._shown = False self.show() @@ -137,6 +142,43 @@ def destroy(self): for comm in self.web_sockets.copy(): comm.on_close() + def clearup_closed(self): + """Clear up any closed Comms.""" + self.web_sockets = set([socket for socket in self.web_sockets + if not socket.is_open()]) + + +class TimerTornado(TimerBase): + def _timer_start(self): + import datetime + self._timer_stop() + if self._single: + ioloop = tornado.ioloop.IOLoop.instance() + self._timer = ioloop.add_timeout( + datetime.timedelta(milliseconds=self.interval), + self._on_timer) + else: + self._timer = tornado.ioloop.PeriodicCallback( + self._on_timer, + self.interval) + self._timer.start() + + def _timer_stop(self): + if self._timer is not None: + self._timer.stop() + self._timer = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started + if self._timer is not None: + self._timer_stop() + self._timer_start() + + +class FigureCanvasNbAgg(FigureCanvasWebAggCore): + def new_timer(self, *args, **kwargs): + return TimerTornado(*args, **kwargs) + def new_figure_manager(num, *args, **kwargs): """ @@ -151,7 +193,7 @@ def new_figure_manager_given_figure(num, figure): """ Create a new figure manager instance for the given figure. """ - canvas = FigureCanvasWebAggCore(figure) + canvas = FigureCanvasNbAgg(figure) manager = FigureManagerNbAgg(canvas, num) return manager @@ -170,6 +212,8 @@ 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}) @@ -178,12 +222,17 @@ def __init__(self, manager): 'instance. Are you in the IPython notebook?') self.comm.on_msg(self.on_message) + manager = self.manager + self.comm.on_close(lambda close_message: manager.clearup_closed()) + + def is_open(self): + return not self.comm._closed + 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() + self.manager.clearup_closed() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index 05e0e1cb23c6..e2330780ca12 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -39,6 +39,7 @@ from matplotlib.figure import Figure from matplotlib._pylab_helpers import Gcf from . import backend_webagg_core as core +from . import backend_nbagg def new_figure_manager(num, *args, **kwargs): @@ -96,39 +97,13 @@ def run(self): webagg_server_thread = ServerThread() -class TimerTornado(backend_bases.TimerBase): - def _timer_start(self): - self._timer_stop() - if self._single: - ioloop = tornado.ioloop.IOLoop.instance() - self._timer = ioloop.add_timeout( - datetime.timedelta(milliseconds=self.interval), - self._on_timer) - else: - self._timer = tornado.ioloop.PeriodicCallback( - self._on_timer, - self.interval) - self._timer.start() - - def _timer_stop(self): - if self._timer is not None: - self._timer.stop() - self._timer = None - - def _timer_set_interval(self): - # Only stop and restart it if the timer has already been started - if self._timer is not None: - self._timer_stop() - self._timer_start() - - class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): def show(self): # show the figure window show() def new_timer(self, *args, **kwargs): - return TimerTornado(*args, **kwargs) + return backend_nbagg.TimerTornado(*args, **kwargs) def start_event_loop(self, timeout): backend_bases.FigureCanvasBase.start_event_loop_default( diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js index caac0c17656d..3232407fa8b0 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/nbagg_mpl.js @@ -25,6 +25,8 @@ mpl.mpl_figure_comm = function(comm, msg) { // 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) @@ -44,7 +46,7 @@ mpl.mpl_figure_comm = function(comm, msg) { // Disable right mouse context menu. $(fig.rubberband_canvas).bind("contextmenu",function(e){ - return false; + return false; }); }; @@ -59,6 +61,7 @@ mpl.figure.prototype.handle_close = function(fig, msg) { } mpl.figure.prototype.push_to_output = function(remove_interactive) { + // Turn the data on the canvas into data in the output cell. var dataURL = this.canvas.toDataURL(); this.cell_info[1]['text/html'] = ''; } diff --git a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb new file mode 100644 index 000000000000..81e47b041e34 --- /dev/null +++ b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb @@ -0,0 +1,365 @@ +{ + "metadata": { + "name": "", + "signature": "sha256:9a73a15660e6912c6bcfaf7c9e8247503738ce5d590a34e08789c2e16de93080" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UAT for NbAgg backend.\n", + "\n", + "The first line simply reloads matplotlib, uses the nbagg backend and then reloads the backend, just to ensure we have the latest modification to the backend code. Note: The underlying JavaScript will not be updated by this process, so a refresh of the browser after clearing the output and saving is necessary to clear everything fully." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib\n", + "reload(matplotlib)\n", + "\n", + "\n", + "matplotlib.use('nbagg')\n", + "\n", + "import matplotlib.backends.backend_nbagg\n", + "reload(matplotlib.backends.backend_nbagg)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 1 - Simple figure creation using pyplot\n", + "\n", + "Should produce a figure window which is interactive with the pan and zoom buttons. (Do not press the close button, but any others may be used)." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib.pyplot as plt\n", + "plt.interactive(False)\n", + "\n", + "fig1 = plt.figure()\n", + "plt.plot(range(10))\n", + "\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 2 - Creation of another figure, without the need to do plt.figure.\n", + "\n", + "As above, a new figure should be created." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot([3, 2, 1])\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 3 - Connection info\n", + "\n", + "The printout should show that there are two figures which have active CommSockets, and no figures pending show." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "print matplotlib.backends.backend_nbagg.connection_info()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 4 - Closing figures\n", + "\n", + "Closing a specific figure instance should turn the figure into a plain image - the UI should have been removed. In this case, scroll back to the first figure and assert this is the case." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.close(fig1)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 5 - No show without plt.show in non-interactive mode\n", + "\n", + "Simply doing a plt.plot should not show a new figure, nor indeed update an existing one (easily verified in UAT 6).\n", + "The output should simply be a list of Line2D instances." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot(range(10))" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 6 - Connection information\n", + "\n", + "We just created a new figure, but didn't show it. Connection info should no longer have \"Figure 1\" (as we closed it in UAT 4) and should have figure 2 and 3, with Figure 3 without any connections. There should be 1 figure pending." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "print matplotlib.backends.backend_nbagg.connection_info()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 7 - Show of previously created figure\n", + "\n", + "We should be able to show a figure we've previously created. The following should produce two figure windows." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.show()\n", + "plt.figure()\n", + "plt.plot(range(5))\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 8 - Interactive mode\n", + "\n", + "In interactive mode, creating a line should result in a figure being shown." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.interactive(True)\n", + "plt.figure()\n", + "plt.plot([3, 2, 1])" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Subsequent lines should be added to the existing figure, rather than creating a new one." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.plot(range(3))" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Disable interactive mode again." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.interactive(False)" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 9 - Multiple shows\n", + "\n", + "Unlike most of the other matplotlib backends, we may want to see a figure multiple times (with or without synchronisation between the views, though the former is not yet implemented). Assert that plt.gcf().canvas.manager.reshow() results in another figure window which is synchronised upon pan & zoom." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plt.gcf().canvas.manager.reshow()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 10 - Saving notebook\n", + "\n", + "Saving the notebook (with CTRL+S or File->Save) should result in the saved notebook having static versions of the figues embedded within. The image should be the last update from user interaction and interactive plotting. (check by converting with ``ipython nbconvert ``)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 11 - Creation of a new figure on second show\n", + "\n", + "Create a figure, show it, then create a new axes and show it. The result should be a new figure.\n", + "\n", + "**BUG: Sometimes this doesn't work - not sure why (@pelson).**" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "fig = plt.figure()\n", + "plt.axes()\n", + "plt.show()\n", + "\n", + "plt.plot([1, 2, 3])\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 12 - OO interface\n", + "\n", + "Should produce a new figure and plot it." + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from matplotlib.backends.backend_nbagg import new_figure_manager,show\n", + "\n", + "manager = new_figure_manager(1000)\n", + "fig = manager.canvas.figure\n", + "ax = fig.add_subplot(1,1,1)\n", + "ax.plot([1,2,3])\n", + "fig.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UAT 13 - Animation\n", + "\n", + "The following should generate an animated line:" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib.animation as animation\n", + "import numpy as np\n", + "\n", + "fig, ax = plt.subplots()\n", + "\n", + "x = np.arange(0, 2*np.pi, 0.01) # x-array\n", + "line, = ax.plot(x, np.sin(x))\n", + "\n", + "def animate(i):\n", + " line.set_ydata(np.sin(x+i/10.0)) # update the data\n", + " return line,\n", + "\n", + "#Init only required for blitting to give a clean slate.\n", + "def init():\n", + " line.set_ydata(np.ma.array(x, mask=True))\n", + " return line,\n", + "\n", + "ani = animation.FuncAnimation(fig, animate, np.arange(1, 200), init_func=init,\n", + " interval=32., blit=True)\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT ?? - Keyboard shortcuts in IPython after close of figure (Current known bug)\n", + "### UAT ?? - Race condition to show means that \"run all\" ends up collapsing some figures into one (Current known bug)" + ] + } + ], + "metadata": {} + } + ] +} \ No newline at end of file From 3e4620ac3d407466f152eb6ef54518468ace2f4d Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Mon, 22 Sep 2014 10:41:54 +0100 Subject: [PATCH 2/4] Fixed firefox keyboard focus bug with nbagg backend. --- lib/matplotlib/backends/backend_webagg.py | 4 ++-- lib/matplotlib/backends/web_backend/nbagg_mpl.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index e2330780ca12..9c1a0e9a7f60 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -39,7 +39,7 @@ from matplotlib.figure import Figure from matplotlib._pylab_helpers import Gcf from . import backend_webagg_core as core -from . import backend_nbagg +from .backend_nbagg import TimerTornado def new_figure_manager(num, *args, **kwargs): @@ -103,7 +103,7 @@ def show(self): show() def new_timer(self, *args, **kwargs): - return backend_nbagg.TimerTornado(*args, **kwargs) + return TimerTornado(*args, **kwargs) def start_event_loop(self, timeout): backend_bases.FigureCanvasBase.start_event_loop_default( diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js index 3232407fa8b0..82c020aa8264 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/nbagg_mpl.js @@ -55,6 +55,9 @@ 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(); + // 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.send_message('closing', {}); fig.ws.close() From 1b96117636aa9c67631ab97e9acde45bb69b48c5 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Mon, 22 Sep 2014 12:19:44 +0100 Subject: [PATCH 3/4] Updated nbagg backend to default to a transparent figure patch. --- lib/matplotlib/backends/backend_nbagg.py | 27 +++++++++++- .../backends/web_backend/nbagg_uat.ipynb | 41 +++++++++++++++++-- lib/matplotlib/rcsetup.py | 1 + matplotlibrc.template | 4 ++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 907c737419b5..231e6ce6bdc2 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -1,5 +1,6 @@ """Interactive figures in the IPython notebook""" from base64 import b64encode +from contextlib import contextmanager import json import io import os @@ -10,11 +11,14 @@ from IPython.display import display, Javascript, HTML from IPython.kernel.comm import Comm +from matplotlib import rcParams from matplotlib.figure import Figure +from matplotlib.backends import backend_agg from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg, FigureCanvasWebAggCore, NavigationToolbar2WebAgg) -from matplotlib.backend_bases import ShowBase, NavigationToolbar2, TimerBase +from matplotlib.backend_bases import (ShowBase, NavigationToolbar2, + TimerBase, FigureCanvasBase) class Show(ShowBase): @@ -179,6 +183,25 @@ class FigureCanvasNbAgg(FigureCanvasWebAggCore): def new_timer(self, *args, **kwargs): return TimerTornado(*args, **kwargs) + def start_event_loop(self, timeout): + FigureCanvasBase.start_event_loop_default(self, timeout) + + def stop_event_loop(self): + FigureCanvasBase.stop_event_loop_default(self) + + def draw(self): + renderer = self.get_renderer() + + self._png_is_old = True + + backend_agg.RendererAgg.lock.acquire() + try: + self.figure.draw(renderer) + finally: + backend_agg.RendererAgg.lock.release() + # Swap the frames + self.manager.refresh_all() + def new_figure_manager(num, *args, **kwargs): """ @@ -194,6 +217,8 @@ def new_figure_manager_given_figure(num, figure): Create a new figure manager instance for the given figure. """ canvas = FigureCanvasNbAgg(figure) + if rcParams['nbagg.transparent']: + figure.patch.set_alpha(0) manager = FigureManagerNbAgg(canvas, num) return manager diff --git a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb index 81e47b041e34..5f3407e68458 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb +++ b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb @@ -1,7 +1,7 @@ { "metadata": { "name": "", - "signature": "sha256:9a73a15660e6912c6bcfaf7c9e8247503738ce5d590a34e08789c2e16de93080" + "signature": "sha256:34c3a5a9d82d5a3c433f0c03f0717d43e1d14eaf78f88a64f094d104fa636ddc" }, "nbformat": 3, "nbformat_minor": 0, @@ -92,7 +92,7 @@ "cell_type": "code", "collapsed": false, "input": [ - "print matplotlib.backends.backend_nbagg.connection_info()" + "print(matplotlib.backends.backend_nbagg.connection_info())" ], "language": "python", "metadata": {}, @@ -354,9 +354,42 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### UAT ?? - Keyboard shortcuts in IPython after close of figure (Current known bug)\n", - "### UAT ?? - Race condition to show means that \"run all\" ends up collapsing some figures into one (Current known bug)" + "### UAT 14 - Keyboard shortcuts in IPython after close of figure\n", + "\n", + "After closing the previous figure (with the close button above the figure) the IPython keyboard shortcuts should still function.\n", + "\n", + "### UAT 15 - Figure face colours\n", + "\n", + "The nbagg honours all colours appart from that of the figure.patch. The two plots below should produce a figure with a transparent background and a red background respectively (check the transparency by closing the figure, and dragging the resulting image over other content). There should be no yellow figure." ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "import matplotlib\n", + "matplotlib.rcParams.update({'figure.facecolor': 'red',\n", + " 'savefig.facecolor': 'yellow'})\n", + "plt.figure()\n", + "plt.plot([3, 2, 1])\n", + "\n", + "with matplotlib.rc_context({'nbagg.transparent': False}):\n", + " plt.figure()\n", + "\n", + "plt.plot([3, 2, 1])\n", + "plt.show()" + ], + "language": "python", + "metadata": {}, + "outputs": [] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [], + "language": "python", + "metadata": {}, + "outputs": [] } ], "metadata": {} diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index f535aec084e6..e0bfb8b96395 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -485,6 +485,7 @@ def __call__(self, s): 'webagg.port': [8988, validate_int], 'webagg.open_in_browser': [True, validate_bool], 'webagg.port_retries': [50, validate_int], + 'nbagg.transparent': [True, validate_bool], 'toolbar': ['toolbar2', validate_toolbar], 'datapath': [None, validate_path_exists], # handled by # _get_data_path_cached diff --git a/matplotlibrc.template b/matplotlibrc.template index bec6ea7e4312..af7940a4ae67 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -57,6 +57,10 @@ backend : %(backend)s # When True, open the webbrowser to the plot that is shown # webagg.open_in_browser : True +# When True, the figures rendered in the nbagg backend are created with +# a transparent background. +# nbagg.transparent : True + # if you are running pyplot inside a GUI and your backend choice # conflicts, we will automatically try to find a compatible one for # you if backend_fallback is True From aa583e4a114cb5883bf43d9d8d63c5323a898800 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 24 Sep 2014 11:08:03 +0100 Subject: [PATCH 4/4] Fixed the webagg backend to handle figures with transparency. --- lib/matplotlib/backends/backend_nbagg.py | 21 ++++-------- .../backends/backend_webagg_core.py | 33 +++++++++++-------- lib/matplotlib/backends/web_backend/mpl.js | 2 ++ .../backends/web_backend/nbagg_uat.ipynb | 14 +++----- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 231e6ce6bdc2..540c4fd28400 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -1,4 +1,8 @@ """Interactive figures in the IPython notebook""" +# Note: There is a notebook in +# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify +# that changes made maintain expected behaviour. + from base64 import b64encode from contextlib import contextmanager import json @@ -33,8 +37,8 @@ def __call__(self, block=None): for manager in managers: manager.show() - if not is_interactive() and manager in Gcf._activeQue: - Gcf._activeQue.remove(manager) + if not is_interactive() and manager in Gcf._activeQue: + Gcf._activeQue.remove(manager) show = Show() @@ -189,19 +193,6 @@ def start_event_loop(self, timeout): def stop_event_loop(self): FigureCanvasBase.stop_event_loop_default(self) - def draw(self): - renderer = self.get_renderer() - - self._png_is_old = True - - backend_agg.RendererAgg.lock.acquire() - try: - self.figure.draw(renderer) - finally: - backend_agg.RendererAgg.lock.release() - # Swap the frames - self.manager.refresh_all() - def new_figure_manager(num, *args, **kwargs): """ diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index eee727dbc7c7..3aa01724932c 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -71,7 +71,7 @@ def show(self): show() def draw(self): - renderer = self.get_renderer() + renderer = self.get_renderer(cleared=True) self._png_is_old = True @@ -91,21 +91,25 @@ 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. - buff = np.frombuffer( - self.get_renderer().buffer_rgba(), dtype=np.uint32) - buff.shape = ( - self._renderer.height, self._renderer.width) + renderer = self.get_renderer() + buff = np.frombuffer(renderer.buffer_rgba(), dtype=np.uint32) - if not self._force_full: - last_buffer = np.frombuffer( - self._last_renderer.buffer_rgba(), dtype=np.uint32) - last_buffer.shape = ( - self._renderer.height, self._renderer.width) + buff.shape = (renderer.height, renderer.width) + + # If any pixels have transparency, we need to force a full draw + # as we cannot overlay new on top of old. + pixels = buff.view(dtype=np.uint8).reshape(buff.shape + (4,)) + some_transparency = np.any(pixels[:, :, 3] != 255) + + output = buff + + if not self._force_full and not some_transparency: + last_buffer = np.frombuffer(self._last_renderer.buffer_rgba(), + dtype=np.uint32) + last_buffer.shape = (renderer.height, renderer.width) diff = buff != last_buffer output = np.where(diff, buff, 0) - else: - output = buff # Clear out the PNG data buffer rather than recreating it # each time. This reduces the number of memory @@ -122,7 +126,7 @@ def get_diff_image(self): # Swap the renderer frames self._renderer, self._last_renderer = ( - self._last_renderer, self._renderer) + self._last_renderer, renderer) self._force_full = False self._png_is_old = False return self._png_buffer.getvalue() @@ -147,6 +151,9 @@ def get_renderer(self, cleared=None): w, h, self.figure.dpi) self._lastKey = key + elif cleared: + self._renderer.clear() + return self._renderer def handle_event(self, event): diff --git a/lib/matplotlib/backends/web_backend/mpl.js b/lib/matplotlib/backends/web_backend/mpl.js index 6f1cf79364cb..5856b2347e32 100644 --- a/lib/matplotlib/backends/web_backend/mpl.js +++ b/lib/matplotlib/backends/web_backend/mpl.js @@ -60,6 +60,7 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) { } this.imageObj.onload = function() { + fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height); fig.context.drawImage(fig.imageObj, 0, 0); fig.waiting = false; }; @@ -322,6 +323,7 @@ mpl.figure.prototype._make_on_message_function = function(fig) { (window.URL || window.webkitURL).revokeObjectURL( fig.imageObj.src); } + fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL( evt.data); fig.updated_canvas_event(); diff --git a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb index 5f3407e68458..3601c1490643 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb +++ b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb @@ -1,7 +1,7 @@ { "metadata": { "name": "", - "signature": "sha256:34c3a5a9d82d5a3c433f0c03f0717d43e1d14eaf78f88a64f094d104fa636ddc" + "signature": "sha256:1d491e506b54b126f6971897d3249a9e6f96f9d50bf4e4ba8d179c6b7b1aefa8" }, "nbformat": 3, "nbformat_minor": 0, @@ -24,7 +24,6 @@ "import matplotlib\n", "reload(matplotlib)\n", "\n", - "\n", "matplotlib.use('nbagg')\n", "\n", "import matplotlib.backends.backend_nbagg\n", @@ -47,6 +46,9 @@ "cell_type": "code", "collapsed": false, "input": [ + "import matplotlib.backends.backend_webagg_core\n", + "reload(matplotlib.backends.backend_webagg_core)\n", + "\n", "import matplotlib.pyplot as plt\n", "plt.interactive(False)\n", "\n", @@ -382,14 +384,6 @@ "language": "python", "metadata": {}, "outputs": [] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [], - "language": "python", - "metadata": {}, - "outputs": [] } ], "metadata": {}