Parameterisation#
Panel supports the use of parameters and dependencies between parameters, expressed in a simple way by param, to encapsulate dashboards as declarative, stand-alone classes.
Parameters are Python attributes that have been extended using the param
library to support types, ranges, and documentation. This is just the information you need to automatically create widgets for each parameter.
Parameters and widgets#
For this purpose, some parameterised classes with different parameters are declared first:
[1]:
import param
import datetime as dt
class BaseClass(param.Parameterized):
x = param.Parameter(default=3.14,doc="X position")
y = param.Parameter(default="Not editable",constant=True)
string_value = param.String(default="str",doc="A string")
num_int = param.Integer(50000,bounds=(-200,100000))
unbounded_int = param.Integer(23)
float_with_hard_bounds = param.Number(8.2,bounds=(7.5,10))
float_with_soft_bounds = param.Number(0.5,bounds=(0,None),softbounds=(0,2))
unbounded_float = param.Number(30.01,precedence=0)
hidden_parameter = param.Number(2.718,precedence=-1)
integer_range = param.Range(default=(3,7),bounds=(0, 10))
float_range = param.Range(default=(0,1.57),bounds=(0, 3.145))
dictionary = param.Dict(default={"a":2, "b":9})
class Example(BaseClass):
"""An example Parameterized class"""
timestamps = []
boolean = param.Boolean(True, doc="A sample Boolean parameter")
color = param.Color(default='#FFFFFF')
date = param.Date(dt.datetime(2017, 1, 1),
bounds=(dt.datetime(2017, 1, 1), dt.datetime(2017, 2, 1)))
select_string = param.ObjectSelector(default="yellow",objects=["red","yellow","green"])
select_fn = param.ObjectSelector(default=list,objects=[list,set,dict])
int_list = param.ListSelector(default=[3,5], objects=[1,3,5,7,9],precedence=0.5)
single_file = param.FileSelector(path='../../*/*.py*',precedence=0.5)
multiple_files = param.MultiFileSelector(path='../../*/*.py?',precedence=0.5)
record_timestamp = param.Action(lambda x: x.timestamps.append(dt.datetime.now()),
doc="""Record timestamp.""",precedence=0.7)
Example.num_int
[1]:
50000
As you can see, the declaration of parameters only depends on the separate param
library. Parameters are a simple idea with a few properties critical to creating clean, usable code:
The
param
library is written in pure Python with no dependencies, which makes it easy to include in any code without tying it to a specific GUI or widgets library, or to Jupyter notebooks.Parameter declarations focus on semantic information that is relevant to your domain. In this way, you avoid contaminating domain-specific code with anything that binds it to a specific display or interaction with it.
Parameters can be defined wherever they make sense in your inheritance hierarchy, and you can document them once, enter them and limit them to a certain area. All these properties are inherited from any base class. For example, all parameters work the same here, regardless of whether they were declared in
BaseClass
orExample
. This makes it easier to provide this metadata once and prevents it from being duplicated anywhere in the code where areas or types need to be checked or documentation saved.
If you then decide to use these parameterised classes in a notebook or web server environment, you can easily display and edit the parameter values as an optional additional step with import panel
:
[2]:
import panel as pn
pn.extension()
base = BaseClass()
pn.Row(Example.param, base.param)
[2]:
As you can see, Panel does not need to have knowledge of your domain-specific application, nor of the names of your parameters. It simply shows widgets for all parameters that have been defined for this object. By using Param with Panel, an almost complete separation between your domain-specific code and your display code is achieved, which considerably simplifies the maintenance of both over a longer period of time. Here even the msg
behavior of the buttons was declared declaratively as an
action that can be called regardless of whether it is used in a GUI or in another context.
Interaction with the above widgets is only supported in the notebook and on the bokeh server. However, you can also export static renderings of the widgets to a file or a website.
If you edit values in this way, you have to run the notebook cell by cell by default. When you get to the cell above, edit the values as you wish and execute the following cells, in which these parameter values are referred to, your interactively selected settings are used:
[3]:
Example.unbounded_int
[3]:
23
[4]:
Example.num_int
[4]:
50000
To work around this and automatically update all widgets generated from the parameter, you can pass the param
object:
[5]:
pn.Row(Example.param.float_range, Example.param.num_int)
[5]:
Custom widgets#
In the previous section we saw how parameters can be automatically converted into widgets. This is possible because the Panel internally manages an assignment between parameter types and widget types. However, sometimes the standard widget doesn’t provide the most convenient user interface, and we want to give Panel an explicit hint on how a parameter should be rendered. This is possible with the widgets
argument for the Param
panel. With the widgets
keyword we can declare an
association between the parameter name and the desired widget type.
As an example we can assign a RadioButtonGroup
and a DiscretePlayer
to a String
and a Number
selector.
[6]:
class CustomExample(param.Parameterized):
"""An example Parameterized class"""
select_string = param.Selector(objects=["red", "yellow", "green"])
select_number = param.Selector(objects=[0, 1, 10, 100])
pn.Param(CustomExample.param, widgets={
'select_string': pn.widgets.RadioButtonGroup,
'select_number': pn.widgets.DiscretePlayer}
)
[6]:
It is also possible to pass arguments to the widget to customise it. Instead of passing the widget, pass a dictionary with the options you want. Uses the type
keyword to map the widget:
[7]:
pn.Param(CustomExample.param, widgets={
'select_string': {'type': pn.widgets.RadioButtonGroup, 'button_type': 'primary'},
'select_number': pn.widgets.DiscretePlayer}
)
[7]:
Parameter dependencies#
Declaring parameters is usually just the beginning of a workflow. In most applications, these parameters are then linked to a computation. To express the relationship between a computation and the parameters on which it depends, the param.depends
decorator for parameterized methods can be used. This decorator gives panels and other param
-based libraries (e.g. HoloViews) an indication that the method should be recalculated if a parameter is changed.
As a simple example with no additional dependencies, let’s write a small class that returns an ASCII representation of a sine wave that depends on phase
and frequency
parameters. When we pass the .view
method to a panel, the view is automatically recalculated and updated as soon as one or more of the parameters change:
[8]:
import numpy as np
class Sine(param.Parameterized):
phase = param.Number(default=0, bounds=(0, np.pi))
frequency = param.Number(default=1, bounds=(0.1, 2))
@param.depends('phase', 'frequency')
def view(self):
y = np.sin(np.linspace(0, np.pi*3, 40)*self.frequency+self.phase)
y = ((y-y.min())/y.ptp())*20
array = np.array([list((' '*(int(round(d))-1) + '*').ljust(20)) for d in y])
return pn.pane.Str('\n'.join([''.join(r) for r in array.T]), height=325, width=500)
sine = Sine(name='ASCII Sine Wave')
pn.Row(sine.param, sine.view)
[8]:
The parameterised and annotated view
method can return any type provided by the Pane-Objects panel. This makes it easy to link parameters and their associated widgets to a plot or other output. Parameterised classes can therefore be a very useful pattern for encapsulating part of a computational workflow with an associated visualisation and for declaratively expressing the dependencies between the parameters and the
computation.
By default, a Param area (Pane) shows widgets for all parameters with a precedence
value above the value pn.Param.display_threshold
, so you can use precedence
to automatically hide parameters. You can also explicitly choose which parameters should contain widgets in a certain area by passing an parameters
argument. For example, this code outputs a phase
widget, keeping sine.frequency
the initial value 1
:
[9]:
pn.Row(pn.panel(sine.param, parameters=['phase']), sine.view)
[9]:
Another common pattern is linking the values of one parameter to another parameter, for example when there are dependencies between parameters. In the following example we define two parameters, one for the continent and one for the country. Since we would like the selection of valid countries to change when we change continent, let’s define a method to do this for us. To connect the two, we express the dependency using the param.depends
decorator and then use watch=True
to ensure that
the method is executed when the continent is changed.
We also define a view
method that returns an HTML iframe showing the country using Google Maps.
[10]:
class GoogleMapViewer(param.Parameterized):
continent = param.ObjectSelector(default='Asia', objects=['Africa', 'Asia', 'Europe'])
country = param.ObjectSelector(default='China', objects=['China', 'Thailand', 'Japan'])
_countries = {'Africa': ['Ghana', 'Togo', 'South Africa', 'Tanzania'],
'Asia' : ['China', 'Thailand', 'Japan'],
'Europe': ['Austria', 'Bulgaria', 'Greece', 'Portugal', 'Switzerland']}
@param.depends('continent', watch=True)
def _update_countries(self):
countries = self._countries[self.continent]
self.param['country'].objects = countries
self.country = countries[0]
@param.depends('country')
def view(self):
iframe = """
<iframe width="800" height="400" src="https://maps.google.com/maps?q={country}&z=6&output=embed"
frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>
""".format(country=self.country)
return pn.pane.HTML(iframe, height=400)
viewer = GoogleMapViewer(name='Google Map Viewer')
pn.Row(viewer.param, viewer.view)
[10]:
Whenever the continent changes, the _update_countries
method for changing the displayed country list is now executed, which in turn triggers an update of the view
method.
[11]:
from bokeh.plotting import figure
class Shape(param.Parameterized):
radius = param.Number(default=1, bounds=(0, 1))
def __init__(self, **params):
super(Shape, self).__init__(**params)
self.figure = figure(x_range=(-1, 1), y_range=(-1, 1))
self.renderer = self.figure.line(*self._get_coords())
def _get_coords(self):
return [], []
def view(self):
return self.figure
class Circle(Shape):
n = param.Integer(default=100, precedence=-1)
def __init__(self, **params):
super(Circle, self).__init__(**params)
def _get_coords(self):
angles = np.linspace(0, 2*np.pi, self.n+1)
return (self.radius*np.sin(angles),
self.radius*np.cos(angles))
@param.depends('radius', watch=True)
def update(self):
xs, ys = self._get_coords()
self.renderer.data_source.data.update({'x': xs, 'y': ys})
class NGon(Circle):
n = param.Integer(default=3, bounds=(3, 10), precedence=1)
@param.depends('radius', 'n', watch=True)
def update(self):
xs, ys = self._get_coords()
self.renderer.data_source.data.update({'x': xs, 'y': ys})
Parameter sub-objects#
Parameterized
objects often have parameter values that are Parameterized
objects themselves and form a tree-like structure. With the control panel you can not only edit the parameters of the main object, but also access sub-objects. Let’s first define a hierarchy of Shape
classes that will draw a bokeh plot of the selected Shape
:
[12]:
from bokeh.plotting import figure
class Shape(param.Parameterized):
radius = param.Number(default=1, bounds=(0, 1))
def __init__(self, **params):
super(Shape, self).__init__(**params)
self.figure = figure(x_range=(-1, 1), y_range=(-1, 1))
self.renderer = self.figure.line(*self._get_coords())
def _get_coords(self):
return [], []
def view(self):
return self.figure
class Circle(Shape):
n = param.Integer(default=100, precedence=-1)
def __init__(self, **params):
super(Circle, self).__init__(**params)
def _get_coords(self):
angles = np.linspace(0, 2*np.pi, self.n+1)
return (self.radius*np.sin(angles),
self.radius*np.cos(angles))
@param.depends('radius', watch=True)
def update(self):
xs, ys = self._get_coords()
self.renderer.data_source.data.update({'x': xs, 'y': ys})
class NGon(Circle):
n = param.Integer(default=3, bounds=(3, 10), precedence=1)
@param.depends('radius', 'n', watch=True)
def update(self):
xs, ys = self._get_coords()
self.renderer.data_source.data.update({'x': xs, 'y': ys})
Now that we have multiple Shape
classes we can create instances of them and create a ShapeViewer
to choose between. We can also declare two methods with parameter dependencies that update the plot and the plot title. It should be noted that the param.depends
decorator can not only depend on parameters on the object itself, but can also be expressed on certain parameters on the subobject, for example shape.radius
or with shape.param
on parameters of the subobject.
[13]:
shapes = [NGon(), Circle()]
class ShapeViewer(param.Parameterized):
shape = param.ObjectSelector(default=shapes[0], objects=shapes)
@param.depends('shape')
def view(self):
return self.shape.view()
@param.depends('shape', 'shape.radius')
def title(self):
return '## %s (radius=%.1f)' % (type(self.shape).__name__, self.shape.radius)
def panel(self):
return pn.Column(self.title, self.view)
Now that we have a class with sub-objects, we can display them as usual. Three main options control how the sub-object is rendered:
expand
: whether the sub-object is expanded during initialisation (default=False
)expand_button
: whether there should be a button to toggle the extension; otherwise it is set to the initialexpand
value (default=True
)expand_layout
: A layout type or instance to extend the plot in (default=Column
)
Let’s start with the standard view, which has a toggle button to expand the sub-object:
[14]:
viewer = ShapeViewer()
pn.Row(viewer.param, viewer.panel())
[14]:
Alternatively, we can offer a completely separate expand_layout
instance for a param area, which with the expand
and expand_button
option always remains expanded. This allows us to separate the main widgets and the sub-object’s widgets:
[15]:
viewer = ShapeViewer()
expand_layout = pn.Column()
pn.Row(
pn.Column(
pn.panel(viewer.param, expand_button=False, expand=True, expand_layout=expand_layout),
"#### Subobject parameters:",
expand_layout),
viewer.panel())
[15]: