Viewable holoviz pane not updating

Hey!

I would like to be able to update options of my HoloViews pane but the structure I’m using doesn’t seem to be working. Here is a MVP:

class MyPlot(pn.viewable.Viewer):
    color = param.Color()
    
    def __init__(self):
        
        self.df = pd.DataFrame({
            'x': [1,2,3,4],
            'y': [2,3,2,4],
        })
        
        self._plot_pane = pn.pane.HoloViews(hv.Curve(self.df, kdims='x', vdims='y'))
        
        super().__init__()
        
        self._layout = pn.Column(
            self.param.color,
            self._plot_pane
        )
    
    @param.depends('color', watch=True)
    def _change_color(self):
        print('change color')
        print(self._plot_pane.object.opts.info())
        self._plot_pane.object.opts(color=self.color)
    
    def __panel__(self):
        return self._layout

MyPlot()

Running this in a notebook, I have output:

Indicating that the callback is activating, however the plot does not seem to change color.

I understand that I could recreate the plot every time, but when the DF is quite large, then this would be slow. Does anyone know how to restructure this such that modifications to the hvplot options are reflected in the output?

Thanks!

This is how I will solve it:

import hvplot.pandas
import panel as pn
import holoviews as hv
import param


class MyPlot(pn.viewable.Viewer):
    color = param.Color("red")
    
    def __init__(self, **params):
        super().__init__(**params)
        self.df = pd.DataFrame({
            'x': [1,2,3,4],
            'y': [2,3,2,4],
        })
        
        self._plot_pane = hv.Curve(self.df, kdims='x', vdims='y').apply.opts(color=self.param.color)
        self._layout = pn.Column(
            self.param.color,
            self._plot_pane
        )
    
    def __panel__(self):
        return self._layout

MyPlot()

Thanks for the solution @Hoxbro, I’m afraid my simplified example was too easy for you to solve and I’m having a hard time transferring your logic to my actual problem.

Here is the “real” component that I’m trying to make:


class Percentile(pn.viewable.Viewer):
    
    percentiles = param.ListSelector(default=[], objects=['10th/90th', '20th/80th', '30th/70th', '40th/60th'])
    
    def __init__(self, df):
        super().__init__()
        self.df = df
        self._plot_pane = pn.pane.HoloViews()
        
        
        self._layout = pn.Column(
            pn.Param(self.param.percentiles, widgets={
                'percentiles': pn.widgets.CheckButtonGroup,
            }),
            self._plot_pane,
            sizing_mode='stretch_width'
        )
        
        self.init_plot()
        
# This solution also does not work
#     @param.depends('percentiles', watch=True, on_init=True)
#     def _update_plot(self):
#         """Change visibility of percentiles based on inputs"""
#         if self.percentiles:
#             print('did update', self._plot_pane.object.opts.info())
#             if '10th/90th' in self.percentiles:
#                 print('we do it')
#                 self._plot_pane.object.Area.I.apply.opts(visible = True)
#                 print(self._plot_pane.object.Area.I.opts.info())
#             if '20th/80th' in self.percentiles:
#                 self.layer4.visible = True
#             if '30th/70th' in self.percentiles:
#                 self.layer3.visible = True
#             if '40th/60th' in self.percentiles:
#                 self.layer3.visible = True
        
        
    
    def init_plot(self):
        
        print('update plot')
        self.layer5 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_1', 'percentile_9'])
        self.layer4 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_2', 'percentile_8'])
        self.layer3 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_3', 'percentile_7'])
        self.layer2 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_4', 'percentile_6'])
        layer1 = hv.Curve(df_subset, kdims='event_date', vdims='percentile_5')

#         self.layer5.apply.opts(alpha=.1, color = 'blue', visible='10th/90th' in self.param.percentiles)
        self.layer4.apply.opts(alpha=.2, color = 'blue', visible=True)
        self.layer3.apply.opts(alpha=.3, color = 'blue', visible=True)
        self.layer2.apply.opts(alpha=.4, color = 'blue', visible=True)
        layer1.opts(color='blue')

        layout = self.layer5 * self.layer4 * self.layer3 * self.layer2 * layer1
        layout.opts(responsive=True, height=500)
        self._plot_pane.object = layout
    
    def __panel__(self):
        return self._layout

p = Percentile(df_subset)
pn.Column(p)

Which outputs something that shows the different percentiles in different alpha hues. The intention is for the user to be able to select which percentiles they wish to view and hide the rest.

In the code snippet above, I try out two solutions. The first solution is commented out and is more similar to my original request with the color param.

The other line that I comment out:
# self.layer5.apply.opts(alpha=.1, color = 'blue', visible='10th/90th' in self.param.percentiles)
Is my attempt to transfer your solution to this new situation. Unfortunately, it doesn’t work.

With this additional context how might you approach making this component?

I was able to achieve the user interaction that I was looking for using this method:

