Search code examples
pythonbokehholoviewspanel-pyviz

Interaction between panel and holoviews


I am working on a little widget with holoviews and panel - it consists of reading a pandas.dataFrame and display a curve for each column. The interaction I need is to be able to add/remove columns from the plot. In my real use case, there are too many columns so I can’t take advantage of the interactive legend already provided by bokeh+holoviews.

I made a little example that ‘’’ kind of works ‘’’ but I am probably doing it wrong, as I am reloading the data for the plot every time there is an interaction with the panel.widgets.MultiChoice (which is obviously wrong)


import holoviews as hv
import numpy as np
import pandas as pd
import colorcet as cc
import panel as pn

pn.extension()
hv.extension("bokeh")

# generate some data
def get_data():
    data = {
        "1998": np.random.rand(365),
        "1999": np.random.rand(365),
        "2000": np.random.rand(365),
        "2002": np.random.rand(365),
        "2003": np.random.rand(365),
    }
    df = pd.DataFrame(data, index=range(0, 365))
    return df

# utility to help me placing the month label around the 2nd week of each month

def split_list(a, n):
    k, m = divmod(len(a), n)
    return list(
        list(a[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)]) for i in range(n)
    )


def get_ticks(df, pos):
    splitter = split_list(df.index, 12)
    months = [
        "Jan",
        "Feb",
        "Mar",
        "Apr",
        "May",
        "Jun",
        "Jul",
        "Aug",
        "Sep",
        "Oct",
        "Nov",
        "Dec",
    ]
    xticks_map = [i for i in zip([splitter[i][pos] for i in range(0, 12)], months)]
    return xticks_map

# plotting method

def get_mplot(df, cols=None):
    if cols:
        df = df[cols]
    if len(df.columns) == 0:
        print("No coumns selected")
        return None
    grid_style = {
        "grid_line_color": "black",
        "grid_line_width": 1.1,
        "minor_ygrid_line_color": "lightgray",
        "minor_xgrid_line_color": "lightgray",
        "xgrid_line_dash": [4, 4],
    }
    colors = cc.glasbey_light[: len(list(df.columns))]
    xticks_map = get_ticks(df, 15)
    multi_curve = [
        hv.Curve((df.index, df[v]), label=str(v)).opts(
            xticks=xticks_map,
            xrotation=45,
            width=900,
            height=400,
            line_color=colors[i],
            gridstyle=grid_style,
            show_grid=True,
        )
        for i, v in enumerate(df)
    ]
    mplot = hv.Overlay(multi_curve)
    return mplot


# get the data
df = get_data()

# create a multi-choice widget

years = pn.widgets.MultiChoice(
    name="Years", options=list(df.columns), margin=(0, 20, 0, 0)
)

# bind plot and multi-choice

@pn.depends(years)
def get_plot(years):
    df = get_data()
    if years:
        df = df[years]
    mplot = get_mplot(df, years)
    return mplot


pn.Column("Plot!", get_plot, pn.Row(years), width_policy="max").servable()

For convenience, I stored the code online as notebook on a gist:

notebook

My issue is with the interaction between holoviews and panel at cell #7 (in the notebook) when I define the @pn.depends method - the only way I got it to work so far, is to “reloading” the data at each interaction … (cell_out: [#21], line [#3], in df = get_data() ) which obviously slows down the whole app if the data starts to increase.

Essentially I need a method to interact with the plot components and not re-executing the plot at each interaction. In plain bokeh I would write a handler that gets connected to the plot but it is my understanding, in holoviews+panel (as they are a higher-level set of libraries built on top of bokeh) there should be a simpler way to achieve the same.

Do you have any hints on how to avoid reload the dataset?


Solution

  • I think you just need to do your data loading first and not overwrite the dataframe, like:

    df = get_data()
    
    @pn.depends(years)
    def get_plot(years):
        if years:
            df1 = df[years]
        mplot = get_mplot(df1, years)
        return mplot