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 -