{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Creating interactive dashboards" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import holoviews as hv\n", "\n", "from bokeh.sampledata import stocks\n", "from holoviews.operation.timeseries import rolling, rolling_outlier_std\n", "\n", "hv.extension('bokeh')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the [Data Processing Pipelines section](./14-Data_Pipelines.ipynb) 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](./12-Responding_to_Events.ipynb) 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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def load_symbol(symbol, variable, **kwargs):\n", " df = pd.DataFrame(getattr(stocks, symbol))\n", " df['date'] = df.date.astype('datetime64[ns]')\n", " return hv.Curve(df, ('date', 'Date'), variable).opts(framewise=True)\n", "\n", "stock_symbols = ['AAPL', 'IBM', 'FB', 'GOOG', 'MSFT']\n", "variables = ['open', 'high', 'low', 'close', 'volume', 'adj_close']\n", "dmap = hv.DynamicMap(load_symbol, kdims=['Symbol','Variable'])\n", "dmap = dmap.redim.values(Symbol=stock_symbols, Variable=variables)\n", "\n", "dmap.opts(framewise=True)\n", "rolling(dmap, rolling_window=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Building dashboards\n", "\n", "For more flexibility, we can build a full-featured dashboard using the [Panel](https://panel.pyviz.org) 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.\n", "\n", "Here, let's start with defining various Panel widgets explicitly, choosing a `RadioButtonGroup` for the `symbol` instead of DynamicMaps's default `Select` widget:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "symbol = pn.widgets.RadioButtonGroup(options=stock_symbols)\n", "variable = pn.widgets.Select(options=variables)\n", "rolling_window = pn.widgets.IntSlider(name='Rolling Window', value=10, start=1, end=365)\n", "\n", "pn.Column(symbol, variable, rolling_window)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.)\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dmap = hv.DynamicMap(pn.bind(load_symbol, symbol=symbol, variable=variable))\n", "smoothed = rolling(dmap, rolling_window=rolling_window)\n", "\n", "app = pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, rolling_window), \n", " smoothed.opts(width=500, framewise=True)).servable()\n", "app" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](https://panel.holoviz.org) for the full set of widgets and layouts supported.\n", "\n", "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 .ipynb`, to serve the object marked `servable` above), or export it to a static HTML file (sampling the space of parameter values using \"embed\"):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "app.save(\"dashboard.html\", embed=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Declarative dashboards\n", "\n", "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](https://param.holoviz.org) 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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class StockExplorer(param.Parameterized):\n", "\n", " rolling_window = param.Integer(default=10, bounds=(1, 365))\n", " symbol = param.ObjectSelector(default='AAPL', objects=stock_symbols)\n", " variable = param.ObjectSelector(default='adj_close', objects=variables)\n", "\n", " @param.depends('symbol', 'variable')\n", " def load_symbol(self):\n", " df = pd.DataFrame(getattr(stocks, self.symbol))\n", " df['date'] = df.date.astype('datetime64[ns]')\n", " return hv.Curve(df, ('date', 'Date'), self.variable).opts(framewise=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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. \n", "\n", "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. \n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "explorer = StockExplorer()\n", "stock_dmap = hv.DynamicMap(explorer.load_symbol)\n", "pn.Row(explorer.param, stock_dmap)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](./14-Data_Pipelines.ipynb), 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``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Apply rolling mean\n", "smoothed = rolling(stock_dmap, rolling_window=explorer.param.rolling_window)\n", "\n", "# Find outliers\n", "outliers = rolling_outlier_std(stock_dmap, rolling_window=explorer.param.rolling_window).opts(\n", " color='red', marker='triangle')\n", "\n", "pn.Row(explorer.param, (smoothed * outliers).opts(width=600))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Replacing the output\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from holoviews.operation.datashader import datashade, dynspread\n", "\n", "class AdvancedStockExplorer(StockExplorer): \n", "\n", " datashade = param.Boolean(default=False)\n", "\n", " @param.depends('datashade')\n", " def view(self):\n", " stocks = hv.DynamicMap(self.load_symbol)\n", "\n", " # Apply rolling mean\n", " smoothed = rolling(stocks, rolling_window=self.param.rolling_window)\n", " if self.datashade:\n", " smoothed = dynspread(datashade(smoothed, aggregator='any')).opts(framewise=True)\n", "\n", " # Find outliers\n", " outliers = rolling_outlier_std(stocks, rolling_window=self.param.rolling_window).opts(\n", " width=600, color='red', marker='triangle', framewise=True)\n", " return (smoothed * outliers)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "explorer = AdvancedStockExplorer()\n", "pn.Row(explorer.param, explorer.view)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](./Deploying_Bokeh_Apps.ipynb) user guide section." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }