From 73cfa6449ca0218d3ea9600a8f3d91a265b96135 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 11 Jul 2018 14:50:02 +0100
Subject: [PATCH 1/7] Add support for custom widget layouts
---
parambokeh/__init__.py | 93 ++++++++------------------------
parambokeh/layout.py | 103 +++++++++++++++++++++++++++++++++++
parambokeh/util.py | 119 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 244 insertions(+), 71 deletions(-)
create mode 100644 parambokeh/layout.py
diff --git a/parambokeh/__init__.py b/parambokeh/__init__.py
index e030c27..34fd369 100644
--- a/parambokeh/__init__.py
+++ b/parambokeh/__init__.py
@@ -25,6 +25,7 @@
except:
IPYTHON_AVAILABLE = False
+from .layout import WidgetBox, Column
from .widgets import wtype, literal_params
from .util import named_objs, get_method_owner
from .view import _View
@@ -50,50 +51,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."
@@ -203,10 +160,11 @@ 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
@@ -214,36 +172,33 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
self.document = doc or Document()
else:
self.document = doc or curdoc()
- self.server_comm = None
- self.comm = None
+ self.client_comm = None
+ self.comm = comm or 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']
+ self.container = Column() 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:
@@ -259,16 +214,12 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
if self.p.on_init:
self.execute()
- if self.p.mode == 'raw':
- return container
+ if self.p.mode in ('raw', 'notebook'):
+ 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
+ # Handle server case
+ model = self.container._get_model(doc, self.comm, self.plot_id)
+ document.add_root(model)
return self.document
@@ -345,12 +296,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
@@ -368,7 +319,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'
@@ -446,7 +397,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)
diff --git a/parambokeh/layout.py b/parambokeh/layout.py
new file mode 100644
index 0000000..13b94bc
--- /dev/null
+++ b/parambokeh/layout.py
@@ -0,0 +1,103 @@
+import param
+
+from bokeh.document import Document
+from bokeh.io import curdoc
+from bokeh.models import LayoutDOM
+from bokeh.layouts import Column as BkColumn, Row as BkRow
+
+from .util import render, process_plot
+
+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
+ IPYTHON_AVAILABLE = True
+except:
+ IPYTHON_AVAILABLE = False
+
+
+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):
+ 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
diff --git a/parambokeh/util.py b/parambokeh/util.py
index 61fba85..8526cfa 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -1,6 +1,18 @@
import sys
import inspect
+import bokeh
+from bokeh.models import Model, CustomJS, LayoutDOM
+
+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
+ IPYTHON_AVAILABLE = True
+except:
+ IPYTHON_AVAILABLE = False
+
if sys.version_info.major == 3:
unicode = str
basestring = str
@@ -41,3 +53,110 @@ def get_method_owner(meth):
return meth.im_class if meth.im_self is None else meth.im_self
else:
return meth.__self__
+
+
+def patch_hv_plot(plot, plot_id, comm):
+ """
+ Update the plot id and comm on a HoloViews plot to allow embedding
+ it in a bokeh layout.
+ """
+ if not hasattr(plot, '_update_callbacks'):
+ return
+
+ for subplot in plot.traverse(lambda x: x):
+ subplot.comm = comm
+ for cb in subplot.callbacks:
+ for c in cb.callbacks:
+ c.code = c.code.replace(plot.id, widgets.plot_id)
+
+
+def patch_bk_plot(plot, plot_id):
+ """
+ Patches bokeh CustomJS models with top-level plot_id
+ """
+ for js in plot.select({'type': CustomJS}):
+ js.code = js.code.replace(plot.ref['id'], plot_id)
+
+
+def patch_widgets(plot, doc, plot_id, comm):
+ """
+ Patches parambokeh Widgets instances with top-level document, comm and plot id
+ """
+ plot.comm = comm
+ plot.document = doc
+ patch_bk_plot(plot.container, plot_id)
+
+
+def process_plot(plot, doc, plot_id, comm):
+ """
+ Converts all acceptable plot and widget objects into displaybel
+ bokeh models. Patches any HoloViews plots or parambokeh Widgets
+ with the top-level comms and plot id.
+ """
+ from . import Widgets
+ if isinstance(plot, LayoutDOM):
+ if plot_id:
+ patch_bk_plot(plot, plot_id)
+ return plot
+ elif isinstance(plot, Widgets):
+ patch_widgets(plot, doc, plot_id, comm)
+ return plot.container
+ elif hasattr(plot, 'kdims') and hasattr(plot, 'vdims'):
+ from holoviews import renderer
+ plot = renderer('bokeh').get_plot(plot, doc=doc)
+ print(plot)
+
+ if not hasattr(plot, '_update_callbacks'):
+ raise ValueError('Can only render bokeh models or HoloViews objects.')
+
+ patch_hv_plot(plot, plot_id, comm)
+ return plot.state
+
+
+def add_to_doc(obj, doc, hold=False):
+ """
+ Adds a model to the supplied Document removing it from any existing Documents.
+ """
+ # Handle previously displayed models
+ for model in obj.select({'type': Model}):
+ prev_doc = model.document
+ model._document = None
+ if prev_doc:
+ prev_doc.remove_root(model)
+
+ # Add new root
+ doc.add_root(obj)
+ if doc._hold is None and hold:
+ doc.hold()
+
+
+def render(obj, doc, comm):
+ """
+ Displays bokeh output inside a notebook using the PyViz display
+ and comms machinery.
+ """
+ if not isinstance(obj, LayoutDOM):
+ raise ValueError('Can only render bokeh LayoutDOM models')
+
+ add_to_doc(obj, doc, True)
+
+ 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 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])
+
+ data = {exec_mime: '', 'text/html': encode_utf8(bokeh_div), 'application/javascript': bokeh_js}
+ metadata = {exec_mime: {'id': target}}
+ return data, metadata
+
From 49c9874646877abc819e3cbfb7e179d19de33773 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 11 Jul 2018 15:12:25 +0100
Subject: [PATCH 2/7] Fixes for server based plotting
---
parambokeh/__init__.py | 9 ++-------
parambokeh/layout.py | 2 +-
parambokeh/util.py | 4 ++--
3 files changed, 5 insertions(+), 10 deletions(-)
diff --git a/parambokeh/__init__.py b/parambokeh/__init__.py
index 34fd369..d60cd98 100644
--- a/parambokeh/__init__.py
+++ b/parambokeh/__init__.py
@@ -173,7 +173,7 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
else:
self.document = doc or curdoc()
self.client_comm = None
- self.comm = comm or None
+ self.comm = None
self._queue = []
self._active = False
@@ -214,13 +214,8 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
if self.p.on_init:
self.execute()
- if self.p.mode in ('raw', 'notebook'):
- return self.container
+ return self.container
- # Handle server case
- model = self.container._get_model(doc, self.comm, self.plot_id)
- document.add_root(model)
- return self.document
def on_msg(self, msg):
diff --git a/parambokeh/layout.py b/parambokeh/layout.py
index 13b94bc..3a693fe 100644
--- a/parambokeh/layout.py
+++ b/parambokeh/layout.py
@@ -5,7 +5,7 @@
from bokeh.models import LayoutDOM
from bokeh.layouts import Column as BkColumn, Row as BkRow
-from .util import render, process_plot
+from .util import render, process_plot, add_to_doc
try:
from IPython.display import publish_display_data
diff --git a/parambokeh/util.py b/parambokeh/util.py
index 8526cfa..b16cf27 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -103,8 +103,8 @@ def process_plot(plot, doc, plot_id, comm):
return plot.container
elif hasattr(plot, 'kdims') and hasattr(plot, 'vdims'):
from holoviews import renderer
- plot = renderer('bokeh').get_plot(plot, doc=doc)
- print(plot)
+ renderer = renderer('bokeh').instance(mode='server' if comm is None else 'default')
+ plot = renderer.get_plot(plot, doc=doc)
if not hasattr(plot, '_update_callbacks'):
raise ValueError('Can only render bokeh models or HoloViews objects.')
From f0f188e74c79af5e24c7753c577b362f196cad71 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 12 Jul 2018 13:56:09 +0100
Subject: [PATCH 3/7] Bug fixes
---
parambokeh/util.py | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/parambokeh/util.py b/parambokeh/util.py
index b16cf27..3a38316 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -18,6 +18,19 @@
basestring = str
+embed_js = """
+// Ugly hack - see HoloViews #2574 for more information
+if (!(document.getElementById('{plot_id}')) && !(document.getElementById('_anim_img{widget_id}'))) {{
+ console.log("Creating DOM nodes dynamically for assumed nbconvert export. To generate clean HTML output set HV_DOC_HTML as an environment variable.")
+ var htmlObject = document.createElement('div');
+ htmlObject.innerHTML = `{html}`;
+ var scriptTags = document.getElementsByTagName('script');
+ var parentTag = scriptTags[scriptTags.length-1].parentNode;
+ parentTag.append(htmlObject)
+}}
+"""
+
+
def as_unicode(obj):
"""
Safely casts any object to unicode including regular string
@@ -65,9 +78,9 @@ def patch_hv_plot(plot, plot_id, comm):
for subplot in plot.traverse(lambda x: x):
subplot.comm = comm
- for cb in subplot.callbacks:
+ for cb in getattr(subplot, 'callbacks', []):
for c in cb.callbacks:
- c.code = c.code.replace(plot.id, widgets.plot_id)
+ c.code = c.code.replace(plot.id, plot_id)
def patch_bk_plot(plot, plot_id):
@@ -146,6 +159,7 @@ def render(obj, doc, comm):
# Publish plot HTML
bokeh_script, bokeh_div, _ = bokeh.embed.notebook.notebook_content(obj, comm.id)
+ html = encode_utf8(bokeh_div)
# Publish comm manager
JS = '\n'.join([PYVIZ_PROXY, JupyterCommManager.js_manager])
@@ -155,8 +169,9 @@ def render(obj, doc, comm):
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])
+ bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=html) + bokeh_js
- data = {exec_mime: '', 'text/html': encode_utf8(bokeh_div), 'application/javascript': bokeh_js}
+ data = {exec_mime: '', 'text/html': html, 'application/javascript': bokeh_js}
metadata = {exec_mime: {'id': target}}
return data, metadata
From ca9adc16a3bed0006b10790a923b38956fc15021 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 12 Jul 2018 14:01:46 +0100
Subject: [PATCH 4/7] Fixed flakes
---
parambokeh/__init__.py | 14 +++++---------
parambokeh/layout.py | 13 +------------
parambokeh/util.py | 2 +-
3 files changed, 7 insertions(+), 22 deletions(-)
diff --git a/parambokeh/__init__.py b/parambokeh/__init__.py
index d60cd98..08ee105 100644
--- a/parambokeh/__init__.py
+++ b/parambokeh/__init__.py
@@ -10,22 +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
+from .layout import WidgetBox, Column, Row
from .widgets import wtype, literal_params
from .util import named_objs, get_method_owner
from .view import _View
@@ -183,8 +179,8 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
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
- self.container = Column() if plots or view_params else WidgetBox(widget_box)
+ 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
diff --git a/parambokeh/layout.py b/parambokeh/layout.py
index 3a693fe..0aa2edb 100644
--- a/parambokeh/layout.py
+++ b/parambokeh/layout.py
@@ -2,22 +2,10 @@
from bokeh.document import Document
from bokeh.io import curdoc
-from bokeh.models import LayoutDOM
from bokeh.layouts import Column as BkColumn, Row as BkRow
from .util import render, process_plot, add_to_doc
-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
- IPYTHON_AVAILABLE = True
-except:
- IPYTHON_AVAILABLE = False
-
-
class Viewable(param.Parameterized):
"""
A Viewable is an abstract baseclass for objects which wrap bokeh
@@ -32,6 +20,7 @@ def _get_model(self, doc, comm=None, plot_id=None):
"""
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)
diff --git a/parambokeh/util.py b/parambokeh/util.py
index 3a38316..f75f4d1 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -8,7 +8,7 @@
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 JupyterCommManager, bokeh_msg_handler, PYVIZ_PROXY
IPYTHON_AVAILABLE = True
except:
IPYTHON_AVAILABLE = False
From 899ea3ab0a4fcc10237273b4ca6d950c747264f8 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sat, 11 Aug 2018 14:10:57 +0100
Subject: [PATCH 5/7] Add support for laying out matplotlib figures
---
parambokeh/util.py | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/parambokeh/util.py b/parambokeh/util.py
index f75f4d1..b1dc21c 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -1,8 +1,10 @@
import sys
import inspect
+import base64
+from io import BytesIO
import bokeh
-from bokeh.models import Model, CustomJS, LayoutDOM
+from bokeh.models import Model, CustomJS, LayoutDOM, Div
try:
from IPython.display import publish_display_data
@@ -102,7 +104,7 @@ def patch_widgets(plot, doc, plot_id, comm):
def process_plot(plot, doc, plot_id, comm):
"""
- Converts all acceptable plot and widget objects into displaybel
+ Converts all acceptable plot and widget objects into displayable
bokeh models. Patches any HoloViews plots or parambokeh Widgets
with the top-level comms and plot id.
"""
@@ -118,7 +120,15 @@ def process_plot(plot, doc, plot_id, comm):
from holoviews import renderer
renderer = renderer('bokeh').instance(mode='server' if comm is None else 'default')
plot = renderer.get_plot(plot, doc=doc)
-
+ elif plot.__class__.__name__ == 'Figure' and hasattr(plot, '_cachedRenderer'):
+ bytes_io = BytesIO()
+ plot.canvas.print_figure(bytes_io)
+ data = bytes_io.getvalue()
+ b64 = base64.b64encode(data).decode("utf-8")
+ src = "data:image/png;base64,{b64}".format(b64=b64)
+ html = "".format(src=src)
+ width, height = plot.canvas.get_width_height()
+ return Div(text=html, width=width, height=height)
if not hasattr(plot, '_update_callbacks'):
raise ValueError('Can only render bokeh models or HoloViews objects.')
From 65d3bda33fb73b344129f0d8fbca85c6db838216 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sat, 11 Aug 2018 17:42:39 +0100
Subject: [PATCH 6/7] Add support for objects with _repr_html_
---
parambokeh/util.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/parambokeh/util.py b/parambokeh/util.py
index b1dc21c..d62acf7 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -129,6 +129,9 @@ def process_plot(plot, doc, plot_id, comm):
html = "".format(src=src)
width, height = plot.canvas.get_width_height()
return Div(text=html, width=width, height=height)
+ elif hasattr(plot, '_repr_html_'):
+ return Div(text=plot._repr_html_())
+
if not hasattr(plot, '_update_callbacks'):
raise ValueError('Can only render bokeh models or HoloViews objects.')
From a439f2e521d4626fba686b11115b01e3757b4c71 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 23 Aug 2018 13:30:23 +0100
Subject: [PATCH 7/7] Moved layout code to pyviz_panels
---
parambokeh/__init__.py | 7 +-
parambokeh/layout.py | 92 --------------------------
parambokeh/util.py | 147 -----------------------------------------
3 files changed, 4 insertions(+), 242 deletions(-)
delete mode 100644 parambokeh/layout.py
diff --git a/parambokeh/__init__.py b/parambokeh/__init__.py
index 08ee105..aa71b88 100644
--- a/parambokeh/__init__.py
+++ b/parambokeh/__init__.py
@@ -15,13 +15,14 @@
from bokeh.models import CustomJS
from bokeh.protocol import Protocol
+from pyviz_panels import Column, Row, View
+from pyviz_panels.comms import JS_CALLBACK, JupyterCommManager
try:
- from pyviz_comms import JS_CALLBACK, JupyterCommManager
+ from ipykernel.comm import Comm as IPyComm
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
@@ -180,7 +181,7 @@ def __call__(self, parameterized, doc=None, plots=[], **params):
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
- self.container = container_type() if plots or view_params else WidgetBox(widget_box)
+ self.container = container_type() if plots or view_params else View(widget_box)
self.plot_id = widget_box.ref['id']
# Initialize widgets and populate container
diff --git a/parambokeh/layout.py b/parambokeh/layout.py
deleted file mode 100644
index 0aa2edb..0000000
--- a/parambokeh/layout.py
+++ /dev/null
@@ -1,92 +0,0 @@
-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
diff --git a/parambokeh/util.py b/parambokeh/util.py
index d62acf7..61fba85 100644
--- a/parambokeh/util.py
+++ b/parambokeh/util.py
@@ -1,38 +1,11 @@
import sys
import inspect
-import base64
-from io import BytesIO
-
-import bokeh
-from bokeh.models import Model, CustomJS, LayoutDOM, Div
-
-try:
- from IPython.display import publish_display_data
- import bokeh.embed.notebook
- from bokeh.util.string import encode_utf8
- from pyviz_comms import JupyterCommManager, bokeh_msg_handler, PYVIZ_PROXY
- IPYTHON_AVAILABLE = True
-except:
- IPYTHON_AVAILABLE = False
if sys.version_info.major == 3:
unicode = str
basestring = str
-embed_js = """
-// Ugly hack - see HoloViews #2574 for more information
-if (!(document.getElementById('{plot_id}')) && !(document.getElementById('_anim_img{widget_id}'))) {{
- console.log("Creating DOM nodes dynamically for assumed nbconvert export. To generate clean HTML output set HV_DOC_HTML as an environment variable.")
- var htmlObject = document.createElement('div');
- htmlObject.innerHTML = `{html}`;
- var scriptTags = document.getElementsByTagName('script');
- var parentTag = scriptTags[scriptTags.length-1].parentNode;
- parentTag.append(htmlObject)
-}}
-"""
-
-
def as_unicode(obj):
"""
Safely casts any object to unicode including regular string
@@ -68,123 +41,3 @@ def get_method_owner(meth):
return meth.im_class if meth.im_self is None else meth.im_self
else:
return meth.__self__
-
-
-def patch_hv_plot(plot, plot_id, comm):
- """
- Update the plot id and comm on a HoloViews plot to allow embedding
- it in a bokeh layout.
- """
- if not hasattr(plot, '_update_callbacks'):
- return
-
- for subplot in plot.traverse(lambda x: x):
- subplot.comm = comm
- for cb in getattr(subplot, 'callbacks', []):
- for c in cb.callbacks:
- c.code = c.code.replace(plot.id, plot_id)
-
-
-def patch_bk_plot(plot, plot_id):
- """
- Patches bokeh CustomJS models with top-level plot_id
- """
- for js in plot.select({'type': CustomJS}):
- js.code = js.code.replace(plot.ref['id'], plot_id)
-
-
-def patch_widgets(plot, doc, plot_id, comm):
- """
- Patches parambokeh Widgets instances with top-level document, comm and plot id
- """
- plot.comm = comm
- plot.document = doc
- patch_bk_plot(plot.container, plot_id)
-
-
-def process_plot(plot, doc, plot_id, comm):
- """
- Converts all acceptable plot and widget objects into displayable
- bokeh models. Patches any HoloViews plots or parambokeh Widgets
- with the top-level comms and plot id.
- """
- from . import Widgets
- if isinstance(plot, LayoutDOM):
- if plot_id:
- patch_bk_plot(plot, plot_id)
- return plot
- elif isinstance(plot, Widgets):
- patch_widgets(plot, doc, plot_id, comm)
- return plot.container
- elif hasattr(plot, 'kdims') and hasattr(plot, 'vdims'):
- from holoviews import renderer
- renderer = renderer('bokeh').instance(mode='server' if comm is None else 'default')
- plot = renderer.get_plot(plot, doc=doc)
- elif plot.__class__.__name__ == 'Figure' and hasattr(plot, '_cachedRenderer'):
- bytes_io = BytesIO()
- plot.canvas.print_figure(bytes_io)
- data = bytes_io.getvalue()
- b64 = base64.b64encode(data).decode("utf-8")
- src = "data:image/png;base64,{b64}".format(b64=b64)
- html = "".format(src=src)
- width, height = plot.canvas.get_width_height()
- return Div(text=html, width=width, height=height)
- elif hasattr(plot, '_repr_html_'):
- return Div(text=plot._repr_html_())
-
- if not hasattr(plot, '_update_callbacks'):
- raise ValueError('Can only render bokeh models or HoloViews objects.')
-
- patch_hv_plot(plot, plot_id, comm)
- return plot.state
-
-
-def add_to_doc(obj, doc, hold=False):
- """
- Adds a model to the supplied Document removing it from any existing Documents.
- """
- # Handle previously displayed models
- for model in obj.select({'type': Model}):
- prev_doc = model.document
- model._document = None
- if prev_doc:
- prev_doc.remove_root(model)
-
- # Add new root
- doc.add_root(obj)
- if doc._hold is None and hold:
- doc.hold()
-
-
-def render(obj, doc, comm):
- """
- Displays bokeh output inside a notebook using the PyViz display
- and comms machinery.
- """
- if not isinstance(obj, LayoutDOM):
- raise ValueError('Can only render bokeh LayoutDOM models')
-
- add_to_doc(obj, doc, True)
-
- 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)
- 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])
- bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=html) + bokeh_js
-
- data = {exec_mime: '', 'text/html': html, 'application/javascript': bokeh_js}
- metadata = {exec_mime: {'id': target}}
- return data, metadata
-