Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Added the nbAgg backend to allow interactive figures in the IPython
notebook.
  • Loading branch information
pelson committed May 19, 2014
commit a70af94a0f5ddbf543bad5ec588fac5bf2354697
2 changes: 1 addition & 1 deletion lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2514,7 +2514,7 @@ class NonGuiException(Exception):
pass


class FigureManagerBase:
class FigureManagerBase(object):
"""
Helper class for pyplot mode, wraps everything up into a neat bundle

Expand Down
195 changes: 195 additions & 0 deletions lib/matplotlib/backends/backend_nbagg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""Interactive figures in the IPython notebook"""
from base64 import b64encode
import json
import io
import os
from uuid import uuid4 as uuid

from IPython.display import display,Javascript,HTML
from IPython.kernel.comm import Comm

from matplotlib.figure import Figure
from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg,
FigureCanvasWebAggCore,
NavigationToolbar2WebAgg)
from matplotlib.backend_bases import ShowBase, NavigationToolbar2


class Show(ShowBase):
def __call__(self, block=None):
import matplotlib._pylab_helpers as pylab_helpers

# XXX How to do this just once? It has to deal with multiple
# browser instances using the same kernel.
display(Javascript(FigureManagerNbAgg.get_javascript()))

queue = pylab_helpers.Gcf._activeQue
for manager in queue[:]:
manager.show()
# If we are not interactive, disable the figure from the active queue,
# but don't destroy it.
queue.remove(manager)

show = Show()


def draw_if_interactive():
# TODO: Sort out the expected interactive interface & make it easy for
# somebody to "re-show" a specific figure.
pass
# from matplotlib import is_interactive
# import matplotlib._pylab_helpers as pylab_helpers
#
# if is_interactive():
# figManager = pylab_helpers.Gcf.get_active()
# if figManager is not None:
# figManager.show()


def connection_info():
"""
Return a string showing the figure and connection status for
the backend.

"""
# TODO: Make this useful!
import matplotlib._pylab_helpers as pylab_helpers
for manager in pylab_helpers.Gcf.get_all_fig_managers():
fig = manager.canvas.figure
print fig.get_label() or "Figure {0}".format(manager.num),
print [socket.supports_binary for socket in manager.web_sockets],
print manager.web_sockets

print 'Figures pending show: ', len(pylab_helpers.Gcf._activeQue)


class NavigationIPy(NavigationToolbar2WebAgg):
# Note: Version 3.2 icons, not the later 4.0 ones.
# http://fontawesome.io/3.2.1/icons/
_font_awesome_classes = {
'home': 'icon-home',
'back': 'icon-arrow-left',
'forward': 'icon-arrow-right',
'zoom_to_rect': 'icon-check-empty',
'move': 'icon-move',
None: None
}

# Use the standard toolbar items + download button
toolitems = [(text, tooltip_text, _font_awesome_classes[image_file], name_of_method)
for text, tooltip_text, image_file, name_of_method
in NavigationToolbar2.toolitems
if image_file in _font_awesome_classes]


class FigureManagerNbAgg(FigureManagerWebAgg):
ToolbarCls = NavigationIPy

def __init__(self, canvas, num):
self._shown = False
FigureManagerWebAgg.__init__(self, canvas, num)

def show(self):
if not self._shown:
self._create_comm()
self._shown = True

@property
def connected(self):
return bool(self.web_sockets)

@classmethod
def get_javascript(cls, stream=None):
if stream is None:
output = io.StringIO()
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')
for comm in self.web_sockets.copy():
comm.on_close()


def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
"""
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
return new_figure_manager_given_figure(num, thisFig)


def new_figure_manager_given_figure(num, figure):
"""
Create a new figure manager instance for the given figure.
"""
canvas = FigureCanvasWebAggCore(figure)
manager = FigureManagerNbAgg(canvas, num)
return manager


class CommSocket(object):
"""
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.

