Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
holoviz
GitHub Repository: holoviz/panel
Path: blob/main/doc/tutorials/intermediate/reusable_components.md
2012 views

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.

:::{dropdown} 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) )

:::

:::{dropdown} 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

:::{dropdown} 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