Stocks hvplot

Download this notebook from GitHub (right-click to download).


In [ ]:
import panel as pn
import pandas as pd
import hvplot.pandas

from bokeh.sampledata import stocks

pn.extension()

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:

In [ ]:
title = '## Stock Explorer hvPlot'

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):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True)

Interact

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:

In [ ]:
interact = pn.interact(get_plot, ticker=tickers, window_size=(1, 21, 5))

pn.Row(
    pn.Column(title, interact[0]),
    interact[1]
)

Reactive

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:

In [ ]:
ticker = pn.widgets.Select(name='Ticker', options=tickers)
window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)

@pn.depends(ticker, window)
def get_plot(ticker, window_size):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True)

pn.Row(
    pn.Column(title, ticker, window),
    get_plot
)

Parameterized class

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.

In [ ]:
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(name='Stock Explorer hvPlot')
pn.Row(explorer.param, explorer.plot)

Callbacks

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:

In [ ]:
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),
    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.servable()

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.


Download this notebook from GitHub (right-click to download).