Spaces:
Running
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_window
width 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)
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.