Skip to content

Nbagg enhancements #3552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 24, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 82 additions & 17 deletions lib/matplotlib/backends/backend_nbagg.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
"""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
import io
import os
from uuid import uuid4 as uuid

import tornado.ioloop

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
from matplotlib.backend_bases import (ShowBase, NavigationToolbar2,
TimerBase, FigureCanvasBase)


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()
Expand All @@ -48,19 +57,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)


Expand Down Expand Up @@ -93,7 +101,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):
Expand All @@ -105,6 +114,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()

Expand Down Expand Up @@ -137,6 +150,49 @@ 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 start_event_loop(self, timeout):
FigureCanvasBase.start_event_loop_default(self, timeout)

def stop_event_loop(self):
FigureCanvasBase.stop_event_loop_default(self)


def new_figure_manager(num, *args, **kwargs):
"""
Expand All @@ -151,7 +207,9 @@ def new_figure_manager_given_figure(num, figure):
"""
Create a new figure manager instance for the given figure.
"""
canvas = FigureCanvasWebAggCore(figure)
canvas = FigureCanvasNbAgg(figure)
if rcParams['nbagg.transparent']:
figure.patch.set_alpha(0)
manager = FigureManagerNbAgg(canvas, num)
return manager

Expand All @@ -170,6 +228,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("<div id=%r></div>" % self.uuid))
try:
self.comm = Comm('matplotlib', data={'id': self.uuid})
Expand All @@ -178,12 +238,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)})
Expand Down
27 changes: 1 addition & 26 deletions lib/matplotlib/backends/backend_webagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from matplotlib.figure import Figure
from matplotlib._pylab_helpers import Gcf
from . import backend_webagg_core as core
from .backend_nbagg import TimerTornado


def new_figure_manager(num, *args, **kwargs):
Expand Down Expand Up @@ -96,32 +97,6 @@ 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
Expand Down
33 changes: 20 additions & 13 deletions lib/matplotlib/backends/backend_webagg_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been raised as a concern by @mdboom in #5419.

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
Expand All @@ -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()
Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/backends/web_backend/mpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 7 additions & 1 deletion lib/matplotlib/backends/web_backend/nbagg_mpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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;
});

};
Expand All @@ -53,12 +55,16 @@ 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('<img src="' + dataURL + '">');
fig.send_message('closing', {});
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 dataURL = this.canvas.toDataURL();
this.cell_info[1]['text/html'] = '<img src="' + dataURL + '">';
}
Expand Down
Loading