Reactive Parameters#

Welcome to the world of reactive parameters in Panel! In this section, we’ll delve into the powerful capabilities offered by Parameters in the HoloViz ecosystem.

Understanding Parameters#

Panel and other projects in the HoloViz ecosystem build on the foundation provided by Param. Param offers a robust framework for adding validation, documentation, and interactivity to projects. Similar to projects like Pydantic in some aspects, but Param focuses on providing APIs that simplify the expression of complex dependencies, reactivity, and UI interactions.

In this section, we won’t delve into the inner workings of Param but rather focus on understanding the fundamentals. By the end of this section, we will:

  • Understand the difference between the Parameter value and the Parameter object.

  • Know how to use Parameter objects, bound functions, and reactive expressions as proxies or references for their current value.

import panel as pn

pn.extension('tabulator')

Exploring Parameters#

Parameters serve as objects expressing the semantics of a value of an attribute on an object, encompassing not only Python types but also broader semantics.

Let’s define a simple Parameterized class with a value Parameter:

import param

class Text(param.Parameterized):
    value = param.String(default='', allow_None=True, doc="The text value")

Now, let’s create an instance and examine its current value:

text = Text(value="Are you a Python programmer? If so, you need Param!")

text.value
Are you a Python programmer? If so, you need Param!

We can also inspect its Parameter instance:

text.param.value
<param.parameterized.String object at 0x10fc85080>

The Utility of Parameters#

Parameters act as references to underlying values, making them invaluable for enhancing UI interactivity.

Parameters imbue validation, documentation, and interactivity into Panel, with most Panel components built as Parameterized classes with Parameters.

For instance, consider the value Parameter of the Panel TextInput widget:

class TextInput(pn.widgets.Widget):
    ...

    value = param.String(default='', allow_None=True, doc="""
        Initial or entered text value updated when <enter> key is pressed.""")

To explore all parameters on a Parameterized class, we inspect the .param namespace:

pn.widgets.TextInput.param
TextInput
NameDefaultTypeRange

align

'start' Align

aspect_ratio

None Aspect nullable

css_classes

[] List (0, None)

description

None String nullable

design

None ObjectSelector None

disabled

False Boolean

height

None Integer nullable >=0

height_policy

'auto' ObjectSelector 'auto', 'fixed', 'fit', 'min', 'max'

loading

False Boolean

margin

(5, 10) Margin nullable

max_height

None Integer nullable >=0

max_length

5000 Integer

max_width

None Integer nullable >=0

min_height

None Integer nullable >=0

min_width

None Integer nullable >=0

name

'TextInput' String

placeholder

'' String

sizing_mode

None ObjectSelector 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None

styles

{} Dict dict

stylesheets

[] List (0, None)

tags

[] List (0, None)

value

'' String nullable

value_input

'' String nullable

visible

True Boolean

width

300 Integer nullable >=0

width_policy

'auto' ObjectSelector 'auto', 'fixed', 'fit', 'min', 'max'

Utilizing Parameters#

Let’s now delve into practical usage scenarios.

Accessing Parameter Values#

Consider working with a TextInput widget:

text_input = pn.widgets.TextInput(value='A string!')

text_input

We can access its current value:

text_input.value
A string!

Additionally, we can access the Parameter instance, acting as a proxy or reference for the value:

text_input.param.value
<param.parameterized.String object at 0x10fd0fba0>

Setting Parameter Values#

We can set it to a new value:

text_input.value = 'Hello World!'

For setting multiple parameter values, use the .param.update method:

text_input.param.update(value='Updated!', width=85);

Or use it as a context manager for temporary value setting:

from time import sleep

with text_input.param.update(value='Temporary'):
    sleep(1)

Validation#

Parameters perform validation, raising errors for invalid assignments:

import traceback as tb

try:
    text_input.value = 3.14
except ValueError as e:
    tb.print_exc()
