Case Study 14-1: Priya Builds Her First Dashboard
The Situation
Three weeks after the weekly regional report incident — where Priya caught a mislabeled axis just before Sandra's review — Sandra has a new request:
"Priya, I love the reports, but I'm spending too much time reading tables. Can you put together something visual? One page, key metrics. I want to be able to glance at it and know where we stand."
Priya has been wanting to build a proper dashboard for months. This is her green light.
She starts by asking Sandra three questions: 1. What decisions do you make from this data? 2. Which numbers do you check first? 3. What would make you say "something is wrong" in under ten seconds?
Sandra's answers: She checks total revenue trend, regional distribution, product mix, and margin first. She worries when North region drops, when margins compress, or when any region shows two consecutive down months.
Priya opens her notebook and sketches a 2×2 layout: - Top-left: Monthly revenue trend (line chart) - Top-right: Revenue by region (bar chart) - Bottom-left: Margin by product category (horizontal bar) - Bottom-right: This week vs. last week performance (grouped bar)
Step 1: Import Libraries and Prepare Data
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# Priya sets consistent styling before drawing any charts
plt.rcParams.update({
"font.family": "sans-serif",
"font.size": 9,
"axes.titlesize": 11,
"axes.titleweight": "bold",
"axes.labelsize": 9,
"xtick.labelsize": 8,
"ytick.labelsize": 8,
"legend.fontsize": 8,
})
BLUE = "#2563EB"
GREEN = "#16A34A"
AMBER = "#D97706"
RED = "#DC2626"
STEEL = "#64748B"
# Monthly revenue data (January through the current month)
monthly = pd.DataFrame({
"month": ["Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"],
"revenue": [42000, 38000, 51000, 55000, 47000, 60000,
63000, 58000, 67000, 71000, 65000, 79000],
"cost": [17000, 15500, 21000, 22500, 19000, 24000,
25500, 23500, 27000, 29000, 26500, 32000],
})
monthly["margin"] = monthly["revenue"] - monthly["cost"]
monthly["margin_pct"] = (monthly["margin"] / monthly["revenue"] * 100).round(1)
monthly["rolling_3m"] = monthly["revenue"].rolling(window=3).mean()
# Regional summary
regional = pd.DataFrame({
"region": ["North", "South", "East", "West"],
"revenue": [292000, 186000, 82000, 137000],
"margin_pct":[45.0, 40.0, 45.0, 45.0],
})
regional = regional.sort_values("revenue", ascending=False)
# Product category data
products = pd.DataFrame({
"category": ["Software", "Services", "Hardware"],
"revenue": [412000, 296000, 189000],
"margin_pct": [52.0, 48.0, 30.0],
})
products = products.sort_values("margin_pct", ascending=True)
# Week-over-week comparison
weeks = pd.DataFrame({
"region": ["North", "South", "East", "West"],
"last_week": [11200, 4200, 5100, 3800],
"this_week": [10900, 6300, 5100, 10300],
})
weeks["change_pct"] = (
(weeks["this_week"] - weeks["last_week"]) / weeks["last_week"] * 100
).round(1)
Step 2: Create the Figure and Axes
fig, axes = plt.subplots(
nrows=2, ncols=2,
figsize=(14, 9),
)
fig.patch.set_facecolor("white")
# Named references for readability
ax_line = axes[0][0] # Top-left: Monthly trend
ax_bar = axes[0][1] # Top-right: Regional comparison
ax_hbar = axes[1][0] # Bottom-left: Product margin
ax_grp = axes[1][1] # Bottom-right: Week-over-week
fig.suptitle(
"Acme Corp — Sales Dashboard | Week ending Jan 19, 2024",
fontsize=14,
fontweight="bold",
y=0.98,
color="#1E293B",
)
Priya names the axes explicitly. When you have a 2×2 grid, axes[0][0] is harder to reason about than ax_line.
Step 3: Panel 1 — Monthly Revenue Trend (Good Chart Checklist)
# ── Draw the chart ─────────────────────────────────────────────────────────
ax_line.plot(
monthly["month"],
monthly["revenue"],
color=BLUE,
linewidth=2.5,
marker="o",
markersize=6,
markerfacecolor="white",
markeredgewidth=2,
label="Monthly Revenue",
zorder=3,
)
ax_line.plot(
monthly["month"],
monthly["rolling_3m"],
color=STEEL,
linewidth=1.5,
linestyle="--",
label="3-Month Avg",
zorder=2,
)
ax_line.fill_between(monthly["month"], monthly["revenue"], alpha=0.06, color=BLUE)
# ── Good Chart Checklist: Panel 1 ──────────────────────────────────────────
# [x] Title: specific and clear
ax_line.set_title("Monthly Revenue Trend", pad=8)
# [x] Axis labels: "Month" and "Revenue (USD)"
ax_line.set_xlabel("Month")
ax_line.set_ylabel("Revenue (USD)")
# [x] Appropriate scale: y-axis starts at 0
ax_line.set_ylim(0, monthly["revenue"].max() * 1.20)
# [x] Legend: present because there are two series
ax_line.legend(loc="upper left")
# [x] Tick formatter: shows $42K not 42000
ax_line.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x/1000:.0f}K"))
# [x] Grid lines: horizontal only, light
ax_line.grid(axis="y", linestyle="--", alpha=0.35, color="lightgray")
# [x] Spine cleanup: remove top and right
ax_line.spines["top"].set_visible(False)
ax_line.spines["right"].set_visible(False)
Priya runs through the checklist out loud as she builds each panel. It is a habit she picked up from a data journalism course.
Step 4: Panel 2 — Revenue by Region (Good Chart Checklist)
bar_colors = [BLUE, "#3B82F6", "#60A5FA", "#93C5FD"]
bars = ax_bar.bar(
regional["region"],
regional["revenue"],
color=bar_colors,
edgecolor="white",
linewidth=0.7,
width=0.6,
)
for bar in bars:
height = bar.get_height()
ax_bar.text(
bar.get_x() + bar.get_width() / 2.0,
height + 3000,
f"${height/1000:.0f}K",
ha="center",
va="bottom",
fontsize=8.5,
fontweight="bold",
)
# ── Good Chart Checklist: Panel 2 ──────────────────────────────────────────
# [x] Title
ax_bar.set_title("Revenue by Region (Year to Date)", pad=8)
# [x] Axis labels
ax_bar.set_xlabel("Region")
ax_bar.set_ylabel("Revenue (USD)")
# [x] Scale starts at zero — critical for bar charts
ax_bar.set_ylim(0, regional["revenue"].max() * 1.20)
# [x] No legend needed — bars are labeled with their region name
# (Legend would be redundant here)
# [x] Data labels added above each bar
# [x] Tick formatter
ax_bar.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x/1000:.0f}K"))
# [x] Grid lines
ax_bar.grid(axis="y", linestyle="--", alpha=0.35, color="lightgray")
ax_bar.spines["top"].set_visible(False)
ax_bar.spines["right"].set_visible(False)
Checklist note — Pie chart temptation: Priya initially considered a pie chart for the regional breakdown. She decided against it because Sandra needs to compare exact magnitudes (North versus South), not just proportions. A bar chart encodes magnitude more accurately.
Step 5: Panel 3 — Margin by Product Category (Good Chart Checklist)
# Color bars: green for above-average margin, red for below
avg_margin = (products["margin_pct"] * products["revenue"]).sum() / products["revenue"].sum()
hbar_colors = [GREEN if m >= avg_margin else RED for m in products["margin_pct"]]
bars_h = ax_hbar.barh(
products["category"],
products["margin_pct"],
color=hbar_colors,
edgecolor="white",
height=0.5,
)
for bar in bars_h:
width = bar.get_width()
ax_hbar.text(
width + 0.5,
bar.get_y() + bar.get_height() / 2.0,
f"{width:.1f}%",
va="center",
fontsize=9,
fontweight="bold",
)
# Reference line for company average
ax_hbar.axvline(avg_margin, color=STEEL, linewidth=1.5, linestyle="--",
label=f"Avg: {avg_margin:.1f}%")
# ── Good Chart Checklist: Panel 3 ──────────────────────────────────────────
# [x] Title
ax_hbar.set_title("Gross Margin % by Product Category", pad=8)
# [x] Axis labels: x-axis shows the measure (Margin %)
ax_hbar.set_xlabel("Gross Margin %")
ax_hbar.set_ylabel("") # Category names are self-labeling
# [x] Scale appropriate: x-axis at 0, room for labels
ax_hbar.set_xlim(0, products["margin_pct"].max() * 1.20)
# [x] Legend: explains the reference line
ax_hbar.legend(loc="lower right")
# [x] Color encodes meaning: green = above average, red = below
# [x] Horizontal bars chosen because category names are multi-word
ax_hbar.grid(axis="x", linestyle="--", alpha=0.35, color="lightgray")
ax_hbar.spines["top"].set_visible(False)
ax_hbar.spines["right"].set_visible(False)
Step 6: Panel 4 — Week-over-Week Comparison (Good Chart Checklist)
x = range(len(weeks["region"]))
width = 0.38
bars_last = ax_grp.bar(
[i - width/2 for i in x],
weeks["last_week"],
width=width,
color=STEEL,
edgecolor="white",
label="Last Week",
)
bars_this = ax_grp.bar(
[i + width/2 for i in x],
weeks["this_week"],
width=width,
color=BLUE,
edgecolor="white",
label="This Week",
)
# Annotate WoW change percentage above each "this week" bar
for bar, (_, row) in zip(bars_this, weeks.iterrows()):
height = bar.get_height()
pct = row["change_pct"]
color = GREEN if pct >= 0 else RED
sign = "+" if pct >= 0 else ""
ax_grp.text(
bar.get_x() + bar.get_width() / 2.0,
height + 200,
f"{sign}{pct:.0f}%",
ha="center",
va="bottom",
fontsize=8,
fontweight="bold",
color=color,
)
# ── Good Chart Checklist: Panel 4 ──────────────────────────────────────────
# [x] Title
ax_grp.set_title("Week-over-Week Revenue by Region", pad=8)
# [x] Axis labels
ax_grp.set_xlabel("Region")
ax_grp.set_ylabel("Revenue (USD)")
# [x] Scale at zero
ax_grp.set_ylim(0, weeks[["last_week","this_week"]].values.max() * 1.22)
# [x] Legend: explains the two bar series
ax_grp.legend(loc="upper left")
# [x] Data labels: WoW % annotated in green/red
ax_grp.set_xticks(list(x))
ax_grp.set_xticklabels(weeks["region"])
ax_grp.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x/1000:.0f}K"))
ax_grp.grid(axis="y", linestyle="--", alpha=0.35, color="lightgray")
ax_grp.spines["top"].set_visible(False)
ax_grp.spines["right"].set_visible(False)
Step 7: Final Touches and Save
# Tight layout with room for the super title
plt.tight_layout(rect=[0, 0.02, 1, 0.96])
# Footer
fig.text(
0.5, 0.005,
"Source: Acme Corp CRM | Priya Okonkwo, Acme Analytics",
ha="center",
fontsize=7.5,
color="#94A3B8",
)
plt.savefig(
"priya_weekly_dashboard.png",
dpi=150,
bbox_inches="tight",
facecolor="white",
)
print("Dashboard saved: priya_weekly_dashboard.png")
plt.show()
Running the Checklist — Summary
| Panel | Title | Axis Labels | Zero Scale | Legend | Appropriate Chart |
|---|---|---|---|---|---|
| Monthly Trend | Yes | Yes | Yes | Yes | Line — correct for time trend |
| Regional Revenue | Yes | Yes | Yes | Omitted (redundant) | Bar — correct for category comparison |
| Product Margin | Yes | Yes | Yes | Yes | Horizontal bar — correct for named categories |
| WoW Comparison | Yes | Yes | Yes | Yes | Grouped bar — correct for two-period comparison |
All four panels pass.
What Sandra Said
Sandra opens the PNG attachment at 8:58 a.m. She looks at it for about fifteen seconds without speaking. Then:
"This is exactly what I needed. West region is up — can you find out why? And Hardware margin looks worrying. Can you add a target line on that panel next week?"
Two questions. Both actionable. Both arising directly from the visual. Not from a table.
Priya nods and adds two items to her notebook:
1. Drill into West region data — which customers, which deals?
2. Add a target_margin reference line to the product margin chart.
The dashboard did its job.
What Priya Learned
-
The checklist before the chart. Running the good-chart checklist during construction — not as a final review — catches problems early. Priya added axis labels immediately after drawing each chart, not as an afterthought.
-
Name your axes.
ax_line,ax_bar,ax_hbar,ax_grpare far easier to work with thanaxes[0][0]throughaxes[1][1]. -
plt.tight_layout()is not optional. Without it, panel titles overlap the chart borders above them, and axis labels are clipped. -
Color encodes meaning. The green/red WoW percentage annotations tell Sandra immediately which regions improved without requiring her to compare bar heights.
-
Save with
facecolor="white". Without this, saved PNG files may have a transparent background that renders as black in some viewers.
Discussion Questions
-
Priya chose a bar chart instead of a pie chart for regional revenue. Build both and compare them. Which communicates Sandra's question ("which region has the most revenue?") more clearly?
-
The WoW panel annotates percentage change in green or red. How would you modify this to also show the dollar change (e.g., "+$6,500") on the same annotation?
-
Sandra asked for a "target line" on the product margin panel. Add an
ax_hbar.axvline()call showing a target margin of 45%. What color and style would you choose, and why? -
The dashboard currently has no visual hierarchy — all four panels are the same size. Which panel is most important to Sandra? How would you redesign the layout to give that panel more prominence? (Hint: look at
gridspecin the chapter.) -
The
rolling_3mline in Panel 1 hasNaNfor the first two months. How does matplotlib handle theseNaNvalues? What appears in the chart for those months?