Panel architecture for only triggering updates using a button

Hi!

I have touched similar needs a couple of times now, and I am struggling to figure out what the best panel set up for my needs is:

  • In a sidebar, I use a set of widgets to select the data set I want to present, and how it should be pre-processed. The widgets can depend on each other, sometimes in multiple levels. In the example below the default date range is updated when the sensor widget is updated.

  • In the main area I have a set of tabs for presenting the data in different ways. In the example below I added one tab with a curve plot and one with a histogram.

  • Here comes the crux. My datasets are typically large, and it can take up to ten seconds to generate the plots, and I would therefore like to only trigger a plot update when the user actively choose to do so. I.e. I don’t want the plots to update on every widget change. I would also like to only update the currently active tab and not all of them to speed things up a bit more. Obviously, I would like the have an automatic update when the user switch tab though, if possible.

I have tried to create a minimal example below, as a starting point, and I would be super happy if someone would be able to have a look at this… In this example, a change on the sensor widget value triggers six function calls, and I would like to avoid all of them, and only trigger an update when the button is clicked. Is this possible?

Thanks,
Johan

import pandas as pd
import numpy as np
import param
import panel as pn
import holoviews as hv
from holoviews import opts
pn.extension()
hv.extension('bokeh')

# Generate dummy data
index1 = pd.date_range(start='2000-01-01', end='2010-01-05')
index2 = pd.date_range(start='2005-01-01', end='2015-01-01')
data = {
    'Sensor A':pd.Series(np.random.randn(len(index1)), index=index1.date),
    'Sensor B':pd.Series(np.random.randn(len(index2)), index=index2.date),
}

class DataExplorer(param.Parameterized):

    # Set up parameters
    button_update = param.Action(lambda x: x.param.trigger('button_update'))        
    _sensor_options = list(data.keys())
    _sensor_default = _sensor_options[0]
    sensor = param.ObjectSelector(objects=_sensor_options, default=_sensor_default)
    date_start = param.CalendarDate(default=min(data.get(_sensor_default).index))
    date_end = param.CalendarDate(default=max(data.get(_sensor_default).index))
    
    def _get_data_set(self):
        _data_set = data.get(explorer.sensor)
        _data_set = _data_set.loc[(_data_set.index > explorer.date_start) & (_data_set.index < explorer.date_end)]
        return _data_set
    
    def plot_curve(self):
        print('Running plot_curve')
        _data_set = self._get_data_set()
        return hv.Curve((_data_set.index, _data_set)).opts(axiswise=True, title=explorer.sensor)
    
    def plot_hist(self):
        print('Running plot_hist')   
        _data_set = self._get_data_set()        
        return hv.Histogram(np.histogram(_data_set, 20)).opts(axiswise=True, title=explorer.sensor)
    
    @param.depends('button_update')
    def button_click(self):
        print('Running button_click')
        # I *only* want to trigger updates of the main content from here (and on tab changes), and *only* update the current-tab.. 
    
    @param.depends('sensor', watch=True)    
    def update_date_pickers(self):  
        print('Running update_date_pickers')           
        self.date_start = min(data.get(self.sensor).index)
        self.date_end = max(data.get(self.sensor).index)
    
explorer = DataExplorer()
dashboard = pn.template.MaterialTemplate()

widgets_width = 200
dashboard.sidebar.append(pn.widgets.Button.from_param(explorer.param.button_update, name='Update View', width=widgets_width))
dashboard.sidebar.append(pn.widgets.Select.from_param(explorer.param.sensor, name='Sensor:', width=widgets_width))
dashboard.sidebar.append(pn.widgets.DatePicker.from_param(explorer.param.date_start, name='Start date', width=widgets_width))
dashboard.sidebar.append(pn.widgets.DatePicker.from_param(explorer.param.date_end, name='End date', width=widgets_width))

tabs = pn.Tabs(
    ('Curve', explorer.plot_curve),
    ('Histogram', explorer.plot_hist),
)
dashboard.main.append(tabs)

