Interaction between panel and holoviews

929 Views Asked by At

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?

2

There are 2 best solutions below

2
On BEST ANSWER

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
0
On

Building on top of @rich-signell, this will also work when removing all the entries from the multi-choice widget:

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

However, the issue I was facing is due to the way how I was prototyping the code, in a jupyter notebook. There is an odd behavior when running holoviews+panel in a jupyter notebook. I was able to replicate it on two different jupyter server with the following version for

Jupyterlab, holoviews, panel

'2.2.9', '1.14.0', '0.11.0a3.post2+g5aa0c91'
'3.0.7', '1.14.2.post2+gd235b1cb0','0.10.3'

The problem is that changes applied to the get_plot() method, decorated with @pn.depends - were not picked up by the panel widget until restart of the notebook kernel, so any attempt to change the code (also working solution) were not effective and confused me.

Tried to show the issue in this recording https://gist.github.com/epifanio/6c9827f9163de359130102a29d7f3079