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. 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:
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)
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
.
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:
explorer = StockExplorer()
stock_dmap = hv.DynamicMap(explorer.load_symbol)
pn.Row(pn.panel(explorer.param, parameters=['symbol', 'variable']), 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 simply pass that parameter into the operation. 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))
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:
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))
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.