Skip to content

Commit

Permalink
Add ability to configure global template (#2271)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jun 17, 2021
1 parent 8c69876 commit 9fd546a
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 72 deletions.
2 changes: 2 additions & 0 deletions examples/user_guide/Overview.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
"> - `safe_embed`: Whether to record all set events when embedding rather than just those that are changed\n",
"> - `session_history`: If set to a non-zero value this determines the maximum length of the pn.state.session_info dictionary, which tracks information about user sessions. A value of -1 indicates an unlimited history.\n",
"> - `sizing_mode`: Specify the default sizing mode behavior of panels.\n",
"> - `template`: The template to render the served application into, e.g. `'bootstrap'` or `'material'`.\n",
"> - `theme`: The theme to apply to the selected template (no effect unless `template` is set)\n",
"> - `throttled`: Whether sliders and inputs should be throttled until release of mouse.\n",
"\n",
"#### Python and Environment variables\n",
Expand Down
172 changes: 104 additions & 68 deletions examples/user_guide/Templates.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,7 @@
"\n",
"## What is a template?\n",
"\n",
"A template is defined using the [Jinja2](http://jinja.pocoo.org/docs/) templating language, which makes it straightforward to extend the default template in various ways or even replace it entirely. Before modifying the default template, let us take a look at it in its entirety:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```html\n",
"{% from macros import embed %}\n",
"\n",
"<!DOCTYPE html>\n",
"<html lang=\"en\">\n",
"{% block head %}\n",
"<head>\n",
" {% block inner_head %}\n",
" <meta charset=\"utf-8\">\n",
" <title>{% block title %}{{ title | e if title else \"Panel App\" }}{% endblock %}</title>\n",
" {% block preamble %}{% endblock %}\n",
" {% block resources %}\n",
" {% block css_resources %}\n",
" {{ bokeh_css | indent(8) if bokeh_css }}\n",
" {% endblock %}\n",
" {% block js_resources %}\n",
" {{ bokeh_js | indent(8) if bokeh_js }}\n",
" {% endblock %}\n",
" {% endblock %}\n",
" {% block postamble %}{% endblock %}\n",
" {% endblock %}\n",
"</head>\n",
"{% endblock %}\n",
"{% block body %}\n",
"<body>\n",
" {% block inner_body %}\n",
" {% block contents %}\n",
" {% for doc in docs %}\n",
" {{ embed(doc) if doc.elementid }}\n",
" {% for root in doc.roots %}\n",
" {{ embed(root) | indent(10) }}\n",
" {% endfor %}\n",
" {% endfor %}\n",
" {% endblock %}\n",
" {{ plot_script | indent(8) }}\n",
" {% endblock %}\n",
"</body>\n",
"{% endblock %}\n",
"</html>\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As we can see the template defines a number of custom blocks, which can be overridden by extending this default template."
"A template is defined using the [Jinja2](http://jinja.pocoo.org/docs/) 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."
]
},
{
Expand All @@ -74,7 +21,7 @@
"import numpy as np\n",
"import holoviews as hv\n",
"\n",
"pn.extension(sizing_mode = 'stretch_width')"
"pn.extension(sizing_mode='stretch_width')"
]
},
{
Expand All @@ -83,11 +30,12 @@
"source": [
"## Using default templates\n",
"\n",
"For a large variety of use cases we do not need complete control over the exact layout of each individual component on the page 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 three main content areas on the page, which can be populated as desired:\n",
"For a large variety of use cases we do not need complete control over the exact layout of each individual component on the page 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:\n",
"\n",
"* **`header`**: The header area of the HTML page\n",
"* **`sidebar`**: A collapsible sidebar\n",
"* **`main`**: The main area of the application\n",
"* **`modal`**: A modal that can be toggled opened and closed with `.open_modal()` and `.close_modal()` methods\n",
"\n",
"These three 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. \n"
]
Expand All @@ -96,19 +44,29 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### Supported themes\n",
"### Supported templates\n",
"\n",
"Panel ships with a number of these default themes built on different CSS frameworks:\n",
" \n",
"* `MaterialTemplate`: Built on [Material Components for the web](https://material.io/develop/web/)\n",
"* `BootstrapTemplate`: Built on [Bootstrap v4](https://getbootstrap.com/docs/4.0/getting-started/introduction/)"
"* `BootstrapTemplate`: Built on [Bootstrap v4](https://getbootstrap.com/docs/4.0/getting-started/introduction/)\n",
"* `VanillaTemplate`: Built using pure CSS without relying on any specific framework\n",
"* `FastListTemplate`: Built on the [Fast UI](https://fast.design/) framework using a list-like API\n",
"* `FastGridTemplate`: Built on the [Fast UI](https://fast.design/) framework using grid-like API\n",
"* `GoldenTemplate`: Built on the [Golden Layout](https://golden-layout.com/) framework"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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:"
"### Using templates\n",
"\n",
"There are two ways of building an application using these templates either we explicitly construct the template or we change the global template.\n",
"\n",
"#### Explicit constructor\n",
"\n",
"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:"
]
},
{
Expand Down Expand Up @@ -157,6 +115,37 @@
"A `Template` can be served or displayed just like any other Panel component, i.e. using `.servable()` or `.show()`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Global template\n",
"\n",
"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\n",
"\n",
"```python\n",
"pn.extension(template='bootstrap')\n",
"\n",
"freq = pn.widgets.FloatSlider(name=\"Frequency\", start=0, end=10, value=2).servable(area='sidebar')\n",
"phase = pn.widgets.FloatSlider(name=\"Phase\", start=0, end=np.pi).servable(area='sidebar')\n",
"\n",
"@pn.depends(freq=freq, phase=phase)\n",
"def sine(freq, phase):\n",
" return hv.Curve((xs, np.sin(xs*freq+phase))).opts(\n",
" responsive=True, min_height=400)\n",
"\n",
"@pn.depends(freq=freq, phase=phase)\n",
"def cosine(freq, phase):\n",
" return hv.Curve((xs, np.cos(xs*freq+phase))).opts(\n",
" responsive=True, min_height=400)\n",
"\n",
"pn.Row(\n",
" pn.Card(hv.DynamicMap(sine), title='Sine'),\n",
" pn.Card(hv.DynamicMap(cosine), title='Cosine')\n",
").servable(area='main') # Note 'main' is the default\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -253,14 +242,54 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Using custom templates"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once we have loaded Panel we can start defining a custom template. As mentioned before, 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.\n",
"## Using custom templates\n",
"\n",
"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:\n",
"\n",
"```html\n",
"{% from macros import embed %}\n",
"\n",
"<!DOCTYPE html>\n",
"<html lang=\"en\">\n",
"{% block head %}\n",
"<head>\n",
" {% block inner_head %}\n",
" <meta charset=\"utf-8\">\n",
" <title>{% block title %}{{ title | e if title else \"Panel App\" }}{% endblock %}</title>\n",
" {% block preamble %}{% endblock %}\n",
" {% block resources %}\n",
" {% block css_resources %}\n",
" {{ bokeh_css | indent(8) if bokeh_css }}\n",
" {% endblock %}\n",
" {% block js_resources %}\n",
" {{ bokeh_js | indent(8) if bokeh_js }}\n",
" {% endblock %}\n",
" {% endblock %}\n",
" {% block postamble %}{% endblock %}\n",
" {% endblock %}\n",
"</head>\n",
"{% endblock %}\n",
"{% block body %}\n",
"<body>\n",
" {% block inner_body %}\n",
" {% block contents %}\n",
" {% for doc in docs %}\n",
" {{ embed(doc) if doc.elementid }}\n",
" {% for root in doc.roots %}\n",
" {{ embed(root) | indent(10) }}\n",
" {% endfor %}\n",
" {% endfor %}\n",
" {% endblock %}\n",
" {{ plot_script | indent(8) }}\n",
" {% endblock %}\n",
"</body>\n",
"{% endblock %}\n",
"</html>\n",
"```\n",
"\n",
"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.\n",
"\n",
"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.\n",
"\n",
"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."
]
Expand Down Expand Up @@ -408,7 +437,14 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.10"
"version": "3.8.8"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
Expand Down
11 changes: 11 additions & 0 deletions panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ class _config(_base_config):
'scale_width', 'scale_height', 'scale_both', None], doc="""
Specify the default sizing mode behavior of panels.""")

