3 Sure Fire Python Streamlit Methods For Reducing Cognitive Load
How to use data storytelling with complex data without overwhelming your users
When we design data visuals, a cardinal rule is to never make our audience work harder than necessary to understand the story.
In the language of design, this idea is captured by the principle of cognitive load — the greater the effort to accomplish a task, the less likely the task will be accomplished successfully.
In other words, if interpreting a chart or navigating a data app requires too much mental or physical effort, many users will simply give up.
So what cognitive load mean for data storytelling?
Let me demonstrate how we can reduce it through a few practical Python and Streamlit examples.
The Dataset
Our examples will use a dataset of global generosity metrics, the World Giving Index:
This dataset is compiles as an annual survey (2011–2021) that measures the percentage of people in each country globally who engaged in certain charitable activities.
Now for this hands-on tutorial, I have downloaded and compiled multiple years of data into a single CSV file (found on my GitHub HERE).
Each row of the dataset represents a country in a given year, and includes the following key fields:
Helping a stranger (%) — the proportion of people who helped a stranger in the past month.
Donating money (%) — the proportion of people who donated money to charity in the past month.
Volunteering time (%) — the proportion of people who volunteered their time for an organization in the past month.
Total Score (%) — an overall generosity score (essentially the average of the above three percentages).
Let’s use this dataset to illustrate how we can reduce cognitive load in data storytelling
Understanding Cognitive Load
Cogntive load is a design principle that refers to the total mental effort required to achieve a goal.
This includes, for example, perception, memory, and problem solving requirements. The viewer has to think hard to decipher the context and meaning.
The cognitive load principle warns us that as these loads increase, success rate decreases.
This can manifest as a busy dashboard where the viewer must mentally sift through multiple visuals and dozens of numbers to find what they are looking for.
Why Does Cognitive Load Matter With Data Viz?
High cognitive load means the audience is expending their limited mental energy just to understand the visualization itself.
Unnecessary clutter, confusing charts, or too much data shown at once can overwhelm the viewer’s working memory.
As Steve Krug (in his famous book on this topic) put it: Don’t make me think.
The best designs require little conscious effort from the user — the story should tell itself.
Example 1: Filtering Out Clutter with Data Subsets
One of the simplest ways to reduce cognitive load is to filter out unnecessary data before visualization.
Instead of plotting everything and forcing the reader to pick out the patterns, we show only the subset that matters for the story.
Let’s start with a super-simple example. We want to highlight the most generous countries in a specific year. Plotting all 175 countries in a bar chart would be extremely cluttered and hard to read.
Instead, we can filter the dataset to one year and take just the top 10 countries by overall generosity score. This focused view clearly shows who ranks at the top without the noise of 165 other bars:
import streamlit as st
import pandas as pd
import plotly.express as px
# Load the dataset
df = pd.read_csv("all_generosity_data.csv")
# Filter data for a specific year (e.g., 2022)
year = 2022
df_year = df[df['Year'] == year].copy()
# Convert percentage strings to numeric for sorting
df_year['Total Score'] = df_year['Total Score'].str.rstrip('%').astype(float)
# Select top 10 countries by Total Score and sort descending
top10 = df_year.nlargest(10, 'Total Score').sort_values(by='Total Score', ascending=True)
# Create horizontal bar chart
fig = px.bar(
top10,
x='Total Score',
y='Country',
orientation='h',
title=f"Top 10 Generous Countries in {year} (Total Score)",
labels={'Total Score': 'Total Score (%)', 'Country': 'Country'},
text='Total Score'
)
# Improve readability
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(yaxis=dict(categoryorder='total ascending'), xaxis_range=[0, 100])
# Display in Streamlit
st.plotly_chart(fig, use_container_width=True)
In this code, we first filter df
to the year 2022.
Then we convert the Total Score from a percentage string to a numeric value (so we can sort by it) and pick the 10 largest values.
The functionpx.par()
will plot a clean bar chart (using Plotly) of those top 10 countries. We set the orientation to “h” (horizontal) for easier reading.
By focusing on a single year and the top results, the chart achieves a high signal-to-noise ratio – it’s immediately clear which countries lead in generosity.
This principle of filtering or decluttering is fundamental in data storytelling — often, less is more.
Example 2: Guiding Attention with Interactive Controls
Another strategy for reducing performance load is to let the user choose what data they want to see, rather than showing everything by default.
Interactive widgets like dropdown menus and sliders allow a one-click narrowing of the information presented.
They also give the user a sense of control to explore at their own pace. However, it’s important that these interactions are simple (to keep kinematic load low).
To allow the exploration the generosity data for different years and different metrics, we can use a dropdown for year selection and another for metric, plus a slider to choose how many top countries to view.
This way, at any given time the user sees a focused chart (e.g. top 5 countries in 2018 by donating money score) and can easily change the view with a couple of clicks.
Here’s a Streamlit app example implementing this idea:
import streamlit as st
import pandas as pd
df = pd.read_csv("all_generosity_data.csv")
st.header("Explore Generosity by Year and Metric")
# Sidebar (or main area) controls for user input
year_options = sorted(df['Year'].unique())
selected_year = st.selectbox("Select a year:", year_options, index=len(year_options)-1) # default to latest year
metric_options = ["Total Score", "Helping a stranger Score",
"Donating money Score", "Volunteering time Score"]
selected_metric = st.selectbox("Select a metric:", metric_options, index=0)
N = st.slider("Number of top countries to display:", min_value=5, max_value=20, value=10)
# Filter and prepare data based on selections
dff = df[df['Year'] == selected_year].copy()
dff[selected_metric] = dff[selected_metric].str.rstrip('%').astype(float)
topN = dff.nlargest(N, selected_metric)
st.subheader(f"Top {N} countries for **{selected_metric}** in {selected_year}")
st.bar_chart(topN.set_index('Country')[selected_metric])
In this app, the user can interact with three controls:
Year dropdown: to pick the year of interest (we default to the most recent year for convenience).
Metric dropdown: to choose which measure of generosity to look at — the overall score or one of the components (helping strangers, donating, volunteering).
Slider: to adjust how many top countries are shown (from a quick top 5 snapshot up to top 20 for more detail).
The code then filters the DataFrame to the selected year, converts the chosen metric to numeric, and selects the top N countries by that metric. The result is a bar chart that updates automatically based on the user’s selections:
This interactive approach guides the viewer’s attention by only showing one subset of data at a time.
At no point do we dump the entire data (all years, all metrics) in one view — the user doesn’t have to mentally parse a complex multi-dimensional chart.
Instead, they can explore slice by slice, which keeps cognitive load low.
Awesome!
Importantly, the interactive controls themselves are straightforward, keeping kinematic load reasonable. Just a few clicks get you a specific insight. If we had forced the user to, say, scroll through a huge table or navigate multiple pages to compare these metrics, it would require far more effort. Here, one selection immediately refines the view. Always consider how to minimize the steps a user must take: the easier it is to change the perspective, the more likely the audience will engage with your data story.
Example 3: Progressive Disclosure with Tabs
Our final example demonstrates progressive disclosure — a technique to manage information complexity by revealing it in stages or sections. Rather than showing all your charts and analysis at once (which can inundate the viewer), you can organize content into tabs or expandable sections. The user can then choose which part to look at, one at a time, making the experience less cognitively demanding.
Scenario: We have multiple insights to share from the generosity dataset: a global trend over time, a breakdown of generosity by category, and an ability to drill down into individual countries. Instead of presenting all these visuals on one long page, we’ll use tabs so that each insight has its own space. The first tab will show a high-level trend, the second tab will show category details for the latest year, and the third tab will allow a country-specific exploration. This way, a reader can absorb the story layer by layer.
Here’s how we can implement this using Streamlit’s tab layout:
import streamlit as st
import pandas as pd
df = pd.read_csv("all_generosity_data.csv")
st.header("Global Generosity Dashboard")
st.write("Use the tabs below to navigate different views of the data.")
# Create three tabs
tab1, tab2, tab3 = st.tabs(["Global Trend", "Category Breakdown", "Country Detail"])
with tab1:
st.subheader("Global Generosity Trend (Overall Score by Year)")
# Compute average Total Score across all countries for each year
avg_score_by_year = df.groupby('Year')['Total Score'].apply(lambda x: pd.to_numeric(x.str.rstrip('%')).mean())
st.line_chart(avg_score_by_year)
st.caption("Overall, the world's generosity (average score) by year. Notice how it changes over time.")
with tab2:
st.subheader(f"Breakdown by Generosity Category (Year {df['Year'].max()})")
latest_year = df['Year'].max()
latest = df[df['Year'] == latest_year]
# Calculate global average for each category in the latest year
help_avg = latest['Helping a stranger Score'].str.rstrip('%').astype(float).mean()
donate_avg = latest['Donating money Score'].str.rstrip('%').astype(float).mean()
vol_avg = latest['Volunteering time Score'].str.rstrip('%').astype(float).mean()
averages = {
'Helping a stranger': help_avg,
'Donating money': donate_avg,
'Volunteering time': vol_avg
}
st.bar_chart(pd.Series(averages), use_container_width=True)
st.caption(f"In {latest_year}, on average ~{int(help_avg)}% helped strangers, ~{int(donate_avg)}% donated money, ~{int(vol_avg)}% volunteered.")
with tab3:
st.subheader("Country Details Over Time")
country_list = sorted(df['Country'].unique())
selected_country = st.selectbox("Select a country:", country_list, index=0)
country_data = df[df['Country'] == selected_country].copy().sort_values('Year')
country_data['Total Score'] = country_data['Total Score'].str.rstrip('%').astype(float)
st.line_chart(country_data.set_index('Year')['Total Score'])
st.caption(f"Overall generosity trend for {selected_country}.")
In this code, the st.tabs()
component creates three tabs and we populate each with a different visualization:
Tab 1 (Global Trend): In the image above, we display the first tab to display the global average generosity Total Score for each year.
Tab 2 (Category Breakdown): Here we focus on the latest year’s breakdown. We calculate the average scores across all countries for that year and display using a bar chart.
Tab 3 (Country Detail): We allow the user to select a specific country and then display that country’s Total Score over the years as a line chart. This is an interactive detail-on-demand — if the reader is curious about a particular country, they can see it in this tab without cluttering the global views.
By structuring the app into tabs, we practice progressive disclosure. The content in one tab is hidden when another tab is active.
This keeps cognitive load low — the viewer only processes the chart they requested.
Each step requires a conscious click, meaning the user opts in to additional information when they’re ready, rather than being bombarded upfront.
Super useful, super cool.
In Summary…
By minimizing the mental and physical effort we maximize the ability for the audience to understand our message.
The bottom line:
Show only what’s necessary: Focus on the data that matters and filter out the rest.
Simplify visuals: Use clear, uncluttered chart designs.
Leverage interactivity thoughtfully: Let users drill down or switch views with intuitive controls (dropdowns, sliders)
Progressively disclose complexity: Start with high-level data and provide pathways for viewers to explore deeper.
Reduce clicks and steps: Make general insights accessible with minimal user actions.
By following these best practices and guidelines, your data story will actually be heard, and remembered.