Set value of ListSelector depending on other ListSelector value

Context :
I have multiple widgets of type Select (Dropdown menu with one possible selection).
Each widget contains several options, retrieved from a dataframe where each column corresponds to a Select widget.

Not all possibles combinaisons are presents in the dataframe. Hence, when a selection is made in one widget, I want the other widgets to be updated so that all the selected values are a combinaison of value present in the dataframe.

Code details
I have a watcher function on all the widget which updates the options of the widget every time a selection is made in one of the widget so that the options match a possible combinaison of values in the dataframe.

@param.depends("countries", "suppliers", "classes", "categories", "brands", watch=True)
    def update_fields(self):
        """
        Update the different filters possible fields on selection
        """
        self.loading = True
        filters = self.get_filters()
        set_new_filters = set(filters.items())
        set_current_filters = set(self.current_filters.items())

        dif_filters = dict(set_new_filters ^ set_current_filters)

        if len(dif_filters) > 0:
            for key, input in dif_filters.items():
                df = self.distinct_fields[self.distinct_fields[key] == filters[key]]
        else:
            df = self.distinct_fields

        self.build_distinct_fields(df)
        self.current_filters = filters
        self.loading = False
        return True

    def build_distinct_fields(self, df, first=False):
        """
        Initialize the possible fields for each filter

        :param df: pandas dataframe
            dataframe containing the possible fields for each filter
        """
        self.param.countries.objects = list(df[CRF_COUNTRY].unique())
        self.param.suppliers.objects = list(df[CRF_SUPPLIER].unique())
        self.param.classes.objects = list(df[CRF_CLASS].unique())
        self.param.categories.objects = list(df[CRF_CATEGORY].unique())
        self.param.brands.objects = list(df[CRF_BRAND].unique())

Problem
The build_distinct_fields function doesn’t update the value of the selector.
More precisely, If the initial values of the selectors are A, B, C.
Then if we changed B->D then the update_fields function could update the selections in A->E, so that we have E, D, C to match a possible combinaison of values on the dataframe.
But when I try to retrieve the value of the selections, I get A, D, C.

Is it possible to update the value of the selectors without hard scpecifying its value with something like : selector = value ?
Because when doing this, the watcher is called infinitely.

1 Like

Hi @hadmaria.

Good question. Could you provide a minimum, reproducible example? It would make it much easier for the community to help you. Thanks.

Hi @Marc

Thank you for your answer and sorry for the delay to provide this minimum, reproducible example.

This snippet shows that when I update a given field using self.param.given_field.objects depending on the selection in another field, the value of the field doesn’t update.
More precisely,

  • Firstly, if you click on “Update”, a table with Asia as continents and Abkhazia as country should be displayed.
  • Secondly, if you select Europe in the continents dropdown selection field, the value updates with Albania in the country dropdown selection field.
  • But, when you click on Update, you can see that the value of the countries selector didn’t update with the new country
import pandas as pd
import time
import panel as pn
import holoviews as hv
import numpy as np
import holoviews.plotting.bokeh
from bokeh.models import (
    ColumnDataSource,
    DataTable,
    HTMLTemplateFormatter,
    NumberFormatter,
    TableColumn,
)
import param

pn.extension(loading_spinner='dots', loading_color='#00aa41', sizing_mode="stretch_width")
hv.extension('bokeh')
pn.param.ParamMethod.loading_indicator = True

class ExclusiveSelector(param.Parameterized):
    
    countries_and_continents = {'countries': {0: 'Abkhazia',
      1: 'Afghanistan',
      2: 'Akrotiri and Dhekelia',
      3: 'Albania',
      4: 'Algeria',
      5: 'American Samoa',
      6: 'Andorra',
      7: 'Angola',
      8: 'Anguilla',
      9: 'Antarctica',
      10: 'Antigua and Barbuda',
      11: 'Argentina',
      12: 'Armenia',
      13: 'Aruba',
      14: 'Australia',
      15: 'Austria',
      16: 'Austria-Hungary',
      17: 'Azerbaijan',
      18: 'Baden',
      19: 'Bahamas'},
     'continents': {0: 'Asia',
      1: 'Asia',
      2: 'Asia',
      3: 'Europe',
      4: 'Africa',
      5: 'Oceania',
      6: 'Europe',
      7: 'Africa',
      8: 'North America',
      9: 'Antarctica',
      10: 'North America',
      11: 'South America',
      12: 'Asia',
      13: 'North America',
      14: 'Oceania',
      15: 'Europe',
      16: 'Europe',
      17: 'Asia',
      18: 'Europe',
      19: 'North America'}}

    button = pn.widgets.Button(name="UPDATE", button_type="primary")
    
    df_continents_countries = pd.DataFrame(countries_and_continents)

    continents_values = list(df_continents_countries["continents"].unique())
    countries_values = list(df_continents_countries["countries"].unique())
    
    reset_button = param.Action(lambda x: x.param.trigger("reset_button"), label="Reset Fields")
    continents = param.ObjectSelector(default=continents_values[0], objects=continents_values)
    countries = param.ObjectSelector(default=countries_values[0], objects=countries_values)

    def __init__(self, **params):
        super().__init__(**params)
        self.current_filters = self.get_filters()
        self.new_df = self.df_continents_countries
        self.settings_panel = pn.Param(
            self,
            parameters=[
                "reset_button",
                "continents",
                "countries"
            ],
            widgets={
                "reset_button": {"type": pn.widgets.Button},
                "continents": {"type": pn.widgets.Select},
                "countries": {"type": pn.widgets.Select},
            },
        )
        self.component = pn.Column(self.button, self.settings_panel, 
                                   pn.panel(pn.bind(self.update_plot, self.button), 
                                            loading_indicator=True))
        
    @param.depends("reset_button", watch=True)
    def reset_fields(self):
        self.build_distinct_fields(self.df_continents_countries, first=True)
        self.new_df = self.df_continents_countries

    def get_filters(self):
        continents_val = self.continents
        countries_val = self.countries
        filters = {
            "continents": continents_val,
            "countries": countries_val,
        }
        return filters

    @param.depends("countries", "continents", watch=True)
    def update_fields(self):
        filters = self.get_filters()

        set_new_filters = set(filters.items())
        set_current_filters = set(self.current_filters.items())
        
        dif_filters = dict(set_new_filters ^ set_current_filters)

        if len(dif_filters) > 0:
            for key, input in dif_filters.items():
                df = self.df_continents_countries[self.df_continents_countries[key] == filters[key]]
        else:
            df = self.df_continents_countries
        
        self.new_df = df
            
        self.build_distinct_fields(df)
        self.current_filters = filters
        return True

    def build_distinct_fields(self, df, first=False):
        self.param.continents.objects = list(df["continents"].unique())
        self.param.countries.objects = list(df["countries"].unique())

    def update_plot(self, event):
        selected_filters = pd.DataFrame(self.current_filters, index=[0])
        source = ColumnDataSource(data=selected_filters.to_dict("list"))
        columns = []
        if event: 
            time.sleep(2)
            for val in self.countries_and_continents:
                columns.append(
                        TableColumn(
                            field=val,
                            title=val,
                        ))
        return DataTable(source=source, columns=columns)