Traceback (most recent call last):
  File "<ast>", line 4, in <module>
  File "/Users/runner/miniconda3/envs/test-environment/lib/python3.11/site-packages/param/parameterized.py", line 528, in _f
    instance_param.__set__(obj, val)
  File "/Users/runner/miniconda3/envs/test-environment/lib/python3.11/site-packages/param/parameterized.py", line 530, in _f
    return f(self, obj, val)
           ^^^^^^^^^^^^^^^^^
  File "/Users/runner/miniconda3/envs/test-environment/lib/python3.11/site-packages/param/parameterized.py", line 1497, in __set__
    self._validate(val)
  File "/Users/runner/miniconda3/envs/test-environment/lib/python3.11/site-packages/param/parameterized.py", line 1647, in _validate
    self._validate_value(val, self.allow_None)
  File "/Users/runner/miniconda3/envs/test-environment/lib/python3.11/site-packages/param/parameterized.py", line 1641, in _validate_value
    raise ValueError(
ValueError: String parameter 'TextInput.value' only takes a string value, not value of <class 'float'>.

Parameters as References#

Parameters serve as proxies for underlying Parameter values, facilitating interactive declarations.

Consider a simple example using the TextInput widget:

import panel as pn

pn.extension()

text_in = pn.widgets.TextInput(value='Hello world!')

text_out = pn.pane.Markdown(text_in.param.value)

pn.Column(text_in, text_out).servable()

Observe how changes in TextInput are automatically reflected in the Markdown output.

This also works when using it for other parameters, e.g. we can add a switch to toggle the visibility of some component:

visible = pn.widgets.Switch(value=True)

pn.Row(visible, pn.pane.Markdown('Hello World!', visible=visible)).servable()

Many parameters that accept a container such as a dictionary or list can also resolve references when they are nested, e.g. if we declare a styles dictionary one of the values can be a widget:

color = pn.widgets.ColorPicker(value='red')

md = pn.pane.Markdown('Some Text!', styles={'color': color})

pn.Row(color, md).servable()

Notice that we passed in the widget object directly instead of the .param.value. This is possible because widgets are treated as a proxy of their value parameter just like a Parameter is treated as a proxy for current value.

Transforming Parameters#

Transforming parameters allows for complex value processing pipelines, offering immense flexibility.

For instance, we can create a reactive format string, filling in values based on widget input:

import panel as pn

pn.extension()

text_input = pn.widgets.TextInput(value='World')

text = pn.rx('**Hello {}!**').format(text_input)

md = pn.pane.Markdown(text)

pn.Row(text_input, md).servable()

Similarly, we can make a DataFrame reactive:

import pandas as pd
import panel as pn

pn.extension()

data_url = 'https://assets.holoviz.org/panel/tutorials/turbines.csv.gz'

df = pn.cache(pd.read_csv)(data_url)

dfrx = pn.rx(df)

dfrx.head(2)

Please explore the potential by replacing 2 with an instance of an IntSlider.

Solution
import pandas as pd
import panel as pn

pn.extension()

data_url = 'https://assets.holoviz.org/panel/tutorials/turbines.csv.gz'

df = pn.cache(pd.read_csv)(data_url)

dfrx = pn.rx(df)

slider = pn.widgets.IntSlider(value=2, start=1, end=10)
pn.panel(dfrx.head(slider)).servable()

Now let’s get a little more complex and write a whole pipeline that selects the desired columns, samples a number of random rows, and then applies some custom styling highlighting the rows with the highest value:

import pandas as pd
import panel as pn
import numpy as np

pn.extension()

data_url = 'https://assets.holoviz.org/panel/tutorials/turbines.csv.gz'
df = pn.cache(pd.read_csv)(data_url)

dfrx = pn.rx(df)

cols  = pn.widgets.MultiChoice(
    options=df.columns.to_list(), value=['p_name', 't_state', 't_county', 'p_year', 'p_cap'], height=300
)
nrows = pn.widgets.IntSlider(start=5, end=20, step=5, value=15, name='Samples')
style = pn.rx('color: white; background-color: {color}')
color = pn.widgets.ColorPicker(value='darkblue', name='Highlight color')

def highlight_max(s, props=''):
    if s.dtype.kind not in 'f':
        return np.full_like(s, False)
    return np.where(s == np.nanmax(s.values), props, '')

styled_df = dfrx[cols].sample(nrows).style.apply(highlight_max, props=style.format(color=color), axis=0)

pn.pane.DataFrame(styled_df).servable()

As you can see, the Pandas code is identical to what you might have written if you were working with a regular DataFrame. However, now you can use widgets and even complex expressions as inputs.

Try replacing pn.pane.DataFrame with pn.panel to display an interactive component with widgets.

Solution
import pandas as pd
import panel as pn
import numpy as np

pn.extension()

data_url = 'https://assets.holoviz.org/panel/tutorials/turbines.csv.gz'
df = pn.cache(pd.read_csv)(data_url)

dfrx = pn.rx(df)

cols  = pn.widgets.MultiChoice(
    options=df.columns.to_list(), value=['p_name', 't_state', 't_county', 'p_year', 'p_cap'], height=300
)
nrows = pn.widgets.IntSlider(start=5, end=20, step=5, value=15, name='Samples')
style = pn.rx('color: white; background-color: {color}')
color = pn.widgets.ColorPicker(value='darkblue', name='Highlight color')

def highlight_max(s, props=''):
    if s.dtype.kind not in 'f':
        return np.full_like(s, False)
    return np.where(s == np.nanmax(s.values), props, '')

styled_df = dfrx[cols].sample(nrows).style.apply(highlight_max, props=style.format(color=color), axis=0)

pn.panel(styled_df).servable()

Exercise: Scale the Font Size#

Write a small app where you can scale the font-size of a Markdown pane with another widget, e.g. an IntSlider. The font-size can be set using the styles parameter.

Hint

  • The styles parameter only accepts dictionaries of strings but IntSlider returns int types.

  • The font-size value should be a string value in pixels, eg. "15px" is a valid font-size.

Solution 1: Reactive String Formatting
import panel as pn

pn.extension()

intslider = pn.widgets.IntSlider(value=10, start=5, end=100, step=10, name="Font Size")

styles_rx = {"font-size": pn.rx("{value}px").format(value=intslider)}
markdown = pn.pane.Markdown("We love dataviz!", styles=styles_rx)
pn.Column(intslider, markdown).servable()
Solution 2: Reactive Function
import panel as pn

pn.extension()

intslider = pn.widgets.IntSlider(value=10, start=5, end=100, step=10, name="Font Size")

def styles(font_size):
    return {"font-size": f"{font_size}px"}

styles_rx = pn.rx(styles)(intslider)
markdown = pn.pane.Markdown("We love dataviz!", styles=styles_rx)
pn.Column(intslider, markdown).servable()
Solution 3: Bound Function
import panel as pn

pn.extension()

intslider = pn.widgets.IntSlider(value=10, start=5, end=100, step=10, name="Font Size")

def styles(font_size):
    return {"font-size": f"{font_size}px"}

styles_bn = pn.bind(styles, font_size=intslider)
markdown = pn.pane.Markdown("We love dataviz!", styles=styles_bn)
pn.Column(intslider, markdown).servable()

Note

pn.bind is the predecessor of pn.rx. We recommend using pn.rx over pn.bind as it’s much more flexible and efficient. We include this example because you will find lots of examples in the Panel documentation and in the Panel community using pn.bind.

Recap#

Param serves as the backbone of Panel, providing a robust framework for adding validation, documentation, and interactivity. By grasping the fundamentals discussed here, you’ll be well-equipped to leverage Panel’s interactivity and reactivity effectively.

Now, we should be able to:

  • Clearly understand the distinction between Parameter values and Parameter objects.

  • Use Parameter objects, bound functions, and expressions as proxies or references for their current value.

Resources#

Explanation#

How-To#