"""
def __init__(self, manager):
self.supports_binary = None
self.manager = manager
self.uuid = str(uuid())
display(HTML("<div id=%r></div>" % 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)

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()

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_uri = "data:image/png;base64,{0}".format(b64encode(blob))
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()
elif message['type'] == 'supports_binary':
self.supports_binary = message['value']
else:
self.manager.handle_json(message)
124 changes: 71 additions & 53 deletions lib/matplotlib/backends/backend_webagg_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def get_diff_image(self):
# pixels can be compared in one numpy call, rather than
# needing to compare each plane separately.
buff = np.frombuffer(
self._renderer.buffer_rgba(), dtype=np.uint32)
self.get_renderer().buffer_rgba(), dtype=np.uint32)
buff.shape = (
self._renderer.height, self._renderer.width)

Expand Down Expand Up @@ -129,7 +129,7 @@ def get_diff_image(self):

def get_renderer(self, cleared=None):
# Mirrors super.get_renderer, but caches the old one
# so that we can do things such as prodce a diff image
# so that we can do things such as produce a diff image
# in get_diff_image
_, _, w, h = self.figure.bbox.bounds
key = w, h, self.figure.dpi
Expand Down Expand Up @@ -200,6 +200,26 @@ 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_{}'.format(e_type), None)
if handler is None:
import warnings
warnings.warn('Unhandled message type {}. {}'.format(e_type, event))
else:
return handler(event)

def handle_resize(self, event):
x, y = event.get('width', 800), event.get('height', 800)
x, y = int(x), int(y)
fig = self.figure
# An attempt at approximating the figure size in pixels.
fig.set_size_inches(x / fig.dpi, y / fig.dpi)

_, _, w, h = self.figure.bbox.bounds
# Acknowledge the resize, and force the viewer to update the canvas size to the
# figure's new size (which is hopefully identical or within a pixel or so).
self._png_is_old = True
self.manager.resize(w, h)

def send_event(self, event_type, **kwargs):
self.manager._send_event(event_type, **kwargs)
Expand All @@ -216,7 +236,54 @@ def stop_event_loop(self):
backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__


class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
_jquery_icon_classes = {
'home': 'ui-icon ui-icon-home',
'back': 'ui-icon ui-icon-circle-arrow-w',
'forward': 'ui-icon ui-icon-circle-arrow-e',
'zoom_to_rect': 'ui-icon ui-icon-search',
'move': 'ui-icon ui-icon-arrow-4',
'download': 'ui-icon ui-icon-disk',
None: None,
}

# Use the standard toolbar items + download button
toolitems = [(text, tooltip_text, _jquery_icon_classes[image_file], name_of_method)
for text, tooltip_text, image_file, name_of_method
in (backend_bases.NavigationToolbar2.toolitems +
(('Download', 'Download plot', 'download', 'download'),))
if image_file in _jquery_icon_classes]

def _init_toolbar(self):
self.message = ''
self.cursor = 0

def set_message(self, message):
if message != self.message:
self.canvas.send_event("message", message=message)
self.message = message

def set_cursor(self, cursor):
if cursor != self.cursor:
self.canvas.send_event("cursor", cursor=cursor)
self.cursor = cursor

def dynamic_update(self):
self.canvas.draw_idle()

def draw_rubberband(self, event, x0, y0, x1, y1):
self.canvas.send_event(
"rubberband", x0=x0, y0=y0, x1=x1, y1=y1)

def release_zoom(self, event):
super(NavigationToolbar2WebAgg, self).release_zoom(event)
self.canvas.send_event(
"rubberband", x0=-1, y0=-1, x1=-1, y1=-1)


class FigureManagerWebAgg(backend_bases.FigureManagerBase):
ToolbarCls = NavigationToolbar2WebAgg

def __init__(self, canvas, num):
backend_bases.FigureManagerBase.__init__(self, canvas, num)

Expand All @@ -228,7 +295,7 @@ def show(self):
pass

def _get_toolbar(self, canvas):
toolbar = NavigationToolbar2WebAgg(canvas)
toolbar = self.ToolbarCls(canvas)
return toolbar

def resize(self, w, h):
Expand Down Expand Up @@ -275,7 +342,7 @@ def get_javascript(cls, stream=None):
output.write(fd.read())

toolitems = []
for name, tooltip, image, method in NavigationToolbar2WebAgg.toolitems:
for name, tooltip, image, method in cls.ToolbarCls.toolitems:
if name is None:
toolitems.append(['', '', '', ''])
else:
Expand Down Expand Up @@ -306,52 +373,3 @@ def _send_event(self, event_type, **kwargs):
payload.update(kwargs)
for s in self.web_sockets:
s.send_json(payload)


class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
_jquery_icon_classes = {
'home': 'ui-icon ui-icon-home',
'back': 'ui-icon ui-icon-circle-arrow-w',
'forward': 'ui-icon ui-icon-circle-arrow-e',
'zoom_to_rect': 'ui-icon ui-icon-search',
'move': 'ui-icon ui-icon-arrow-4',
'download': 'ui-icon ui-icon-disk',
None: None
}


# Use the standard toolbar items + download button
toolitems = [(text, tooltip_text, _jquery_icon_classes[image_file], name_of_method)
for text, tooltip_text, image_file, name_of_method
in (backend_bases.NavigationToolbar2.toolitems +
(('Download', 'Download plot', 'download', 'download'),))
if image_file in _jquery_icon_classes]

def _init_toolbar(self):
self.message = ''
self.cursor = 0

def set_message(self, message):
if message != self.message:
self.canvas.send_event("message", message=message)
self.message = message

def set_cursor(self, cursor):
if cursor != self.cursor:
self.canvas.send_event("cursor", cursor=cursor)
self.cursor = cursor

def dynamic_update(self):
self.canvas.draw_idle()

def draw_rubberband(self, event, x0, y0, x1, y1):
self.canvas.send_event(
"rubberband", x0=x0, y0=y0, x1=x1, y1=y1)

def release_zoom(self, event):
super(NavigationToolbar2WebAgg, self).release_zoom(event)
self.canvas.send_event(
"rubberband", x0=-1, y0=-1, x1=-1, y1=-1)

FigureCanvas = FigureCanvasWebAggCore
FigureManager = FigureManagerWebAgg
Loading