New Component: Dynamic multi-select filters

Hi All! :balloon: I built a new (and my first) component. It’s a fairly simple wrapper for st.multiselect' which uses st.session_state and st.experimental_rerun under the hood to show only relevant values in filter options and dynamically filter a dataframe (similar to Google Sheets slicers or Only Relevant Values in Tableau behaviour).

Check out the demo app here - Dynamic Filters Demo App

How to install and use the package:

  1. Install the package using pip:
    pip install streamlit-dynamic-filters

  2. Import the DynamicFilters class:
    from streamlit_dynamic_filters import DynamicFilters

  3. Create an instance of the DynamicFilters class and pass the dataframe and the list of fields that will serve as filters:

    dynamic_filters = DynamicFilters(df, filters=['col1', 'col2', 'col3', 'col4'])

  4. Display the filters in your app:
    dynamic_filters.display_filters()

  5. Display the filtered dataframe:
    dynamic_filters.display_df()

Demo GIF:

Sample usage:

import streamlit as st
from streamlit_dynamic_filters import DynamicFilters

data = {
    'Region': ['North America', 'North America', 'North America', 'Europe', 'Europe', 'Asia', 'Asia'],
    'Country': ['USA', 'USA', 'Canada', 'Germany', 'France', 'Japan', 'China'],
    'City': ['New York', 'Los Angeles', 'Toronto', 'Berlin', 'Paris', 'Tokyo', 'Beijing']
    }

df = pd.DataFrame(data)

dynamic_filters = DynamicFilters(df, filters=['Region', 'Country', 'City'])

with st.sidebar:
    dynamic_filters.display_filters()

dynamic_filters.display_df()

Any feedback is much appreciated :innocent: Hopefully, it’ll be useful for someone, personally I’ve been struggling with if/else logic to make the filters dynamic and our users had to only apply them from top to bottom, so being able to use session state is a game changer for me.

13 Likes

What an awesome idea and component! :heart_eyes:

1 Like

This is a really great feature, Thanks for bringing this up! I wanted to ask if there’s any way to align these filters using st.columns?

1 Like

@Rajat_Choudhary great question. I don’t think there is an option for this with the current setup. I’ll see if I can update the component to allow more flexibility

2 Likes

Hi @Oleksandr_Arsentiev , another question I had. Can we save this filtered dataframe that’s displayed using display_df() into a new dataframe as I might use it for further drill down, Thanks

2 Likes

You should be able to get that using dynamic_filters.filter_except(). This seems to be undocumented API so you are on your own.

3 Likes

Is there a way to set default values in the multip-select box. Thanks

2 Likes

Thanks @Goyo

1 Like

Take a look at the default argument to multiselect

1 Like

I just updated the package to include this functionality. You can achieve the columns alignment in the latest version (0.1.3):

df = pd.DataFrame(data)

dynamic_filters = DynamicFilters(df, filters=[‘region’, ‘country’, ‘city’, ‘district’])
dynamic_filters.display_filters(location=‘columns’, num_columns=2, gap=‘large’)
dynamic_filters.display_df()

See the sample app for more details - https://dynamic-filters-demo.streamlit.app/Columns_Example

3 Likes

Awesome, will check this out. Thanks a lot!

1 Like

Hi @Oleksandr_Arsentiev great effort thank you for sharing. When we disable the top most parent filter in the filter hierarchy, the expected behaviour would be the child filters should clear as well. For instance, if we clear the “Oceania” filter, we should expect Australia, Sydney, Circular Quay to clear too. However, those child selections remain on clearing the Oceania filter.

Probably worth considering as it isn’t typical UX behaviour. Great job otherwise though!

1 Like

Hey there! Great job with that component. However is there any way to check which columns are currently filtered? I try the dynamic_filters bo the dict seems to not be updating. Also can we perform some statistical data with the use of .display_df() ?

1 Like

Great component! One little detail: st.experimental_rerun was deprecated in version 1.27.0. Use [st.rerun] (st.rerun - Streamlit Docs) instead. It goes into an infinite rerun otherwise when filters are changed