class Percentile(pn.viewable.Viewer):
    
#     percentiles = param.ListSelector(default=[], objects=['10th/90th', '20th/80th', '30th/70th', '40th/60th'])
    p1 = param.Boolean(True)
    p2 = param.Boolean(True)
    p3 = param.Boolean(True)
    p4 = param.Boolean(True)
    
    def __init__(self, df):
        super().__init__()
        self.df = df
        self._plot_pane = pn.pane.HoloViews()
        
        self._layout = pn.Column(
            pn.Param(self.param, widgets={
                'p1': pn.widgets.Toggle(value=self.p1, name='10th / 90th percentile'),
                'p2': pn.widgets.Toggle(value=self.p2, name='20th / 80th percentile'),
                'p3': pn.widgets.Toggle(value=self.p3, name='30th / 70th percentile'),
                'p4': pn.widgets.Toggle(value=self.p4, name='40th / 60th percentile'),
            }, default_layout=pn.Row, sizing_mode='stretch_width'),
            self._plot_pane,
            width=1200,
        )
        
        self.init_plot()
        
    
    def init_plot(self):
        print('init plot')
        self.layer5 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_1', 'percentile_9'])\
                        .apply.opts(alpha=.1, color = 'blue', visible=self.param.p1)
        self.layer4 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_2', 'percentile_8'])\
                        .apply.opts(alpha=.2, color = 'blue', visible=self.param.p2)
        self.layer3 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_3', 'percentile_7'])\
                        .apply.opts(alpha=.3, color = 'blue', visible=self.param.p3)
        self.layer2 = hv.Area(df_subset, kdims='event_date', vdims=['percentile_4', 'percentile_6'])\
                        .apply.opts(alpha=.4, color = 'blue', visible=self.param.p4)
            
        self.layer1 = hv.Curve(df_subset, kdims='event_date', vdims='percentile_5')\
                        .apply.opts(color='blue')
        
        layout = self.layer5 * self.layer4 * self.layer3 * self.layer2 * self.layer1
        layout.opts(responsive=True, height=500)
        self._plot_pane.object = layout
    
    def __panel__(self):
        return self._layout

p = Percentile(df_subset)
pn.Column(p)

I would prefer to use a toggle group or button group though… This solution is also not very DRY/clean. Still very open to improvements / suggestions

1 Like

I can’t seem to get your first example to work either. So I used a DynamicMap instead; this will call the init_plot every time percentile changes but caches the plots, so it only sends changes to the frontend (as far as I understand)

import numpy as np
import pandas as pd

import holoviews as hv
import panel as pn
import param


class Percentile(pn.viewable.Viewer):

    percentiles = param.ListSelector(
        default=[], objects=["10th/90th", "20th/80th", "30th/70th", "40th/60th"]
    )

    def __init__(self, df):
        super().__init__()
        self.df = df
        self._plot_pane = hv.DynamicMap(self.init_plot)

        self._layout = pn.Column(
            # pn.param is also valid but I think this is cleaner with one parameter
            pn.widgets.CheckButtonGroup.from_param(self.param.percentiles),
            self._plot_pane,
            sizing_mode="stretch_width",
        )

    @param.depends("percentiles")
    def init_plot(self):
        print("update plot")
        self.layer5 = hv.Area(
            df_subset, kdims="event_date", vdims=["percentile_1", "percentile_9"]
        )
        self.layer4 = hv.Area(
            df_subset, kdims="event_date", vdims=["percentile_2", "percentile_8"]
        )
        self.layer3 = hv.Area(
            df_subset, kdims="event_date", vdims=["percentile_3", "percentile_7"]
        )
        self.layer2 = hv.Area(
            df_subset, kdims="event_date", vdims=["percentile_4", "percentile_6"]
        )
        layer1 = hv.Curve(df_subset, kdims="event_date", vdims="percentile_5")

        self.layer5.opts(
            alpha=0.1, color="blue", visible="10th/90th" in self.percentiles
        )
        self.layer4.opts(
            alpha=0.2, color="blue", visible="20th/80th" in self.percentiles
        )
        self.layer3.opts(
            alpha=0.3, color="blue", visible="30th/70th" in self.percentiles
        )
        self.layer2.opts(
            alpha=0.4, color="blue", visible="40th/60th" in self.percentiles
        )

        layer1.opts(color="blue")

        layout = self.layer5 * self.layer4 * self.layer3 * self.layer2 * layer1
        layout.opts(responsive=True, height=500)
        return layout

    def __panel__(self):
        return self._layout

# Create data
r = np.random.rand(100, 100) * 100
data = {f"percentile_{n}": np.quantile(r, n / 10, axis=1) for n in range(1, 10)}
data["event_date"] = pd.date_range("2020-01-01", periods=100, freq="1d")
df_subset = pd.DataFrame(data)

p = Percentile(df_subset)
pn.Column(p).servable()