Interactive#

import ipywidgets as ipw
import hvplot.xarray # noqa
import hvplot.pandas # noqa
import panel as pn
import pandas as pd
import panel.widgets as pnw
import xarray as xr

Interactive command-line or notebook interfaces are incredibly powerful tools for quickly doing exploratory analysis, letting you supply arguments to Python methods and functions and see the results immediately. However, this process of exploration can be slow and awkward for large parameter spaces because it requires manually typing each argument value. To further ease exploratory workflows, hvPlot ships with a convenient .interactive API, which mirrors the regular API of your favorite data analysis libraries like Pandas, Dask, and xarray but makes it possible to pass in widgets for each argument value, not just a constant. When the widgets are used, the output will dynamically update the full pipeline of method calls so that it works just as if that particular value had been specified in the call being wrapped.

In this user guide we will explore how to use the .interactive API on xarray and pandas objects:

ds = xr.tutorial.load_dataset('air_temperature')
ds
<xarray.Dataset>
Dimensions:  (lat: 25, time: 2920, lon: 53)
Coordinates:
  * lat      (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0
  * lon      (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0
  * time     (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00
Data variables:
    air      (time, lat, lon) float32 241.2 242.5 243.5 ... 296.5 296.2 295.7
Attributes:
    Conventions:  COARDS
    title:        4x daily NMC reanalysis (1948)
    description:  Data is from NMC initialized reanalysis\n(4x/day).  These a...
    platform:     Model
    references:   http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanaly...
from bokeh.sampledata.stocks import IBM

df = pd.DataFrame(IBM)
df['date'] = pd.to_datetime(df.date)

Interactive widgets#

We can supply both regular values, widgets and parameters as arguments to methods on the .interactive accessor. Here, we’ll use widgets from the Panel library. The repr of the resulting object will contain a layout of the widget and a view of the resulting output:

slider = pnw.IntSlider(name='time', start=0, end=10)

ds.air.interactive(width=800).isel(time=slider)

You can also use widgets from the ipywidgets library:

slider = ipw.IntSlider(description='time', min=0, max=10)

ds.air.interactive(width=800).isel(time=slider)

Note that this works just as well for DataFrame objects whether they are Pandas, Dask or cuDF dataframes:

nrows = pn.widgets.IntSlider(start=1, end=100, value=10)

df.interactive(width=500).head(nrows)

For Panel widgets, we can let .interactive automatically configure the widget, which is particularly convenient when working with DiscreteSlider widgets:

ds.air.interactive(width=800).sel(time=pnw.DiscreteSlider)

Functions as inputs#

In some cases your starting point for your interactive pipeline may not simply be a DataFrame or xarray Dataset but a function that fetches some data or applies some initial processing on your data. In such a case you can use the hvplot.bind function to bind static AND dynamic arguments to your function. Binding dynamic arguments such as a widget or parameter means that whenever the widget/parameter value changes the output of the function will change as well. This makes it possible to construct functions as the input to your interactive pipeline that themselves represent some data pipeline.

In the example below we will explicitly declare a Select widget to select between multiple stock tickers and a function that loads dataframes containing data for each of those stocks. Using the hvplot.bind function we then bind the ticker select widget to the ticker argument of the stock_df function and call .interactive on the resulting bound function:

from bokeh import sampledata

ticker = pn.widgets.Select(options=['AAPL', 'IBM', 'GOOG', 'MSFT'], name='Ticker')

def stock_df(ticker):
    df = pd.DataFrame(getattr(sampledata.stocks, ticker))
    df['date'] = pd.to_datetime(df.date)
    return df

stock_dfi = hvplot.bind(stock_df, ticker).interactive(width=600)

stock_dfi.head(10)

As you can see this interactive component behaves just like any other, allowing us to chain .head on it and updating when the ticker widget changes.

Just like any other interactive component you may also chain it further:

ticker = pn.widgets.Select(options=['AAPL', 'IBM', 'GOOG', 'MSFT'], name='Ticker')

def stock_df(ticker):
    df = pd.DataFrame(getattr(sampledata.stocks, ticker))
    df['date'] = pd.to_datetime(df.date)
    return df

stock_dfi = hvplot.bind(stock_df, ticker).interactive()

dt_range = pn.widgets.DateRangeSlider(start=df.date.iloc[-1000], end=df.date.max(), value=(df.date.iloc[-100], df.date.max()))

stock_dfi[(stock_dfi.date>=dt_range.param.value_start) & (stock_dfi.date<=dt_range.param.value_end)].hvplot(kind='ohlc', grid=True, title=ticker)

Docstrings#

When accessing a method on the .interactive accessor it will transparently mirror the docstring of the equivalent method in the underlying library being wrapped:

print(ds.air.interactive.isel.__doc__)
Return a new DataArray whose data is given by selecting indexes
        along the specified dimension(s).

        Parameters
        ----------
        indexers : dict, optional
            A dict with keys matching dimensions and values given
            by integers, slice objects or arrays.
            indexer can be a integer, slice, array-like or DataArray.
            If DataArrays are passed as indexers, xarray-style indexing will be
            carried out. See :ref:`indexing` for the details.
            One of indexers or indexers_kwargs must be provided.
        drop : bool, default: False
            If ``drop=True``, drop coordinates variables indexed by integers
            instead of making them scalar.
        missing_dims : {"raise", "warn", "ignore"}, default: "raise"
            What to do if dimensions that should be selected from are not present in the
            DataArray:
            - "raise": raise an exception
            - "warn": raise a warning, and ignore the missing dimensions
            - "ignore": ignore the missing dimensions
        **indexers_kwargs : {dim: indexer, ...}, optional
            The keyword arguments form of ``indexers``.

        Returns
        -------
        indexed : xarray.DataArray

        See Also
        --------
        Dataset.isel
        DataArray.sel

        :doc:`xarray-tutorial:intermediate/indexing/indexing`
            Tutorial material on indexing with Xarray objects

        :doc:`xarray-tutorial:fundamentals/02.1_indexing_Basic`
            Tutorial material on basics of indexing

        Examples
        --------
        >>> da = xr.DataArray(np.arange(25).reshape(5, 5), dims=("x", "y"))
        >>> da
        <xarray.DataArray (x: 5, y: 5)>
        array([[ 0,  1,  2,  3,  4],
               [ 5,  6,  7,  8,  9],
               [10, 11, 12, 13, 14],
               [15, 16, 17, 18, 19],
               [20, 21, 22, 23, 24]])
        Dimensions without coordinates: x, y

        >>> tgt_x = xr.DataArray(np.arange(0, 5), dims="points")
        >>> tgt_y = xr.DataArray(np.arange(0, 5), dims="points")
        >>> da = da.isel(x=tgt_x, y=tgt_y)
        >>> da
        <xarray.DataArray (points: 5)>
        array([ 0,  6, 12, 18, 24])
        Dimensions without coordinates: points
        

Plotting#

One of the most useful aspects of the .interactive API is to feed the output of chained method calls into a plot.

Matplotlib#

The output can be almost anything, such as the HTML repr (above) or a matplotlib plot:

ds.air.interactive.sel(time=pnw.DiscreteSlider).plot()

If we like, we can animate the output with a Player widget, and customize the location of the widget using the loc keyword argument to .interactive:

time = pnw.Player(name='time', start=0, end=10, loop_policy='loop', interval=100)

ds.air.interactive(loc='bottom').isel(time=time).plot()

hvPlot#

We can also make use of the .hvplot method to get fully interactive Bokeh-based plots:

slider = pnw.FloatSlider(name='quantile', start=0, end=1)

ds.air.interactive.quantile(slider, dim='time').hvplot(data_aspect=1)

You can chain any number of methods, with as many widgets controlling steps in this pipeline as you wish:

q = pnw.FloatSlider(name='quantile', start=0, end=1)

(ds.air.interactive(loc='left')
 .sel(time=pnw.DiscreteSlider)
 .quantile(q=q, dim='lon')
 .hvplot(aspect=1))

We can also use a RangeSlider to select a slice and compute the mean over that range instead of selecting a specific time:

range_slider = pnw.IntRangeSlider

(ds.air.interactive
 .isel(time=range_slider)
 .mean('time')
 .hvplot())

.interactive supports arbitrary chains of method calls, including anything that is supported by your data object. For instance, you can even convert your xarray object into a dataframe using .to_dataframe, then call pandas methods:

ds.air.interactive.sel(lat=pnw.DiscreteSlider).to_dataframe().groupby('time').mean().hvplot('time', 'air')

Operators#

You can further transform your output, if desired, by applying math operators on the interactive object:

slider = pnw.IntSlider(name='time', start=0, end=10)
baseline = ds.air.mean().item()
baseline
281.255126953125
ds.air.interactive(width=800).isel(time=slider).mean().item() - baseline

You can even do math with a widget:

slider = pnw.IntSlider(name='time',   start=0, end=10)
offset = pnw.IntSlider(name='offset', start=0, end=500)

ds.air.interactive.isel(time=slider).mean().item() + offset

Math operators work with array data as well, such as the time-averaged value of each array value:

diff = ds.air.interactive.sel(time=pnw.DiscreteSlider) - ds.air.mean('time')
kind = pnw.Select(options=['contour', 'contourf', 'image'])

diff.hvplot(cmap='RdBu_r', clim=(-20, 20), kind=kind)