Dynamic date range update for Tabulator's row_content?

Hi, I’m trying to build an app that lets my users inspect timeseries of data points in a table. For this, I would like to use the row_content, because it makes it easy to compare rows that are not neighbors. The plots are usually more involved than in the minimal example below, and take data from the row in question into account.

import datetime as dt
import numpy as np
import pandas as pd
import panel as pn
import param
import hvplot.pandas  # noqa

datetime_range_slider = pn.widgets.DatetimeRangeSlider(
    name="Select timerange:",
    start=dt.datetime(2021, 1, 1),
    end=dt.datetime(2021, 1, 10),
    value=(dt.datetime(2021, 1, 1), dt.datetime(2021, 1, 3)),
    step=3600000,
)


class MyTable(param.Parameterized):
    states = param.DataFrame(doc="Plant state")

    def __init__(self, timerange_widget=None, **kwargs):
        super().__init__(**kwargs)
        self.timerange_widget = timerange_widget
        # this will normally be triggered by some widget:
        self.states = pd.DataFrame([[1, 3], [2, 4]], columns=["foo", "bar"])

    def get_tabulator(self):
        table_ = pn.widgets.Tabulator.from_param(
            self.param.states,
            sizing_mode="stretch_width",
            row_content=pn.bind(  # doesn't work
                self.myplot,
                timerange=self.timerange_widget.value_throttled,
                watch=True,
            ),
            height=700,
            embed_content=True,  # makes no difference?
            layout="fit_columns",
        )
        return table_

    def get_timeseries(self, name):
        dates = pd.date_range(
            self.timerange_widget.value[0], self.timerange_widget.value[1], freq="h"
        )
        ts = pd.DataFrame(
            np.random.normal(size=len(dates)), index=dates, columns=["state"]
        )
        return ts

    def myplot(self, state_row, timerange=None):
        state_condition_ts = self.get_timeseries(state_row.foo)
        return pn.pane.HoloViews(
            state_condition_ts.hvplot(),
            sizing_mode="stretch_width",
        )


mytab = MyTable(timerange_widget=datetime_range_slider)

tab_states = mytab.get_tabulator()

template = pn.template.VanillaTemplate(
    title="Test",
)
template.main.append(
    pn.Column(
        datetime_range_slider,
        pn.pane.Markdown("### Plant states"),
        pn.Row(tab_states, sizing_mode="stretch_width"),
        width=1000,
    )
)
template.servable(target="main", title="Plant manager")

So far, so good. If anyone could show me how to integrate the extra widgets into my parameterized class, all the better!
But my main issue is that the row_content is not dynamically updating if I move the slider. It only updates when I collapse and reopen a row. What am I doing wrong?

Any hints would be greatly appreciated. Thank you in advance!

Hi there,
First thing that I’ve noticed is that myplot() takes two parameters, then you need to pass two arguments to it.
Second, when you bind a widget to another one, you need to use xxx.param.value as the parameter.
Good luck

1 Like

@Mana: Well, yes, the idea is that the static parameter state_row is passed in by Tabulator’s row_content, while the dynamic timerange needs to be bound to a widget. My assumption was that since the description of bind says it acts a little like functools.partial, the returned function will have one positional argument, just as row_content expects.

Interestingly, the code works as is, but if I change it to use timerange=self.timerange_widget.param.value_throttled as per your suggestion, I get

TypeError: MyTable.myplot() missing 1 required positional argument: 'state_row'

So I’m guessing without the .param., bind indeed acts like partial, since failing to detect dynamic widget parameters, it returns a static partial function. But with the .param., it tries to evaluate the entire function dynamically “before” row_content has a chance to pass its parameter.

But with a few modifications, I got it to work! Thank you for pointing me in the right direction.

import datetime as dt
import numpy as np
import pandas as pd
import panel as pn
import param
import hvplot.pandas  # noqa

datetime_range_slider = pn.widgets.DatetimeRangeSlider(
    name="Select timerange:",
    start=dt.datetime(2021, 1, 1),
    end=dt.datetime(2021, 1, 10),
    value=(dt.datetime(2021, 1, 1), dt.datetime(2021, 1, 3)),
    step=3600000,
)


class MyTable(param.Parameterized):
    states = param.DataFrame(doc="Plant state")

    def __init__(self, timerange_widget=None, **kwargs):
        super().__init__(**kwargs)
        self.timerange_widget = timerange_widget
        # this will normally be triggered by some widget:
        self.states = pd.DataFrame(
            [["title1", 3], ["title2", 4]], columns=["foo", "bar"]
        )

    def get_tabulator(self):
        table_ = pn.widgets.Tabulator.from_param(
            self.param.states,
            sizing_mode="stretch_width",
            row_content=self.myplot,
            height=700,
            embed_content=True,  # makes no difference?
            layout="fit_columns",
        )
        return table_

    def get_timeseries(self, name, start_ts, end_ts):
        dates = pd.date_range(start_ts, end_ts, freq="h")
        ts = pd.DataFrame(
            np.random.normal(size=len(dates)), index=dates, columns=["state"]
        )
        return ts

    def myplot(self, state_row):
        def dynamic_plot(timerange):
            state_condition_ts = self.get_timeseries(state_row.foo, *timerange)
            return pn.pane.HoloViews(
                state_condition_ts.hvplot(title=state_row.foo),
                sizing_mode="stretch_width",
            )

        return pn.bind(
            dynamic_plot,
            timerange=self.timerange_widget.param.value_throttled,
            watch=False,
        )


mytab = MyTable(timerange_widget=datetime_range_slider)

tab_states = mytab.get_tabulator()

template = pn.template.VanillaTemplate(
    title="Test",
)
template.main.append(
    pn.Column(
        datetime_range_slider,
        pn.pane.Markdown("### Plant states"),
        pn.Row(tab_states, sizing_mode="stretch_width"),
        width=1000,
    )
)
template.servable(target="main", title="Plant manager")
1 Like