Panel & DataStore adaptation not working

Hi all,

I’m trying to adapt this DataStore tutorial.

I’ve created my own DataStore that is meant to work with pandas .loc[] for a multiindex. I’ve confirmed that all of this is working ok. If I click a widget, it indeed changes self.filtered - yay!

class DataStore(pn.viewable.Viewer):    
    diff = param.Boolean(False)    
    data = param.DataFrame()
    filtered = param.DataFrame()
    series = param.Tuple(allow_None=False)        
    name = param.String('##### Series Options')

    def __init__(self, **kwargs):                
        super().__init__(**kwargs)        
        
        self.param.series.length=len(self.data.columns.levels)
        self.series = tuple(['total'] + [slice(None)]*(len(self.data.columns.levels)-1))
                
        self._get_filtered() # <-- set init filtered data
        self._layout = self._get_layout()                    

    @param.depends("data")
    def _subset_options(self):
        return _multiindex_2_dict(self.data.columns).keys()
     
    @param.depends('series', 'diff', watch=True)
    def _get_filtered(self):
        filtered = self.data.loc[:, self.series].copy()
        if self.diff:
            filtered = filtered.diff(1)
        self.filtered = filtered

    def _get_layout(self):        
        subset = pn.widgets.RadioButtonGroup(name = 'subset',
                                             options = list(self._subset_options()),
                                             button_type = 'primary',
                                             value = 'total',
                                             align=('center', 'start'),
                                             width = 150)
        
        fields = pn.widgets.CheckButtonGroup(name = 'fields',
                                             options = list(self.data.columns.levels[-1].values),
                                             button_type = 'default',                                             
                                             value = ['total_pv'],
                                             align=('center', 'start'),
                                             width = 150)
            
        
        @pn.depends(subset, fields, watch=True)
        def _update_series_on_widgets(subset_value, fields_value):            
            self.series = pd.IndexSlice[subset.value, :, fields_value]            
        
        return pn.Row(       
                pn.Column(                 
                self.name,
                subset,
                fields,
                pn.Row(
                    pn.widgets.StaticText(name='T-1 Diff:',value=''),
                    pn.widgets.Switch.from_param(self.param.diff, name = 'Diff'),
                    align=('start', 'start')
                    )                    
                ),
            margin=25
        )

    def __panel__(self):
        return self._layout

My issue seems to be in View child classes. They render initial without issue, but they aren’t reactive to the datastore.filtered changing. Here is a simple example that doesn’t update properly:

class View(Viewer):
    datastore = param.ClassSelector(class_= DataStore)        

class DataFrame(View):    
    def __panel__(self):         
        data = self.filtered
        return pn.pane.DataFrame(data, min_width = 250)

any help would be greatly appreciated!

Could you try passing a reference to the pn.paneDataFrame pane instead of a value? When you pass a value, there’s no chance for Panel to automatically update a component. You have to do the work yourself of setting up callbacks to update the pane object. But, when you pass a reference (a Parameter, an rx expression, etc.), Panel sets up callbacks under the hood to update a component object based on the new value a reference holds.

        return pn.pane.DataFrame(self.param.filtered, min_width = 250)

Ah, that makes sense. If I wanted to add any additional transformation to the data frame though (like, .loc on something), would I have to go the callback method to get the actual df instance?

As usual in the Panel/HoloViz world, you have a few options :slight_smile:

  • Use the new reactive expression rx API to skip writing callbacks entirely: pn.pane.DataFrame(self.param.filtered.rx.loc[...])
  • You could add another method that returns your transformed version of filtered to the pane with pn.pane.DataFrame(self.transform_filtered).
    @param.depends('filtered')
    def transform_filtered(self):
        return self.filtered.loc[...]
  • Or with the approach that consists in writing mostly side-effect callbacks (i.e. with watch=True) that update panes in place.

Let me know if that makes any sense!

1 Like

That helps a ton! I had something like your #2, but I had it on panel which seemed so wrong.