Part 1: Beyond Static Charts: Building Interactive Dashboards with Plotly/Streamlit

,
Updated Feb 6, 2026

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:

  1. Exploration: Users can filter, zoom, and drill down into data without requesting new charts from you.
  2. Engagement: Hover tooltips, animations, and clickable legends make data more accessible to non-technical stakeholders.
  3. Shareability: A single interactive dashboard can replace dozens of static slides.
  4. 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)

  1. Push your code to GitHub
  2. Visit share.streamlit.io
  3. Connect your GitHub repo
  4. Select the main script (happiness_dashboard.py)
  5. Deploy (takes ~2 minutes)

Your dashboard gets a public URL like https://username-happiness-dashboard.streamlit.app.

Hugging Face Spaces

  1. Create a new Space on huggingface.co/spaces
  2. Select Streamlit as the SDK
  3. Upload your files or connect a Git repo
  4. Add a requirements.txt with dependencies:
streamlit==1.31.0
plotly==5.18.0
pandas==2.2.0
  1. 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_data to 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!

The Art of Data Storytelling: Insights from Kaggle Datasets Series (1/4)

Did you find this helpful?

โ˜• Buy me a coffee

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

TODAY 372 | TOTAL 2,595