Multi & Exclusive filters with dynamic reload

Good day everyone!

I’m currently building a (for now) local search engine, so far so good.
Most of it works, but I’m breaking my brain for the last two days on how to get st.pills to behave like I want/imagine.

The goal is to have pills, reload it’s available tags, as soon as new selection was made.
Doesn’t sound too complicated, but somehow I’m completely stuck on that part.

Right now it looks like this:

and when a pill is selected the search does update correctly, as well as the df behind it.

But it should reduce the possible pill select down to the tags shown under the image “mid mod…”

my code rn is a mess, however, the important(?) bits are here:

def pills(avail_tags, df_start, color_selection):

    tag_selection =st.pills('Tags',avail_tags, selection_mode='multi')

df_start, avail_tags, tag_selection = filter_fragment(df_start, color_selection ,tag_selection)

st.write(tag_selection)

st.write(avail_tags)

return df_start




@st.fragment

def filter_fragment(df_start, color_selection ,tag_selection):

avail_tags = button_tag_list(df_start)

search_input = color_selection["color_name"].to_list() + tag_selection

df_start, df_data = color_search_func(df_start, search_input)

st.write(df_start)

return df_start, avail_tags, tag_selection




def filter_color(df_start, color_selection):

avail_tags = button_tag_list(df_start)

search_input = color_selection["color_name"].to_list()

df_start, df_data = color_search_func(df_start, search_input)

return df_start, avail_tags

and

elif search_method == 'Tag filter':

            #df_colors = get_availible_colors()

    color_picker = DynamicFilters(get_availible_colors(), filters=['color_name'])

    color_picker.display_filters()

    df_chosen_colors = color_picker.filter_df()

    search_input = df_chosen_colors["color_name"]

   df_start, df_data = color_search_func(df_data, search_input)




   if len(df_start) >= 1:

      if color_picker:

         df_start, avail_tags = filter_color(df_start, df_chosen_colors)




     df_start = pills(avail_tags, df_start, df_chosen_colors)

It would be super awsome if one of you could lead me to the solution :slight_smile:

The code is running on python 3.12.8 latest mac OS
pip freeze:
streamlit==1.52.0
streamlit_dynamic_filters==0.1.9

direct link to st folder on github:

Welcome to the Streamlit community! :balloon: Thanks for the detailed question and screenshots—they really help! Your goal is to have st.pills dynamically update its available tags based on the filtered DataFrame, so only relevant tags are selectable after each search/filter action.

Currently, Streamlit’s st.pills widget (as of v1.52.0) does not natively support dynamic option updates in the same rerun—changing the options list for a widget will reset its selection, and the widget is re-instantiated, which can cause the selection to be lost or reset (see docs). To achieve the behavior you want, you need to carefully manage the widget’s options and its selected values in st.session_state, and ensure that after each filter, the pills widget is re-rendered with the new set of tags, while preserving only the valid selections.

Here’s a minimal reproducible example pattern you can adapt:

import streamlit as st

# Example: tags available in your filtered DataFrame
def get_available_tags(df):
    # Replace with your logic to extract tags from df
    return sorted(set(tag for tags in df['tags'] for tag in tags))

# Simulate a DataFrame with tags
import pandas as pd
df = pd.DataFrame({
    "image": ["img1", "img2", "img3"],
    "tags": [["red", "green"], ["green", "blue"], ["red", "blue", "yellow"]]
})

# Initialize session state for tag selection
if "selected_tags" not in st.session_state:
    st.session_state.selected_tags = []

# Filter DataFrame based on selected tags
def filter_df(df, selected_tags):
    if not selected_tags:
        return df
    return df[df['tags'].apply(lambda tags: any(tag in tags for tag in selected_tags))]

# Get available tags from filtered DataFrame
filtered_df = filter_df(df, st.session_state.selected_tags)
available_tags = get_available_tags(filtered_df)

# Remove any selected tags that are no longer available
st.session_state.selected_tags = [tag for tag in st.session_state.selected_tags if tag in available_tags]

# Pills widget with dynamic options
selected = st.pills("Tags", available_tags, selection_mode="multi", key="selected_tags")

# Update session state
st.session_state.selected_tags = selected

st.write("Selected tags:", st.session_state.selected_tags)
st.write("Available tags:", available_tags)
st.write(filtered_df)

This approach ensures that after each selection, the available tags update to reflect only those present in the filtered DataFrame, and the selection is always valid. If you want to see a more advanced or production-ready version, let us know!

Sources:

1 Like

Okay, so Mr. Clanker Bot had some good ideas, wasn’t the entire story to the solution.

I got it fixed now and it came down to some wrong filtering in the dataset, as well as some fancy “loop back” with the st.state_sessions.

Finished code for main looks like:




 
if "selected_tags" not in st.session_state:
    st.session_state.selected_tags = []
if "selected_color" not in st.session_state:
    st.session_state.selected_color = []
if 'avail_colors' not in st.session_state:
    st.session_state.avail_colors = []



if len(st.session_state.selected_color) or len(st.session_state.selected_tags) >=1:
                st.button('Reset Search',on_click=reset_search)
            set_all_colors = get_availible_colors()

            df_start = filter_for_color(df_data, st.session_state.selected_color)

            set_start_tags = set()
            if st.session_state.selected_color ==[]:
                for col in df_data.tagnames:
                    set_start_tags.update(col.split(','))
            else:
                for col in df_start.tagnames:
                     set_start_tags.update(col.split(','))

            set_start_tags = {item.strip(',').strip()for item in set_start_tags}
            st.session_state.avail_colors = set_all_colors.intersection(set_start_tags)  
            st.session_state.selected_color = [color for color in st.session_state.selected_color if color in set_all_colors]
            st.pills("Colors", st.session_state.avail_colors, selection_mode="multi", key="selected_color")


            # Get available tags from filtered DataFrame
            df_start = filter_for_tags(df_start, st.session_state.selected_tags)
            avail_tags_uniq = button_tag_list(df_start)
            avail_tags_uniq = set(avail_tags_uniq).difference(st.session_state.avail_colors)         
            # Remove any selected tags that are no longer available
            st.session_state.selected_tags = [tag for tag in st.session_state.selected_tags if tag in avail_tags_uniq]

            # Pills widget with dynamic options
            st.pills("Tags", avail_tags_uniq, selection_mode="multi", key="selected_tags")

& the search functions:


def filter_for_color(df, selected_tags):
    if not selected_tags:
        print('returning df1')
        return df

    else:
        output_search_str = '|'.join(selected_tags)
        def count_matches(row_tags):
            matches = re.findall(output_search_str, row_tags)
            return len(set(matches))
        if len(selected_tags) >=1:
            threshold =1
        else:
            threshold = len(selected_tags)
        threshold =  1
        df['match_count'] = df['tagnames'].apply(count_matches)
        df = df[df['match_count'] >= threshold]
        df = df.sort_values(by='match_count', ascending=False).drop(columns=['match_count'])
        return df


def reset_search():
    st.session_state.selected_tags = []
    st.session_state.selected_color = []
    st.session_state.avail_colors = []



def filter_color(df_start, color_selection):
    avail_tags = button_tag_list(df_start)
    search_input = color_selection["color_name"].to_list()
    df_start, df_data = color_search_func(df_start, search_input)
    return df_start

and looks like this:

maybe this will help somebody make their project more dynamic :slight_smile:

1 Like