Introduction
Static charts have served data analysts well for decades, but in today’s collaborative and presentation-driven environment, interactive visualizations are becoming the norm. When you share a matplotlib PNG on Slack or embed a seaborn chart in a report, your audience sees a frozen snapshot. But what if they could hover to see exact values, filter data on the fly, zoom into specific regions, or toggle between different views?
This is where Plotly and Streamlit shine. Plotly transforms your Python data into rich, interactive HTML visualizations that work in notebooks, dashboards, and web apps. Streamlit wraps these visualizations in a clean, shareable web interface with minimal code. Together, they enable you to build data products that tell stories, invite exploration, and go viral on platforms like Twitter and LinkedIn.
In this first episode of our data storytelling series, we’ll transition from static charts to interactive dashboards. We’ll cover Plotly Express for rapid prototyping, Plotly Graph Objects for fine-grained control, and Streamlit for building full-featured dashboards. We’ll use the World Happiness Report dataset from Kaggle to demonstrate real-world techniques, then discuss deployment options and best practices for creating visualizations that resonate with audiences.
Why Interactive Visualizations Matter
Interactive visualizations offer several advantages over static charts:
- Exploration: Users can filter, zoom, and drill down into data without requesting new charts from you.
- Engagement: Hover tooltips, animations, and clickable legends make data more accessible to non-technical stakeholders.
- Shareability: A single interactive dashboard can replace dozens of static slides.
- Storytelling: Animations and transitions guide viewers through your narrative, revealing insights step by step.
According to data visualization studies, interactive charts increase audience retention by up to 40% compared to static images. They also reduce the cognitive load required to interpret complex datasets.
Plotly Express: Quick Interactive Plots
Plotly Express is the high-level API of Plotly, designed for rapid chart creation with sensible defaults. Let’s start by loading the World Happiness Report dataset and creating basic interactive plots.
Loading the Data
import pandas as pd
import plotly.express as px
# Load World Happiness Report 2023 data
df = pd.read_csv('world_happiness_2023.csv')
# Inspect the dataset
print(df.head())
print(df.columns)
The dataset typically includes columns like:
– Country: Country name
– Happiness Score: Overall happiness score (0-10 scale)
– GDP per capita: Economic wealth indicator
– Social support: Social safety net strength
– Healthy life expectancy: Average healthy lifespan
– Freedom to make life choices: Personal freedom measure
– Generosity: Charitable giving indicator
– Perceptions of corruption: Trust in government
Creating a Scatter Plot
Let’s explore the relationship between GDP per capita and happiness:
# Interactive scatter plot with hover data
fig = px.scatter(
df,
x='GDP per capita',
y='Happiness Score',
color='Social support', # Color by social support level
size='Healthy life expectancy', # Size by life expectancy
hover_name='Country', # Show country name on hover
hover_data={'GDP per capita': ':.2f', 'Happiness Score': ':.2f'},
title='GDP vs Happiness: The Role of Social Support',
labels={'GDP per capita': 'GDP per Capita (log scale)', 'Happiness Score': 'Happiness Score'},
color_continuous_scale='Viridis'
)
# Enhance readability
fig.update_xaxes(type='log') # Log scale for GDP
fig.update_layout(height=600, font=dict(size=12))
fig.show()
What makes this interactive?
– Hover tooltips display country names and exact values
– Zoom and pan with mouse controls
– Click legend items to hide/show groups
– Color scale dynamically maps social support values
Bar Charts with Drill-Down
Compare happiness scores across regions:
# Group by region and calculate mean happiness
region_happiness = df.groupby('Region')['Happiness Score'].mean().reset_index()
region_happiness = region_happiness.sort_values('Happiness Score', ascending=False)
fig = px.bar(
region_happiness,
x='Region',
y='Happiness Score',
color='Happiness Score',
color_continuous_scale='RdYlGn', # Red-Yellow-Green scale
title='Average Happiness by Region',
labels={'Happiness Score': 'Avg Happiness Score'}
)
fig.update_layout(xaxis_tickangle=-45, height=500)
fig.show()
Animated Scatter Plots
If you have multi-year data, animation reveals trends over time:
# Assuming multi-year dataset with 'Year' column
fig = px.scatter(
df_multi_year,
x='GDP per capita',
y='Happiness Score',
animation_frame='Year', # Animate across years
animation_group='Country', # Track countries across frames
size='Population',
color='Region',
hover_name='Country',
range_x=[6, 12], # Fix axes for smooth animation
range_y=[2, 8],
title='Happiness Evolution (2015-2023)'
)
fig.update_xaxes(type='log')
fig.show()
The animation plays automatically, showing how countries move through the GDP-happiness space over time. This technique went viral with Hans Rosling’s Gapminder visualizations.
Plotly Graph Objects: Full Customization
While Plotly Express is great for quick plots, Graph Objects (plotly.graph_objects) gives you complete control over every element. Use it for:
– Complex subplots
– Custom annotations and shapes
– Advanced hover templates
– Multi-axis charts
Creating Subplots
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Create 2x2 subplot grid
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('GDP Impact', 'Social Support Impact',
'Freedom Impact', 'Corruption Impact'),
specs=[[{'type': 'scatter'}, {'type': 'scatter'}],
[{'type': 'scatter'}, {'type': 'scatter'}]]
)
# Define factors to plot
factors = ['GDP per capita', 'Social support', 'Freedom to make life choices', 'Perceptions of corruption']
positions = [(1, 1), (1, 2), (2, 1), (2, 2)]
for factor, (row, col) in zip(factors, positions):
fig.add_trace(
go.Scatter(
x=df[factor],
y=df['Happiness Score'],
mode='markers',
marker=dict(size=8, opacity=0.6, color='steelblue'),
text=df['Country'],
hovertemplate='<b>%{text}</b><br>' + factor + ': %{x:.2f}<br>Happiness: %{y:.2f}<extra></extra>',
name=factor
),
row=row, col=col
)
fig.update_xaxes(title_text=factors[0], row=1, col=1)
fig.update_xaxes(title_text=factors[1], row=1, col=2)
fig.update_xaxes(title_text=factors[2], row=2, col=1)
fig.update_xaxes(title_text=factors[3], row=2, col=2)
fig.update_yaxes(title_text='Happiness Score', row=1, col=1)
fig.update_yaxes(title_text='Happiness Score', row=2, col=1)
fig.update_layout(height=800, title_text='Happiness Drivers: Multi-Factor Analysis', showlegend=False)
fig.show()
Custom Hover Templates
Create rich, multi-line hover tooltips:
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df['GDP per capita'],
y=df['Happiness Score'],
mode='markers',
marker=dict(size=df['Healthy life expectancy']*2, color=df['Social support'],
colorscale='Plasma', showscale=True, colorbar=dict(title='Social Support')),
text=df['Country'],
customdata=df[['Freedom to make life choices', 'Generosity', 'Perceptions of corruption']],
hovertemplate=
'<b>%{text}</b><br><br>' +
'GDP per capita: $%{x:,.0f}<br>' +
'Happiness Score: %{y:.2f}<br>' +
'Freedom: %{customdata[0]:.2f}<br>' +
'Generosity: %{customdata[1]:.2f}<br>' +
'Corruption: %{customdata[2]:.2f}<br>' +
'<extra></extra>'
))
fig.update_layout(title='Comprehensive Happiness Analysis', xaxis_title='GDP per capita', yaxis_title='Happiness Score')
fig.show()
3D Scatter Plots
Visualize three dimensions simultaneously:
fig = go.Figure(data=[go.Scatter3d(
x=df['GDP per capita'],
y=df['Social support'],
z=df['Happiness Score'],
mode='markers',
marker=dict(
size=8,
color=df['Freedom to make life choices'],
colorscale='Turbo',
showscale=True,
colorbar=dict(title='Freedom')
),
text=df['Country'],
hovertemplate='<b>%{text}</b><br>GDP: %{x:.2f}<br>Social: %{y:.2f}<br>Happiness: %{z:.2f}<extra></extra>'
)])
fig.update_layout(
title='3D Happiness Space',
scene=dict(
xaxis_title='GDP per capita',
yaxis_title='Social Support',
zaxis_title='Happiness Score'
),
height=700
)
fig.show()
Building a Streamlit Dashboard
Streamlit transforms Python scripts into interactive web apps with minimal code. Let’s build a complete happiness dashboard.
Basic Dashboard Structure
Create a file happiness_dashboard.py:
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
# Page configuration
st.set_page_config(
page_title='World Happiness Dashboard',
page_icon='๐',
layout='wide',
initial_sidebar_state='expanded'
)
# Title and description
st.title('๐ World Happiness Report Dashboard')
st.markdown("""
Explore factors that contribute to happiness across countries.
Data source: World Happiness Report 2023
""")
# Load data with caching
@st.cache_data
def load_data():
df = pd.read_csv('world_happiness_2023.csv')
return df
df = load_data()
# Sidebar filters
st.sidebar.header('Filters')
region_filter = st.sidebar.multiselect(
'Select Regions',
options=df['Region'].unique(),
default=df['Region'].unique()
)
happiness_range = st.sidebar.slider(
'Happiness Score Range',
min_value=float(df['Happiness Score'].min()),
max_value=float(df['Happiness Score'].max()),
value=(float(df['Happiness Score'].min()), float(df['Happiness Score'].max()))
)
# Filter data
filtered_df = df[
(df['Region'].isin(region_filter)) &
(df['Happiness Score'] >= happiness_range[0]) &
(df['Happiness Score'] <= happiness_range[1])
]
st.sidebar.markdown(f'**{len(filtered_df)} countries selected**')
Layout with Columns and Tabs
# Key metrics row
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric('Avg Happiness', f"{filtered_df['Happiness Score'].mean():.2f}")
with col2:
st.metric('Avg GDP per capita', f"${filtered_df['GDP per capita'].mean():,.0f}")
with col3:
st.metric('Avg Social Support', f"{filtered_df['Social support'].mean():.2f}")
with col4:
st.metric('Avg Life Expectancy', f"{filtered_df['Healthy life expectancy'].mean():.1f} years")
st.markdown('---')
# Tabs for different views
tab1, tab2, tab3 = st.tabs(['๐ Overview', '๐ Factor Analysis', '๐บ๏ธ Geographic View'])
with tab1:
st.subheader('Happiness Score Distribution')
# Histogram
fig_hist = px.histogram(
filtered_df,
x='Happiness Score',
nbins=30,
color='Region',
title='Distribution of Happiness Scores',
labels={'Happiness Score': 'Happiness Score'},
marginal='box' # Add box plot on top
)
st.plotly_chart(fig_hist, use_container_width=True)
# Top 10 countries
col_a, col_b = st.columns(2)
with col_a:
st.subheader('Top 10 Happiest Countries')
top10 = filtered_df.nlargest(10, 'Happiness Score')[['Country', 'Happiness Score']]
st.dataframe(top10, hide_index=True, use_container_width=True)
with col_b:
st.subheader('Bottom 10 Countries')
bottom10 = filtered_df.nsmallest(10, 'Happiness Score')[['Country', 'Happiness Score']]
st.dataframe(bottom10, hide_index=True, use_container_width=True)
with tab2:
st.subheader('Factor Correlation with Happiness')
# Factor selection
factor_x = st.selectbox(
'X-axis factor',
['GDP per capita', 'Social support', 'Healthy life expectancy',
'Freedom to make life choices', 'Generosity', 'Perceptions of corruption'],
index=0
)
factor_y = st.selectbox(
'Y-axis factor',
['Happiness Score', 'GDP per capita', 'Social support', 'Healthy life expectancy'],
index=0
)
# Scatter plot
fig_scatter = px.scatter(
filtered_df,
x=factor_x,
y=factor_y,
color='Region',
size='Happiness Score',
hover_name='Country',
trendline='ols', # Add regression line
title=f'{factor_x} vs {factor_y}'
)
st.plotly_chart(fig_scatter, use_container_width=True)
# Correlation heatmap
st.subheader('Correlation Heatmap')
corr_cols = ['Happiness Score', 'GDP per capita', 'Social support', 'Healthy life expectancy',
'Freedom to make life choices', 'Generosity', 'Perceptions of corruption']
corr_matrix = filtered_df[corr_cols].corr()
fig_heatmap = px.imshow(
corr_matrix,
text_auto='.2f',
color_continuous_scale='RdBu_r',
aspect='auto',
title='Feature Correlation Matrix'
)
st.plotly_chart(fig_heatmap, use_container_width=True)
with tab3:
st.subheader('Global Happiness Map')
# Choropleth map
fig_map = px.choropleth(
filtered_df,
locations='Country',
locationmode='country names',
color='Happiness Score',
hover_name='Country',
hover_data={'Happiness Score': ':.2f', 'GDP per capita': ':,.0f'},
color_continuous_scale='RdYlGn',
title='World Happiness Map'
)
fig_map.update_layout(height=600)
st.plotly_chart(fig_map, use_container_width=True)
Interactive Filters and Downloads
# Add download button for filtered data
st.sidebar.markdown('---')
st.sidebar.subheader('Export Data')
csv = filtered_df.to_csv(index=False).encode('utf-8')
st.sidebar.download_button(
label='Download Filtered Data (CSV)',
data=csv,
file_name='filtered_happiness_data.csv',
mime='text/csv'
)
Running the Dashboard
streamlit run happiness_dashboard.py
Streamlit launches a local server (usually at http://localhost:8501) where you can interact with your dashboard. Every widget interaction triggers a rerun, updating all charts dynamically.
Deployment Options
Streamlit Cloud (Free)
- Push your code to GitHub
- Visit share.streamlit.io
- Connect your GitHub repo
- Select the main script (
happiness_dashboard.py) - Deploy (takes ~2 minutes)
Your dashboard gets a public URL like https://username-happiness-dashboard.streamlit.app.
Hugging Face Spaces
- Create a new Space on huggingface.co/spaces
- Select Streamlit as the SDK
- Upload your files or connect a Git repo
- Add a
requirements.txtwith dependencies:
streamlit==1.31.0
plotly==5.18.0
pandas==2.2.0
- The Space auto-deploys and provides a permanent URL
Docker Deployment (Self-Hosted)
For full control, containerize your dashboard:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "happiness_dashboard.py", "--server.port=8501", "--server.address=0.0.0.0"]
Tips for Viral Data Visualizations
1. Storytelling Flow
Structure your dashboard like a narrative:
– Hook: Start with a surprising insight (e.g., “Money can’t buy happiness… or can it?”)
– Context: Provide background and data sources
– Exploration: Let users discover patterns interactively
– Conclusion: Summarize key takeaways
2. Color Psychology
Choose color scales that match your data’s meaning:
– Diverging (RdBu, RdYlGn): For data with a meaningful midpoint (positive/negative sentiment)
– Sequential (Viridis, Plasma): For continuous data (temperature, GDP)
– Categorical (Plotly, D3): For distinct groups (regions, categories)
Avoid rainbow color scales (Jet) โ they’re perceptually non-uniform and mislead viewers.
3. Animation for Engagement
Animated transitions reveal temporal patterns that static charts hide. Use Plotly’s animation_frame parameter:
fig = px.scatter(
df_time_series,
x='GDP per capita',
y='Life expectancy',
animation_frame='Year',
size='Population',
color='Continent',
hover_name='Country',
log_x=True,
range_x=[100, 100000],
range_y=[25, 90]
)
fig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 500 # 500ms per frame
fig.show()
4. Mobile Responsiveness
Ensure your dashboard works on mobile devices:
– Use Streamlit’s use_container_width=True for plots
– Test on different screen sizes
– Avoid tiny text and overly complex charts
5. Performance Optimization
- Cache data loading: Use
@st.cache_datato avoid reloading CSVs on every interaction - Downsample large datasets: For >100k points, aggregate or sample data
- Lazy loading: Use Streamlit’s
st.spinner()to indicate loading states
Plotly vs Bokeh vs Altair: When to Use Which
| Library | Best For | Pros | Cons |
|---|---|---|---|
| Plotly | Dashboards, presentations, general-purpose interactive viz | Rich features, easy to learn, great docs, 3D support | Large JS bundle (~3MB) |
| Bokeh | Real-time streaming data, complex linked plots | Server-side data updates, low-level control | Steeper learning curve |
| Altair | Statistical exploration, declarative syntax | Grammar of graphics, concise code, Vega-Lite backend | Limited customization, no 3D |
Recommendation: Start with Plotly Express for 90% of use cases. Switch to Plotly Graph Objects when you need fine-grained control. Use Bokeh for streaming dashboards (e.g., live sensor data). Use Altair for exploratory data analysis when you value concise, declarative code.
Real-World Example: Building a Shareable Insight
Let’s create a viral-worthy visualization that answers: “Does money buy happiness?”
import streamlit as st
import plotly.express as px
import pandas as pd
import numpy as np
# Load data
df = pd.read_csv('world_happiness_2023.csv')
# Bin countries by GDP quartile
df['GDP Quartile'] = pd.qcut(df['GDP per capita'], q=4, labels=['Low', 'Medium-Low', 'Medium-High', 'High'])
# Calculate average happiness per quartile
quartile_happiness = df.groupby('GDP Quartile')['Happiness Score'].mean().reset_index()
st.title('๐ฐ Does Money Buy Happiness?')
st.markdown("""
We analyzed 150+ countries from the World Happiness Report.
Here's what we found:
""")
# Bar chart with insight annotation
fig = px.bar(
quartile_happiness,
x='GDP Quartile',
y='Happiness Score',
color='Happiness Score',
color_continuous_scale='Greens',
title='Average Happiness by GDP Quartile',
text='Happiness Score'
)
fig.update_traces(texttemplate='%{text:.2f}', textposition='outside')
fig.update_layout(showlegend=False, height=500)
# Add annotation
fig.add_annotation(
x=1.5, y=6.5,
text='Happiness increases<br>with GDP, but<br>plateaus after<br>$50k/capita',
showarrow=True,
arrowhead=2,
arrowsize=1,
arrowwidth=2,
arrowcolor='red',
font=dict(size=14, color='red'),
bgcolor='white',
bordercolor='red',
borderwidth=2
)
st.plotly_chart(fig, use_container_width=True)
# Key insight
st.info("""
**Key Insight**: Moving from low to medium-high GDP correlates with a +1.5 point happiness gain.
But the jump from medium-high to high GDP? Only +0.3 points. Diminishing returns kick in.
""")
# Interactive scatter for user exploration
st.subheader('Explore Individual Countries')
fig2 = px.scatter(
df,
x='GDP per capita',
y='Happiness Score',
size='Population',
color='Region',
hover_name='Country',
log_x=True,
trendline='lowess', # Non-linear trendline
title='GDP vs Happiness (Log Scale)'
)
st.plotly_chart(fig2, use_container_width=True)
st.markdown("""
**Takeaway**: Money matters, but social support, freedom, and health matter more beyond a certain GDP threshold.
Explore the scatter plot above to find outliers like Costa Rica (high happiness, medium GDP).
""")
This dashboard:
– Starts with a provocative question
– Provides a clear visual answer
– Adds an explanatory annotation
– Invites exploration with an interactive scatter plot
– Ends with an actionable takeaway
Share the deployed link on Twitter with a screenshot and a hook like: “We analyzed 150 countries. Here’s the exact GDP threshold where money stops buying happiness.” This format drives engagement.
Conclusion
Interactive visualizations transform static data into engaging stories. Plotly Express gets you started with minimal code, while Plotly Graph Objects provides surgical control for complex dashboards. Streamlit wraps it all in a web app that non-technical stakeholders can explore without installing Python.
In this first episode, we covered:
– Plotly Express for rapid prototyping (scatter, bar, 3D, animations)
– Plotly Graph Objects for custom subplots, hover templates, and annotations
– Streamlit for building full-featured dashboards with filters, tabs, and downloads
– Deployment options (Streamlit Cloud, Hugging Face Spaces, Docker)
– Best practices for viral data viz (storytelling, color psychology, mobile optimization)
– Library comparison (Plotly vs Bokeh vs Altair)
We demonstrated these techniques using the World Happiness Report, building a dashboard that answers the question: “Does money buy happiness?” The answer: yes, but with diminishing returns after ~$50k GDP per capita. Social support and freedom become the dominant factors.
In the next episode, we’ll dive into statistical rigor: why your correlation might be spurious, and how to avoid p-hacking traps when exploring Kaggle datasets. We’ll cover confounding variables, Simpson’s paradox, and proper hypothesis testing. Stay tuned!
Did you find this helpful?
โ Buy me a coffee
Leave a Reply