holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.39k stars 482 forks source link

No Event Triggered with Cascading Dependencies #6899

Open krishanunayak opened 3 weeks ago

krishanunayak commented 3 weeks ago

Description of expected behavior and the observed behavior

I am trying to cascade selection operations based on a predefined dictionary. But the events do not trigger appropriately if the cascading selections have the same value.

My expectation is that, when I change a selection in Select A, it will trigger Select B which in turn automatically triggers Select C because of the cascading dependencies. But if the Selection Value is same in the updates, then the cascading triggers are not happening downstream.

Current Workaround: I have to put "sel_a" in the change_c()'s dependency as well. This approach becomes problematic when the dependencies become complex, and multiple triggers happen for a single selection change.

Complete, minimal, self-contained example code that reproduces the issue

import panel as pn
import param

selections = {
    "A": {
        "X": [1, 2, 3],
        "Y": [4, 5, 6],
        "Z": [7, 8, 9],
    },
    "B": {
        "X": [1, 2, 3],
        "Y": [4, 5, 6],
        "Z": [7, 8, 9],
    },
    "C": {
        "X": [10, 20, 30], # change X to XX and the cascading triggers work
        "Y": [40, 50, 60],
        "Z": [70, 80, 90],
    },
}

class Demo(pn.viewable.Viewer):
    sel_a = param.Selector(
        default="A", objects=list(selections.keys()), label="Select A"
    )
    sel_b = param.Selector(label="Select B")
    sel_c = param.Selector(label="Select C")

    @param.depends("sel_a", watch=True, on_init=True)
    def change_b(self):
        ops = list(selections[self.sel_a].keys())
        self.param.sel_b.objects = ops
        self.sel_b = ops[0]

    @param.depends("sel_b", watch=True, on_init=True)
    def change_c(self):
        ops = selections[self.sel_a][self.sel_b]
        self.param.sel_c.objects = ops
        self.sel_c = ops[0]

    def __panel__(self):
        return pn.Column(
            pn.widgets.Select.from_param(self.param.sel_a),
            pn.widgets.Select.from_param(self.param.sel_b),
            pn.widgets.Select.from_param(self.param.sel_c),
        )

Demo().servable()
MarcSkovMadsen commented 3 weeks ago

Workaround

You can use self.param.trigger to trigger an event when the value does not change.

import panel as pn
import param

selections = {
    "A": {
        "X": [1, 2, 3],
        "Y": [4, 5, 6],
        "Z": [7, 8, 9],
    },
    "B": {
        "X": [1, 2, 3],
        "Y": [4, 5, 6],
        "Z": [7, 8, 9],
    },
    "C": {
        "X": [10, 20, 30], # change X to XX and the cascading triggers work
        "Y": [40, 50, 60],
        "Z": [70, 80, 90],
    },
}

class Demo(pn.viewable.Viewer):
    sel_a = param.Selector(
        default="A", objects=list(selections.keys()), label="Select A"
    )
    sel_b = param.Selector(label="Select B")
    sel_c = param.Selector(label="Select C")

    def _update_or_trigger(self, name, new_value):
        old_value = getattr(self, name)
        if old_value==new_value:
            self.param.trigger(name)
        else:
            setattr(self, name, new_value)

    @param.depends("sel_a", watch=True, on_init=True)
    def change_b(self):
        ops = list(selections[self.sel_a].keys())
        self.param.sel_b.objects = ops
        self._update_or_trigger("sel_b", ops[0])

    @param.depends("sel_b", watch=True, on_init=True)
    def change_c(self):
        ops = selections[self.sel_a][self.sel_b]
        self.param.sel_c.objects = ops
        self._update_or_trigger("sel_c", ops[0])

    def __panel__(self):
        return pn.Column(
            pn.widgets.Select.from_param(self.param.sel_a),
            pn.widgets.Select.from_param(self.param.sel_b),
            pn.widgets.Select.from_param(self.param.sel_c),
        )

Demo().servable()
krishanunayak commented 2 weeks ago

This a neat trick :)

But shouldn't this be happening by default?