Skip to content
This repository has been archived by the owner on Nov 29, 2019. It is now read-only.

Add support for custom widget layouts #66

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
98 changes: 20 additions & 78 deletions parambokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,18 @@

from bokeh.document import Document
from bokeh.io import curdoc
from bokeh.layouts import row, column, widgetbox
from bokeh.layouts import widgetbox
from bokeh.models.widgets import Div, Button, CheckboxGroup, TextInput
from bokeh.models import CustomJS
from bokeh.protocol import Protocol

try:
from IPython.display import publish_display_data

import bokeh.embed.notebook
from bokeh.util.string import encode_utf8
from pyviz_comms import JupyterCommManager, JS_CALLBACK, bokeh_msg_handler, PYVIZ_PROXY
from pyviz_comms import JS_CALLBACK, JupyterCommManager
IPYTHON_AVAILABLE = True
except:
IPYTHON_AVAILABLE = False

from .layout import WidgetBox, Column, Row
from .widgets import wtype, literal_params
from .util import named_objs, get_method_owner
from .view import _View
Expand All @@ -50,50 +47,6 @@ def _err(): raise ValueError(_missing_cmd())
##


def notebook_show(obj, doc, comm):
"""
Displays bokeh output inside a notebook.
"""
target = obj.ref['id']
load_mime = 'application/vnd.holoviews_load.v0+json'
exec_mime = 'application/vnd.holoviews_exec.v0+json'

# Publish plot HTML
bokeh_script, bokeh_div, _ = bokeh.embed.notebook.notebook_content(obj, comm.id)
publish_display_data(data={'text/html': encode_utf8(bokeh_div)})

# Publish comm manager
JS = '\n'.join([PYVIZ_PROXY, JupyterCommManager.js_manager])
publish_display_data(data={load_mime: JS, 'application/javascript': JS})

# Publish bokeh plot JS
msg_handler = bokeh_msg_handler.format(plot_id=target)
comm_js = comm.js_template.format(plot_id=target, comm_id=comm.id, msg_handler=msg_handler)
bokeh_js = '\n'.join([comm_js, bokeh_script])

# Note: extension should be altered so text/html is not required
publish_display_data(data={exec_mime: '', 'text/html': '',
'application/javascript': bokeh_js},
metadata={exec_mime: {'id': target}})


def process_hv_plots(widgets, plots):
"""
Temporary fix to patch HoloViews plot comms
"""
bokeh_plots = []
for plot in plots:
if hasattr(plot, '_update_callbacks'):
for subplot in plot.traverse(lambda x: x):
subplot.comm = widgets.server_comm
for cb in subplot.callbacks:
for c in cb.callbacks:
c.code = c.code.replace(plot.id, widgets.plot_id)
plot = plot.state
bokeh_plots.append(plot)
return bokeh_plots


class default_label_formatter(param.ParameterizedFunction):
"Default formatter to turn parameter names into appropriate widget labels."

Expand Down Expand Up @@ -203,47 +156,45 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
if not IPYTHON_AVAILABLE:
raise ImportError('IPython is not available, cannot use '
'Widgets in notebook mode.')
self.comm = JupyterCommManager.get_client_comm(on_msg=self.on_msg)
self.client_comm = JupyterCommManager.get_client_comm(on_msg=self.on_msg)
self.comm = JupyterCommManager.get_server_comm()

# HACK: Detects HoloViews plots and lets them handle the comms
hv_plots = [plot for plot in plots if hasattr(plot, 'comm')]
self.server_comm = JupyterCommManager.get_server_comm()
if hv_plots:
self.document = [p.document for p in hv_plots][0]
self.p.push = False
else:
self.document = doc or Document()
else:
self.document = doc or curdoc()
self.server_comm = None
self.client_comm = None
self.comm = None

self._queue = []
self._active = False
self._widget_options = {}
self.shown = False

# Initialize root container
widget_box = widgetbox(width=self.p.width)
view_params = any(isinstance(p, _View) for p in parameterized.params().values())
layout = self.p.view_position
container_type = column if layout in ['below', 'above'] else row
container = container_type() if plots or view_params else widget_box
self.plot_id = container.ref['id']
container_type = Column if layout in ['below', 'above'] else Row
self.container = container_type() if plots or view_params else WidgetBox(widget_box)
self.plot_id = widget_box.ref['id']

# Initialize widgets and populate container
widgets, views = self.widgets()
plots = views + plots
widget_box.children = widgets