2 Likes

@Oleksandr_Arsentiev First off awesome work!

support st.dataframe arguments by c-bik · Pull Request #2 · arsentievalex/streamlit-dynamic-filters · GitHub is small contribution form me to consider.

With this users will be able to pass parameters that st.dataframe(...) supports like use_container_width, hide_index etc.

2 Likes

Rendering multiple different dataframes in a single app isn’t supported yet it seems. For example, with this following snippet…

import streamlit as st
from streamlit_dynamic_filters import DynamicFilters
import pandas as pd

left, right = st.columns(2)
with left:
    df = pd.DataFrame({"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]})
    dynamic_filters = DynamicFilters(df, filters=["col1", "col2"])
    dynamic_filters.display_filters(location="columns", num_columns=2, gap="large")
    dynamic_filters.display_df(use_container_width=True, hide_index=True)
with right:
    df = pd.DataFrame({"col3": [11, 12, 13, 14], "col4": ["aa", "bb", "cc", "dd"]})
    dynamic_filters = DynamicFilters(df, filters=["col1", "col2"])
    dynamic_filters.display_filters(location="columns", num_columns=2, gap="large")
    dynamic_filters.display_df(use_container_width=True, hide_index=True)

…crashes with…

KeyError: 'col1'

Traceback (most recent call last):
  File ".../lib/python3.11/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 534, in _run_script
    exec(code, module.__dict__)
  File ".../pages/search.py", line 50, in <module>
    dynamic_filters.display_filters(location="columns", num_columns=2, gap="large")
  File ".../lib/python3.11/site-packages/streamlit_dynamic_filters/dynamic_filters.py", line 144, in display_filters
    filtered_df = self.filter_df(filter_name)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/streamlit_dynamic_filters/dynamic_filters.py", line 63, in filter_df
    filtered_df = self.df.copy()
                  ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'copy'

Am I mising anything?

1 Like

I think the issue comes from using filters=["col1", "col2"] with a dataframe that doesn’t have those columns.

1 Like

You are right. Did a silly copy-paste! Here is the corrected snipptet and still with the same issue:

import streamlit as st
from streamlit_dynamic_filters import DynamicFilters
import pandas as pd

left, right = st.columns(2)
with left:
    df = pd.DataFrame({"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]})
    dynamic_filters = DynamicFilters(df, filters=["col1", "col2"])
    dynamic_filters.display_filters(location="columns", num_columns=2, gap="large")
    dynamic_filters.display_df(use_container_width=True, hide_index=True)
with right:
    df = pd.DataFrame({"col3": [11, 12, 13, 14], "col4": ["aa", "bb", "cc", "dd"]})
    dynamic_filters = DynamicFilters(df, filters=["col3", "col4"])
    dynamic_filters.display_filters(location="columns", num_columns=2, gap="large")
    dynamic_filters.display_df(use_container_width=True, hide_index=True)
KeyError: 'col1'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File ".../lib/python3.11/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 534, in _run_script
    exec(code, module.__dict__)
  File ".../test.py", line 14, in <module>
    dynamic_filters.display_filters(location="columns", num_columns=2, gap="large")
  File ".../lib/python3.11/site-packages/streamlit_dynamic_filters/dynamic_filters.py", line 145, in display_filters
    options = filtered_df[filter_name].unique().tolist()
              ~~~~~~~~~~~^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/pandas/core/frame.py", line 3896, in __getitem__
    indexer = self.columns.get_loc(key)
              ^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.11/site-packages/pandas/core/indexes/base.py", line 3797, in get_loc
    raise KeyError(key) from err
2 Likes

Deployed to quickly see the issue:

2 Likes

The problem seems to be in DynamicFilters.check_state():

It stores the filters (column names) in the session state, but only the first time DynamicFilters is instantiated. Then that stored columns are used elsewhere, no matter how the DynamicFilters instance was created.

Probably not very hard to fix but not trivial either.

2 Likes