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.
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
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
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”):
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_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
# 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))