Creating interactive dashboards#

import pandas as pd
import holoviews as hv

from bokeh.sampledata import stocks
from holoviews.operation.timeseries import rolling, rolling_outlier_std

hv.extension('bokeh')

In the Data Processing Pipelines section we discovered how to declare a DynamicMap and control multiple processing steps with the use of custom streams as described in the Responding to Events guide. A DynamicMap works like a tiny web application, with widgets that select values along a dimension, and a plot that updates. Let’s start with a function that loads stock data and see what a DynamicMap can do:

def load_symbol(symbol, variable, **kwargs):
    df = pd.DataFrame(getattr(stocks, symbol))
    df['date'] = df.date.astype('datetime64[ns]')
    return hv.Curve(df, ('date', 'Date'), variable).opts(framewise=True)

stock_symbols = ['AAPL', 'IBM', 'FB', 'GOOG', 'MSFT']
variables = ['open', 'high', 'low', 'close', 'volume', 'adj_close']
dmap = hv.DynamicMap(load_symbol, kdims=['Symbol','Variable'])
dmap = dmap.redim.values(Symbol=stock_symbols, Variable=variables)

dmap.opts(framewise=True)
rolling(dmap, rolling_window=2)

Here we already have widgets for Symbol and Variable, as those are dimensions in the DynamicMap, but what if we wanted a widget to control the rolling_windowwidth value in the HoloViews operation? We could redefine the DynamicMap to include the operation and accept that parameter as another dimension, but in complex cases we would quickly find we need more flexibility in defining widgets and layouts than DynamicMap can give us directly.

Building dashboards#

For more flexibility, we can build a full-featured dashboard using the Panel library, which is what a DynamicMap is already using internally to generate widgets and layouts. We can easily declare our own custom Panel widgets and link them to HoloViews streams to get dynamic, user controllable analysis workflows.

Here, let’s start with defining various Panel widgets explicitly, choosing a RadioButtonGroup for the symbol instead of DynamicMaps’s default Select widget:

import panel as pn

symbol = pn.widgets.RadioButtonGroup(options=stock_symbols)
variable = pn.widgets.Select(options=variables)
rolling_window = pn.widgets.IntSlider(name='Rolling Window', value=10, start=1, end=365)

pn.Column(symbol, variable, rolling_window)

As you can see, these widgets can be displayed but they aren’t yet attached to anything, so they don’t do much. We can now use pn.bind to bind the symbol and variable widgets to the arguments of the DynamicMap callback function, and provide rolling_window to the rolling operation argument. (HoloViews operations accept Panel widgets or param Parameter values, and they will then update reactively to changes in those widgets.)

We can then lay it all out into a simple application that works similarly to the regular DynamicMap display but where we can add our additional widget and control every aspect of the widget configuration and the layout:

dmap = hv.DynamicMap(pn.bind(load_symbol, symbol=symbol, variable=variable))
smoothed = rolling(dmap, rolling_window=rolling_window)

app = pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, rolling_window), 
             smoothed.opts(width=500, framewise=True)).servable()
app

Here we chose to lay the widgets out into a box to the left of the plot, but we could put the widgets each in different locations, add different plots, etc., to create a full-featured dashboard. See panel.holoviz.org for the full set of widgets and layouts supported.

Now that we have an app, we can launch it in a separate server if we wish (using app.show()), run it as an entirely separate process (panel serve <thisfilename>.ipynb, to serve the object marked servable above), or export it to a static HTML file (sampling the space of parameter values using “embed”):