dashboard.show()
'''

Try the following code. Note that I haven’t implemented the update of data when shifting tabs.

import pandas as pd
import numpy as np
import param
import panel as pn
import holoviews as hv
from holoviews import opts
pn.extension()
hv.extension('bokeh')

# Generate dummy data
index1 = pd.date_range(start='2000-01-01', end='2010-01-05')
index2 = pd.date_range(start='2005-01-01', end='2015-01-01')
data = {
    'Sensor A':pd.Series(np.random.randn(len(index1)), index=index1.date),
    'Sensor B':pd.Series(np.random.randn(len(index2)), index=index2.date),
}

class DataExplorer(param.Parameterized):

    # Set up parameters
    button_update = param.Action(lambda x: x.param.trigger('button_update'))
    _sensor_options = list(data.keys())
    _sensor_default = _sensor_options[0]
    sensor = param.ObjectSelector(objects=_sensor_options, default=_sensor_default)
    date_start = param.CalendarDate(default=min(data.get(_sensor_default).index))
    date_end = param.CalendarDate(default=max(data.get(_sensor_default).index))

    def _get_data_set(self):
        _data_set = data.get(self.sensor)
        _data_set = _data_set.loc[(_data_set.index > self.date_start) & (_data_set.index < self.date_end)]
        return _data_set

    def plot_curve(self):
        print('Running plot_curve')
        _data_set = self._get_data_set()
        return hv.Curve((_data_set.index, _data_set)).opts(axiswise=True, title=self.sensor)

    def plot_hist(self):
        print('Running plot_hist')
        _data_set = self._get_data_set()
        return hv.Histogram(np.histogram(_data_set, 20)).opts(axiswise=True, title=self.sensor)

    @param.depends('button_update')
    def plot(self):
        print('Running button_click')
        return pn.Tabs(
            ('Curve', self.plot_curve()),
            ('Histogram', self.plot_hist()),
            dynamic=True,
        )
        # I *only* want to trigger updates of the main content from here (and on tab changes), and *only* update the current-tab..

    @param.depends('sensor', watch=True)
    def update_date_pickers(self):
        print('Running update_date_pickers')
        self.date_start = min(data.get(self.sensor).index)
        self.date_end = max(data.get(self.sensor).index)


explorer = DataExplorer()
dashboard = pn.template.MaterialTemplate()

widgets_width = 200

dashboard.sidebar.append(pn.widgets.Button.from_param(explorer.param.button_update, name='Update View', width=widgets_width))
dashboard.sidebar.append(pn.widgets.Select.from_param(explorer.param.sensor, name='Sensor:', width=widgets_width))
dashboard.sidebar.append(pn.widgets.DatePicker.from_param(explorer.param.date_start, name='Start date', width=widgets_width))
dashboard.sidebar.append(pn.widgets.DatePicker.from_param(explorer.param.date_end, name='End date', width=widgets_width))

dashboard.main.append(explorer.plot)

dashboard.servable()

Thanks a lot for engaging with me, Hoxbro. I have tested your code, and yes, returning and redrawing the whole tab structure seems to solve the issue of the tabs updating on every widget change. However, what I see is that this means that the plot functions for all the tabs are still triggered on each button click, and also that this means that the first tab always becomes active on every button click. I do not know if perhaps the last issue could be worked around by storing the active-tab value. Do you see any other options for achieving these goals?

Thanks again,
Johan

What about this? What I have done is adding DynamicMap and your suggest of active tab. Note that if you change sensor before changing having opened a tab it will have the new sensor.

import pandas as pd
import numpy as np
import param
import panel as pn
import holoviews as hv
from holoviews import opts
pn.extension()
hv.extension('bokeh')

# Generate dummy data
index1 = pd.date_range(start='2000-01-01', end='2010-01-05')
index2 = pd.date_range(start='2005-01-01', end='2015-01-01')
data = {
    'Sensor A':pd.Series(np.random.randn(len(index1)), index=index1.date),
    'Sensor B':pd.Series(np.random.randn(len(index2)), index=index2.date),
}

class DataExplorer(param.Parameterized):

    # Set up parameters
    button_update = param.Action(lambda x: x.param.trigger('button_update'))
    _sensor_options = list(data.keys())
    _sensor_default = _sensor_options[0]
    sensor = param.ObjectSelector(objects=_sensor_options, default=_sensor_default)
    date_start = param.CalendarDate(default=min(data.get(_sensor_default).index))
    date_end = param.CalendarDate(default=max(data.get(_sensor_default).index))
    data_set = param.Series()

    def __init__(self, **params):
        super().__init__(**params)
        self.plot_curve_dmap = hv.DynamicMap(self.plot_curve)
        self.plot_hist_dmap = hv.DynamicMap(self.plot_hist)

    def _update_data_set(self):
        _data_set = data.get(self.sensor)
        _data_set = _data_set.loc[(_data_set.index > self.date_start) & (_data_set.index < self.date_end)]
        if self.data_set is None or not self.data_set.equals(_data_set):
            self.data_set = _data_set

    @param.depends("data_set")
    def plot_curve(self):
        print('Running plot_curve')
        return hv.Curve((self.data_set.index, self.data_set)).opts(axiswise=True, title=self.sensor)

    @param.depends("data_set")
    def plot_hist(self):
        print('Running plot_hist')
        return hv.Histogram(np.histogram(self.data_set, 20)).opts(axiswise=True, title=self.sensor)

    @param.depends('button_update')
    def plot(self):
        active_tab = self.tabs.active if hasattr(self, "tabs") else 0
        self._update_data_set()
        print('Running button_click')
        self.tabs = pn.Tabs(
            ('Curve', self.plot_curve_dmap),
            ('Histogram', self.plot_hist_dmap),
            dynamic=True,
            active=active_tab,
        )
        return self.tabs
        # I *only* want to trigger updates of the main content from here (and on tab changes), and *only* update the current-tab..

    @param.depends('sensor', watch=True)
    def update_date_pickers(self):
        print('Running update_date_pickers')
        self.date_start = min(data.get(self.sensor).index)
        self.date_end = max(data.get(self.sensor).index)


explorer = DataExplorer()
dashboard = pn.template.MaterialTemplate()

widgets_width = 200

dashboard.sidebar.append(pn.widgets.Button.from_param(explorer.param.button_update, name='Update View', width=widgets_width))
dashboard.sidebar.append(pn.widgets.Select.from_param(explorer.param.sensor, name='Sensor:', width=widgets_width))
dashboard.sidebar.append(pn.widgets.DatePicker.from_param(explorer.param.date_start, name='Start date', width=widgets_width))
dashboard.sidebar.append(pn.widgets.DatePicker.from_param(explorer.param.date_end, name='End date', width=widgets_width))

dashboard.main.append(explorer.plot)

dashboard.servable()