Dashboard Design — COVID-19 Data Analysis¶
Dashboard Purpose¶
A public-facing tracker that informs without misleading. The design challenge here is unique: every choice (raw vs per-capita, log vs linear scale, date range) can distort the story. This dashboard prioritises honesty over drama.
Python Visualisation (matplotlib/seaborn focus)¶
Unlike the other projects, this one emphasises Python charting because the time-series work and per-capita normalisation are best demonstrated in code.
Chart 1 — Global Wave Timeline¶
import pandas as pd
import matplotlib.pyplot as plt
world = pd.read_csv("covid_world_clean.csv", parse_dates=["date"])
fig, ax = plt.subplots(figsize=(14, 5))
ax.fill_between(world["date"], world["new_cases_7d"], alpha=0.3, color="#0D9488")
ax.plot(world["date"], world["new_cases_7d"], color="#0D9488", linewidth=1.5)
ax.set_title("Global Daily COVID-19 Cases (7-Day Average)", fontsize=14)
ax.set_ylabel("New Cases (7-day avg)")
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1e6:.1f}M"))
ax.grid(True, alpha=0.3)
# Annotate the waves
ax.annotate("Delta wave", xy=(pd.Timestamp("2021-08-15"), 650000),
xytext=(pd.Timestamp("2021-05-01"), 800000),
arrowprops=dict(arrowstyle="->", color="#64748B"), fontsize=9)
ax.annotate("Omicron wave", xy=(pd.Timestamp("2022-01-20"), 3000000),
xytext=(pd.Timestamp("2021-10-01"), 3200000),
arrowprops=dict(arrowstyle="->", color="#64748B"), fontsize=9)
plt.tight_layout()
plt.savefig("global_waves.png", dpi=150, bbox_inches="tight")
plt.show()
Chart 2 — Fair Country Comparison (Small Multiples)¶
focus = pd.read_csv("covid_focus_clean.csv", parse_dates=["date"])
countries = ["United States", "United Kingdom", "India", "Brazil",
"Germany", "South Korea"]
fig, axes = plt.subplots(2, 3, figsize=(16, 8), sharex=True)
for ax, country in zip(axes.flat, countries):
sub = focus[focus["location"] == country]
ax.plot(sub["date"], sub["new_cases_per_million"].rolling(7).mean(),
color="#0D9488", linewidth=1.5)
ax.set_title(country)
ax.grid(True, alpha=0.3)
fig.suptitle("Daily New Cases per Million (7-Day Avg) — Fair Comparison", fontsize=14)
plt.tight_layout()
plt.savefig("country_comparison.png", dpi=150, bbox_inches="tight")
plt.show()
Small multiples beat one cluttered chart
Plotting 6 countries on one axis creates spaghetti. Small multiples (one panel per country, shared axes) let readers compare shapes fairly. Per-million on the y-axis ensures the comparison is honest.
Chart 3 — Log vs Linear Scale (Teaching Moment)¶
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
for country in ["United States", "South Korea"]:
sub = focus[focus["location"] == country]
ax1.plot(sub["date"], sub["total_cases_per_million"], label=country)
ax2.plot(sub["date"], sub["total_cases_per_million"], label=country)
ax1.set_title("Linear Scale")
ax2.set_yscale("log")
ax2.set_title("Log Scale")
for ax in (ax1, ax2):
ax.legend()
ax.grid(True, alpha=0.3)
plt.suptitle("Same Data, Two Scales — Choose Honestly")
plt.tight_layout()
plt.show()
If Building in Tableau or Power BI¶
Page 1 — Global Tracker¶
- KPI cards: total cases, total deaths, global CFR, % vaccinated
- World map (choropleth) coloured by cases per million, NOT total cases
- Global wave timeline (area chart, 7-day average)
Page 2 — Country Comparison¶
- Multi-select country slicer
- Per-capita line chart (small multiples or a single chart with selectable countries)
- A toggle for linear/log scale
- A "data caveats" text box (always visible)
Ethical Design Rules¶
Every choice can mislead — make honest defaults
- Default to per-capita, not raw counts — raw counts always favour large countries
- Use 7-day averages — raw daily data has weekend sawtooth noise
- Show full date ranges — don't let users cherry-pick a misleading window
- Display caveats prominently — testing differences, reporting lags, CFR limitations
- Avoid red-heavy "scary" palettes — inform, don't frighten
- Never compare CFR across countries without noting testing differences