{ "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. Here we will use the same example exploring a dataset of stock timeseries and build a small dashboard using the [Panel](https://panel.pyviz.org) 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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def load_symbol(symbol, variable='adj_close', **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", "dmap = hv.DynamicMap(load_symbol, kdims='Symbol').redim.values(Symbol=stock_symbols)\n", "\n", "dmap.opts(framewise=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Building dashboards\n", "\n", "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.\n", "\n", "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``.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import param\n", "import panel as pn\n", "\n", "variables = ['open', 'high', 'low', 'close', 'volume', 'adj_close']\n", "\n", "class StockExplorer(param.Parameterized):\n", "\n", " rolling_window = param.Integer(default=10, bounds=(1, 365))\n", " \n", " symbol = param.ObjectSelector(default='AAPL', objects=stock_symbols)\n", " \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": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "explorer = StockExplorer()\n", "\n", "stock_dmap = hv.DynamicMap(explorer.load_symbol)\n", "\n", "pn.Row(pn.panel(explorer.param, parameters=['symbol', 'variable']), stock_dmap)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The ``rolling_window`` parameter is not yet connected to anything however, so just like in the [Data Processing Pipelines section](./14-Data_Pipelines.ipynb) 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``:" ] }, { "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": [ "## A function based approach\n", "\n", "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "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.depends(symbol=symbol.param.value, variable=variable.param.value)\n", "def load_symbol_cb(symbol, variable):\n", " return load_symbol(symbol, variable)\n", "\n", "dmap = hv.DynamicMap(load_symbol_cb)\n", "\n", "smoothed = rolling(dmap, rolling_window=rolling_window.param.value)\n", "\n", "pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, rolling_window), smoothed.opts(width=500, framewise=True))" ] }, { "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)." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }