import panel as pn
import pandas as pd
from matplotlib.figure import Figure
# not needed for mpl >= 3.1
from matplotlib.backends.backend_agg import FigureCanvas
from bokeh.sampledata import stocks
pn.extension(sizing_mode="stretch_width")
This example is meant to make it easy to compare and contrast the different APIs Panel provides to declare apps and dashboards. Specifically, it compares four different implementations of the same app using 1) the quick and easy interact
function, 2) more flexible reactive functions, 3) declarative Param-based code, and 4) explicit callbacks.
Before comparing the different approaches, we will first declare some components of the app that will be shared, including the title of the app, a set of stock tickers, a function to return a dataframe given the stock ticker
and the rolling mean window_size
, and another function to return a plot given those same inputs:
title = '## Stock Explorer Matplotlib'
tickers = ['AAPL', 'FB', 'GOOG', 'IBM', 'MSFT']
def get_df(ticker, window_size):
df = pd.DataFrame(getattr(stocks, ticker))
df['date'] = pd.to_datetime(df.date)
return df.set_index('date').rolling(window=window_size).mean().reset_index()
def get_plot(ticker, window_size):
fig = Figure(figsize=(10, 6))
ax = fig.subplots()
FigureCanvas(fig) # not needed for mpl >= 3.1
df = get_df(ticker, window_size)
df.plot.line('date', 'close', ax=ax)
return fig
In the interact
model the widgets are automatically generated from the arguments to the function or by providing additional hints to the interact
call. This is a very convenient way to generate a simple app, particularly when first exploring some data. However, because widgets are created implicitly based on introspecting the code, it is difficult to see how to modify the behavior. Also, to compose the different components in a custom way it is necessary to unpack the layout returned by the interact
call, as we do here:
interact = pn.interact(get_plot, ticker=tickers, window_size=(1, 21, 5))
pn.Row(
pn.Column(title, interact[0], sizing_mode="fixed", width=300),
interact[1]
)
The reactive programming model is similar to the interact
function but relies on the user (a) explicitly instantiating widgets, (b) declaring how those widgets relate to the function arguments (using the depends
decorator), and (c) laying out the widgets and other components explicitly. In principle we could reuse the get_plot
function from above here and wrap it in the decorator but to be clearer we will repeat it:
ticker = pn.widgets.Select(name='Ticker', options=tickers)
window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)
@pn.depends(ticker.param.value, window.param.value)
def get_plot(ticker, window_size):
fig = Figure(figsize=(10, 6))
ax = fig.subplots()
FigureCanvas(fig) # not needed for mpl >= 3.1
df = get_df(ticker, window_size)
df.plot.line('date', 'close', ax=ax)
return fig
pn.Row(
pn.Column(title, ticker, window, sizing_mode="fixed", width=300),
get_plot
)
Another approach expresses the app entirely as a single Parameterized
class with parameters to declare the inputs, rather than explicit widgets. The parameters are independent of any GUI code, which can be important for maintaining large codebases, with parameters and functionality defined separately from any GUI or panel code. Once again the depends
decorator is used to express the dependencies, but in this case the dependencies are expressed as strings referencing class parameters, not parameters of widgets. The parameters and the plot
method can then be laid out independently, with Panel used only for this very last step.
import param
class StockExplorer(param.Parameterized):
ticker = param.Selector(default='AAPL', objects=tickers)
window_size = param.Integer(default=6, bounds=(1, 21))
@param.depends('ticker', 'window_size')
def plot(self):
return get_plot(self.ticker, self.window_size)
explorer = StockExplorer()
pn.Row(pn.Column(explorer.param, sizing_mode="fixed", width=300), explorer.plot)
The above approaches are all reactive in some way, triggering actions whenever manipulating a widget causes a parameter to change, without users writing code to trigger callbacks explicitly. Explicit callbacks allow complete low-level control of precisely how the different components of the app are updated, but they can quickly become unmaintainable because the complexity increases dramatically as more callbacks are added. The approach works by defining callbacks using the .param.watch
API that either update or replace the already rendered components when a watched parameter changes:
ticker = pn.widgets.Select(name='Ticker', options=['AAPL', 'FB', 'GOOG', 'IBM', 'MSFT'])
window = pn.widgets.IntSlider(name='Window', value=6, start=1, end=21)
row = pn.Row(
pn.Column(title, ticker, window, sizing_mode="fixed", width=300),
get_plot(ticker.options[0], window.value),
)
def update(event):
row[1].object = get_plot(ticker.value, window.value)
ticker.param.watch(update, 'value')
window.param.watch(update, 'value')
row
In practice, different projects will be suited to one or the other of these APIs, and most of Panel's functionality should be available from any API.
Lets wrap it into nice template that can be served via panel serve stocks_matplotlib.ipynb
description="""This example compares **four different implementations of the same app** using
- the quick and easy ``interact`` function,
- more flexible *reactive* functions,
- declarative *Param-based* code, and
- explicit *callbacks*."""
pn.template.FastListTemplate(site="Panel", title="Matplotlib Stock Explorer", sidebar=[ticker, window], main=[description, row[1]]).servable();