From cf1174334e1e386248659288ee7b028311ff3d26 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 29 Aug 2016 23:55:46 +0100
Subject: [PATCH 01/20] DimensionedPlot now have access to a renderer instance
---
holoviews/plotting/bokeh/plot.py | 2 +-
holoviews/plotting/mpl/plot.py | 4 ++--
holoviews/plotting/plot.py | 10 ++++++----
holoviews/plotting/renderer.py | 8 +++++---
4 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py
index 1fec126b62..1c6be8b54c 100644
--- a/holoviews/plotting/bokeh/plot.py
+++ b/holoviews/plotting/bokeh/plot.py
@@ -42,7 +42,7 @@ class BokehPlot(DimensionedPlot):
The formatting string for the title of this plot, allows defining
a label group separator and dimension labels.""")
- renderer = BokehRenderer
+ backend = 'bokeh'
@property
def document(self):
diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py
index 884e5973d8..274fa3ad32 100644
--- a/holoviews/plotting/mpl/plot.py
+++ b/holoviews/plotting/mpl/plot.py
@@ -16,7 +16,6 @@
from ...core import traversal
from ..plot import DimensionedPlot, GenericLayoutPlot, GenericCompositePlot
from ..util import get_dynamic_mode, initialize_sampled
-from .renderer import MPLRenderer
from .util import compute_ratios, fix_aspect
@@ -30,7 +29,8 @@ class MPLPlot(DimensionedPlot):
via the anim() method.
"""
- renderer = MPLRenderer
+ backend = 'matplotlib'
+
sideplots = {}
fig_alpha = param.Number(default=1.0, bounds=(0, 1), doc="""
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 58bcfa0044..988ba19450 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -68,12 +68,12 @@ def __len__(self):
@classmethod
def lookup_options(cls, obj, group):
try:
- plot_class = cls.renderer.plotting_class(obj)
+ plot_class = Store.renderers[cls.backend].plotting_class(obj)
style_opts = plot_class.style_opts
except SkipRendering:
style_opts = None
- node = Store.lookup_options(cls.renderer.backend, obj, group)
+ node = Store.lookup_options(cls.backend, obj, group)
if group == 'style' and style_opts:
return node.filtered(style_opts)
else:
@@ -178,7 +178,7 @@ class DimensionedPlot(Plot):
def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
uniform=True, subplot=False, adjoined=None, layout_num=0,
- style=None, subplots=None, dynamic=False, **params):
+ style=None, subplots=None, dynamic=False, renderer=None, **params):
self.subplots = subplots
self.adjoined = adjoined
self.dimensions = dimensions
@@ -195,6 +195,8 @@ def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
self.current_frame = None
self.current_key = None
self.ranges = {}
+ self.renderer = renderer if renderer else Store.renderers[self.backend].instance()
+
params = {k: v for k, v in params.items()
if k in self.params()}
super(DimensionedPlot, self).__init__(**params)
@@ -420,7 +422,7 @@ def lookup(x):
selected = {o: options.options[o]
for o in opts if o in options.options}
if opt_type == 'plot' and defaults:
- plot = Store.registry[cls.renderer.backend].get(type(x))
+ plot = Store.registry[cls.backend].get(type(x))
selected['defaults'] = {o: getattr(plot, o) for o in opts
if o not in selected and hasattr(plot, o)}
key = keyfn(x) if keyfn else None
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index 825ce51d8a..f7b2757709 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -149,7 +149,7 @@ def __init__(self, **params):
@bothmethod
- def get_plot(self_or_cls, obj):
+ def get_plot(self_or_cls, obj, renderer=None):
"""
Given a HoloViews Viewable return a corresponding plot instance.
"""
@@ -170,10 +170,12 @@ def get_plot(self_or_cls, obj):
except StopIteration: # Exhausted DynamicMap
raise SkipRendering("DynamicMap generator exhausted.")
+ if not renderer: renderer = self_or_cls.instance()
if not isinstance(obj, Plot):
obj = Layout.from_values(obj) if isinstance(obj, AdjointLayout) else obj
plot_opts = self_or_cls.plot_options(obj, self_or_cls.size)
- plot = self_or_cls.plotting_class(obj)(obj, **plot_opts)
+ plot = self_or_cls.plotting_class(obj)(obj, renderer=renderer,
+ **plot_opts)
plot.update(0)
else:
plot = obj
@@ -187,7 +189,7 @@ def _validate(self, obj, fmt):
"""
if isinstance(obj, tuple(self.widgets.values())):
return obj, 'html'
- plot = self.get_plot(obj)
+ plot = self.get_plot(obj, renderer=self)
fig_formats = self.mode_formats['fig'][self.mode]
holomap_formats = self.mode_formats['holomap'][self.mode]
From 0a1f3b68b208e380713373e7e3656cdb6fa3a4e5 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 30 Aug 2016 16:54:59 +0100
Subject: [PATCH 02/20] Added Comms to plot updates dynamically
---
holoviews/plotting/bokeh/renderer.py | 18 +++--
holoviews/plotting/comms.py | 113 +++++++++++++++++++++++++++
holoviews/plotting/mpl/comms.py | 89 +++++++++++++++++++++
holoviews/plotting/mpl/renderer.py | 44 ++++++++---
holoviews/plotting/mpl/widgets.py | 38 ---------
holoviews/plotting/plot.py | 31 +++++++-
holoviews/plotting/renderer.py | 18 ++++-
7 files changed, 286 insertions(+), 65 deletions(-)
create mode 100644 holoviews/plotting/comms.py
create mode 100644 holoviews/plotting/mpl/comms.py
diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py
index 20903626fb..5b3a2c8094 100644
--- a/holoviews/plotting/bokeh/renderer.py
+++ b/holoviews/plotting/bokeh/renderer.py
@@ -55,25 +55,27 @@ def __call__(self, obj, fmt=None):
html = "%s
" % html
return self._apply_post_render_hooks(html, obj, fmt), info
elif fmt == 'json':
- plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles)
- for h in handles]
- patch = compute_static_patch(plot.document, plotobjects)
- data = dict(root=plot.state._id, patch=patch)
- return self._apply_post_render_hooks(serialize_json(data), obj, fmt), info
+ return self.patch(plot), info
def figure_data(self, plot, fmt='html', **kwargs):
doc_handler = add_to_document(plot.state)
with doc_handler:
doc = doc_handler._doc
- comms_target = str(uuid.uuid4())
- doc.last_comms_target = comms_target
- div = notebook_div(plot.state, comms_target)
+ if plot.comm:
+ div = notebook_div(plot.state, plot.comm.target)
plot.document = doc
doc.add_root(plot.state)
return div
+ def patch(self, plot):
+ plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles)
+ for h in handles]
+ patch = compute_static_patch(plot.document, plotobjects)
+ return self._apply_post_render_hooks(serialize_json(patch), plot, 'json')
+
+
@classmethod
def plot_options(cls, obj, percent_size):
"""
diff --git a/holoviews/plotting/comms.py b/holoviews/plotting/comms.py
new file mode 100644
index 0000000000..27dd3c4d49
--- /dev/null
+++ b/holoviews/plotting/comms.py
@@ -0,0 +1,113 @@
+import uuid
+
+import param
+from ipykernel.comm import Comm as IPyComm
+
+
+class Comm(param.Parameterized):
+ """
+ Comm encompasses any uni- or bi-directional connection between
+ a python process and a frontend allowing passing of messages
+ between the two. A Comms class must implement methods
+ to initialize the connection, send data and handle received
+ message events.
+ """
+
+ def __init__(self, plot, target=None, on_msg=None):
+ """
+ Initializes a Comms object
+ """
+ self.target = target if target else str(uuid.uuid4())
+ self._plot = plot
+ self._on_msg = on_msg
+ self._comm = None
+
+
+ def init(self, on_msg=None):
+ """
+ Initializes comms channel.
+ """
+
+
+ def send(self, data):
+ """
+ Sends data to the frontend
+ """
+
+
+ def decode(self, msg):
+ """
+ Decode incoming message, e.g. by parsing json.
+ """
+ return msg
+
+
+ @property
+ def comm(self):
+ if not self._comm:
+ raise ValueError('Comm has not been initialized')
+ return self._comm
+
+
+ def _handle_msg(self, msg):
+ """
+ Decode received message before passing it to on_msg callback
+ if it has been defined.
+ """
+ if self._on_msg:
+ self._on_msg(self.decode(msg))
+
+
+
+class JupyterComm(Comm):
+ """
+ JupyterComm allows for a bidirectional communication channel
+ inside the Jupyter notebook. A JupyterComm requires the comm to be
+ registered on the frontend along with a message handler. This is
+ handled by the template, which accepts three arguments:
+
+ * comms_target - A unique id to register to register as the comms target.
+ * msg_handler - JS code that processes messages sent to the frontend.
+ * init_frame - The initial frame to render on the frontend.
+ """
+
+ template = """
+
+
+
+ {init_frame}
+
+ """
+
+ def init(self):
+ if self._comm:
+ self.warning("Comms already initialized")
+ return
+ self._comm = IPyComm(target_name=self.target, data={})
+ self._comm.on_msg(self._handle_msg)
+
+
+ def send(self, data):
+ """
+ Pushes data to comms socket
+ """
+ if not self._comm:
+ raise ValueError("Comm has not been initialized.")
+ self._comm.send(data)
+
+
+ def decode(self, msg):
+ return msg['content']['data']
diff --git a/holoviews/plotting/mpl/comms.py b/holoviews/plotting/mpl/comms.py
new file mode 100644
index 0000000000..9c7c877a54
--- /dev/null
+++ b/holoviews/plotting/mpl/comms.py
@@ -0,0 +1,89 @@
+import uuid
+import warnings
+
+try:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ from matplotlib.backends.backend_nbagg import CommSocket, new_figure_manager_given_figure
+except ImportError:
+ CommSocket = object
+from mpl_toolkits.mplot3d import Axes3D
+
+from ..comms import JupyterComm
+
+mpl_msg_handler = """
+var data = msg.content.data;
+target = $('#{comms_target}');
+img = $('').html(data);
+target.children().each(function () {{ $(this).remove() }})
+target.append(img)
+"""
+
+mpld3_msg_handler = """
+var data = msg.content.data;
+target = $('#fig_el{comms_target}');
+target.children().each(function () {{ $(this).remove() }});
+mpld3.draw_figure("fig_el{comms_target}", data);
+"""
+
+class WidgetCommSocket(CommSocket):
+ """
+ CustomCommSocket provides communication between the IPython
+ kernel and a matplotlib canvas element in the notebook.
+ A CustomCommSocket is required to delay communication
+ between the kernel and the canvas element until the widget
+ has been rendered in the notebook.
+ """
+
+ def __init__(self, manager, target=None):
+ self.supports_binary = None
+ self.manager = manager
+ self.target = str(uuid.uuid4()) if target is None else target
+ self.html = "" % self.target
+
+ def start(self):
+ try:
+ # Jupyter/IPython 4.0
+ from ipykernel.comm import Comm
+ except:
+ # IPython <=3.0
+ from IPython.kernel.comm import Comm
+
+ try:
+ self.comm = Comm('matplotlib', data={'id': self.target})
+ 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)
+ self.comm.on_close(lambda close_message: self.manager.clearup_closed())
+
+
+
+class NbAggJupyterComm(JupyterComm):
+
+ def get_figure_manager(self):
+ fig = self._plot.state
+ count = self._plot.renderer.counter
+ self.manager = new_figure_manager_given_figure(count, fig)
+
+ # Need to call mouse_init on each 3D axis to enable rotation support
+ for ax in fig.get_axes():
+ if isinstance(ax, Axes3D):
+ ax.mouse_init()
+ self._comm_socket = WidgetCommSocket(target=self.target,
+ manager=self.manager)
+ return self.manager
+
+
+ def init(self, on_msg=None):
+ if not self._comm:
+ self._comm_socket.start()
+ self._comm = self._comm_socket.comm
+ self.manager.add_web_socket(self._comm_socket)
+
+
+ def send(self, data):
+ if not self._comm:
+ self.init()
+ self._comm_socket.send_json({'type':'draw'})
+
diff --git a/holoviews/plotting/mpl/renderer.py b/holoviews/plotting/mpl/renderer.py
index 15ed87cc51..7e517a83e2 100644
--- a/holoviews/plotting/mpl/renderer.py
+++ b/holoviews/plotting/mpl/renderer.py
@@ -9,7 +9,6 @@
from matplotlib import pyplot as plt
from matplotlib.transforms import Bbox, TransformedBbox, Affine2D
-from mpl_toolkits.mplot3d import Axes3D
import param
from param.parameterized import bothmethod
@@ -18,6 +17,8 @@
from ...core.options import Store
from ..renderer import Renderer, MIME_TYPES
+from .comms import (JupyterComm, NbAggJupyterComm,
+ mpl_msg_handler, mpld3_msg_handler)
from .widgets import MPLSelectionWidget, MPLScrubberWidget
from .util import get_tight_bbox
@@ -81,6 +82,11 @@ class MPLRenderer(Renderer):
widgets = {'scrubber': MPLScrubberWidget,
'widgets': MPLSelectionWidget}
+ # Define comm targets by mode
+ comms = {'default': (JupyterComm, mpl_msg_handler),
+ 'nbagg': (NbAggJupyterComm, None),
+ 'mpld3': (JupyterComm, mpld3_msg_handler)}
+
def __call__(self, obj, fmt='auto'):
"""
Render the supplied HoloViews component or MPLPlot instance
@@ -135,6 +141,19 @@ def get_size(self_or_cls, plot):
return (w*dpi, h*dpi)
+ def patch(self, plot):
+ data = None
+ if self.mode != 'nbagg':
+ if self.mode == 'mpld3':
+ figure_format = 'json'
+ elif self.fig == 'auto':
+ figure_format = self.renderer.params('fig').objects[0]
+ else:
+ figure_format = self.fig
+ data = self.html(plot, figure_format, comm=False)
+ return data
+
+
def _figure_data(self, plot, fmt='png', bbox_inches='tight', **kwargs):
"""
Render matplotlib figure object and return the corresponding data.
@@ -144,7 +163,7 @@ def _figure_data(self, plot, fmt='png', bbox_inches='tight', **kwargs):
"""
fig = plot.state
if self.mode == 'nbagg':
- manager = self.get_figure_manager(plot.state)
+ manager = plot.comm.get_figure_manager()
if manager is None: return ''
self.counter += 1
manager.show()
@@ -156,7 +175,16 @@ def _figure_data(self, plot, fmt='png', bbox_inches='tight', **kwargs):
if fmt == 'json':
return mpld3.fig_to_dict(fig)
else:
- return "" + mpld3.fig_to_html(fig) + ""
+ figid = "fig_el"+plot.comm.target if plot.comm else None
+ html = mpld3.fig_to_html(fig, figid=figid)
+ html = "" + html + ""
+ if plot.comm:
+ comm, msg_handler = self.comms[self.mode]
+ msg_handler = msg_handler.format(comms_target=plot.comm.target)
+ return comm.template.format(init_frame=html,
+ msg_handler=msg_handler,
+ comms_target=plot.comm.target)
+ return html
traverse_fn = lambda x: x.handles.get('bbox_extra_artists', None)
extra_artists = list(chain(*[artists for artists in plot.traverse(traverse_fn)
@@ -186,16 +214,6 @@ def _figure_data(self, plot, fmt='png', bbox_inches='tight', **kwargs):
return data
- def get_figure_manager(self, fig):
- from matplotlib.backends.backend_nbagg import new_figure_manager_given_figure
- manager = new_figure_manager_given_figure(self.counter, fig)
- # Need to call mouse_init on each 3D axis to enable rotation support
- for ax in fig.get_axes():
- if isinstance(ax, Axes3D):
- ax.mouse_init()
- return manager
-
-
def _anim_data(self, anim, fmt):
"""
Render a matplotlib animation object and return the corresponding data.
diff --git a/holoviews/plotting/mpl/widgets.py b/holoviews/plotting/mpl/widgets.py
index 48a189a28a..2bd6572427 100644
--- a/holoviews/plotting/mpl/widgets.py
+++ b/holoviews/plotting/mpl/widgets.py
@@ -3,44 +3,6 @@
from ..widgets import NdWidget, SelectionWidget, ScrubberWidget
-try:
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- from matplotlib.backends.backend_nbagg import CommSocket
-except ImportError:
- CommSocket = object
-
-class WidgetCommSocket(CommSocket):
- """
- CustomCommSocket provides communication between the IPython
- kernel and a matplotlib canvas element in the notebook.
- A CustomCommSocket is required to delay communication
- between the kernel and the canvas element until the widget
- has been rendered in the notebook.
- """
-
- def __init__(self, manager):
- self.supports_binary = None
- self.manager = manager
- self.uuid = str(uuid.uuid4())
- self.html = "" % self.uuid
-
- def start(self):
- try:
- # Jupyter/IPython 4.0
- from ipykernel.comm import Comm
- except:
- # IPython <=3.0
- from IPython.kernel.comm import Comm
-
- 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)
- self.comm.on_close(lambda close_message: self.manager.clearup_closed())
-
class MPLWidget(NdWidget):
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 988ba19450..00a192d209 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -197,6 +197,9 @@ def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
self.ranges = {}
self.renderer = renderer if renderer else Store.renderers[self.backend].instance()
+ comm = None
+ if self.dynamic or self.renderer.widget_mode == 'live':
+ self.comm = self.renderer.comms[self.renderer.mode][0](self)
params = {k: v for k, v in params.items()
if k in self.params()}
super(DimensionedPlot, self).__init__(**params)
@@ -474,6 +477,29 @@ def update(self, key):
return self.__getitem__(key)
+ def refresh(self):
+ """
+ Refreshes the plot by rerendering it and then pushing
+ the updated data if the plot has an associated Comm.
+ """
+ if self.current_key:
+ self.update(self.current_key)
+ else:
+ self.update(0)
+ if self.comm is not None:
+ self.push()
+
+
+ def push(self):
+ """
+ Pushes updated plot data via the Comm.
+ """
+ if self.comm is None:
+ raise Exception('Renderer does not have a comm.')
+ patch = self.renderer.patch(self)
+ self.comm.send(patch)
+
+
def __len__(self):
"""
Returns the total number of available frames.
@@ -572,10 +598,7 @@ def _get_frame(self, key):
if isinstance(key, int):
key = self.hmap.keys()[min([key, len(self.hmap)-1])]
- if key == self.current_key:
- return self.current_frame
- else:
- self.current_key = key
+ self.current_key = key
if self.uniform:
if not isinstance(key, tuple): key = (key,)
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index f7b2757709..a063992d30 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -17,6 +17,7 @@
from .. import DynamicMap
from . import Plot
+from .comms import JupyterComm
from .util import displayable, collate
from param.parameterized import bothmethod
@@ -129,6 +130,9 @@ class Renderer(Exporter):
mode_formats = {'fig': {'default': [None, 'auto']},
'holomap': {'default': [None, 'auto']}}
+ # Define comms class and message handler for each mode
+ comms = {'default': (JupyterComm, None)}
+
# Define appropriate widget classes
widgets = {'scrubber': ScrubberWidget, 'widgets': SelectionWidget}
@@ -245,9 +249,11 @@ def _apply_post_render_hooks(self, data, obj, fmt):
return data
- def html(self, obj, fmt=None, css=None):
+ def html(self, obj, fmt=None, css=None, comm=True):
"""
Renders plot or data structure and wraps the output in HTML.
+ The comm argument defines whether the HTML output includes
+ code to initialize a Comm, if the plot supplies one.
"""
plot, fmt = self._validate(obj, fmt)
figdata, _ = self(plot, fmt)
@@ -270,7 +276,15 @@ def html(self, obj, fmt=None, css=None):
b64 = base64.b64encode(figdata).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
- return tag.format(src=src, mime_type=mime_type, css=css)
+ html = tag.format(src=src, mime_type=mime_type, css=css)
+ if comm and plot.comm is not None:
+ comm, msg_handler = self.comms[self.mode]
+ msg_handler = msg_handler.format(comms_target=plot.comm.target)
+ return comm.template.format(init_frame=html,
+ msg_handler=msg_handler,
+ comms_target=plot.comm.target)
+ else:
+ return html
def static_html(self, obj, fmt=None, template=None):
From 004eb3835a052fece5bdb9b7cde88a817642a225 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 30 Aug 2016 17:36:22 +0100
Subject: [PATCH 03/20] JupyterWidget unpacks the comms msg
---
holoviews/plotting/comms.py | 1 +
holoviews/plotting/mpl/comms.py | 2 --
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/holoviews/plotting/comms.py b/holoviews/plotting/comms.py
index 27dd3c4d49..c1e8b7c973 100644
--- a/holoviews/plotting/comms.py
+++ b/holoviews/plotting/comms.py
@@ -74,6 +74,7 @@ class JupyterComm(Comm):
template = """
@@ -93,11 +91,17 @@ class JupyterComm(Comm):
"""
- def init(self):
- if self._comm:
- self.warning("Comms already initialized")
- return
- self._comm = IPyComm(target_name=self.target, data={})
+ def __init__(self, plot, target=None, on_msg=None):
+ """
+ Initializes a Comms object
+ """
+ super(JupyterComm, self).__init__(plot, target, on_msg)
+ self.manager = get_ipython().kernel.comm_manager
+ self.manager.register_target(self.target, self._handle_open)
+
+
+ def _handle_open(self, comm, msg):
+ self._comm = comm
self._comm.on_msg(self._handle_msg)
@@ -106,7 +110,9 @@ def send(self, data):
Pushes data to comms socket
"""
if not self._comm:
- self.init()
+ raise Exception('Comm has not been initialized, ensure '
+ 'it has been opened on the frontend before '
+ 'sending data.')
self._comm.send(data)
diff --git a/holoviews/plotting/widgets/widgets.js b/holoviews/plotting/widgets/widgets.js
index 8b61477c83..bd337f5053 100644
--- a/holoviews/plotting/widgets/widgets.js
+++ b/holoviews/plotting/widgets/widgets.js
@@ -99,10 +99,8 @@ HoloViewsWidget.prototype.update = function(current){
HoloViewsWidget.prototype.init_comms = function() {
var widget = this;
var comm_manager = Jupyter.notebook.kernel.comm_manager
- comm_manager.register_target(this.id, function (comm, msg) {
- widget.comm = comm;
- comm.on_msg(function(msg) { widget.process_msg(msg) });
- });
+ comm = comm_manager.new_comm(this.id, {}, {}, {}, this.id);
+ comm.on_msg(function (msg) { widget.process_msg(msg) })
}
HoloViewsWidget.prototype.process_msg = function(msg) {
From ccf3edd17e5c8dbab3cae393ba3b07f833de8777 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 13:05:44 +0100
Subject: [PATCH 12/20] Renamed Rendererer.patch to Renderer.diff
---
holoviews/plotting/bokeh/renderer.py | 8 ++++++--
holoviews/plotting/bokeh/widgets.py | 4 ++--
holoviews/plotting/mpl/renderer.py | 5 ++++-
holoviews/plotting/plot.py | 4 ++--
4 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py
index e2424b9a63..9781527548 100644
--- a/holoviews/plotting/bokeh/renderer.py
+++ b/holoviews/plotting/bokeh/renderer.py
@@ -54,7 +54,7 @@ def __call__(self, obj, fmt=None):
html = "%s
" % html
return self._apply_post_render_hooks(html, obj, fmt), info
elif fmt == 'json':
- return self.patch(plot), info
+ return self.diff(plot), info
def figure_data(self, plot, fmt='html', **kwargs):
@@ -68,7 +68,11 @@ def figure_data(self, plot, fmt='html', **kwargs):
return div
- def patch(self, plot, serialize=True):
+ def diff(self, plot, serialize=True):
+ """
+ Returns a json diff required to update an existing plot with
+ the latest plot data.
+ """
plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles)
for h in handles]
patch = compute_static_patch(plot.document, plotobjects)
diff --git a/holoviews/plotting/bokeh/widgets.py b/holoviews/plotting/bokeh/widgets.py
index 4157baff8a..0433ec4018 100644
--- a/holoviews/plotting/bokeh/widgets.py
+++ b/holoviews/plotting/bokeh/widgets.py
@@ -5,7 +5,7 @@
from bokeh.util.notebook import get_comms
from ..widgets import NdWidget, SelectionWidget, ScrubberWidget
-from .util import compute_static_patch, serialize_json
+from .util import serialize_json
class BokehWidget(NdWidget):
@@ -39,7 +39,7 @@ def _plot_figure(self, idx, fig_format='json'):
if fig_format == 'html':
msg = self.renderer.html(self.plot, fig_format)
else:
- json_patch = self.renderer.patch(self.plot, serialize=False)
+ json_patch = self.renderer.diff(self.plot, serialize=False)
msg = dict(patch=json_patch, root=self.plot.state._id)
msg = serialize_json(msg)
return msg
diff --git a/holoviews/plotting/mpl/renderer.py b/holoviews/plotting/mpl/renderer.py
index 7e517a83e2..dd0e2063ff 100644
--- a/holoviews/plotting/mpl/renderer.py
+++ b/holoviews/plotting/mpl/renderer.py
@@ -141,7 +141,10 @@ def get_size(self_or_cls, plot):
return (w*dpi, h*dpi)
- def patch(self, plot):
+ def diff(self, plot):
+ """
+ Returns the latest plot data to update an existing plot.
+ """
data = None
if self.mode != 'nbagg':
if self.mode == 'mpld3':
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index f6e2d2d439..71e2b0f2d3 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -497,8 +497,8 @@ def push(self):
"""
if self.comm is None:
raise Exception('Renderer does not have a comm.')
- patch = self.renderer.patch(self)
- self.comm.send(patch)
+ diff = self.renderer.diff(self)
+ self.comm.send(diff)
def __len__(self):
From 118af7bbbdf2bae93171577f8b14cfae5f7e5e47 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 13:06:12 +0100
Subject: [PATCH 13/20] Renamed NbAggCommSocket
---
holoviews/plotting/mpl/comms.py | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/holoviews/plotting/mpl/comms.py b/holoviews/plotting/mpl/comms.py
index 37e296e530..342c7a51de 100644
--- a/holoviews/plotting/mpl/comms.py
+++ b/holoviews/plotting/mpl/comms.py
@@ -24,13 +24,11 @@
mpld3.draw_figure("fig_el{comms_target}", data);
"""
-class WidgetCommSocket(CommSocket):
+class NbAggCommSocket(CommSocket):
"""
- CustomCommSocket provides communication between the IPython
- kernel and a matplotlib canvas element in the notebook.
- A CustomCommSocket is required to delay communication
- between the kernel and the canvas element until the widget
- has been rendered in the notebook.
+ NbAggCommSocket subclasses the matplotlib CommSocket allowing
+ the opening of a comms channel to be delayed until the plot
+ is displayed.
"""
def __init__(self, manager, target=None):
@@ -58,6 +56,10 @@ def start(self):
class NbAggJupyterComm(JupyterComm):
+ """
+ Wraps a NbAggCommSocket to provide a consistent API to work for
+ updating nbagg plots.
+ """
def get_figure_manager(self):
fig = self._plot.state
@@ -68,8 +70,8 @@ def get_figure_manager(self):
for ax in fig.get_axes():
if isinstance(ax, Axes3D):
ax.mouse_init()
- self._comm_socket = WidgetCommSocket(target=self.target,
- manager=self.manager)
+ self._comm_socket = NbAggCommSocket(target=self.target,
+ manager=self.manager)
return self.manager
From 17f3760c1717e3875bccd9df0fdc9f4e6b2cdb99 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 13:39:41 +0100
Subject: [PATCH 14/20] Added JupyterComms for uni- and bi-directional
communication
---
holoviews/plotting/comms.py | 81 ++++++++++++++++++++++++++--------
holoviews/plotting/renderer.py | 6 ++-
2 files changed, 67 insertions(+), 20 deletions(-)
diff --git a/holoviews/plotting/comms.py b/holoviews/plotting/comms.py
index a2255e701a..a62dc42841 100644
--- a/holoviews/plotting/comms.py
+++ b/holoviews/plotting/comms.py
@@ -10,10 +10,21 @@ class Comm(param.Parameterized):
Comm encompasses any uni- or bi-directional connection between
a python process and a frontend allowing passing of messages
between the two. A Comms class must implement methods
- to initialize the connection, send data and handle received
- message events.
+ send data and handle received message events.
+
+ If the Comm has to be set up on the frontend a template to
+ handle the creation of the comms channel along with a message
+ handler to process incoming messages must be supplied.
+
+ The template must accept three arguments:
+
+ * comms_target - A unique id to register to register as the comms target.
+ * msg_handler - JS code that processes messages sent to the frontend.
+ * init_frame - The initial frame to render on the frontend.
"""
+ template = ''
+
def __init__(self, plot, target=None, on_msg=None):
"""
Initializes a Comms object
@@ -59,17 +70,58 @@ def _handle_msg(self, msg):
self._on_msg(self.decode(msg))
+class SimpleJupyterComm(Comm):
+ """
+ SimpleJupyterComm provides a Comm for simple unidirectional
+ communication from the python process to a frontend. The
+ Comm is opened before the first event is sent to the frontend.
+ """
+
+ template = """
+
+
+
+ {init_frame}
+
+ """
+
+ def init(self):
+ if self._comm:
+ return
+ self._comm = IPyComm(target_name=self.target, data={})
+ self._comm.on_msg(self._handle_msg)
+
+
+ def decode(self, msg):
+ return msg['content']['data']
+
+
+ def send(self, data):
+ """
+ Pushes data across comm socket.
+ """
+ if not self._comm:
+ self.init()
+ self.comm.send(data)
+
+
class JupyterComm(Comm):
"""
JupyterComm allows for a bidirectional communication channel
- inside the Jupyter notebook. A JupyterComm requires the comm to be
- registered on the frontend along with a message handler. This is
- handled by the template, which accepts three arguments:
-
- * comms_target - A unique id to register to register as the comms target.
- * msg_handler - JS code that processes messages sent to the frontend.
- * init_frame - The initial frame to render on the frontend.
+ inside the Jupyter notebook. The JupyterComm will register
+ a comm target on the IPython kernel comm manager, which will then
+ be opened by the templated code on the frontend.
"""
template = """
@@ -107,14 +159,7 @@ def _handle_open(self, comm, msg):
def send(self, data):
"""
- Pushes data to comms socket
+ Pushes data across comm socket.
"""
- if not self._comm:
- raise Exception('Comm has not been initialized, ensure '
- 'it has been opened on the frontend before '
- 'sending data.')
- self._comm.send(data)
-
+ self.comm.send(data)
- def decode(self, msg):
- return msg['content']['data']
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index a063992d30..a5570c59a8 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -17,7 +17,7 @@
from .. import DynamicMap
from . import Plot
-from .comms import JupyterComm
+from .comms import SimpleJupyterComm
from .util import displayable, collate
from param.parameterized import bothmethod
@@ -131,7 +131,9 @@ class Renderer(Exporter):
'holomap': {'default': [None, 'auto']}}
# Define comms class and message handler for each mode
- comms = {'default': (JupyterComm, None)}
+ # The Comm opens a communication channel and the message
+ # handler defines how the message is processed on the frontend
+ comms = {'default': (SimpleJupyterComm, None)}
# Define appropriate widget classes
widgets = {'scrubber': ScrubberWidget, 'widgets': SelectionWidget}
From a59dc85aa49e3f2bb00a42f73bb3b4a4b324ad94 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 13:41:55 +0100
Subject: [PATCH 15/20] Small fix for Plot pprint
---
holoviews/core/pprint.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/holoviews/core/pprint.py b/holoviews/core/pprint.py
index e7f9363bba..28a4d04018 100644
--- a/holoviews/core/pprint.py
+++ b/holoviews/core/pprint.py
@@ -228,7 +228,7 @@ def object_info(cls, obj, name, ansi=False):
@classmethod
def options_info(cls, plot_class, ansi=False, pattern=None):
if plot_class.style_opts:
- backend_name = plot_class.renderer.backend
+ backend_name = plot_class.backend
style_info = ("\n(Consult %s's documentation for more information.)" % backend_name)
style_keywords = '\t%s' % ', '.join(plot_class.style_opts)
style_msg = '%s\n%s' % (style_keywords, style_info)
From f3e8bd43317c1ef3013c928cea5421bce3b91017 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 14:35:09 +0100
Subject: [PATCH 16/20] Renamed SimpleJupyterComm to JupyterPushComm
---
holoviews/plotting/comms.py | 4 ++--
holoviews/plotting/renderer.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/holoviews/plotting/comms.py b/holoviews/plotting/comms.py
index a62dc42841..dc2498df5f 100644
--- a/holoviews/plotting/comms.py
+++ b/holoviews/plotting/comms.py
@@ -70,9 +70,9 @@ def _handle_msg(self, msg):
self._on_msg(self.decode(msg))
-class SimpleJupyterComm(Comm):
+class JupyterPushComm(Comm):
"""
- SimpleJupyterComm provides a Comm for simple unidirectional
+ JupyterPushComm provides a Comm for simple unidirectional
communication from the python process to a frontend. The
Comm is opened before the first event is sent to the frontend.
"""
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index a5570c59a8..4104ee3d2a 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -17,7 +17,7 @@
from .. import DynamicMap
from . import Plot
-from .comms import SimpleJupyterComm
+from .comms import JupyterPushComm
from .util import displayable, collate
from param.parameterized import bothmethod
@@ -133,7 +133,7 @@ class Renderer(Exporter):
# Define comms class and message handler for each mode
# The Comm opens a communication channel and the message
# handler defines how the message is processed on the frontend
- comms = {'default': (SimpleJupyterComm, None)}
+ comms = {'default': (JupyterPushComm, None)}
# Define appropriate widget classes
widgets = {'scrubber': ScrubberWidget, 'widgets': SelectionWidget}
From f2892cbd9048bc6f98e7c10e1cc7d4347b3d399b Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 14:42:24 +0100
Subject: [PATCH 17/20] Small fix for plot instantiation tests
---
tests/testplotinstantiation.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py
index 96c043d092..f71239e12b 100644
--- a/tests/testplotinstantiation.py
+++ b/tests/testplotinstantiation.py
@@ -13,6 +13,7 @@
from matplotlib import pyplot
pyplot.switch_backend('agg')
from holoviews.plotting.mpl import OverlayPlot
+ from holoviews.plotting.comms import JupyterPushComms
renderer = Store.renderers['matplotlib']
except:
pyplot = None
@@ -23,6 +24,11 @@ class TestPlotInstantiation(ComparisonTestCase):
def setUp(self):
if pyplot is None:
raise SkipTest("Matplotlib required to test plot instantiation")
+ self.default_comm = renderer.comms['default']
+ renderer.comms['default'] = JupyterPushComms
+
+ def teardown(self):
+ renderer.comms['default'] = self.default_comm
def test_interleaved_overlay(self):
"""
From 3295c92fbd3508c841a2401d7a09c3dc62e94398 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 31 Aug 2016 15:18:01 +0100
Subject: [PATCH 18/20] Improved docstrings for msg_handlers
---
holoviews/plotting/comms.py | 10 ++++++----
holoviews/plotting/mpl/comms.py | 6 ++++--
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/holoviews/plotting/comms.py b/holoviews/plotting/comms.py
index dc2498df5f..553d9be1fb 100644
--- a/holoviews/plotting/comms.py
+++ b/holoviews/plotting/comms.py
@@ -18,8 +18,10 @@ class Comm(param.Parameterized):
The template must accept three arguments:
- * comms_target - A unique id to register to register as the comms target.
- * msg_handler - JS code that processes messages sent to the frontend.
+ * comms_target - A unique id to register to register as the
+ comms target.
+ * msg_handler - JS code which has the msg variable in scope and
+ performs appropriate action for the supplied message.
* init_frame - The initial frame to render on the frontend.
"""
@@ -80,7 +82,7 @@ class JupyterPushComm(Comm):
template = """