Set value of ListSelector depending on other ListSelector value

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