Helpful best practices when dashboards with param and panel

Hi all,

I really liked this post from /u/ahuang11:

I think it’s a great idea to post learnings from making complicated applications. I recently also finished making my first real world app using param and panel at work, and I wanted to add a few of my learnings here.

Jupyter autoreload

I like to develop and prototype python in jupyter notebooks. And once I have code working I like to move it to a package. Doing this normally requires reloading the notebook kernel. But if you add the following code in your notebook at the very top, your Python modules will automatically be reloaded on any change.

# autoreload all modules every time before executing the Python code
%reload_ext autoreload
%autoreload 2

I found I still had to restart my kernel every now and then, but doing this did make me so much more productive.

Identifying state in UI

When making any UI it can be useful to identify the variables that represent “state” of a component. This usually involves understanding a couple of things:

  1. What are the inputs to the component?
  2. What are the derived properties of the component?

This typically forms a directed graph:

Here’s a simple example of an input and derived properties for a very simple dashboard:

import param
import panel as pn

class Movies(param.Parameterized):
    input_df = param.DataFrame() # input
    start_year = param.Integer() # derived property from input
    end_year = param.Integer() # derived property from input
    filtered_df = param.DataFrame() # derived property from input, start_year, end_year

    @param.depends("input_df", watch=True)
    def _update_bounds(self):
        self.start_year = self.input_df.startYear.min()
        self.end_year = self.input_df.startYear.max()

    @param.depends("input_df", "start_year", "end_year", watch=True)
    def _update_output(self):
        self.filtered_df = (
            self.input_df
                .query(f"startYear >= {self.start_year}")
                .query(f"startYear <= {self.end_year}")
            )

Thinking about UI as a state machine can make it easier to design complicated UIs in a more systematic manner.

This manner of organizing your code also allows separating “state” from the “view”.

In the above example, all panel related code will be in a separate function like so:

    def panel(self):
        return pn.Column(
            pn.Row(
                pn.widgets.IntInput.from_param(self.param.start_year, name="Start Year"),
                pn.widgets.IntInput.from_param(self.param.end_year, name="End Year"),
            ),
            pn.widgets.Tabulator.from_param(
                self.param.filtered_df, pagination="remote", page_size=5, disabled=True,
            ),
        )

param.bind vs @param.depends

Sometimes you want to run a function when a param changes, and you can use pn.bind for this:

class Object(param.Parameterized):

    ready = param.Boolean(default=False)

def _update_notifications(ready):
    if ready:
        pn.state.notifications.success('Done.')

obj = Object()
param.bind(_update_notifications, obj.param.ready, watch=True)

But you can also do this with @pn.depends:

class Object(param.Parameterized):

    ready = param.Boolean(default=False)

obj = Object()

@param.depends(obj.param.ready, watch=True)
def _update_notifications(ready):
    if ready:
        pn.state.notifications.success('Done.')

imho, using param.depends lends itself better to refactoring into a class based approach, e.g.:

class Object(param.Parameterized):

    ready = param.Boolean(default=False)

    @pm.depends("ready", watch=True)
    def _update_notifications(self):
        if self.ready:
            pn.state.notifications.success('Done.')

I think most people would already be aware of all of this so I’ve only summarized above what I wrote in a longer version on my blog:

1 Like