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: