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 = """