Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to render Template inside notebook #666

Merged
merged 8 commits into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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 *~
Expand Down
42 changes: 36 additions & 6 deletions examples/user_guide/Templates.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"<h1>Custom Template App</h1>\n",
"<p>This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.</p>\n",
"<br>\n",
"<div style=\"display:table; width: 100%\">\n",
" <div style=\"display:table-cell; margin: auto\">\n",
" {{ embed(roots.A) }}\n",
" </div>\n",
" <div style=\"display:table-cell; margin: auto\">\n",
" {{ embed(roots.B) }}\n",
" </div>\n",
"</div>\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()"
]
},
{
Expand All @@ -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]))"
]
}
],
Expand All @@ -187,5 +217,5 @@
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
43 changes: 43 additions & 0 deletions panel/_templates/nb_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{#
Renders Bokeh models into a basic .html file.

:param title: value for ``<title>`` 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 %}
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
96 changes: 92 additions & 4 deletions panel/io/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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")
Expand Down
Loading