Search code examples
python-3.xpandasfilterfrontendstreamlit

Streamlit : update options multiselect


I have a dataframe with 3 columns, let's say they are ['animal type','breed','colour']. I have created 3 multi select filters for the 3 columns. Now, when I change one filter, I want the options of the other filters to be updated.

Ex:

Animal type Breed Colour
Dog bulldog brown
Dog X Black
Dog X Black
Dog asky Green
Cat Y Yellow
Cat Y Brown
Spider asky White
Spider asky Black

If I select the option['Animal Type']=['Dog'] in a multiselect, I want to update the possible colour options to ['Brown','Black','Green']. When I select the option['Colour']=['Black'] in a multiselect, I want to update the possible animal type options to ['Dog','Spider'].

I have tried this, but it only updates when I filter by animal type.

ANIMAL = list(set(df['Animal Type']))
BREED = list(set(df['Breed']))
COLOR = list(set(df['Colour']))
options1=  st.sidebar.multiselect('Animal Type',ANIMAL)
if options1:
    options2 =  st.sidebar.multiselect('Select BREED',set(df[df['Animal Type'].isin(options1)]['Breed']))
    options3 =  st.sidebar.multiselect('Select COLOR',set(df[df['Breed'].isin(options2)]['Color']))
else:
    options2 =  st.sidebar.multiselect('Select BREED',BREED)
    options3 =  st.sidebar.multiselect('Select COLOR',COLOR)

Does anyone know how to update all option filters, after a filtering?


Solution

  • This is basically the same solution mentioned in the comments. However, the mechanism by which a user chooses the order of filters has been changed. This creates extra code, so the logic is a bit more convoluted.

    As mentioned in the other solution, there is a lot more complexity if you want to allow a user to go back and forth between filters to make selections, as such some kind of "order of filtering" should be established. In the implementation below, three filters are presented, and as a selection is made, session state values track what was last edited and moves things into an ordered "confirmed" list as the user moves on to edit the next filter. If the currently-being-edited filter is cleared, the last confirmation will be undone.

    import streamlit as st
    import pandas as pd
    
    if 'df' not in st.session_state:
        df = pd.DataFrame({
            'Animal':['Dog','Dog','Dog','Dog','Cat','Cat','Spider','Spider'],
            'Breed':['bulldog','X','X','asky','Y','Y','asky','asky'],
            'Color':['Brown','Black','Black','Green','Yellow','Brown','White','Black']
        })
        st.session_state.df = df
    
    df = st.session_state.df
    df_filtered = df.copy()
    
    # Session state values to track the order of filter selection
    if 'confirmed' not in st.session_state:
        st.session_state.confirmed = []
        st.session_state.last_edited = None
    
    def last_edited(i,col):
        '''Update session state values to track order of editing
        
        i:int
            index of the column that was last edited
        col:str
            name of the column that was last edited
        '''
        if st.session_state.last_edited is None: # Nothing was previously selected/edited
            st.session_state.last_edited = (i,col)
            return
        if st.session_state.last_edited == (i,col): # The same column was last edited
            undo(col)
            return
        # Some other column was last edited:
        confirmed(*st.session_state.last_edited)
        st.session_state.last_edited = (i,col)
        return
            
    def undo(col):
        '''Undoes the last confirmation if the last edit was to clear a filter
    
        col : str
            name of the column that was last edited
        '''
        if st.session_state['col_'+col] == []: # Check state of widget by key
            last_confirmed = safe_pop(st.session_state.confirmed,-1)
            st.session_state.last_edited = last_confirmed
    
    def safe_pop(lst, i):
        '''Pops the ith element of a list, returning None if the index is out of bounds
        
        lst : list
            list to pop from
        i : int
            index to pop
        '''
        try:
            return lst.pop(i)
        except IndexError:
            return None
    
    def confirmed(i,col):
        '''Adds the last edited column to the confirmed list
        
        i:int
            index of the column that was last edited
        col:str
            name of the column that was last edited
        '''
        st.session_state.confirmed.append((i,col))
    
    # Columns to display the filters (Streamlit with create multiselect widgets
    # according to the order of user edits, but columns will keep them displaying
    # in their original order for the user)
    cols = st.columns(3)
    
    selected = dict(zip(df.columns, [[],[],[]]))
    
    # Confirmed filters
    for i,col in st.session_state.confirmed:
        selected[col] = cols[i].multiselect(
            col, df_filtered[col].unique(), key=f'col_{col}', 
            on_change=last_edited, args=[i,col], disabled=True
        )
        df_filtered = df_filtered[df_filtered[col].isin(selected[col])]
    
    #Currently editing
    if st.session_state.last_edited is not None:
        i,col = st.session_state.last_edited
        selected[col] = cols[i].multiselect(
            col, df_filtered[col].unique(), key=f'col_{col}', 
            on_change=last_edited, args=[i,col]
        )
        df_filtered = df_filtered[df_filtered[col].isin(selected[col])]
    
    # Not yet edited filters
    for i, col in enumerate(df_filtered.columns):
        if (i,col) not in st.session_state.confirmed and (i,col) != st.session_state.last_edited:
            selected[col] = cols[i].multiselect(
                col, df_filtered[col].unique(), key=f'col_{col}', 
                on_change=last_edited, args=[i,col]
            )
        if selected[col] != []:
            df_filtered = df_filtered[df_filtered[col].isin(selected[col])]
    
    cols = st.columns(2)
    cols[0].write(df)
    cols[1].write(df_filtered)
    

    enter image description here