chart-check / make_examples.py
cetusian's picture
Deploy chart-check
0226ff4 verified
Raw History Blame Contribute Delete
11.8 kB
"""Draw the example charts in examples/. Run once after an edit; the PNGs are committed.
python make_examples.py
matplotlib is only needed here, not in the Space. Every company, place and number is made up.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402
from matplotlib.patches import Polygon # noqa: E402
OUT = Path(__file__).parent / "examples"
SIZE, DPI = (10, 6.25), 110
def figure(bg="white"):
fig, ax = plt.subplots(figsize=SIZE, dpi=DPI, facecolor=bg)
ax.set_facecolor(bg)
fig.subplots_adjust(left=0.09, right=0.93, top=0.8, bottom=0.14)
for s in ("top", "right"):
ax.spines[s].set_visible(False)
return fig, ax
def titles(fig, title, sub, source, color="#111", sub_color="#555"):
fig.text(0.09, 0.93, title, fontsize=22, fontweight="bold", color=color)
fig.text(0.09, 0.87, sub, fontsize=13, color=sub_color)
fig.text(0.09, 0.03, source, fontsize=9.5, color=sub_color)
def save(fig, name):
fig.savefig(OUT / f"{name}.png", facecolor=fig.get_facecolor())
plt.close(fig)
def upside_down_deaths():
years = np.arange(2012, 2026)
deaths = [196, 188, 192, 185, 190, 183, 187, 191, 228, 251, 262, 280, 297, 311]
fig, ax = figure()
ax.fill_between(years, deaths, 0, color="#b3121f", alpha=0.95)
ax.plot(years, deaths, color="#7a0a14", lw=2)
ax.set_ylim(350, 0) # upside down: 0 at the top
ax.set_xlim(2012, 2025)
ax.set_yticks([0, 50, 100, 150, 200, 250, 300, 350])
ax.axvline(2019, color="white", lw=1.5, ls="--")
ax.text(2019.15, 30, "2019: speed-camera law", color="white", fontsize=12, fontweight="bold")
ax.set_ylabel("Road deaths per year")
titles(fig, "Road deaths in Norvale", "Yearly road deaths, 2012–2025", "Source: Norvale Transport Office")
save(fig, "upside-down-deaths")
def ice_cream_sharks():
years = np.arange(2015, 2026)
ice = [51.2, 51.9, 52.3, 53.4, 53.9, 54.1, 55.6, 56.2, 57.0, 57.4, 58.3]
sharks = [2, 3, 3, 5, 5, 6, 7, 8, 9, 9, 11]
fig, ax = figure()
ax.plot(years, ice, color="#e07b00", lw=4, marker="o", ms=8, label="Ice cream sales (€ million)")
ax.set_ylim(50, 59)
ax.set_ylabel("Ice cream sales (€ million)", color="#e07b00", fontsize=12)
ax.tick_params(axis="y", colors="#e07b00")
ax2 = ax.twinx()
ax2.plot(years, sharks, color="#1560bd", lw=4, marker="s", ms=8, label="Shark attacks")
ax2.set_ylim(0, 12)
ax2.set_ylabel("Shark attacks", color="#1560bd", fontsize=12)
ax2.tick_params(axis="y", colors="#1560bd")
ax2.spines["top"].set_visible(False)
ax.legend(handles=ax.get_lines() + ax2.get_lines(), loc="upper left", frameon=False, fontsize=12)
titles(fig, "Ice cream sales vs. shark attacks", "Coral Bay, 2015–2025. Left axis: sales. Right axis: attacks.",
"Source: Coral Bay Tourism Board, Coral Bay Lifeguard Service")
save(fig, "ice-cream-sharks")
def users_still_growing():
new = [1.9, 1.8, 1.7, 1.6, 1.4, 1.2, 1.0, 0.85, 0.7, 0.55, 0.45, 0.35]
total = np.cumsum(new)
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
fig, ax = figure("#0f1b2d")
ax.bar(months, total, color="#39d98a", width=0.7)
for i, v in enumerate(total):
ax.text(i, v + 0.2, f"{v:.1f}M", ha="center", color="white", fontsize=11, fontweight="bold")
ax.set_ylim(0, 15)
ax.set_ylabel("Total users since launch (millions)", color="#c9d3e0")
ax.tick_params(colors="#c9d3e0")
for s in ("left", "bottom"):
ax.spines[s].set_color("#51607a")
titles(fig, f"Pingly: {total[-1]:.1f} million users and counting", "Cumulative sign-ups since launch, 2025",
"Pingly investor update, Q4 2025", color="white", sub_color="#c9d3e0")
save(fig, "users-still-growing")
def pie_3d():
shares = {"Brightwave": 24, "Kestrel": 29, "Novaline": 26, "Others": 21}
colors = {"Brightwave": "#ff5a1f", "Kestrel": "#6b7a8f", "Novaline": "#9aa7b8", "Others": "#c8d0da"}
fig, ax = plt.subplots(figsize=SIZE, dpi=DPI, facecolor="white")
ax.set_aspect("equal")
ax.axis("off")
tilt, depth = 0.42, 0.22
# Brightwave sits in front (around 270°) and is pulled out toward the viewer.
start = 270 - shares["Brightwave"] * 3.6 / 2
wedges = []
for name, pct in shares.items():
a0, a1 = start, start + pct * 3.6
start = a1
mid = np.radians((a0 + a1) / 2)
off = 0.22 if name == "Brightwave" else 0.0
wedges.append((name, pct, a0, a1, off * np.cos(mid), off * np.sin(mid) * tilt))
def rim(a0, a1, dx, dy, dz=0.0):
t = np.radians(np.linspace(a0, a1, 80))
return np.column_stack([dx + np.cos(t), dy + tilt * np.sin(t) - dz])
def shade(c, f):
r, g, b = matplotlib.colors.to_rgb(c)
return (r * f, g * f, b * f)
# back to front: wedges whose middle is further up are drawn first
for name, pct, a0, a1, dx, dy in sorted(wedges, key=lambda w: -np.sin(np.radians((w[2] + w[3]) / 2))):
top = rim(a0, a1, dx, dy)
side = np.vstack([top, rim(a1, a0, dx, dy, depth)])
ax.add_patch(Polygon(side, color=shade(colors[name], 0.7), lw=0))
ax.add_patch(Polygon(np.vstack([[dx, dy], top]), color=colors[name], ec="white", lw=1.5))
mid = np.radians((a0 + a1) / 2)
big = name == "Brightwave"
ax.text(dx + 0.62 * np.cos(mid), dy + 0.62 * tilt * np.sin(mid) - (0.05 if big else 0), f"{name}\n{pct}%",
ha="center", va="center", fontsize=20 if big else 11, fontweight="bold",
color="white" if big else "#222")
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-0.95, 0.75)
fig.subplots_adjust(left=0.02, right=0.98, top=0.82, bottom=0.08)
titles(fig, "Smart speaker market share", "Eastland, 2025 unit sales", "Source: Brightwave marketing team")
save(fig, "pie-3d")
def revenue_from_98():
q = ["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"]
rev = [98.4, 98.9, 99.3, 100.6]
fig, ax = figure("#101828")
ax.bar(q, rev, color=["#475467"] * 3 + ["#fdb022"], width=0.6)
for i, v in enumerate(rev):
ax.text(i, v + 0.05, f"${v:.1f}M", ha="center", color="white", fontsize=15, fontweight="bold")
ax.set_ylim(98, 101)
ax.set_yticks([98, 98.5, 99, 99.5, 100, 100.5, 101])
ax.tick_params(colors="#d0d5dd", labelsize=12)
for s in ("left", "bottom"):
ax.spines[s].set_color("#475467")
ax.set_ylabel("Revenue ($ million)", color="#d0d5dd")
titles(fig, "Q4: record revenue", "Halvorsen Logistics, quarterly revenue", "Halvorsen Logistics Q4 2025 earnings deck",
color="white", sub_color="#d0d5dd")
save(fig, "revenue-from-98")
def best_three_months():
rng = np.random.default_rng(7)
days = np.arange(92)
walk = rng.normal(0, 0.12, 92).cumsum()
price = 41 + 7.4 * days / 91 + walk - walk[-1] * days / 91 # ends at exactly $48.40
fig, ax = figure()
ax.plot(days, price, color="#12b76a", lw=2.5)
ax.fill_between(days, price, 0, color="#12b76a", alpha=0.12)
ax.set_ylim(0, 55)
ax.set_xlim(0, 91)
ax.set_xticks([0, 30, 61, 91], ["1 Jul", "31 Jul", "31 Aug", "30 Sep"])
ax.yaxis.tick_right()
ax.spines["left"].set_visible(False)
ax.spines["right"].set_visible(True)
ax.set_yticks(range(0, 51, 10), [f"${v}" for v in range(0, 51, 10)])
for i, r in enumerate(["1M", "3M", "1Y", "5Y"]):
sel = r == "3M"
fig.text(0.62 + i * 0.075, 0.87, r, fontsize=13, fontweight="bold", ha="center",
color="white" if sel else "#667085",
bbox=dict(boxstyle="round,pad=0.35", fc="#12b76a" if sel else "#f2f4f7", ec="none"))
fig.text(0.09, 0.87, "HRBR $48.40 +18.0% (3M)", fontsize=14, color="#12b76a", fontweight="bold")
# the 5-year view the post left out: flat, with the last 3 months shaded
t = np.arange(60)
five = 41 + np.sin(t / 4.0) * 3.5 + rng.normal(0, 0.6, 60)
five[-3:] = [43.5, 46, 48.4]
five[0] = 47.8
inset = fig.add_axes([0.14, 0.2, 0.42, 0.3], facecolor="white")
inset.plot(t, five, color="#98a2b3", lw=1.8)
inset.axvspan(57, 59, color="#12b76a", alpha=0.3)
inset.set_ylim(30, 55)
inset.set_xticks([0, 59], ["2021", "2026"], fontsize=9)
inset.set_yticks([])
for s in ("top", "right", "left"):
inset.spines[s].set_visible(False)
inset.set_title("Same stock over 5 years: +1.2%", fontsize=13, fontweight="bold", color="#344054", loc="left")
inset.text(58, 51, "these\n3 months", fontsize=9.5, color="#067647", ha="right", va="top")
fig.text(0.09, 0.93, "Harbor Bank", fontsize=22, fontweight="bold", color="#111")
fig.text(0.09, 0.03, "Share price, last 3 months · 2026", fontsize=9.5, color="#555")
save(fig, "best-three-months")
def log_flattening():
weeks = np.arange(1, 21)
cases = np.round(3 * np.exp(np.cumsum(np.linspace(0.7, 0.12, 20)))).astype(int)
fig, ax = figure()
ax.plot(weeks, cases, color="#7a5af8", lw=3.5, marker="o", ms=6)
ax.set_yscale("log")
ax.set_ylim(1, 100000)
ax.set_yticks([1, 10, 100, 1000, 10000, 100000], ["1", "10", "100", "1,000", "10,000", "100,000"])
ax.minorticks_off()
ax.set_xticks([1, 5, 10, 15, 20])
ax.set_xlabel("Week of outbreak")
ax.set_ylabel("Total measles cases")
ax.axvspan(16, 20, color="#7a5af8", alpha=0.08)
ax.text(18, 1.6, "last month", ha="center", color="#7a5af8", fontsize=11)
titles(fig, "Measles in Ostmark County", f"Total confirmed cases: {cases[-1]:,}", "Source: Ostmark County Health Department")
save(fig, "log-flattening")
def crime_totals():
cities = ["Riverton\npop. 2,100,000", "Millbrook\npop. 310,000", "Ashby\npop. 95,000", "Elm Falls\npop. 38,000"]
burglaries = [8400, 2100, 610, 420]
fig, ax = figure()
ax.bar(cities, burglaries, color=["#d92d20", "#98a2b3", "#98a2b3", "#98a2b3"], width=0.6)
for i, v in enumerate(burglaries):
ax.text(i, v + 150, f"{v:,}", ha="center", fontsize=15, fontweight="bold")
ax.set_ylim(0, 9500)
ax.set_ylabel("Reported burglaries, 2025")
ax.tick_params(axis="x", labelsize=12)
titles(fig, "Where are the most break-ins?", "Reported burglaries by city, 2025", "Source: State Police annual report")
save(fig, "crime-totals")
def honest_library():
years = ["2021", "2022", "2023", "2024", "2025"]
loans = [41, 49, 58, 70, 83]
fig, ax = figure()
ax.bar(years, loans, color="#2e90fa", width=0.6)
for i, v in enumerate(loans):
ax.text(i, v + 1.5, f"{v}k", ha="center", fontsize=14, fontweight="bold")
ax.set_ylim(0, 100)
ax.set_ylabel("Books loaned (thousands)")
titles(fig, "Tallow Creek Library loans", "Books loaned per year", "Source: Tallow Creek Public Library")
save(fig, "honest-library")
def honest_tram():
periods = ["2024\nbefore the tram", "2025\nafter the tram"]
commute = [32.1, 29.0]
fig, ax = figure()
ax.bar(periods, commute, color=["#98a2b3", "#0e9384"], width=0.5)
for i, v in enumerate(commute):
ax.text(i, v + 0.8, f"{v:.1f} min", ha="center", fontsize=16, fontweight="bold")
ax.set_ylim(0, 40)
ax.set_ylabel("Average one-way commute (minutes)")
ax.tick_params(axis="x", labelsize=13)
titles(fig, "Commute times in Larkspur", "Average one-way commute, the year before and after the new tram line",
"Source: Larkspur City Travel Survey")
save(fig, "honest-tram")
if __name__ == "__main__":
OUT.mkdir(exist_ok=True)
for draw in (upside_down_deaths, ice_cream_sharks, users_still_growing, pie_3d, revenue_from_98,
best_three_months, log_flattening, crime_totals, honest_library, honest_tram):
draw()