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
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()