Reusable Components#

In this guide, we will explore how to structure our components to make them easily reusable and avoid callback hell:

  • Write Parameterized and Viewer classes that encapsulate multiple components.

Writing Parameterized Classes#

When creating larger Panel projects, we recommend using Parameterized classes. This approach is useful for several reasons:

  1. Organizing intricate sections of code and functionality

  2. Crafting reusable components composed of multiple Panel objects

  3. Incorporating validation and documentation

  4. Facilitating seamless testing

A Parameterized class must inherit from param.Parameterized and should declare one or more parameters. Here, we will start building a DataExplorer by declaring two parameters:

  • data: Accepts a DataFrame

  • page_size: Controls the page size

import pandas as pd
import panel as pn
import param

pn.extension("tabulator")

class DataExplorer(param.Parameterized):
    data = param.DataFrame(doc="Stores a DataFrame to explore")

    page_size = param.Integer(
        default=10, doc="Number of rows per page.", bounds=(1, 20)
    )

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

explorer = DataExplorer(data=df, page_size=5)

This explorer doesn’t do anything yet, so let’s learn how we can turn the UI-agnostic parameter declarations into a UI. For that purpose, we will learn about pn.Param.

pn.Param allows mapping parameter declarations to widgets that allow editing the parameter value. There is a default mapping from Parameter type to the appropriate type, but as long as the input matches, this can be overridden.

Let’s start with the simplest case:

pn.Param(explorer.param, widgets={"page_size": pn.widgets.IntInput}).servable()

Notice that each parameter was mapped to a widget appropriate for editing its value, i.e., the data was mapped to a Tabulator widget, and the page_size was mapped to an IntInput widget.

If you try playing with the page_size widget, you will notice that it doesn’t actually do anything.

So next, let’s explicitly map the parameter to a widget using the Widget.from_param method. This will also let us provide additional options, e.g., to provide start and end values for the slider and layout options for the table.

pn.Column(
    pn.widgets.IntSlider.from_param(explorer.param.page_size, start=5, end=20, step=5),
    pn.widgets.Tabulator.from_param(explorer.param.data, page_size=explorer.param.page_size, sizing_mode='stretch_width')
).servable()

Exercise: Add Typehints#

Tip

If you or your team are working in editors or IDEs like VS Code or PyCharm, or using static analysis tools like mypy, we recommend adding type hints to your reusable Parameterized classes.

Please add typehints to the DataExplorer.

Solution: Basic
import pandas as pd
import panel as pn
import param

pn.extension("tabulator")

class DataExplorer(param.Parameterized):
    data: pd.DataFrame | None = param.DataFrame(doc="Stores a DataFrame to explore")

    page_size: int = param.Integer(
        default=10, doc="Number of rows per page.", bounds=(1, 20)
    )
Solution: Extended
import pandas as pd
import panel as pn
import param

pn.extension("tabulator")

class DataExplorer(param.Parameterized):
    data: pd.DataFrame = param.DataFrame(doc="Stores a DataFrame to explore", allow_None=False)

    page_size: int = param.Integer(
        default=10, doc="Number of rows per page.", bounds=(1, 20)
    )

    def __init__(self, data: pd.DataFrame, page_size: int=10):
        super().__init__(data=data, page_size=page_size)

Note

We hope and dream that Param 3.0 will function much like dataclasses, enabling editors, IDEs, and static analysis tools like mypy to automatically infer parameter types and __init__ signatures.

Creating Reusable Viewer Components#

The whole point of using classes is to encapsulate the logic in them, so let’s do that. For that, we can use a slight extension of the Parameterized class that makes the object behave as if it were a regular Panel object. The Viewer class does exactly that; all you have to do is implement the __panel__ method:

import pandas as pd
import panel as pn
import param

pn.extension("tabulator")


class DataExplorer(pn.viewable.Viewer):

    data = param.DataFrame(doc="Stores a DataFrame to explore")
    page_size = param.Integer(default=10, doc="Number of rows per page.", bounds=(1, None))

    def __panel__(self):
        return pn.Column(
            pn.widgets.IntSlider.from_param(self.param.page_size, start=5, end=25, step=5),
            pn.widgets.Tabulator.from_param(self.param.data, page_size=self.param.page_size, sizing_mode='stretch_width')
        )

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

DataExplorer(data=df).servable()

Exercise: Extend the DataExplorer#

Extend the DataExplorer class by adding parameters to control the Tabulator theme and toggling the show_index option

Solution
import pandas as pd
import param

import panel as pn

from panel.widgets import IntSlider, Tabulator

pn.extension("tabulator")

class DataExplorer(pn.viewable.Viewer):
    data = param.DataFrame(doc="Stores a DataFrame to explore")
    page_size = param.Integer(
        default=10, doc="Number of rows per page.", bounds=(1, None)
    )
    theme = param.Selector(
        default="simple",
        objects=["simple", "default", "site", "midnight"],
    )
    show_index = param.Boolean(
        default=True, doc="Whether or not to display the index of the data"
    )

    def __panel__(self):
        return pn.Column(
            IntSlider.from_param(self.param.page_size, start=5, end=25, step=5),
            self.param.theme,
            self.param.show_index,
            Tabulator.from_param(
                self.param.data,
                page_size=self.param.page_size,
                sizing_mode="stretch_width",
                theme=self.param.theme,
                show_index=self.param.show_index,
            ),
        )

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

DataExplorer(data=df).servable()

Allow References#

We can make our components much more flexible if we allow their parameters to take references. We can do this by setting allow_refs=True on the parameters.

References can be

  • Parameters

  • Widgets (.value)

  • Bound functions (pn.bind or @pn.depends)

  • Reactive Expressions (.rx)

  • Sync and async generators (yield)

Lets take a simple example where use a widget as an argument instead of a string.

import param

import panel as pn

from panel.viewable import Viewer

pn.extension()

map_iframe = """
<iframe width="100%" height="100%" src="https://maps.google.com/maps?q={country}&z=6&output=embed"
frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>
"""


class GoogleMapViewer(Viewer):
    country = param.String(allow_refs=True)

    def __init__(self, **params):
        super().__init__(**params)

        map_iframe_rx = pn.rx(map_iframe).format(country=self.param.country)
        self._layout = pn.pane.HTML(map_iframe_rx)

    def __panel__(self):
        return self._layout


country = pn.widgets.Select(options=["Germany", "Nigeria", "Thailand"], name="Country")
view = GoogleMapViewer(name="Google Map viewer", country=country)
pn.Column(country, view).servable()

If you want to learn more about references try using other types of references as input to the GoogleMapViewer.

Recap#

We have learned how to structure our components to make them easily reusable and avoid callback hell.

We should now be able to write reusable Parameterized and Viewer classes that encapsulate multiple components.

Resources#

Reference Guide#

How-To#