plots = process_hv_plots(self, plots)

if plots:
view_box = column(plots)
view_box = Column(*plots)
if layout in ['below', 'right']:
children = [widget_box, view_box]
else:
children = [view_box, widget_box]
container.children = children
self.container.children = children

# Initialize view parameters
for view in views:
Expand All @@ -259,17 +210,8 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
if self.p.on_init:
self.execute()

if self.p.mode == 'raw':
return container
return self.container

self.document.add_root(container)
if self.p.mode == 'notebook':
notebook_show(container, self.document, self.server_comm)
if self.document._hold is None:
self.document.hold()
self.shown = True
return
return self.document


def on_msg(self, msg):
Expand Down Expand Up @@ -345,12 +287,12 @@ def _send_notebook_diff(self):
self.document._held_events = []
if msg is None:
return
self.server_comm.send(msg.header_json)
self.server_comm.send(msg.metadata_json)
self.server_comm.send(msg.content_json)
self.comm.send(msg.header_json)
self.comm.send(msg.metadata_json)
self.comm.send(msg.content_json)
for header, payload in msg.buffers:
self.server_comm.send(json.dumps(header))
self.server_comm.send(buffers=[payload])
self.comm.send(json.dumps(header))
self.comm.send(buffers=[payload])

def _update_trait(self, p_name, p_value, widget=None):
widget = self._widgets[p_name] if widget is None else widget
Expand All @@ -368,7 +310,7 @@ def _make_widget(self, p_name):
p_obj = self.parameterized.params(p_name)

if isinstance(p_obj, _View):
p_obj._comm = self.server_comm
p_obj._comm = self.comm
p_obj._document = self.document
p_obj._notebook = self.p.mode == 'notebook'

Expand Down Expand Up @@ -446,7 +388,7 @@ def _get_customjs(self, change, p_name):
"""
data_template = "data = {{p_name: '{p_name}', value: cb_obj['{change}']}};"
fetch_data = data_template.format(change=change, p_name=p_name)
self_callback = JS_CALLBACK.format(comm_id=self.comm.id,
self_callback = JS_CALLBACK.format(comm_id=self.client_comm.id,
timeout=self.timeout,
debounce=self.debounce,
plot_id=self.plot_id)
Expand Down
92 changes: 92 additions & 0 deletions parambokeh/layout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import param

from bokeh.document import Document
from bokeh.io import curdoc
from bokeh.layouts import Column as BkColumn, Row as BkRow

from .util import render, process_plot, add_to_doc

class Viewable(param.Parameterized):
"""
A Viewable is an abstract baseclass for objects which wrap bokeh
models and display them using the PyViz display and comms machinery.
"""

__abstract = True

def _get_model(self, doc, comm=None, plot_id=None):
"""
Should return the bokeh model to be rendered.
"""

def _repr_mimebundle_(self, include=None, exclude=None):
from pyviz_comms import JupyterCommManager
doc = Document()
comm = JupyterCommManager.get_server_comm()
return render(self._get_model(doc, comm), doc, comm)

def server_doc(self, doc=None):
doc = doc or curdoc()
model = self._get_model(doc)
add_to_doc(model, doc)
return doc


class Plot(Viewable):
"""
A wrapper for bokeh plots and objects that can be converted to
bokeh plots.
"""

def __init__(self, obj, **params):
self.object = obj
super(Plot, self).__init__(**params)

def _get_model(self, doc, comm=None, plot_id=None):
"""
Should return the bokeh model to be rendered.
"""
return process_plot(self.object, doc, plot_id, comm)


class WidgetBox(Plot):
"""
A wrapper for bokeh WidgetBox and parambokeh.Widgets making them
displayable in the notebook.
"""


class Layout(Viewable):

children = param.List(default=[])

_bokeh_model = None

__abstract = True

def __init__(self, *children, **params):
super(Layout, self).__init__(children=list(children), **params)

def _get_model(self, doc, comm=None, plot_id=None):
"""
Should return the bokeh model to be rendered.
"""
model = self._bokeh_model()
plot_id = model.ref['id'] if plot_id is None else plot_id
children = []
for child in self.children:
if not isinstance(child, Viewable):
child = Plot(child)
children.append(child._get_model(doc, comm, plot_id))
model.children = children
return model


class Row(Layout):

_bokeh_model = BkRow


class Column(Layout):

_bokeh_model = BkColumn
Loading