When deploying a Panel app or dashboard as a Bokeh application, it is rendered into a default template that serves the JS and CSS resources as well as the actual Panel object being shown. However, it is often desirable to customize the layout of the deployed app, or even to embed multiple separate panels into an app. The Template
component in Panel allows customizing this default template, including the ability to rendering multiple components in a single document easily.
A template is defined using the Jinja2 templating language, which makes it straightforward to extend the default template in various ways or even replace it entirely. However most users can avoid modifying the jinja2 template directly by using one of the default templates shipped with Panel itself.
import panel as pn
import numpy as np
import holoviews as hv
pn.extension(sizing_mode='stretch_width')
For a large variety of use cases we do not need complete control over the exact layout of each individual component on the page, instead we just want to achieve a more polished look and feel. For these cases Panel ships with a number of default templates, which are defined by declaring four main content areas on the page, which can be populated as desired:
header
: The header area of the HTML pagesidebar
: A collapsible sidebarmain
: The main area of the applicationmodal
: A modal, i.e. a dialog box/popup windowThese four areas behave very similarly to other Panel layout components and have list-like semantics. This means we can easily append new components into these areas. Unlike other layout components however, the contents of the areas is fixed once rendered. If you need a dynamic layout you should therefore insert a regular Panel layout component (e.g. a Column
or Row
) and modify it in place once added to one of the content areas.
Panel ships with a number of these default themes built on different CSS frameworks:
MaterialTemplate
: Built on Material Components for the webBootstrapTemplate
: Built on Bootstrap v4VanillaTemplate
: Built using pure CSS without relying on any specific frameworkFastListTemplate
: Built on the Fast UI framework using a list-like APIFastGridTemplate
: Built on the Fast UI framework using grid-like APIGoldenTemplate
: Built on the Golden Layout frameworkThere are two ways of building an application using these templates either we explicitly construct the template or we change the global template.
The explicit way to use templates is to instantiate them directly and adding components to the different parts of the template directly. Let us construct a very simple app containing two plots in the main
area and two widgets in the sidebar based on the BootstrapTemplate
class:
bootstrap = pn.template.BootstrapTemplate(title='Bootstrap Template')
xs = np.linspace(0, np.pi)
freq = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2)
phase = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi)
@pn.depends(freq=freq, phase=phase)
def sine(freq, phase):
return hv.Curve((xs, np.sin(xs*freq+phase))).opts(
responsive=True, min_height=400)
@pn.depends(freq=freq, phase=phase)
def cosine(freq, phase):
return hv.Curve((xs, np.cos(xs*freq+phase))).opts(
responsive=True, min_height=400)
bootstrap.sidebar.append(freq)
bootstrap.sidebar.append(phase)
bootstrap.main.append(
pn.Row(
pn.Card(hv.DynamicMap(sine), title='Sine'),
pn.Card(hv.DynamicMap(cosine), title='Cosine')
)
)
A Template
can be served or displayed just like any other Panel component, i.e. using .servable()
or .show()
.
Another, often simpler approach is to set the global template can be set via the pn.extension()
call, e.g. we can set pn.extension(template='material')
and even toggle between different themes with pn.extension(template='material', theme='dark')
. Once selected we can easily add components to the template using .servable(area=...)
calls, e.g. the same example looks like this when constructed via the global template
pn.extension(template='bootstrap')
freq = pn.widgets.FloatSlider(name="Frequency", start=0, end=10, value=2).servable(area='sidebar')
phase = pn.widgets.FloatSlider(name="Phase", start=0, end=np.pi).servable(area='sidebar')
@pn.depends(freq=freq, phase=phase)
def sine(freq, phase):
return hv.Curve((xs, np.sin(xs*freq+phase))).opts(
responsive=True, min_height=400)
@pn.depends(freq=freq, phase=phase)
def cosine(freq, phase):
return hv.Curve((xs, np.cos(xs*freq+phase))).opts(
responsive=True, min_height=400)
pn.Row(
pn.Card(hv.DynamicMap(sine), title='Sine'),
pn.Card(hv.DynamicMap(cosine), title='Cosine')
).servable(area='main') # Note 'main' is the default
A modal can be toggled opened and closed with .open_modal()
and .close_modal()
methods. For example, a temporary About modal could be added to the previous app with:
import time
# Callback that will be called when the About button is clicked
def about_callback(event):
bootstrap.open_modal()
time.sleep(10)
bootstrap.close_modal()
# Create, link and add the button to the sidebar
btn = pn.widgets.Button(name="About")
btn.on_click(about_callback)
bootstrap.sidebar.append(btn)
# Add some content to the modal
bootstrap.modal.append("# About...")
Default template classes provide a unified approach to theming, which currently allow specifying custom CSS and the Bokeh Theme
to apply to the Template
. The way it is implemented a user declares a generic Theme
class to use (e.g. DarkTheme
) which loads the specific theme implementation (e.g. MaterialDarkTheme
) for a particular Template
(e.g MaterialTemplate
). To make this more concrete, by default a Template uses the DefaultTheme
, but then uses the find_theme
method to look up the implementation of that theme for the Template being used:
from panel.template import DefaultTheme
DefaultTheme.find_theme(pn.template.MaterialTemplate)
To implement your own theme you should therefore declare a generic class for use by the enduser and a specific implementation for all the themes that should be supported, e.g. here is an example of what the definition of a dark theme might look like:
import param
from panel.template.theme import Theme
from bokeh.themes import DARK_MINIMAL
class DarkTheme(Theme):
"""
The DarkTheme provides a dark color palette
"""
bokeh_theme = param.ClassSelector(class_=(Theme, str), default=DARK_MINIMAL)
class MaterialDarkTheme(DarkTheme):
# css = param.Filename() Here we could declare some custom CSS to apply
# This tells Panel to use this implementation
_template = pn.template.MaterialTemplate
To apply the theme we now merely have to provide the generic DarkTheme
class to the Template (we will import the DarkTheme
that ships with panel here:
from panel.template import DarkTheme
dark_material = pn.template.MaterialTemplate(title='Material Template', theme=DarkTheme)
dark_material.sidebar.append(freq)
dark_material.sidebar.append(phase)
dark_material.main.append(
pn.Row(
pn.Card(hv.DynamicMap(sine), title='Sine'),
pn.Card(hv.DynamicMap(cosine), title='Cosine')
)
)
Completely custom templates extend the default jinja2 template in various ways. Before we dive into modifying such a template let us take a look at the default template used by Panel:
{% from macros import embed %}
<!DOCTYPE html>
<html lang="en">
{% block head %}
<head>
{% block inner_head %}
<meta charset="utf-8">
<title>{% block title %}{{ title | e if title else "Panel App" }}{% endblock %}</title>
{% block preamble %}{% endblock %}
{% block resources %}
{% block css_resources %}
{{ bokeh_css | indent(8) if bokeh_css }}
{% endblock %}
{% block js_resources %}
{{ bokeh_js | indent(8) if bokeh_js }}
{% endblock %}
{% endblock %}
{% block postamble %}{% endblock %}
{% endblock %}
</head>
{% endblock %}
{% block body %}
<body>
{% block inner_body %}
{% block contents %}
{% for doc in docs %}
{{ embed(doc) if doc.elementid }}
{% for root in doc.roots %}
{{ embed(root) | indent(10) }}
{% endfor %}
{% endfor %}
{% endblock %}
{{ plot_script | indent(8) }}
{% endblock %}
</body>
{% endblock %}
</html>
As you may be able to note if you are familiar with jinja2 templating or similar languages, this template can easily be extended by overriding the existing {% block ... %}
definitions. However it is also possible to completely override this default template instead.
That said it is usually easiest to simply extend an existing template by overriding certain blocks. To begin with we start by using {% extends base %}
to declare that we are merely extending an existing template rather than defining a whole new one; otherwise we would have to repeat the entire header sections of the full template to ensure all the appropriate resources are loaded.
In this case we will extend the postamble block of the header to load some additional resources, and the contents block to redefine how the components will be laid out. Specifically, we will load bootstrap.css in the preamble allowing us to use the bootstrap grid system to lay out the output.
template = """
{% extends base %}
<!-- goes in body -->
{% block postamble %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
{% endblock %}
<!-- goes in body -->
{% block contents %}
{{ app_title }}
<p>This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.</p>
<br>
<div class="container">
<div class="row">
<div class="col-sm">
{{ embed(roots.A) }}
</div>
<div class="col-sm">
{{ embed(roots.B) }}
</div>
</div>
</div>
{% endblock %}
"""
If you look closely we defined two different roots in the template using the embed
macro. In order to be able to render the template we now have to first construct the pn.Template
object with the template HTML and then populate the template with the two required roots, in this case 'A'
and 'B'
by using the add_panel
method. If either of the roots is not defined the app is invalid and will fail to launch. The app will also fail to launch if any panels are added that are not referenced in the template.
Additionally we have also declared a new app_title
variable in the template, which we can populate by using the add_variable
method:
tmpl = pn.Template(template)
tmpl.add_variable('app_title', '<h1>Custom Template App</h1>')
tmpl.add_panel('A', hv.Curve([1, 2, 3]))
tmpl.add_panel('B', hv.Curve([1, 2, 3]))
tmpl
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:
nb_template = """
{% extends base %}
{% block contents %}
{{ app_title }}
<p>This is a Panel app with a custom template allowing us to compose multiple Panel objects into a single HTML document.</p>
<br>
<div style="display:table; width: 100%">
<div style="display:table-cell; margin: auto">
{{ embed(roots.A) }}
</div>
<div style="display:table-cell; margin: auto">
{{ embed(roots.B) }}
</div>
</div>
{% endblock %}
"""
tmpl = pn.Template(template, nb_template=nb_template)
tmpl.add_variable('app_title', '<h1>Custom Template App</h1>')
tmpl.add_panel('A', hv.Curve([1, 2, 3]))
tmpl.add_panel('B', hv.Curve([1, 2, 3]))
tmpl
If the template is larger it is often cleaner to define it in a separate file. You can either read it in as a string, or use the official loading mechanism available for Jinja2 templates by defining a Environment
along with a loader
.
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('.'))
jinja_template = env.get_template('sample_template.html')
tmpl = pn.Template(jinja_template)
tmpl.add_panel('A', hv.Curve([1, 2, 3]))
tmpl.add_panel('B', hv.Curve([1, 2, 3]))
tmpl.servable()