Skip to content
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

Add Comms to allow updating plots dynamically #838

Merged
merged 20 commits into from
Aug 31, 2016
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion holoviews/core/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 1 addition & 39 deletions holoviews/plotting/bokeh/bokehwidgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,7 @@ var BokehMethods = {
doc.apply_json_patch(data.patch);
}
},
dynamic_update : function(current){
if (current === undefined) {
return
}
if(this.dynamic) {
current = JSON.stringify(current);
}
function callback(initialized, msg){
/* This callback receives data from Python as a string
in order to parse it correctly quotes are sliced off*/
if (msg.content.ename != undefined) {
this.process_error(msg);
}
if (msg.msg_type != "execute_result") {
console.log("Warning: HoloViews callback returned unexpected data for key: (", current, ") with the following content:", msg.content)
this.time = undefined;
this.wait = false;
return
}
this.timed = (Date.now() - this.time) * 1.1;
if (msg.msg_type == "execute_result") {
if (msg.content.data['text/plain'] === "'Complete'") {
this.wait = false;
if (this.queue.length > 0) {
this.time = Date.now();
this.dynamic_update(this.queue[this.queue.length-1]);
this.queue = [];
}
return
}
var data = msg.content.data['text/plain'].slice(1, -1);
this.frames[current] = JSON.parse(data);
this.update(current);
}
}
var kernel = IPython.notebook.kernel;
callbacks = {iopub: {output: $.proxy(callback, this, this.initialized)}};
var cmd = "holoviews.plotting.widgets.NdWidget.widgets['" + this.id + "'].update(" + current + ")";
kernel.execute("import holoviews;" + cmd, callbacks, {silent : false});
init_comms : function() {
}
}

Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
25 changes: 15 additions & 10 deletions holoviews/plotting/bokeh/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from ...core import Store, HoloMap
from ..renderer import Renderer, MIME_TYPES
from .widgets import BokehScrubberWidget, BokehSelectionWidget
from .util import compute_static_patch
from .util import compute_static_patch, serialize_json

import param
from param.parameterized import bothmethod
Expand All @@ -12,7 +12,6 @@
from bokeh.io import load_notebook
from bokeh.resources import CDN, INLINE

from bokeh.core.json_encoder import serialize_json
from bokeh.model import _ModelInDocument as add_to_document


Expand Down Expand Up @@ -55,25 +54,31 @@ def __call__(self, obj, fmt=None):
html = "<div style='display: table; margin: 0 auto;'>%s</div>" % 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.diff(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)
target = plot.comm.target if plot.comm else None
div = notebook_div(plot.state, target)
plot.document = doc
doc.add_root(plot.state)
return div


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)
processed = self._apply_post_render_hooks(patch, plot, 'json')
return serialize_json(processed) if serialize else processed

@classmethod
def plot_options(cls, obj, percent_size):
"""
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
import bokeh
bokeh_version = LooseVersion(bokeh.__version__)
from bokeh.core.enums import Palette
from bokeh.core.json_encoder import serialize_json # noqa (API import)
from bokeh.document import Document
from bokeh.models.plots import Plot
from bokeh.models import GlyphRenderer
from bokeh.models.widgets import DataTable, Tabs
from bokeh.plotting import Figure

if bokeh_version >= '0.12':
from bokeh.layouts import WidgetBox

Expand Down
26 changes: 10 additions & 16 deletions holoviews/plotting/bokeh/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from bokeh.util.notebook import get_comms

from ..widgets import NdWidget, SelectionWidget, ScrubberWidget
from .util import compute_static_patch
from .util import serialize_json

class BokehWidget(NdWidget):

Expand Down Expand Up @@ -36,22 +36,16 @@ def _plot_figure(self, idx, fig_format='json'):
"""
self.plot.update(idx)
if self.embed or fig_format == 'html':
html = self.renderer.html(self.plot, fig_format)
return html
else:
doc = self.plot.document
if hasattr(doc, 'last_comms_handle'):
handle = doc.last_comms_handle
if fig_format == 'html':
msg = self.renderer.html(self.plot, fig_format)
else:
handle = _CommsHandle(get_comms(doc.last_comms_target),
doc, doc.to_json())
doc.last_comms_handle = handle

plotobjects = [h for handles in self.plot.traverse(lambda x: x.current_handles)
for h in handles]
msg = compute_static_patch(doc, plotobjects)
handle.comms.send(json.dumps(msg))
return 'Complete'
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
else:
self.plot.push()
return "Complete"


class BokehSelectionWidget(BokehWidget, SelectionWidget):
Expand Down
165 changes: 165 additions & 0 deletions holoviews/plotting/comms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import uuid

import param
from ipykernel.comm import Comm as IPyComm
from IPython import get_ipython


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
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
"""
self.target = target if target else uuid.uuid4().hex
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 JupyterPushComm(Comm):
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

How about calling it JupyterPushComm? From the sound of it, we may not need it in future one bokeh works with bi-directional comms.

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

template = """
<script>
function msg_handler(msg) {{
var data = msg.content.data;
{msg_handler}
}}

if ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel !== undefined)) {{
comm_manager = Jupyter.notebook.kernel.comm_manager;
comm_manager.register_target("{comms_target}", register_handler);
}}
</script>

<div id="{comms_target}">
{init_frame}
</div>
"""

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. 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 = """
<script>
function msg_handler(msg) {{
var data = msg.content.data;
{msg_handler}
}}

if ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel !== undefined)) {{
var comm_manager = Jupyter.notebook.kernel.comm_manager;
comm = comm_manager.new_comm("{comms_target}", {{}}, {{}}, {{}}, "{comms_target}");
comm.on_msg(msg_handler);
}}
</script>

<div id="{comms_target}">
{init_frame}
</div>
"""

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)


def send(self, data):
"""
Pushes data across comm socket.
"""
self.comm.send(data)

Loading