Case Study 1: An Interactive Global Health Dashboard
Tier 2 — Attributed Example: This case study follows Mei, a data analyst at a fictional NGO called Global Health Forward, as she builds an interactive dashboard for her organization's annual report. The dashboard uses WHO and World Bank data structures, but all specific values have been simplified or adjusted for pedagogical clarity. Country names reference real nations; all other details are illustrative. The dashboard architecture and plotly/Dash techniques are representative of real-world practice.
The Setting
Mei has been at Global Health Forward for six months. Every year, the organization publishes a report on global vaccination progress. In previous years, the report included static charts created in Excel — bar charts of coverage by region, a table of country-level data, and a single world map designed by a graphic artist.
This year, Mei's director has a new idea: "What if donors could explore the data themselves? What if they could click on their country of interest, see the trend, compare it to neighbors?"
Mei's task: build an interactive dashboard that replaces the static Excel charts. It should be shareable as an HTML file (no Python installation required on the donor's end) and intuitive enough for non-technical board members.
She has three days.
Day 1: Individual Charts in a Notebook
Mei starts in a Jupyter notebook, building each chart independently before assembling them.
Chart 1: The World Map
import pandas as pd
import plotly.express as px
df = pd.read_csv("who_vaccination_enriched.csv")
latest = df[df["year"] == 2023]
fig_map = px.choropleth(
latest,
locations="iso_alpha",
color="coverage_pct",
hover_name="country",
hover_data={
"coverage_pct": ":.1f",
"region": True,
"income_group": True,
"gdp_per_capita": ":,.0f"
},
color_continuous_scale="YlGnBu",
range_color=[40, 100],
projection="natural earth",
title="Vaccination Coverage by Country (2023)"
)
fig_map.update_layout(
coloraxis_colorbar_title="Coverage (%)",
margin=dict(l=0, r=0, t=40, b=0))
fig_map.show()
Mei hovers over countries, checking that the tooltips are informative. She notices that Chad shows 42% coverage (yellow), while neighboring Cameroon shows 71% (green). The geographic proximity highlights how much coverage can differ between neighbors. This is exactly the kind of discovery she wants donors to make.
Chart 2: Regional Trends
yearly = df.groupby(["year", "region"],
as_index=False).agg(
mean_coverage=("coverage_pct", "mean"),
n_countries=("country", "nunique")
)
fig_trends = px.line(
yearly, x="year", y="mean_coverage",
color="region", markers=True,
hover_data={"n_countries": True,
"mean_coverage": ":.1f"},
title="Regional Coverage Trends"
)
fig_trends.update_layout(
yaxis_title="Mean Coverage (%)",
yaxis_range=[50, 100],
template="plotly_white")
fig_trends.show()
The trend lines show steady improvement in most regions, with AFRO starting lowest but showing the steepest upward slope in recent years. Mei clicks on "EMRO" in the legend to hide it and focus on the AFRO-SEARO comparison. The click-to-filter legend is surprisingly useful for presentations.
Chart 3: Income Group Comparison
fig_box = px.box(
latest, x="income_group", y="coverage_pct",
color="income_group",
category_orders={
"income_group": ["Low", "Lower-Middle",
"Upper-Middle", "High"]
},
hover_data=["country"],
title="Coverage Distribution by Income (2023)"
)
fig_box.update_layout(
showlegend=False,
yaxis_title="Coverage (%)",
template="plotly_white")
fig_box.show()
The interactive box plot lets Mei hover over each outlier to identify it. The lowest outlier in the "Upper-Middle" income group turns out to be a country recovering from conflict. This narrative detail — discoverable through hovering — is exactly what the old Excel charts could never provide.
Chart 4: GDP vs. Coverage Scatter
fig_scatter = px.scatter(
latest, x="gdp_per_capita",
y="coverage_pct",
color="region",
size="population",
size_max=40,
hover_name="country",
hover_data={
"gdp_per_capita": ":,.0f",
"coverage_pct": ":.1f",
"income_group": True
},
opacity=0.7,
title="GDP vs. Coverage (2023)"
)
fig_scatter.update_layout(
xaxis_title="GDP per Capita (USD)",
yaxis_title="Coverage (%)",
template="plotly_white")
fig_scatter.show()
Mei spots a cluster of small dots in the upper-left: low GDP but high coverage. She hovers over them — they are countries with strong public health systems relative to their income. These are the "overachievers" that her organization often highlights as success stories.
Day 2: The Animated Map
Mei adds the temporal dimension:
fig_animated = px.choropleth(
df, locations="iso_alpha",
color="coverage_pct",
hover_name="country",
hover_data={
"coverage_pct": ":.1f",
"gdp_per_capita": ":,.0f"
},
animation_frame="year",
color_continuous_scale="YlGnBu",
range_color=[40, 100],
projection="natural earth",
title="Global Vaccination Coverage Over Time"
)
fig_animated.update_layout(
coloraxis_colorbar_title="Coverage (%)",
margin=dict(l=0, r=0, t=40, b=0))
fig_animated.show()
She presses play and watches sub-Saharan Africa gradually shift from yellow to green over two decades. Southeast Asia brightens. A few countries in the Middle East darken briefly (conflict years) before recovering. The animation tells a story that no static chart could match.
Mei makes a practical decision: the animated map is impressive but takes time to absorb. For the dashboard, she will include both the animated version and a static latest-year version, so donors can choose their experience.
Day 3: Assembly and Export
Mei decides against building a full Dash app — her donors need to receive a file via email, not run a web server. Instead, she creates a single HTML page by combining multiple plotly charts:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# Create a 2x2 grid of charts
fig = make_subplots(
rows=2, cols=2,
subplot_titles=[
"Coverage by Region (Latest Year)",
"GDP vs Coverage",
"Regional Trends",
"Distribution by Income Group"
],
specs=[[{"type": "bar"}, {"type": "scatter"}],
[{"type": "scatter"}, {"type": "box"}]]
)
# Add traces to each subplot
# (simplified — each subplot gets manually
# constructed traces from graph_objects)
After some iteration, Mei realizes that make_subplots with mixed types is complex. She takes a simpler approach — export each chart individually and use a minimal HTML wrapper:
# Export individual charts
fig_map.write_html("map.html",
full_html=False,
include_plotlyjs="cdn")
fig_trends.write_html("trends.html",
full_html=False,
include_plotlyjs=False)
fig_box.write_html("box.html",
full_html=False,
include_plotlyjs=False)
fig_scatter.write_html("scatter.html",
full_html=False,
include_plotlyjs=False)
fig_animated.write_html(
"animated_map.html",
include_plotlyjs=True)
She also creates standalone self-contained versions:
fig_map.write_html(
"vaccination_dashboard_map.html",
include_plotlyjs=True)
fig_animated.write_html(
"vaccination_animated_map.html",
include_plotlyjs=True)
The Director's Reaction
Mei emails the animated map HTML file to her director. Ten minutes later, she gets a message: "I've been clicking on this for twenty minutes. I found three countries I didn't know were improving. Can we add this to the board presentation?"
The director then asks: "Can we make it so board members can type in a country name and see its trend?"
Mei smiles. That is a Dash app. For next year's report, she will build one.
Key Technical Decisions
Looking back, Mei identifies the decisions that mattered most:
-
Color scale choice:
"YlGnBu"(sequential, accessible) was better than"RdYlGn"(diverging, red-green problematic for colorblind viewers) for the choropleth. -
Fixed color range:
range_color=[40, 100]ensured that the same color meant the same value across all views. Without this, the animated map would have been misleading. -
Hover design: Including country name as
hover_nameand limitinghover_datato 3-4 fields kept tooltips readable. Her first version showed 10 columns in the tooltip and was overwhelming. -
Export strategy: Self-contained HTML files (
include_plotlyjs=True) were large but worked offline — important for board members on airplanes or in areas with poor connectivity. -
Simplicity over complexity: A single animated map conveyed more than a complex multi-panel dashboard. Sometimes the most impactful interactive visualization is one great chart, not five adequate ones.
Pedagogical Reflection
This case study demonstrates the practical workflow for interactive visualization:
- Build charts individually in a notebook before combining them.
- Test tooltips exhaustively — hover over edge cases (small countries, missing data, outliers) to ensure they display correctly.
- Choose export format based on audience — HTML for exploration, static images for reports, Dash for recurring dashboards.
- Start simple — one excellent interactive chart is better than a mediocre multi-panel dashboard.
- Let the audience drive the next iteration — Mei's director identified the next feature (country search) that will motivate her Dash app.