Creating interactive dashboards

In [1]:
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 [2]:
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).opts(framewise=True)

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

dmap.opts(framewise=True)
Out[2]:

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 [3]:
import param
import panel as pn

variables = ['open', 'high', 'low', 'close', 'volume', 'adj_close']

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)

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 [4]:
explorer = StockExplorer()

stock_dmap = hv.DynamicMap(explorer.load_symbol)

pn.Row(pn.panel(explorer.param, parameters=['symbol', 'variable']), stock_dmap)
Out[4]:

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 simply pass that parameter into the operation. Finally we compose everything into a panel Row:

In [5]:
# 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))
Out[5]:

A function based approach

Instead of defining a whole Parameterized class we can also use the depends decorator to directly link the widgets to a DynamicMap callback function. This approach makes the link between the widgets and the computation very explicit at the cost of tying the widget and display code very closely together.

Instead of declaring the dependencies as strings we map the parameter instance to a particular keyword argument in the depends call. In this way we can link the symbol to the RadioButtonGroup value and the variable to the Select widget value:

In [6]:
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.depends(symbol=symbol.param.value, variable=variable.param.value)
def load_symbol_cb(symbol, variable):
    return load_symbol(symbol, variable)

dmap = hv.DynamicMap(load_symbol_cb)

smoothed = rolling(dmap, rolling_window=rolling_window.param.value)

pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, rolling_window), smoothed.opts(width=500, framewise=True))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/miniconda/envs/test-environment/lib/python3.7/site-packages/IPython/core/formatters.py in __call__(self, obj, include, exclude)
    968 
    969             if method is not None:
--> 970                 return method(include=include, exclude=exclude)
    971             return None
    972         else:

~/miniconda/envs/test-environment/lib/python3.7/site-packages/panel/viewable.py in _repr_mimebundle_(self, include, exclude)
    554         doc = _Document()
    555         comm = state._comm_manager.get_server_comm()
--> 556         model = self._render_model(doc, comm)
    557         ref = model.ref['id']
    558         manager = CommManager(comm_id=comm.id, plot_id=ref)

~/miniconda/envs/test-environment/lib/python3.7/site-packages/panel/viewable.py in _render_model(self, doc, comm)
    430                         save_path=config.embed_save_path,
    431                         load_path=config.embed_load_path,
--> 432                         progress=False)
    433         else:
    434             add_to_doc(model, doc)

~/miniconda/envs/test-environment/lib/python3.7/site-packages/panel/io/embed.py in embed_state(panel, model, doc, max_states, max_opts, json, json_prefix, save_path, load_path, progress, states)
    298         for wmo in w_models[1:]:
    299             attr = ws[0]._rename.get('value', 'value')
--> 300             wm.js_link(attr, wmo, attr)
    301             wmo.js_link(attr, wm, attr)
    302 

~/miniconda/envs/test-environment/lib/python3.7/site-packages/bokeh/model.py in js_link(self, attr, other, other_attr, attr_selector)
    452 
    453         if other_attr not in other.properties():
--> 454             raise ValueError("%r is not a property of other (%r)" % (other_attr, other))
    455 
    456         from bokeh.models import CustomJS

ValueError: 'active' is not a property of other (Select(id='2979', ...))
Out[6]:
Row
    [0] WidgetBox
        [0] Markdown(str)
        [1] RadioButtonGroup(options=['AAPL', 'IBM', ...], value='AAPL')
        [2] Select(options=['open', 'high', ...], value='open')
        [3] IntSlider(end=365, name='Rolling Window', start=1, value=10, value_throttled=10)
    [1] HoloViews(DynamicMap)

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
        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:

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.