diff --git a/MANIFEST.in b/MANIFEST.in index 13ef4cae6c..c84c139020 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include panel/assets/*.gif include panel/models/*.ts include panel/models/*.json include panel/_templates/*.js +include panel/_templates/*.html include panel/_styles/*.css global-exclude *.py[co] global-exclude *~ diff --git a/examples/user_guide/Templates.ipynb b/examples/user_guide/Templates.ipynb index 162facea72..38d50dfa44 100644 --- a/examples/user_guide/Templates.ipynb +++ b/examples/user_guide/Templates.ipynb @@ -141,14 +141,46 @@ "tmpl.add_panel('A', hv.Curve([1, 2, 3]))\n", "tmpl.add_panel('B', hv.Curve([1, 2, 3]))\n", "\n", - "tmpl.servable()" + "tmpl.servable();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the notebook a `Template` will helpfully provide a repr that will allow you to launch a local server to check the output is as you expect." + "Embedding a different CSS framework (like Bootstrap) in the notebook can have undesirable side-effects so a `Template` may also be given a separate `nb_template` that will be used when rendering inside the notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nb_template = \"\"\"\n", + "{% extends base %}\n", + "\n", + "{% block contents %}\n", + "

Custom Template App

\n", + "

This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.

\n", + "
\n", + "
\n", + "
\n", + " {{ embed(roots.A) }}\n", + "
\n", + "
\n", + " {{ embed(roots.B) }}\n", + "
\n", + "
\n", + "{% endblock %}\n", + "\"\"\"\n", + "\n", + "tmpl = pn.Template(template, nb_template=nb_template)\n", + "\n", + "tmpl.add_panel('A', hv.Curve([1, 2, 3]))\n", + "tmpl.add_panel('B', hv.Curve([1, 2, 3]))\n", + "\n", + "tmpl.servable()" ] }, { @@ -174,9 +206,7 @@ "tmpl = pn.Template(jinja_template)\n", "\n", "tmpl.add_panel('A', hv.Curve([1, 2, 3]))\n", - "tmpl.add_panel('B', hv.Curve([1, 2, 3]))\n", - "\n", - "tmpl" + "tmpl.add_panel('B', hv.Curve([1, 2, 3]))" ] } ], @@ -187,5 +217,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/panel/_templates/nb_template.html b/panel/_templates/nb_template.html new file mode 100644 index 0000000000..1befb0df65 --- /dev/null +++ b/panel/_templates/nb_template.html @@ -0,0 +1,43 @@ +{# +Renders Bokeh models into a basic .html file. + +:param title: value for ```` tags +:type title: str + +:param plot_resources: typically the output of RESOURCES +:type plot_resources: str + +:param plot_script: typically the output of PLOT_SCRIPT +:type plot_script: str + +Users can customize the file output by providing their own Jinja2 template +that accepts these same parameters. + +#} + +{% from macros import embed %} + +{% block inner_head %} + {% block preamble %}{% endblock %} + {% block resources %} + {% block js_resources %} + {{ bokeh_js | indent(8) if bokeh_js }} + {% endblock %} + {% endblock %} +{% endblock %} +{% block postamble %}{% endblock %} +{% block body %} + {% block inner_body %} + {% block contents %} + {% for doc in docs %} + {{ embed(doc) if doc.elementid }} + {% for root in doc.roots %} + {% block root scoped %} + {{ embed(root) | indent(10) }} + {% endblock %} + {% endfor %} + {% endfor %} + {% endblock %} + {{ plot_script | indent(8) }} + {% endblock %} +{% endblock %} diff --git a/panel/io/notebook.py b/panel/io/notebook.py index 52a3b78619..e77ac31413 100644 --- a/panel/io/notebook.py +++ b/panel/io/notebook.py @@ -9,24 +9,28 @@ import uuid from contextlib import contextmanager +from six import string_types import bokeh import bokeh.embed.notebook from bokeh.core.templates import DOC_NB_JS from bokeh.core.json_encoder import serialize_json +from bokeh.core.templates import MACROS from bokeh.document import Document from bokeh.embed import server_document from bokeh.embed.bundle import bundle_for_objs_and_resources -from bokeh.embed.elements import div_for_render_item +from bokeh.embed.elements import div_for_render_item, script_for_render_items from bokeh.embed.util import standalone_docs_json_and_render_items +from bokeh.embed.wrappers import wrap_in_script_tag from bokeh.models import CustomJS, LayoutDOM, Model from bokeh.resources import CDN, INLINE -from bokeh.util.string import encode_utf8 +from bokeh.util.string import encode_utf8, escape +from bokeh.util.serialization import make_id from jinja2 import Environment, Markup, FileSystemLoader from pyviz_comms import ( JS_CALLBACK, PYVIZ_PROXY, Comm, JupyterCommManager as _JupyterCommManager, - bokeh_msg_handler, nb_mime_js) + nb_mime_js) from ..compiler import require_components from .embed import embed_state @@ -66,6 +70,39 @@ }} """ +# Following JS block becomes body of the message handler callback +bokeh_msg_handler = """ +var plot_id = "{plot_id}"; + +if ((plot_id in window.PyViz.plot_index) && (window.PyViz.plot_index[plot_id] != null)) {{ + var plot = window.PyViz.plot_index[plot_id]; +}} else if ((Bokeh !== undefined) && (plot_id in Bokeh.index)) {{ + var plot = Bokeh.index[plot_id]; +}} + +if (plot == null) {{ + return +}} + +if (plot_id in window.PyViz.receivers) {{ + var receiver = window.PyViz.receivers[plot_id]; +}} else {{ + var receiver = new Bokeh.protocol.Receiver(); + window.PyViz.receivers[plot_id] = receiver; +}} + +if ((buffers != undefined) && (buffers.length > 0)) {{ + receiver.consume(buffers[0].buffer) +}} else {{ + receiver.consume(msg) +}} + +const comm_msg = receiver.message; +if ((comm_msg != null) && (Object.keys(comm_msg.content).length > 0)) {{ + plot.model.document.apply_json_patch(comm_msg.content, comm_msg.buffers) +}} +""" + def get_comm_customjs(change, client_comm, plot_id, timeout=5000, debounce=50): """ Returns a CustomJS callback that can be attached to send the @@ -109,7 +146,7 @@ def get_env(): _env = get_env() _env.filters['json'] = lambda obj: Markup(json.dumps(obj)) AUTOLOAD_NB_JS = _env.get_template("autoload_panel_js.js") - +NB_TEMPLATE_BASE = _env.get_template('nb_template.html') def _autoload_js(bundle, configs, requirements, exports, load_timeout=5000): return AUTOLOAD_NB_JS.render( @@ -122,6 +159,57 @@ def _autoload_js(bundle, configs, requirements, exports, load_timeout=5000): ) +def html_for_render_items(comm_js, docs_json, render_items, template=None, template_variables={}): + comm_js = wrap_in_script_tag(comm_js) + + json_id = make_id() + json = escape(serialize_json(docs_json), quote=False) + json = wrap_in_script_tag(json, "application/json", json_id) + + script = wrap_in_script_tag(script_for_render_items(json_id, render_items)) + + context = template_variables.copy() + + context.update(dict( + title = '', + bokeh_js = comm_js, + plot_script = json + script, + docs = render_items, + base = NB_TEMPLATE_BASE, + macros = MACROS, + )) + + if len(render_items) == 1: + context["doc"] = context["docs"][0] + context["roots"] = context["doc"].roots + + if template is None: + template = NB_TEMPLATE_BASE + elif isinstance(template, string_types): + template = _env.from_string("{% extends base %}\n" + template) + + html = template.render(context) + return encode_utf8(html) + + +def render_template(document, comm=None): + plot_id = document.roots[0].ref['id'] + (docs_json, render_items) = standalone_docs_json_and_render_items(document) + + if comm: + msg_handler = bokeh_msg_handler.format(plot_id=plot_id) + comm_js = comm.js_template.format(plot_id=plot_id, comm_id=comm.id, msg_handler=msg_handler) + else: + comm_js = '' + + html = html_for_render_items( + comm_js, docs_json, render_items, template=document.template, + template_variables=document.template_variables) + + return ({'text/html': html, EXEC_MIME: ''}, + {EXEC_MIME: {'id': plot_id}}) + + def render_model(model, comm=None): if not isinstance(model, Model): raise ValueError("notebook_content expects a single Model instance") diff --git a/panel/template.py b/panel/template.py index 2011bfa238..cc9ccb12df 100644 --- a/panel/template.py +++ b/panel/template.py @@ -4,15 +4,23 @@ """ from __future__ import absolute_import, division, unicode_literals +import sys + +import param + +from bokeh.document.document import Document as _Document from bokeh.io import curdoc as _curdoc from jinja2.environment import Template as _Template from six import string_types +from pyviz_comms import JupyterCommManager as _JupyterCommManager +from .config import panel_extension from .io.model import add_to_doc +from .io.notebook import render_template from .io.server import StoppableThread, get_server from .io.state import state from .layout import Column -from .pane import panel as _panel, HTML, Str +from .pane import panel as _panel, PaneBase, HTML, Str from .widgets import Button _server_info = ( @@ -27,8 +35,8 @@ class Template(object): given a string or Jinja2 Template object in the constructor and can then be populated with Panel objects. When adding panels to the Template a unique name must be provided, making it possible to - refer to them uniquely in the template. For instance, two panels added like - this: + refer to them uniquely in the template. For instance, two panels + added like this: template.add_panel('A', pn.panel('A')) template.add_panel('B', pn.panel('B')) @@ -40,18 +48,27 @@ class Template(object): Once a template has been fully populated it can be rendered using the same API as other Panel objects. + + Since embedding complex CSS frameworks inside a notebook can have + undesirable side-effects and a notebook does not afford the same + amount of screen space a Template may given separate template + and nb_template objects. This allows for different layouts when + served as a standalone server and when used in the notebook. """ - def __init__(self, template=None, items=None): + def __init__(self, template=None, items=None, nb_template=None): if isinstance(template, string_types): template = _Template(template) self.template = template + if isinstance(nb_template, string_types): + nb_template = _Template(nb_template) + self.nb_template = nb_template or template self._render_items = {} + self._server = None + self._layout = self._build_layout() items = {} if items is None else items for name, item in items.items(): self.add_panel(name, item) - self._server = None - self._layout = self._build_layout() def _build_layout(self): str_repr = Str(repr(self)) @@ -92,13 +109,60 @@ def __repr__(self): return template.format( cls=cls, objs=('%s' % spacer).join(objs), spacer=spacer) + def _init_doc(self, doc=None, comm=None, title=None, notebook=False): + doc = doc or _curdoc() + if title is not None: + doc.title = title + + root = None + for name, obj in self._render_items.items(): + if root is None: + model = obj.get_root(doc, comm) + root = model + elif isinstance(obj, PaneBase): + if obj._updates: + model = obj._get_model(doc, root, root, comm=comm) + else: + model = obj.layout._get_model(doc, root, root, comm=comm) + else: + model = obj._get_model(doc, root, root, comm) + model.name = name + if hasattr(doc, 'on_session_destroyed'): + doc.on_session_destroyed(obj._server_destroy) + obj._documents[doc] = model + add_to_doc(model, doc, hold=bool(comm)) + if notebook: + doc.template = self.nb_template + else: + doc.template = self.template + return doc + def _repr_mimebundle_(self, include=None, exclude=None): - return self._layout._repr_mimebundle_(include, exclude) + loaded = panel_extension._loaded + if not loaded and 'holoviews' in sys.modules: + import holoviews as hv + loaded = hv.extension._loaded + if not loaded: + param.main.warning('Displaying Panel objects in the notebook ' + 'requires the panel extension to be loaded. ' + 'Ensure you run pn.extension() before ' + 'displaying objects in the notebook.') + return None + + try: + assert get_ipython().kernel is not None # noqa + state._comm_manager = _JupyterCommManager + except: + pass + doc = _Document() + comm = state._comm_manager.get_server_comm() + self._init_doc(doc, comm, notebook=True) + return render_template(doc, comm) #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- - + def add_panel(self, name, panel): """ Add panels to the Template, which may then be referenced by @@ -136,18 +200,7 @@ def server_doc(self, doc=None, title=None): doc : bokeh.Document The Bokeh document the panel was attached to """ - doc = doc or _curdoc() - if title is not None: - doc.title = title - for name, obj in self._render_items.items(): - model = obj.get_root(doc) - model.name = name - if hasattr(doc, 'on_session_destroyed'): - doc.on_session_destroyed(obj._server_destroy) - obj._documents[doc] = model - add_to_doc(model, doc) - doc.template = self.template - return doc + return self._init_doc(doc, title=title) def servable(self, title=None): """ diff --git a/panel/viewable.py b/panel/viewable.py index b5651f10d4..ca9fdc6b40 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -629,6 +629,7 @@ def param_change(*events): if ref not in state._views: continue viewable, root, doc, comm = state._views[ref] + if comm or state._unblocked(doc): self._update_model(events, msg, root, model, doc, comm) if comm and 'embedded' not in root.tags: