From bd0a3f8de6e40bf595bf5fd2be7712d24df65f8c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 22 May 2015 10:39:20 -0400 Subject: [PATCH 1/6] FIX : first pass at fixing nbagg close issue Add callback to destroy figure when the user hits the red x in the notebook. This is not the correct solution, but it (might) work for now. --- lib/matplotlib/backends/backend_nbagg.py | 10 ++++++++++ lib/matplotlib/backends/backend_webagg_core.py | 1 + 2 files changed, 11 insertions(+) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 146dd91dc8cc..63f958d529e7 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -231,13 +231,22 @@ def new_figure_manager_given_figure(num, figure): """ Create a new figure manager instance for the given figure. """ + from .._pylab_helpers import Gcf + + def closer(event): + Gcf.destroy(num) + canvas = FigureCanvasNbAgg(figure) if rcParams['nbagg.transparent']: figure.patch.set_alpha(0) manager = FigureManagerNbAgg(canvas, num) + if is_interactive(): manager.show() figure.canvas.draw_idle() + + canvas.mpl_connect('close_event', closer) + return manager @@ -298,6 +307,7 @@ def on_message(self, message): message = json.loads(message['content']['data']) if message['type'] == 'closing': self.on_close() + self.manager.canvas.close_event() elif message['type'] == 'supports_binary': self.supports_binary = message['value'] else: diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index fd0585e7eb97..4d503d5c6854 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -329,6 +329,7 @@ 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_{0}'.format(e_type), None) if handler is None: From b16fcf9d9f2b74afa560e03b98e4bed3d7abbd9a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 5 Jul 2015 11:11:28 -0400 Subject: [PATCH 2/6] FIX: move logic where it should be --- lib/matplotlib/backends/backend_nbagg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 63f958d529e7..7cd76eb9d53e 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -285,6 +285,7 @@ def on_close(self): # the FigureManager. self.comm.close() self.manager.clearup_closed() + self.manager.canvas.close_event() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) @@ -307,7 +308,6 @@ def on_message(self, message): message = json.loads(message['content']['data']) if message['type'] == 'closing': self.on_close() - self.manager.canvas.close_event() elif message['type'] == 'supports_binary': self.supports_binary = message['value'] else: From 6678e029c566a9b0bb6e4261c8422dfd4795cc69 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 23 Jul 2015 13:29:13 -0400 Subject: [PATCH 3/6] WIP: race condition? --- lib/matplotlib/backends/backend_nbagg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 7cd76eb9d53e..1068e582293f 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -172,7 +172,10 @@ def destroy(self): 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()]) + if socket.is_open()]) + + if len(self.web_sockets) == 0: + self.manager.canvas.close_event() class TimerTornado(TimerBase): @@ -284,8 +287,6 @@ def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. self.comm.close() - self.manager.clearup_closed() - self.manager.canvas.close_event() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) From 436dfdbc4cb6b4a9cd9868b4dd36d60cdcc7da97 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 15 Aug 2015 23:19:49 -0400 Subject: [PATCH 4/6] FIX: destroy figs in python when destroyed in js - only actually closes the figure (in the mpl sense) if all of the managers active comms are dead - closes figure when all visible copies are removed from dom - eliminates many of the [IPKernelApp] ERROR | No such comm: XXXX errors closes #4841 --- lib/matplotlib/backends/backend_nbagg.py | 29 +++++++++++++++---- .../backends/web_backend/nbagg_mpl.js | 16 ++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 1068e582293f..1d9a35333539 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -166,8 +166,10 @@ def _create_comm(self): def destroy(self): self._send_event('close') - for comm in self.web_sockets.copy(): + # 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.""" @@ -175,7 +177,11 @@ def clearup_closed(self): if socket.is_open()]) if len(self.web_sockets) == 0: - self.manager.canvas.close_event() + self.canvas.close_event() + + 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]) class TimerTornado(TimerBase): @@ -278,15 +284,27 @@ def __init__(self, manager): self.comm.on_msg(self.on_message) manager = self.manager - self.comm.on_close(lambda close_message: manager.clearup_closed()) + 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.comm._closed + return not (self._ext_close or self.comm._closed) def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. - self.comm.close() + 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)}) @@ -309,6 +327,7 @@ def on_message(self, message): 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: diff --git a/lib/matplotlib/backends/web_backend/nbagg_mpl.js b/lib/matplotlib/backends/web_backend/nbagg_mpl.js index 863e171bd8c0..b5e143017561 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_mpl.js +++ b/lib/matplotlib/backends/web_backend/nbagg_mpl.js @@ -55,6 +55,8 @@ mpl.mpl_figure_comm = function(comm, msg) { }; mpl.figure.prototype.handle_close = function(fig, msg) { + 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(); @@ -62,8 +64,12 @@ mpl.figure.prototype.handle_close = function(fig, msg) { // the notebook keyboard shortcuts fail. IPython.keyboard_manager.enable() $(fig.parent_element).html(''); - fig.send_message('closing', {}); - fig.ws.close() + 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) { @@ -126,6 +132,12 @@ mpl.figure.prototype._init_toolbar = function() { 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 From f1c9d7881d690e42cce8dbe2931638330a705fad Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 16 Aug 2015 07:59:04 -0400 Subject: [PATCH 5/6] MNT: suppress pending figure count in interactive In interactive mode the number of pending figures will always be equal to the number of open figures. Only include this count in `connection_info` in non-interactive mode. --- lib/matplotlib/backends/backend_nbagg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 1d9a35333539..068d4bb0d716 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -78,9 +78,10 @@ def connection_info(): for manager in Gcf.get_all_fig_managers(): fig = manager.canvas.figure result.append('{0} - {0}'.format((fig.get_label() or - "Figure {0}".format(manager.num)), - manager.web_sockets)) - result.append('Figures pending show: {0}'.format(len(Gcf._activeQue))) + "Figure {0}".format(manager.num)), + manager.web_sockets)) + if not is_interactive(): + result.append('Figures pending show: {0}'.format(len(Gcf._activeQue))) return '\n'.join(result) From 3aa39094d1a167dd233c17359fa7fa7a19fe9937 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 17 Aug 2015 11:40:19 -0400 Subject: [PATCH 6/6] TST: update UAT - update to use python3 - update to use nbformat v4 - add test for close figure on remove from DOM --- .../backends/web_backend/nbagg_uat.ipynb | 1197 +++++++++-------- 1 file changed, 629 insertions(+), 568 deletions(-) diff --git a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb index 591a20644cd7..dedefb2d540f 100644 --- a/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb +++ b/lib/matplotlib/backends/web_backend/nbagg_uat.ipynb @@ -1,583 +1,644 @@ { + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from __future__ import print_function\n", + "from imp import reload" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import matplotlib\n", + "reload(matplotlib)\n", + "\n", + "matplotlib.use('nbagg')\n", + "\n", + "import matplotlib.backends.backend_nbagg\n", + "reload(matplotlib.backends.backend_nbagg)" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "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", + "fig1 = plt.figure()\n", + "plt.plot(range(10))\n", + "\n", + "plt.show()" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.plot([3, 2, 1])\n", + "plt.show()" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "print(matplotlib.backends.backend_nbagg.connection_info())" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.close(fig1)" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.plot(range(10))" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "print(matplotlib.backends.backend_nbagg.connection_info())" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.show()\n", + "plt.figure()\n", + "plt.plot(range(5))\n", + "plt.show()" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.interactive(True)\n", + "plt.figure()\n", + "plt.plot([3, 2, 1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Subsequent lines should be added to the existing figure, rather than creating a new one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.plot(range(3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calling connection_info in interactive mode should not show any pending figures." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "print(matplotlib.backends.backend_nbagg.connection_info())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Disable interactive mode again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.interactive(False)" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "plt.gcf().canvas.manager.reshow()" + ] + }, + { + "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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig = plt.figure()\n", + "plt.axes()\n", + "plt.show()\n", + "\n", + "plt.plot([1, 2, 3])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 12 - OO interface\n", + "\n", + "Should produce a new figure and plot it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "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()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UAT 13 - Animation\n", + "\n", + "The following should generate an animated line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "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()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 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", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "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()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 16 - Events\n", + "\n", + "Pressing any keyboard key or mouse button (or scrolling) should cycle the line line while the figure has focus. The figure should have focus by default when it is created and re-gain it by clicking on the canvas. Clicking anywhere outside of the figure should release focus, but moving the mouse out of the figure should not release focus." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import itertools\n", + "fig, ax = plt.subplots()\n", + "x = np.linspace(0,10,10000)\n", + "y = np.sin(x)\n", + "ln, = ax.plot(x,y)\n", + "evt = []\n", + "colors = iter(itertools.cycle(['r', 'g', 'b', 'k', 'c']))\n", + "def on_event(event):\n", + " if event.name.startswith('key'):\n", + " fig.suptitle('%s: %s' % (event.name, event.key))\n", + " elif event.name == 'scroll_event':\n", + " fig.suptitle('%s: %s' % (event.name, event.step))\n", + " else:\n", + " fig.suptitle('%s: %s' % (event.name, event.button))\n", + " evt.append(event)\n", + " ln.set_color(next(colors))\n", + " fig.canvas.draw()\n", + " fig.canvas.draw_idle()\n", + "\n", + "fig.canvas.mpl_connect('button_press_event', on_event)\n", + "fig.canvas.mpl_connect('button_release_event', on_event)\n", + "fig.canvas.mpl_connect('scroll_event', on_event)\n", + "fig.canvas.mpl_connect('key_press_event', on_event)\n", + "fig.canvas.mpl_connect('key_release_event', on_event)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT 17 - Timers\n", + "\n", + "Single-shot timers follow a completely different code path in the nbagg backend than regular timers (such as those used in the animation example above.) The next set of tests ensures that both \"regular\" and \"single-shot\" timers work properly.\n", + "\n", + "The following should show a simple clock that updates twice a second:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "fig, ax = plt.subplots()\n", + "text = ax.text(0.5, 0.5, '', ha='center')\n", + "\n", + "def update(text):\n", + " text.set(text=time.ctime())\n", + " text.axes.figure.canvas.draw()\n", + " \n", + "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", + "timer.start()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, the following should only update once and then stop:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "text = ax.text(0.5, 0.5, '', ha='center') \n", + "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", + "\n", + "timer.single_shot = True\n", + "timer.start()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And the next two examples should never show any visible text at all:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "text = ax.text(0.5, 0.5, '', ha='center')\n", + "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", + "\n", + "timer.start()\n", + "timer.stop()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "text = ax.text(0.5, 0.5, '', ha='center')\n", + "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", + "\n", + "timer.single_shot = True\n", + "timer.start()\n", + "timer.stop()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UAT17 - stoping figure when removed from DOM\n", + "\n", + "When the div that contains from the figure is removed from the DOM the figure should shut down it's comm, and if the python-side figure has no more active comms, it should destroy the figure. Repeatedly running the cell below should always have the same figure number" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.plot(range(5))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Running the cell below will re-show the figure. After this, re-running the cell above should result in a new figure number." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig.canvas.manager.reshow()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.10" - }, - "name": "" - }, - "nbformat": 3, - "nbformat_minor": 0, - "worksheets": [ - { - "cells": [ - { - "cell_type": "code", - "collapsed": false, - "input": [ - "from __future__ import print_function\n", - "from imp import reload" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "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", - "matplotlib.use('nbagg')\n", - "\n", - "import matplotlib.backends.backend_nbagg\n", - "reload(matplotlib.backends.backend_nbagg)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "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.backends.backend_webagg_core\n", - "reload(matplotlib.backends.backend_webagg_core)\n", - "\n", - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Disable interactive mode again." - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "plt.interactive(False)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "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": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 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": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### UAT 16 - Events\n", - "\n", - "Pressing any keyboard key or mouse button (or scrolling) should cycle the line line while the figure has focus. The figure should have focus by default when it is created and re-gain it by clicking on the canvas. Clicking anywhere outside of the figure should release focus, but moving the mouse out of the figure should not release focus." - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "import itertools\n", - "fig, ax = plt.subplots()\n", - "x = np.linspace(0,10,10000)\n", - "y = np.sin(x)\n", - "ln, = ax.plot(x,y)\n", - "evt = []\n", - "colors = iter(itertools.cycle(['r', 'g', 'b', 'k', 'c']))\n", - "def on_event(event):\n", - " if event.name.startswith('key'):\n", - " fig.suptitle('%s: %s' % (event.name, event.key))\n", - " elif event.name == 'scroll_event':\n", - " fig.suptitle('%s: %s' % (event.name, event.step))\n", - " else:\n", - " fig.suptitle('%s: %s' % (event.name, event.button))\n", - " evt.append(event)\n", - " ln.set_color(next(colors))\n", - " fig.canvas.draw()\n", - " fig.canvas.draw_idle()\n", - "\n", - "fig.canvas.mpl_connect('button_press_event', on_event)\n", - "fig.canvas.mpl_connect('button_release_event', on_event)\n", - "fig.canvas.mpl_connect('scroll_event', on_event)\n", - "fig.canvas.mpl_connect('key_press_event', on_event)\n", - "fig.canvas.mpl_connect('key_release_event', on_event)\n", - "\n", - "plt.show()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### UAT 17 - Timers\n", - "\n", - "Single-shot timers follow a completely different code path in the nbagg backend than regular timers (such as those used in the animation example above.) The next set of tests ensures that both \"regular\" and \"single-shot\" timers work properly.\n", - "\n", - "The following should show a simple clock that updates twice a second:" - ] - }, - { - "cell_type": "code", - "collapsed": true, - "input": [ - "import time\n", - "\n", - "fig, ax = plt.subplots()\n", - "text = ax.text(0.5, 0.5, '', ha='center')\n", - "\n", - "def update(text):\n", - " text.set(text=time.ctime())\n", - " text.axes.figure.canvas.draw()\n", - " \n", - "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", - "timer.start()\n", - "plt.show()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, the following should only update once and then stop:" - ] - }, - { - "cell_type": "code", - "collapsed": true, - "input": [ - "fig, ax = plt.subplots()\n", - "text = ax.text(0.5, 0.5, '', ha='center') \n", - "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", - "\n", - "timer.single_shot = True\n", - "timer.start()\n", - "\n", - "plt.show()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And the next two examples should never show any visible text at all:" - ] - }, - { - "cell_type": "code", - "collapsed": true, - "input": [ - "fig, ax = plt.subplots()\n", - "text = ax.text(0.5, 0.5, '', ha='center')\n", - "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", - "\n", - "timer.start()\n", - "timer.stop()\n", - "\n", - "plt.show()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "code", - "collapsed": true, - "input": [ - "fig, ax = plt.subplots()\n", - "text = ax.text(0.5, 0.5, '', ha='center')\n", - "timer = fig.canvas.new_timer(500, [(update, [text], {})])\n", - "\n", - "timer.single_shot = True\n", - "timer.start()\n", - "timer.stop()\n", - "\n", - "plt.show()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - } - ], - "metadata": {} + "pygments_lexer": "ipython3", + "version": "3.4.3" } - ] + }, + "nbformat": 4, + "nbformat_minor": 0 }