exc_sel = ExclusiveSelector()
pn.template.FastListTemplate(
    site="Panel", title="Loading Indicator", 
    main=[exc_sel.component]).servable();

As I emphasised in the previous post, trying to set the value using for example in this context countries = value launches the watcher function infinitely.

Do you have any idea how to solve this ?

Hey @hadmaria,

I didn’t really get into the logic of your code, but I managed to get the update to work by:

  • calling again self.get_filters() in self.update_fields() since self.update_plot() uses .current_filters
  • used the param.discard_events function to avoid to executing again self.update_fields
        ...
        self.build_distinct_fields(df)
        self.current_filters = self.get_filters()
        return True

    def build_distinct_fields(self, df, first=False):
        self.param.continents.objects = list(df["continents"].unique())
        self.param.countries.objects = list(df["countries"].unique())
        with param.discard_events(self):
            self.countries = list(df["countries"].unique())[0]
        

Thanks a lot, this is exactly what I was looking for.

I also just needed exactly this and I came up with the following solution to create a set of interdependent Selectors which populate their options based on the columns in a pandas DataFrame and the settings of the previous values.

I do seem to run into this more frequently it might be nice to make a function which returns a set of Selectors which are interconnected through their options given some nested structure of options.

EDIT: You dont see the dropdowns because I’m using windows game bar to record :slightly_frowning_face:

Code:


import pandas as pd
import panel as pn
import numpy as np
import param

pn.extension(sizing_mode="stretch_width")

arrays = [
    ['foo']*6 + ['bar']*6,
    [c for pair in zip('abcdef', 'abcdef') for c in pair],
    [f'A{i}' for i in np.random.choice(8, 6, False)] + [f'A{i}' for i in np.random.choice(8, 6, False)]

]
col_index = pd.MultiIndex.from_arrays(arrays, names=['foobar', 'alphabet', 'A-level'])

ncols = 12
nrows = 20

data = np.random.randint(5, 100, ncols*nrows).reshape((nrows, ncols))
df = pd.DataFrame(data, columns=col_index)


class MultiIndexSelector(param.Parameterized):
    df = param.ClassSelector(pd.DataFrame)

    def __init__(self, **params):
        super().__init__(**params)

        self.selectors = [pn.widgets.Select(name=name) for name in self.df.columns.names]
        self.df_pane = pn.pane.DataFrame(self.df)

        for selector in self.selectors:
            selector.param.watch(self._selector_changed, ['value'], onlychanged=True)

        # Set the first selector
        level_0_values = ['None'] + list(df.columns.get_level_values(0).unique())
        self.selectors[0].options = level_0_values
        self.selectors[0].value = level_0_values[0]

    def _selector_changed(self, *events):
        for event in events:
            current_index = self.selectors.index(event.obj)  # Index of the selector which was changed

            try:  # try/except when we are at the last selector
                next_selector = self.selectors[current_index + 1]

                # Determine key/level to obtain new index which gives options for the next slidfer
                values = np.array([selector.value for selector in self.selectors[:current_index + 1]])
                key = [value if value != 'None' else slice(None) for value in values]
                level = list(range(current_index+1))
                bools, current_columns = self.df.columns.get_loc_level(key=key, level=level)
                options = list(current_columns.get_level_values(0).unique())

                next_selector.options = ['None'] + options
                if next_selector.value is None:  # If the selector was not set yet, set it to 'None'
                    next_selector.value = 'None'
            except IndexError:
                pass

        # set the df
        all_values = [selector.value for selector in self.selectors]
        key = [value if value != 'None' else slice(None) for value in all_values]
        level = list(range(len(all_values)))
        self.df_pane.object = self.df.xs(key=tuple(key), level=level, axis=1)


mis = MultiIndexSelector(df=df)
pn.template.FastListTemplate(
    site="Panel", title="MultiIndex DataFrame select",
    sidebar=list(mis.selectors),
    main=mis.df_pane
    ).servable()

2 Likes