template = param.ObjectSelector(default=None, doc="""
The default template to render served applications into.""")

theme = param.ObjectSelector(default='default', objects=['default', 'dark'], doc="""
The theme to apply to the selected global template.""")

throttled = param.Boolean(default=False, doc="""
If sliders and inputs should be throttled until release of mouse.""")

Expand Down Expand Up @@ -234,6 +240,11 @@ def __getattribute__(self, attr):
def _console_output_hook(self, value):
return value if value else 'disable'

def _template_hook(self, value):
if isinstance(value, str):
return self.param.template.names[value]
return value

@property
def _doc_build(self):
return os.environ.get('PANEL_DOC_BUILD')
Expand Down
6 changes: 6 additions & 0 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ async def on_session_created(self, session_context):
cb(session_context)
await super().on_session_created(session_context)

def initialize_document(self, doc):
super().initialize_document(doc)
if doc in state._templates:
template = state._templates[doc]
template.server_doc(title=template.title, location=True, doc=doc)

bokeh.command.util.Application = Application


Expand Down
18 changes: 18 additions & 0 deletions panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class _state(param.Parameterized):
_location = None # Global location, e.g. for notebook context
_locations = WeakKeyDictionary() # Server locations indexed by document

# Templates
_templates = WeakKeyDictionary() # Server templates indexed by document
_template = None

