Creating interactive dashboards


In [ ]:
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. Here we will use the same example exploring a dataset of stock timeseries and build a small dashboard using the Panel library, which allows us to declare easily declare custom widgets and link them to our streams. We will begin by once again declaring our function that loads the stock data:


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

stock_symbols = ['AAPL', 'IBM', 'FB', 'GOOG', 'MSFT']
dmap = hv.DynamicMap(load_symbol, kdims='Symbol').redim.values(Symbol=stock_symbols)

dmap.opts(framewise=True)

Building dashboards

Controlling stream events manually from the Python prompt can be a bit cumbersome. However since you can now trigger events from Python we can easily bind any Python based widget framework to the stream. HoloViews itself is based on param and param has various UI toolkits that accompany it and allow you to quickly generate a set of widgets. Here we will use panel, which is based on bokeh to control our stream values.

To do so we will declare a StockExplorer class subclassing Parameterized and defines two parameters, the rolling_window as an integer and the symbol as an ObjectSelector. Additionally we define a view method, which defines the DynamicMap and applies the two operations we have already played with, returning an overlay of the smoothed Curve and outlier Scatter.


In [ ]:
import param
import panel as pn
from holoviews.streams import Params

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=[
        'date', 'open', 'high', 'low', 'close', 'volume', 'adj_close'])

    @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)

You will have noticed the param.depends decorator on the load_symbol method above, this declares that the method depends on these two parameters. When we pass the method to a DynamicMap it will now automatically listen to changes to the 'symbol', and 'variable' parameters. To generate a set of widgets to control these parameters we can simply supply the explorer.param accessor to a panel layout, and combining the two we can quickly build a little GUI:


In [ ]:
explorer = StockExplorer()

stock_dmap = hv.DynamicMap(explorer.load_symbol)

pn.Row(explorer.param, stock_dmap)

The rolling_window parameter is not yet connected to anything however, so just like in the Data Processing Pipelines section we will see how we can get the widget to control the parameters of an operation. Both the rolling and rolling_outlier_std operations accept a rolling_window parameter, so we create a Params stream to listen to that parameter and then pass it to the operations. Finally we compose everything into a panel Row:


In [ ]:
# Apply rolling mean
window = Params(explorer, ['rolling_window'])
smoothed = rolling(stock_dmap, streams=[window])

# Find outliers
outliers = rolling_outlier_std(stock_dmap, streams=[window]).opts(
    hv.opts.Scatter(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:


In [ ]:
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
        window = Params(self, ['rolling_window'])
        smoothed = rolling(stocks, streams=[window])
        if self.datashade:
            smoothed = dynspread(datashade(smoothed)).opts(framewise=True)

        # Find outliers
        outliers = rolling_outlier_std(stocks, streams=[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:


In [ ]:
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.