app.save("dashboard.html", embed=True)
  0%|          | 0/90 [00:00<?, ?it/s]
  1%|          | 1/90 [00:00<00:26,  3.40it/s]
  3%|▎         | 3/90 [00:00<00:13,  6.47it/s]
  4%|▍         | 4/90 [00:00<00:15,  5.71it/s]
  7%|▋         | 6/90 [00:00<00:10,  7.66it/s]
  8%|▊         | 7/90 [00:01<00:11,  7.40it/s]
 10%|█         | 9/90 [00:01<00:09,  8.77it/s]
 11%|█         | 10/90 [00:01<00:11,  6.78it/s]
 12%|█▏        | 11/90 [00:01<00:10,  7.19it/s]
 13%|█▎        | 12/90 [00:01<00:10,  7.43it/s]
 14%|█▍        | 13/90 [00:01<00:11,  6.99it/s]
 16%|█▌        | 14/90 [00:01<00:10,  7.44it/s]
 18%|█▊        | 16/90 [00:02<00:09,  7.69it/s]
 20%|██        | 18/90 [00:02<00:08,  8.63it/s]
 21%|██        | 19/90 [00:02<00:09,  7.34it/s]
 23%|██▎       | 21/90 [00:02<00:07,  9.41it/s]
 26%|██▌       | 23/90 [00:02<00:06,  9.80it/s]
 28%|██▊       | 25/90 [00:03<00:07,  8.83it/s]
 30%|███       | 27/90 [00:03<00:06,  9.67it/s]
 32%|███▏      | 29/90 [00:03<00:06,  9.44it/s]
 34%|███▍      | 31/90 [00:03<00:05, 10.07it/s]
 37%|███▋      | 33/90 [00:03<00:05, 10.85it/s]
 39%|███▉      | 35/90 [00:04<00:05, 10.02it/s]
 41%|████      | 37/90 [00:04<00:04, 10.86it/s]
 44%|████▍     | 40/90 [00:04<00:03, 14.22it/s]
 49%|████▉     | 44/90 [00:04<00:02, 18.69it/s]
 52%|█████▏    | 47/90 [00:04<00:02, 20.88it/s]
 57%|█████▋    | 51/90 [00:04<00:01, 24.87it/s]
 60%|██████    | 54/90 [00:04<00:01, 25.82it/s]
 63%|██████▎   | 57/90 [00:05<00:02, 14.80it/s]
 67%|██████▋   | 60/90 [00:05<00:02, 13.41it/s]
 69%|██████▉   | 62/90 [00:05<00:02, 11.51it/s]
 71%|███████   | 64/90 [00:06<00:02, 10.91it/s]
 73%|███████▎  | 66/90 [00:06<00:02, 11.24it/s]
 76%|███████▌  | 68/90 [00:06<00:02, 10.31it/s]
 78%|███████▊  | 70/90 [00:06<00:02,  9.48it/s]
 80%|████████  | 72/90 [00:06<00:01,  9.88it/s]
 82%|████████▏ | 74/90 [00:07<00:01,  8.09it/s]
 84%|████████▍ | 76/90 [00:07<00:01,  7.66it/s]
 86%|████████▌ | 77/90 [00:07<00:01,  7.96it/s]
 88%|████████▊ | 79/90 [00:07<00:01,  7.95it/s]
 90%|█████████ | 81/90 [00:08<00:01,  8.72it/s]
 91%|█████████ | 82/90 [00:08<00:01,  7.64it/s]
 92%|█████████▏| 83/90 [00:08<00:00,  7.94it/s]
 93%|█████████▎| 84/90 [00:08<00:00,  8.31it/s]
 94%|█████████▍| 85/90 [00:08<00:00,  7.37it/s]
 96%|█████████▌| 86/90 [00:08<00:00,  7.91it/s]
 98%|█████████▊| 88/90 [00:08<00:00,  7.80it/s]
100%|██████████| 90/90 [00:09<00:00,  8.67it/s]
                                               

Declarative dashboards#

What if we want our analysis code usable both as a dashboard and also in “headless” contexts such as batch jobs or remote execution? Both Panel and HoloViews are built on the param library, which lets you capture the definitions and allowable values for your widgets in a way that’s not attached to any GUI. That way you can declare all of your attributes and allowed values once, presenting a GUI if you want to explore them interactively or else simply provide specific values if you want batch operation.

