Custom Dashboards#

import pathlib
import panel as pn

pn.extension()

In the previous sections we learned the very basics of working with Panel’s API. In this section we will learn how to link widgets to outputs “manually” rather than using pn.rx.

In this section we will once again make use of the earthquake dataset we loaded previously and compute some statistics:

import pandas as pd

df = pd.read_parquet(pathlib.Path('../data/earthquakes-projected.parq'), columns=['time', 'place', 'mag'])
df = df.reset_index()
df['time'] = df.time.dt.strftime('%m/%d/%Y %H:%M:%S') 

Widgets and reactive components#

Widgets are built on Parameters provided by the Param library. E.g., consider a RangeSlider:

mag_filter = pn.widgets.RangeSlider(name='Magnitude', start=0, end=df.mag.max())

mag_filter

Here the widget value is a Parameter that is set to a tuple of the selected upper and lower bound. Parameters are an extended type of Python attribute that declare their type, range, etc. so that other code can interact with them in a consistent way. When we change the range using the widget the value parameter updates, and vice versa if you change the value parameter manually:

mag_filter.value
(0, np.float64(9.1))

Now we will declare a second widget:

place_filter = pn.widgets.TextInput(placeholder='Enter a placename')

place_filter

In addition to the fully automated .rx() interface inherited from Param, Panel offers a very concise, powerful approach of declaring dependencies between the parameters of a object and the arguments to a function. In practice, this middle ground provides enough control for nearly any app, without the complexity of explicit chains of callbacks that would otherwise be required when customizing the behavior.

Here we will create a little function that can filter the dataframe:

def filter_df(mag_range, place):
    lower = df.mag>mag_range[0]
    upper = df.mag<mag_range[1]
    dffilter = lower & upper
    if place:
        dffilter &= df.place.str.contains(place)
    return df[dffilter].head()

Then we can bind the the widgets we created to the inputs of this function using pn.bind and lay out the widget and the function:

filtered_view = pn.Row(
    pn.Column(mag_filter, place_filter),
    pn.panel(pn.bind(filter_df, mag_range=mag_filter, place=place_filter), width=400))

filtered_view

Whenever one of the widgets is changed, the filter_df function will be triggered and the DataFrame pane will update with the updated data.

Let us also take a look at the repr():

print(filtered_view)
Row
    [0] Column
        [0] RangeSlider(end=np.float64(9.1), name='Magnitude', value=(0, np.float64(9.1)), value_end=np.float64(9.1))
        [1] TextInput(placeholder='Enter a placename')
    [1] ParamFunction(function, _pane=DataFrame, defer_load=False, width=400)

The ParamFunction pane is what listens to changes in the parameters on the widgets and updates the displayed output.

Exercise#

Declare two IntInput widgets with an initial value of 1, then declare a function that depends on the values of both widgets and adds them together. Finally lay out the two widgets and the function in a Panel:

w1 = pn.widgets.IntInput(value=1, width=60)
w2 = pn.widgets.IntInput(value=1, width=60)

def adder(v1, v2):
    return pn.panel(v1 + v2, width=50)

pn.Row(w1, '+', w2, '=', pn.bind(adder, v1=w1, v2=w2))

Callbacks#

The pn.bind function is still a very high level way of declaring interactive components. Panel also supports the more low-level approach of writing explicit callbacks that are executed in response to changes in some parameter, e.g. the value of a widget. All parameters can be watched using the .param.watch API, which will call the provided callback with an event object containing the old and new value of the widget.

Now that it is loaded we will create a slider which we will eventually use to select the row of the dataframe that we want to display:

row_slider = pn.widgets.IntSlider(value=0, start=0, end=len(df)-1)

Next we create a Pane to display the current row of the dataframe with times formatted nicely:

row_pane = pn.panel(df.loc[row_slider.value])

Now that we have defined both the widget and the object we want to update, we can declare a callback to link the two. As we learned in the previous section, assigning a new value to the object of a pane will update the display. In the callback we select the row of the dataframe and then assign it to the pane.object:

def df_callback(event):
    row_pane.object = df.loc[event.new]

Lastly we actually have to register this callback. To do so we provide the callback and the parameter we want to trigger the event on the slider’s .param.watch method:

row_slider.param.watch(df_callback, 'value')
Watcher(inst=IntSlider(end=695925), cls=<class 'panel.widgets.slider.IntSlider'>, fn=<function df_callback at 0x7fa53b327eb0>, mode='args', onlychanged=True, parameter_names=('value',), what='value', queued=False, precedence=0)

Now that everything is connected up, we can put both the widget and the pane in a panel and display them:

pn.Column(row_slider, row_pane, width=400)

As you can see, this process is slightly more laborious than .rx() or even pn.bind, but doing it in this way should help you see how everything fits together and can be useful to more precisely control callbacks that update particular parameters or the contents of a larger layout.

Moving onwards#

Now that we have learned low-level ways to link parameters between displayed objects and build interactive components, we can start building more highly customized apps and dashboards. Before we move on to plotting and visualization let us quickly use what we have learned by adding interactivity to the dashboard we built in the previous exercise.

This web page was generated from a Jupyter notebook and not all interactivity will work on this website. Right click to download and run locally for full Python-backed interactivity.

Right click to download this notebook from GitHub.