Client-side filtering in Streamlit using Altair Sliders
When visualizing data, sometimes we want to interact with visualizations by selecting ranges or individual rows. In Streamlit, any action on a widget causes the script to rerun, sometimes requiring more code to preserve the visualization state. Altair, allows you to create selections that run client-side and do not cause your Python app to rerun.
In this article, I'll cover using a binding_range to create a slider, which is an excellent way to add an interactive widget that belongs to Altair and allows filtering a chart client side.
Packages and Sample Data
For this example, we'll use Faker and Random to generate some data to visualize.
First, let's create 10k rows with Countries and Transactions that will be aggregated before visualization:
import streamlit as st
import pandas as pd
import altair as alt
import random
from faker import Faker
fake = Faker("en-US")
st.set_page_config(layout="wide")
st.subheader("Client Side Filtering using Altair Selections")
df = pd.DataFrame(range(1, 10001), columns=["ID"])
df["country"] = df.apply(lambda r: fake.country(), axis=1)
df["transaction"] = df.apply(lambda x: random.random() * 100, axis=1)
df = (
df[["transaction", "country"]]
.groupby("country")
.sum("transaction")
.reset_index("country")
)
df["transaction"] = df.apply(lambda x: round(x["transaction"], 2), axis=1)
Altair Selections and Slider
Our Slider will contain Min, Max, Step, and Name and will be created using Altair's binding_range function. Once the Slider is created, a selection is created. In this case, I'll use single, but for other examples multi and interval could be used. This Selection creates a new field—we call it "threshold."
To this Selection, let's assign the variable Slider to the bind property and initialize it by assigning a dictionary with the key "threshold" and the value corresponding to the max value of the transactions column in our dataframe to the init property.
slider = alt.binding_range(
min=df["transaction"].min(),
max=df["transaction"].max(),
step=1,
name="Transactions Lower or Equal than:",
)
selector = alt.selection_single(
fields=["threshold"],
bind=slider,
init={"threshold": df["transaction"].max()},
)
Base Chart
Our base chart will be a Bar Chart using Country as X and Transactions as Y. Now, let's use the Selection we created and attach it to the color spec of the chart. To do this, I will leverage an Altair condition.
Altair conditions behave like an IF expression. You'll specify an expression and values for True and False. For this condition, using Datum (Allows access to a column in the frame), the value must be less than or equal to the value in the selector. If the value is true, the color will be blue. Otherwise, it'll be light gray.
As a final step, let's add a .add_selection() function after the encoding of our chart to bind the selector to it (this is crucial):
base = (
alt.Chart(df)
.mark_bar()
.encode(
x="country:N",
y="transaction",
color=alt.condition(
alt.datum.transaction <= selector.threshold,
alt.value("blue"),
alt.value("lightgray"),
),
)
.add_selection(selector)
).properties(width=1000)
Filtered Chart
Very similar to the previous step, a bar chart is used. Still, instead of binding a selector using the .add_selection(), a .transform_filter() function is leveraged to add a predicate to the chart where the transaction field is less than or equal to the selector:
selected = (
alt.Chart(df)
.mark_bar()
.encode(
x="country:N",
y="transaction:Q",
color=alt.value("orange"),
)
.transform_filter(alt.datum.transaction < selector.threshold)
).properties(width=1000)
Render in Streamlit
Once the charts are ready, let's bind them together and display them using st.altair_chart and adding both charts—base and selected—joined with an ampersand symbol (&):
st.altair_chart(base & selected, use_container_width=True)
See it in action!
The Slider changes the color of the first chart to light gray when the value of transactions for a country is greater than the Slider (evaluates false) on the top chart (base), and the bottom chart (selected) is thoroughly filtered to match the criteria of the selector slider.
Full Code
import streamlit as st
import pandas as pd
import altair as alt
import random
from faker import Faker
fake = Faker("en-US")
st.set_page_config(layout="wide")
st.subheader("Client Side Filtering using Altair Selections")
df = pd.DataFrame(range(1, 10001), columns=["ID"])
df["country"] = df.apply(lambda r: fake.country(), axis=1)
df["transaction"] = df.apply(lambda x: random.random() * 100, axis=1)
df = (
df[["transaction", "country"]]
.groupby("country")
.sum("transaction")
.reset_index("country")
)
df["transaction"] = df.apply(lambda x: round(x["transaction"], 2), axis=1)
slider = alt.binding_range(
min=df["transaction"].min(),
max=df["transaction"].max(),
step=1,
name="Transactions Lower or Equal than:",
)
selector = alt.selection_single(
fields=["threshold"],
bind=slider,
init={"threshold": df["transaction"].max()},
)
base = (
alt.Chart(df)
.mark_bar()
.encode(
x="country:N",
y="transaction",
color=alt.condition(
alt.datum.transaction <= selector.threshold,
alt.value("blue"),
alt.value("lightgray"),
),
)
.add_selection(selector)
).properties(width=1000)
selected = (
alt.Chart(df)
.mark_bar()
.encode(
x="country:N",
y="transaction:Q",
color=alt.value("orange"),
)
.transform_filter(alt.datum.transaction <= selector.threshold)
).properties(width=1000)
st.altair_chart(base & selected, use_container_width=True)
This type of filtering using Altair charts and Selections is a great way to work with smaller datasets while avoiding reruns in your Streamlit app. There are also several ways to use selections that can be tailored to the user experience you want for your app. Enjoy!