I am trying to create a dashboard that automatically refreshes each time the user clicks on a chart, by filtering all other charts. For that, I built a mixin as follows:
class ClickableChartMixin:
def show(self) -> dict:
return st.plotly_chart(self.fig, key=self.title, on_select="rerun", config={"displayModeBar": False})
It returns the plotly event, should I need it, but also sets the chart’s key in the session state, in order to retrieve the event data from it.
Two (sample for brevity) classes that implement it:
class BinnedRatingBarchart(ClickableChartMixin):
def __init__(self, df: pd.DataFrame, title: str):
rating_counts = (
df['Valutazione_bin']
.value_counts()
.reindex(RATING_LABELS, fill_value=0)
)
fig = go.Figure(go.Bar(
x=rating_counts.index.tolist(),
y=rating_counts.values,
text=rating_counts.values,
textposition='outside',
marker_color=RATING_COLORS,
marker_line_color="#4F4F4F",
marker_line_width=1,
customdata=[["Valutazione_bin"]] * len(rating_counts),
hovertemplate="<extra></extra>",
))
fig.update_layout(
hovermode=False, dragmode=False,
margin=dict(l=0, r=0, t=10, b=0),
)
fig.update_xaxes(showgrid=False, fixedrange=True, tickangle=-90)
fig.update_yaxes(showgrid=False, visible=False, fixedrange=True)
self.fig = fig
self.title = title
class BinnedPriceBarchart(ClickableChartMixin):
def __init__(self, df: pd.DataFrame, title: str):
price_counts = (
df['Price_cat']
.value_counts()
.reindex(PRICE_LABELS, fill_value=0)
)
fig = go.Figure(go.Bar(
x=price_counts.index.tolist(),
y=price_counts.values,
text=price_counts.values,
textposition='outside',
marker_color=px.colors.sample_colorscale("oryel", len(PRICE_LABELS)),
marker_line_color="#4F4F4F",
marker_line_width=1,
customdata=[["Price_cat"]] * len(price_counts),
hovertemplate="<extra></extra>",
))
fig.update_layout(
hovermode=False, dragmode=False,
margin=dict(l=0, r=0, t=10, b=0),
)
fig.update_xaxes(showgrid=False, fixedrange=True, tickangle=-90)
fig.update_yaxes(showgrid=False, visible=False, fixedrange=True)
self.fig = fig
self.title = title
Now, here’s the main function:
def dashboard(df: pd.DataFrame):
df_filtered = df.copy()
# Set the charts titles: these will also be used as element keys,
# which can be used to retrieve the plotly events.
binned_rating_title = "Binned Ratings"
binned_price_title = "Binned Price"
# To each chart, we map a dataframe column
CHART_FILTER_MAP = [
(binned_rating_title, "Valutazione_bin"),
(binned_price_title, "Price_cat"),
]
# Check which filters have been applied, i.e. which charts have been clicked
filters = {}
for chart_key, dimension in CHART_FILTER_MAP:
points = (st.session_state
.get(chart_key, {}) # retrieve a clicked chart's state
.get("selection", {}) # then the selected dimension
.get("points", []) # then the selected points
)
if points:
value = points[0].get("x", None) # get the actual clicked value
if value is not None:
filters[dimension] = f"{dimension} == '{value}'"
# Apply all the filters encountered before
for query in filters.values():
df_filtered = df_filtered.query(query)
with st.container(horizontal=True):
BinnedRatingBarchart(df_filtered, title=binned_rating_title).show()
BinnedPriceBarchart(df_filtered, title=binned_price_title).show()
Functionally, it works. But I have to click on a chart at least twice before the others get filtered. Both for “setting” the filter and to “clear” it.
Environment: local Windows, streamlit 1.59 and python 3.12