|
| 1 | +#----------------------------------------------------------------------------- |
| 2 | +# Copyright (c) 2012 - 2019, Anaconda, Inc., and Bokeh Contributors. |
| 3 | +# All rights reserved. |
| 4 | +# |
| 5 | +# The full license is in the file LICENSE.txt, distributed with this software. |
| 6 | +#----------------------------------------------------------------------------- |
| 7 | +""" |
| 8 | +Defines a Bokeh model wrapper for Jupyter notebook/lab, which renders Bokeh models |
| 9 | +and performs bi-directional syncing just like bokeh server does. |
| 10 | +
|
| 11 | +""" |
| 12 | + |
| 13 | +#----------------------------------------------------------------------------- |
| 14 | +# Boilerplate |
| 15 | +#----------------------------------------------------------------------------- |
| 16 | +from __future__ import absolute_import, division, print_function, unicode_literals |
| 17 | + |
| 18 | +# Standard library imports |
| 19 | +import json |
| 20 | + |
| 21 | +# External imports |
| 22 | +from ipywidgets import DOMWidget |
| 23 | +from traitlets import Unicode, Dict |
| 24 | + |
| 25 | +# Bokeh imports |
| 26 | +from bokeh.core.json_encoder import serialize_json |
| 27 | +from bokeh.models import LayoutDOM |
| 28 | +from bokeh.document import Document |
| 29 | +from bokeh.protocol import Protocol |
| 30 | +from bokeh.util.dependencies import import_optional |
| 31 | +from bokeh.embed.elements import div_for_render_item |
| 32 | +from bokeh.embed.util import standalone_docs_json_and_render_items |
| 33 | + |
| 34 | +#----------------------------------------------------------------------------- |
| 35 | +# Globals and constants |
| 36 | +#----------------------------------------------------------------------------- |
| 37 | + |
| 38 | +__all__ = ( |
| 39 | + "BokehModel", |
| 40 | +) |
| 41 | + |
| 42 | +_module_name = "@bokeh/jupyter_bokeh" |
| 43 | +_module_version = "1.1.0-dev.1" |
| 44 | + |
| 45 | +#----------------------------------------------------------------------------- |
| 46 | +# General API |
| 47 | +#----------------------------------------------------------------------------- |
| 48 | + |
| 49 | +class BokehModel(DOMWidget): |
| 50 | + |
| 51 | + _model_name = Unicode("BokehModel").tag(sync=True) |
| 52 | + _model_module = Unicode(_module_name).tag(sync=True) |
| 53 | + _model_module_version = Unicode(_module_version).tag(sync=True) |
| 54 | + |
| 55 | + _view_name = Unicode("BokehView").tag(sync=True) |
| 56 | + _view_module = Unicode(_module_name).tag(sync=True) |
| 57 | + _view_module_version = Unicode(_module_version).tag(sync=True) |
| 58 | + |
| 59 | + render_bundle = Dict().tag(sync=True, to_json=lambda obj, _: serialize_json(obj)) |
| 60 | + |
| 61 | + @property |
| 62 | + def _document(self): |
| 63 | + return self._model.document |
| 64 | + |
| 65 | + def __init__(self, model, **kwargs): |
| 66 | + assert isinstance(model, LayoutDOM) |
| 67 | + self.update_from_model(model) |
| 68 | + super(BokehModel, self).__init__(**kwargs) |
| 69 | + self.on_msg(self._sync_model) |
| 70 | + |
| 71 | + def close(self): |
| 72 | + if self._document is not None: |
| 73 | + self._document.remove_on_change(self) |
| 74 | + |
| 75 | + @classmethod |
| 76 | + def _model_to_traits(cls, model): |
| 77 | + if model.document is None: |
| 78 | + document = Document() |
| 79 | + document.add_root(model) |
| 80 | + (docs_json, [render_item]) = standalone_docs_json_and_render_items([model], True) |
| 81 | + render_bundle = dict( |
| 82 | + docs_json=docs_json, |
| 83 | + render_items=[render_item.to_json()], |
| 84 | + div=div_for_render_item(render_item), |
| 85 | + ) |
| 86 | + return render_bundle |
| 87 | + |
| 88 | + def update_from_model(self, model): |
| 89 | + self._model = model |
| 90 | + self.render_bundle = self._model_to_traits(model) |
| 91 | + self._document.on_change_dispatch_to(self) |
| 92 | + |
| 93 | + def _document_patched(self, event): |
| 94 | + msg = Protocol("1.0").create("PATCH-DOC", [event]) |
| 95 | + |
| 96 | + self.send({"msg": "patch", "payload": msg.header_json}) |
| 97 | + self.send({"msg": "patch", "payload": msg.metadata_json}) |
| 98 | + self.send({"msg": "patch", "payload": msg.content_json}) |
| 99 | + for header, buffer in msg.buffers: |
| 100 | + self.send({"msg": "patch", "payload": json.dumps(header)}) |
| 101 | + self.send({"msg": "patch"}, [buffer]) |
| 102 | + |
| 103 | + def _sync_model(self, _, content, _buffers): |
| 104 | + if content.get("event", "") != "jsevent": |
| 105 | + return |
| 106 | + new, old, attr = content["new"], content["old"], content["attr"] |
| 107 | + submodel = self._model.select_one({"id": content["id"]}) |
| 108 | + try: |
| 109 | + setattr(submodel, attr, new) |
| 110 | + except Exception: |
| 111 | + return |
| 112 | + for cb in submodel._callbacks.get(attr, []): |
| 113 | + cb(attr, old, new) |
| 114 | + |
| 115 | +#----------------------------------------------------------------------------- |
| 116 | +# Dev API |
| 117 | +#----------------------------------------------------------------------------- |
0 commit comments