# An index of all currently active views
_views = {}

Expand Down Expand Up @@ -346,6 +350,20 @@ def location(self):
def session_args(self):
return self.curdoc.session_context.request.arguments if self.curdoc else {}

@property
def template(self):
from ..config import config
if self.curdoc in self._templates:
return self._templates[self.curdoc]
elif self.curdoc is None and self._template:
return self._template
template = config.template(theme=config.theme)
if self.curdoc is None:
self._template = template
else:
self._templates[self.curdoc] = template
return template

@property
def user(self):
from ..config import config
Expand Down
15 changes: 14 additions & 1 deletion panel/template/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
from ..config import _config
from .base import Template, BaseTemplate # noqa
from .bootstrap import BootstrapTemplate # noqa
from .fast import FastListTemplate, FastGridTemplate # noqa
from .material import MaterialTemplate # noqa
from .theme import DarkTheme, DefaultTheme # noqa
from .golden import GoldenTemplate # noqa
from .vanilla import VanillaTemplate # noqa
from .react import ReactTemplate # noqa
from .vanilla import VanillaTemplate # noqa

templates = {
'bootstrap' : BootstrapTemplate,
'fast' : FastListTemplate,
'fast-list' : FastListTemplate,
'material' : MaterialTemplate,
'golden' : GoldenTemplate,
'vanilla' : VanillaTemplate
}

_config.param.template.names = templates
_config.param.template.objects = list(templates)
4 changes: 3 additions & 1 deletion panel/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from ..viewable import ServableMixin, Viewable
from ..widgets import Button
from ..widgets.indicators import BooleanIndicator, LoadingSpinner
from .theme import DefaultTheme, Theme
from .theme import THEMES, DefaultTheme, Theme

_server_info = (
'<b>Running server:</b> <a target="_blank" href="https://localhost:{port}">'
Expand Down Expand Up @@ -470,6 +470,8 @@ def __init__(self, **params):
params['modal'] = ListLike()
else:
params['modal'] = self._get_params(params['modal'], self.param.modal.class_)
if 'theme' in params and isinstance(params['theme'], str):
params['theme'] = THEMES[params['theme']]
super().__init__(template=template, **params)
if self.busy_indicator:
state.sync_busy(self.busy_indicator)
Expand Down
20 changes: 18 additions & 2 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def _on_stdout(self, ref, stdout):
# Public API
#----------------------------------------------------------------

def servable(self, title=None, location=True):
def servable(self, title=None, location=True, area='main'):
"""
Serves the object if in a `panel serve` context and returns
the Panel object to allow it to display itself in a notebook
Expand All @@ -302,6 +302,9 @@ def servable(self, title=None, location=True):
location : boolean or panel.io.location.Location
Whether to create a Location component to observe and
set the URL location.
area: str
The area of a template to add the component too. Only has an
effect if pn.config.template has been set.
Returns
-------
Expand All @@ -312,7 +315,20 @@ def servable(self, title=None, location=True):
for handler in logger.handlers:
if isinstance(handler, logging.StreamHandler):
handler.setLevel(logging.WARN)
self.server_doc(title=title, location=True)
if config.template:
template = state.template
if template.title == template.param.title.default and title:
template.title = title
if area == 'main':
template.main.append(self)
elif area == 'sidebar':
template.sidebar.append(self)
elif area == 'modal':
template.modal.append(self)
elif area == 'header':
template.header.append(self)
else:
self.server_doc(title=title, location=location)
return self

def show(self, title=None, port=0, address=None, websocket_origin=None,
Expand Down

0 comments on commit 9fd546a

Please sign in to comment.