With this approach, we declare a StockExplorer class subclassing Parameterized and defining three parameters, namely the rolling window, the symbol, and the variable to show for that symbol:

import param

class StockExplorer(param.Parameterized):

    rolling_window = param.Integer(default=10, bounds=(1, 365))
    symbol = param.ObjectSelector(default='AAPL', objects=stock_symbols)
    variable = param.ObjectSelector(default='adj_close', objects=variables)

    @param.depends('symbol', 'variable')
    def load_symbol(self):
        df = pd.DataFrame(getattr(stocks, self.symbol))
        df['date'] = df.date.astype('datetime64[ns]')
        return hv.Curve(df, ('date', 'Date'), self.variable).opts(framewise=True)

Here the StockExplorer class will look similar to the Panel code above, defining most of the same information that’s in the Panel widgets, but without any dependency on Panel or other GUI libraries; it’s simply declaring that this code accepts certain parameter values of the specified types and ranges. These declarations are useful even outside a GUI context, because they allow type and range checking for detecting user errors, but they are also sufficient for creating a GUI later.

Instead of using pn.bind to bind widget values to functions, here we are declaring that each method depends on the specified parameters, which can be expressed independently of whether there is a widget controlling those parameters; it simply declares (in a way that Panel can utilize) that the given method needs re-running when any of the parameters in that list changes.

Now let’s use the load_symbol method, which already declares which parameters it depends on, as the callback of a DynamicMap and create widgets out of those parameters to build a little GUI:

explorer = StockExplorer()
stock_dmap = hv.DynamicMap(explorer.load_symbol)
pn.Row(explorer.param, stock_dmap)

Here you’ll notice that the rolling_window widget doesn’t do anything, because it’s not connected to anything (e.g., nothing @param.depends on it). As we saw in the Data Processing Pipelines section, the rolling and rolling_outlier_std operations both accept a rolling_window parameter, so lets provide that to the operations and display the output of those operations. Finally we compose everything into a panel Row:

# Apply rolling mean
smoothed = rolling(stock_dmap, rolling_window=explorer.param.rolling_window)

# Find outliers
outliers = rolling_outlier_std(stock_dmap, rolling_window=explorer.param.rolling_window).opts(
    color='red', marker='triangle')

pn.Row(explorer.param, (smoothed * outliers).opts(width=600))

Replacing the output#

Updating plots using a DynamicMap is a very efficient means of updating a plot since it will only update the data that has changed. In some cases it is either necessary or more convenient to redraw a plot entirely. Panel makes this easy by annotating a method with any dependencies that should trigger the plot to be redrawn. In the example below we extend the StockExplorer by adding a datashade boolean and a view method which will flip between a datashaded and regular view of the plot:

from holoviews.operation.datashader import datashade, dynspread

class AdvancedStockExplorer(StockExplorer):    

    datashade = param.Boolean(default=False)

    @param.depends('datashade')
    def view(self):
        stocks = hv.DynamicMap(self.load_symbol)

        # Apply rolling mean
        smoothed = rolling(stocks, rolling_window=self.param.rolling_window)
        if self.datashade:
            smoothed = dynspread(datashade(smoothed, aggregator='any')).opts(framewise=True)

        # Find outliers
        outliers = rolling_outlier_std(stocks, rolling_window=self.param.rolling_window).opts(
            width=600, color='red', marker='triangle', framewise=True)
        return (smoothed * outliers)

In the previous example we explicitly called the view method, but to allow panel to update the plot when the datashade parameter is toggled we instead pass it the actual view method. Whenever the datashade parameter is toggled panel will call the method and update the plot with whatever is returned:

explorer = AdvancedStockExplorer()
pn.Row(explorer.param, explorer.view)

As you can see using streams we have bound the widgets to the streams letting us easily control the stream values and making it trivial to define complex dashboards. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the Deploying Bokeh Apps user guide section.

This web page was generated from a Jupyter notebook and not all interactivity will work on this website. Right click to download and run locally for full Python-backed interactivity.