Interactivity#
In the previous sections, we delved into Param, which not only forms the core architecture of Panel but also serves as the foundation for adding interactivity to your applications. This section explores how to leverage Parameters and their dependencies to incorporate interactivity. We’ll focus on implementing interactivity through reactivity, departing from the more imperative style of programming commonly found in other UI frameworks.
By the end of this tutorial, you’ll learn:
How to utilize both declarative and imperative APIs for interactivity
How to develop both functional and class-based interactive apps
Imperative vs Declarative Programming#
To create an interactive component in Panel, we have two approaches: defining callbacks for explicit actions or declaring reactive functions, methods, or expressions that automatically manage state changes.
Tip
For users of all skill levels, we recommend employing the Declarative Programming approach as it enhances code maintainability and efficiency.
Let’s explore these approaches by building a simple app that enables us to select a subset of columns to display in a table.
Imperative#
We begin by loading our data and defining a widget for interacting with it:
import panel as pn
import pandas as pd
pn.extension("tabulator")
data_url = 'https://assets.holoviz.org/panel/tutorials/turbines.csv.gz'
turbines = pn.cache(pd.read_csv)(data_url)
cols = pn.widgets.MultiChoice(
options=turbines.columns.to_list(), value=['p_name', 't_state', 't_county', 'p_year', 't_manu', 'p_cap'],
width=500, height=100, name='Columns'
)
In the imperative approach, we use .param.watch
to set up a callback that updates the data when the widget changes:
table = pn.widgets.Tabulator(turbines[cols.value], page_size=5, pagination="remote")
def update_data(event):
table.value = turbines[event.new]
cols.param.watch(update_data, 'value')
pn.Column(cols, table).servable()
Declarative#
The declarative and reactive approach involves declaring what we want to display, leaving Panel to handle the mechanics of updating the table:
dfrx = pn.rx(turbines)[cols]
pn.Column(cols, pn.widgets.Tabulator(dfrx, page_size=5, pagination="remote")).servable()
Note how we pass the reactive DataFrame dfrx
to the Tabulator
widget. This aligns with the concept of passing references, which Param and Panel resolve. Valid references include:
param.Parameter()
param.rx(...)
pn.bind(...)
/@pn.depends
pn.widgets.Widget()
Exercise: Add more Widgets#
Enhance the app by adding widgets to filter the data by year (p_year
) and capacity (p_cap
):
Hint
You can filter a reactive DataFrame in the same way as a regular DataFrame.
Solution: Declarative (Recommended)
import pandas as pd
import panel as pn
pn.extension("tabulator")
data_url = "https://assets.holoviz.org/panel/tutorials/turbines.csv.gz"
turbines = pn.cache(pd.read_csv)(data_url)
cols = pn.widgets.MultiChoice(
options=turbines.columns.to_list(),
value=["p_name", "t_state", "t_county", "p_year", "t_manu", "p_cap"],
width=500,
height=100,
name="Columns",
)
p_year_options = sorted(int(year) for year in turbines.p_year.unique() if not pd.isna(year))
p_year = pn.widgets.Select(value=max(p_year_options), options=p_year_options, name="Year")
p_cap_bounds = (turbines.p_cap.min(), turbines.p_cap.max())
p_cap = pn.widgets.RangeSlider(value=p_cap_bounds, start=p_cap_bounds[0], end=p_cap_bounds[1])
dfrx = pn.rx(turbines)
dfrx = dfrx[
(dfrx.p_year == p_year)
& (dfrx.p_cap.between(p_cap.param.value_start, p_cap.param.value_end))
][cols]
pn.Column(
cols, p_year, p_cap, pn.widgets.Tabulator(dfrx, pagination="remote", page_size=5)
).servable()
Solution: Imperative (Not recommended)
import pandas as pd
import panel as pn
pn.extension("tabulator")
data_url = "https://assets.holoviz.org/panel/tutorials/turbines.csv.gz"
turbines = pn.cache(pd.read_csv)(data_url)
cols = pn.widgets.MultiChoice(
options=turbines.columns.to_list(),
value=["p_name", "t_state", "t_county", "p_year", "t_manu", "p_cap"],
width=500,
height=100,
name="Columns",
)
p_year_options = sorted(
int(year) for year in turbines.p_year.unique() if not pd.isna(year)
)
p_year = pn.widgets.Select(
value=max(p_year_options), options=p_year_options, name="Year"
)
p_cap_bounds = (turbines.p_cap.min(), turbines.p_cap.max())
p_cap = pn.widgets.RangeSlider(
value=p_cap_bounds, start=p_cap_bounds[0], end=p_cap_bounds[1], name="Capacity"
)
table = pn.widgets.Tabulator(turbines[cols.value], page_size=5, pagination="remote")
def update_data(event):
value = turbines[
(turbines.p_year == p_year.value)
& (turbines.p_cap.between(p_cap.value_start, p_cap.value_end))
][cols.value]
table.value = value
cols.param.watch(update_data, "value")
p_year.param.watch(update_data, "value")
p_cap.param.watch(update_data, "value")
pn.Column(cols, p_year, p_cap, table).servable()
Function vs. Class-based#
Reactive functions and expressions based on pn.rx
or pn.bind
provide an excellent entry point for writing dynamic UIs. However, when we need to track state or have many consumers of the output, it can be challenging to manage. This is where Parameterized
classes come in handy.
If you recall the Reactive Parameters Section, a Parameterized
class enables you to encapsulate state as parameters, which can then be passed around to set up interactivity.
Tip
The class-based approach is recommended for larger, more complex applications.
Making the Class-Based Approach Efficient#
Let’s revisit our DataExplorer
class from the previous lesson and see how we can structure a filtering application like before:
import pandas as pd
import panel as pn
import param
from panel.viewable import Viewer
pn.extension("tabulator")
data_url = "https://assets.holoviz.org/panel/tutorials/turbines.csv.gz"
turbines = pn.cache(pd.read_csv)(data_url)
class DataExplorer(Viewer):
data = param.DataFrame(doc="Stores a DataFrame to explore")
columns = param.ListSelector(
default=["p_name", "t_state", "t_county", "p_year", "t_manu", "p_cap"]
)
year = param.Range(default=(1981, 2022), bounds=(1981, 2022))
capacity = param.Range(default=(0, 1100), bounds=(0, 1100))
def __init__(self, **params):
super().__init__(**params)
self.param.columns.objects = self.data.columns.to_list()
@param.depends("data", "columns", "year", "capacity")
def filtered_data(self):
df = self.data
return df[df.p_year.between(*self.year) & df.p_cap.between(*self.capacity)][
self.columns
]
@param.depends('filtered_data')
def number_of_rows(self):
return f"Rows: {len(self.filtered_data())}"
def __panel__(self):
return pn.Column(
pn.Row(
pn.widgets.MultiChoice.from_param(self.param.columns, width=400),
pn.Column(self.param.year, self.param.capacity),
),
self.number_of_rows,
pn.widgets.Tabulator(self.filtered_data, page_size=10, pagination="remote"),
)
DataExplorer(data=turbines).servable()
As you can see, param.depends
allows us to set up a method that depends on specific parameters of the class (much like pn.bind
) and then use that method as a proxy for the filtered data. If you observe the execution flow, you’ll notice that the filtered_data
method is executed twice whenever one of data
, columns
, year
, or capacity
is changed. You can avoid this inefficiency by using pn.cache
.
An alternative and slightly more efficient approach would be to create a parameter to store the filtered data and update it every time one of the dependencies changes.
import pandas as pd
import panel as pn
import param
from panel.viewable import Viewer
pn.extension("tabulator")
data_url = "https://assets.holoviz.org/panel/tutorials/turbines.csv.gz"
turbines = pn.cache(pd.read_csv)(data_url)
class DataExplorer(Viewer):
data = param.DataFrame(doc="Stores a DataFrame to explore")
columns = param.ListSelector(
default=["p_name", "t_state", "t_county", "p_year", "t_manu", "p_cap"]
)
year = param.Range(default=(1981, 2022), bounds=(1981, 2022))
capacity = param.Range(default=(0, 1100), bounds=(0, 1100))
filtered_data = param.DataFrame(doc="Stores the filtered DataFrame")
def __init__(self, **params):
super().__init__(**params)
self.param.columns.objects = self.data.columns.to_list()
@param.depends("data", "columns", "year", "capacity", watch=True, on_init=True)
def _update_filtered_data(self):
df = self.data
self.filtered_data=df[df.p_year.between(*self.year) & df.p_cap.between(*self.capacity)][
self.columns
]
@param.depends('filtered_data')
def number_of_rows(self):
return f"Rows: {len(self.filtered_data)}"
def __panel__(self):
return pn.Column(
pn.Row(
pn.widgets.MultiChoice.from_param(self.param.columns, width=400),
pn.Column(self.param.year, self.param.capacity),
),
self.number_of_rows,
pn.widgets.Tabulator(self.param.filtered_data, page_size=10, pagination="remote"),
)
DataExplorer(data=turbines).servable()
Storing the filtered_data
has the benefit that multiple consumers can now access it without recalculating it.
We can also combine the class-based approach with pn.rx
to achieve an efficient solution:
import pandas as pd
import panel as pn
import param
from panel.viewable import Viewer
pn.extension("tabulator")
data_url = "https://assets.holoviz.org/panel/tutorials/turbines.csv.gz"
turbines = pn.cache(pd.read_csv)(data_url)
class DataExplorer(Viewer):
data = param.DataFrame(doc="Stores a DataFrame to explore")
columns = param.ListSelector(
default=["p_name", "t_state", "t_county", "p_year", "t_manu", "p_cap"]
)
year = param.Range(default=(1981, 2022), bounds=(1981, 2022))
capacity = param.Range(default=(0, 1100), bounds=(0, 1100))
filtered_data = param.Parameter()
number_of_rows = param.Parameter()
def __init__(self, **params):
super().__init__(**params)
self.param.columns.objects = self.data.columns.to_list()
dfrx = self.param.data.rx()
p_year_min = self.param.year.rx().rx.pipe(lambda x: x[0])
p_year_max = self.param.year.rx().rx.pipe(lambda x: x[1])
p_cap_min = self.param.capacity.rx().rx.pipe(lambda x: x[0])
p_cap_max = self.param.capacity.rx().rx.pipe(lambda x: x[1])
self.filtered_data = dfrx[
dfrx.p_year.between(p_year_min, p_year_max)
& dfrx.p_cap.between(p_cap_min, p_cap_max)
][self.param.columns]
self.number_of_rows = pn.rx("Rows: {len_df}").format(len_df=pn.rx(len)(dfrx))
def __panel__(self):
return pn.Column(
pn.Row(
pn.widgets.MultiChoice.from_param(self.param.columns, width=400),
pn.Column(self.param.year, self.param.capacity),
),
self.number_of_rows,
pn.widgets.Tabulator(self.filtered_data, page_size=10, pagination="remote"),
)
DataExplorer(data=turbines).servable()
Tip
If your dependent function (
@pn.depends
) will only be executed once per update, the first approach is recommended as it’s simple to implement and reason about.If not, we recommend using the second approach (
@pn.depends
withwatch=True
) or the third approach (pn.rx
) because it’s much easier to implement efficiently.
Exercise: Add a Plot#
Write an app that allows filtering the DataFrame and displays both a table and a plot, caching the data on an intermediate parameter. You can use any plotting library you want.
Hint
import hvplot.pandas
plot = turbines.hvplot.hist("p_cap", height=400)
Solution
import hvplot.pandas
import pandas as pd
import panel as pn
import param
from panel.viewable import Viewer
pn.extension("tabulator")
data_url = "https://assets.holoviz.org/panel/tutorials/turbines.csv.gz"
turbines = pn.cache(pd.read_csv)(data_url)
class DataExplorer(Viewer):
data = param.DataFrame(doc="Stores a DataFrame to explore")
columns = param.ListSelector(
default=["p_name", "t_state", "t_county", "p_year", "t_manu", "p_cap"]
)
year = param.Range(default=(1981, 2022), bounds=(1981, 2022))
capacity = param.Range(default=(0, 1100), bounds=(0, 1100))
filtered_data = param.DataFrame(doc="Stores the filtered DataFrame")
def __init__(self, **params):
super().__init__(**params)
self.param.columns.objects = self.data.columns.to_list()
@param.depends("data", "year", "capacity", watch=True, on_init=True)
def _update_filtered_data(self):
df = self.data
self.filtered_data = df[
df.p_year.between(*self.year) & df.p_cap.between(*self.capacity)
]
@param.depends("filtered_data", "columns")
def table(self):
return self.filtered_data[self.columns]
@param.depends("filtered_data")
def plot(self):
return self.filtered_data.hvplot.hist("p_cap", height=400)
def __panel__(self):
return pn.Column(
pn.Row(
pn.widgets.MultiChoice.from_param(self.param.columns, width=400),
pn.Column(self.param.year, self.param.capacity),
),
pn.widgets.Tabulator(self.table, page_size=10, pagination="remote"),
pn.pane.HoloViews(self.plot),
)
DataExplorer(data=turbines).servable()
Recap#
In this tutorial, we explored how to build an efficient filtering application using Panel. The focus was on optimizing the class-based approach to handle filtering operations on a DataFrame efficiently.
Key Concepts Covered#
Class-Based Approach:
We started by revisiting a class-based approach for building interactive apps in Panel. This involved creating a
DataExplorer
class to handle filtering operations on a DataFrame.
Parameter Dependencies:
Utilizing
param.depends
, we established dependencies between different parameters and methods within theDataExplorer
class. This allowed us to trigger updates to specific methods whenever the parameters changed.
Imperative vs Declarative Programming:
We discussed two programming paradigms for building interactive components in Panel:
Imperative: Defining explicit callbacks to perform actions based on widget changes.
Declarative: Using reactive functions or expressions to automatically manage state updates based on input changes. We recommended the declarative approach for its maintainability and efficiency.
Efficiency Considerations:
We explored the importance of efficiency when dealing with large datasets or complex filtering operations. Inefficient code can lead to unnecessary recalculations and decreased performance.
Improving Efficiency:
To enhance efficiency, we explored three approaches:
Using
pn.cache
to cache the filtered data.Storing the filtered data as a parameter and updating it when dependencies change.
Combining the class-based approach with
pn.rx
for reactive programming.
Key Takeaways#
Imperative vs Declarative Programming: Understanding the difference between imperative and declarative programming paradigms helps in choosing the most suitable approach for building interactive components in Panel.
Parameter Dependencies: Establishing dependencies between parameters and methods using
param.depends
is fundamental for reactive updates in Panel apps.Efficiency is Crucial: When building interactive applications, especially with large datasets, prioritizing efficiency is essential to ensure optimal performance.
Caching and Storing Data: Techniques like caching filtered data and storing it as a parameter can significantly improve efficiency by minimizing redundant computations.
Flexibility with Panel: Panel provides flexibility in designing interactive web apps, allowing developers to integrate various visualization components seamlessly.
By applying these concepts and techniques, developers can create efficient and responsive filtering applications using Panel, tailored to specific data exploration needs.