Skip to content

Instantly share code, notes, and snippets.

@fzakaria
Created July 28, 2026 03:48
Show Gist options
  • Select an option

  • Save fzakaria/17c72f0eddc0f10469e008e67e1385cc to your computer and use it in GitHub Desktop.

Select an option

Save fzakaria/17c72f0eddc0f10469e008e67e1385cc to your computer and use it in GitHub Desktop.
#!/usr/bin/env nix-shell
#! nix-shell -i python3 --pure
#! nix-shell -p "python3.withPackages(ps: with ps; [ plotnine pandas numpy scipy ])"
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/72841a4a8761d1aed92ef6169a636872c986c76d.tar.gz
"""
Generate the figures for the "look at your data" post.
Self-contained: the shebang above pins nixpkgs to an exact commit and pulls in
plotnine + friends, so `./latency_charts.py` reproduces every figure on any
machine with Nix installed -- no virtualenv, no pip, no system packages.
One synthetic dataset, one fixed seed. A service rolls out a new caching tier
over seven days. Cache *hits* are much faster; cache *misses* pay an extra hop
and are much slower. The rollout fraction climbs 0 -> 100%, so the population
morphs from one hump (all baseline) into two (fast hits + a slow miss tail).
Every chart below is a different lens on the SAME numbers.
chmod +x latency_charts.py && ./latency_charts.py # writes SVGs beside it
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from scipy.stats import gaussian_kde
from plotnine import *
from plotnine import options as p9options
# ----------------------------------------------------------------------------
# Site palette. Charts are transparent line-art on the page's #ebe6da matte, so
# nothing here paints a background. Ink + one accent, echoing the site itself.
# ----------------------------------------------------------------------------
INK = "#1a1815" # near-black warm ink -> "before" / cache hit
INK_MUTED = "#565046"
INK_FAINT = "#6f685b" # muted neutral -> baseline reference
ACCENT = "#9e3413" # burnt vermillion -> "after" / cache miss
RULE = "#dcd5c5" # hairline gridlines
RULE_STRONG= "#c2b9a6"
MATTE = "#ebe6da"
# sequential ramp (one hue, light -> dark) for the rollout magnitude
RAMP7 = ["#e7c9ba", "#d8a888", "#c8855c", "#b56336", "#9e3413", "#7c2810", "#5a1c0b"]
SERIF = "Newsreader, Georgia, 'Times New Roman', serif"
MONO = "JetBrains Mono, ui-monospace, Menlo, monospace"
OUT = "."
W, H = 7.2, 4.3 # inches; ~720px, matches the 700px prose column
# keep text as real <text> (tiny SVG, selectable, sharp in both themes)
import matplotlib
matplotlib.use("Agg") # headless: render straight to file, no display needed
matplotlib.rcParams["svg.fonttype"] = "none"
p9options.figure_size = (W, H)
p9options.dpi = 100
def base_theme(h=H):
return (
theme_minimal()
+ theme(
figure_size=(W, h),
text=element_text(family=SERIF, color=INK),
axis_text=element_text(family=MONO, color=INK_MUTED, size=8),
axis_title=element_text(family=SERIF, color=INK, size=11),
plot_title=element_text(family=SERIF, color=INK, size=13, weight="bold", ha="left"),
plot_subtitle=element_text(family=SERIF, color=INK_MUTED, size=10, ha="left"),
plot_caption=element_text(family=MONO, color=INK_FAINT, size=7, ha="right"),
panel_grid_major=element_line(color=RULE, size=0.4),
panel_grid_minor=element_blank(),
panel_background=element_rect(fill="none", color="none"),
plot_background=element_rect(fill="none", color="none"),
legend_background=element_rect(fill="none", color="none"),
legend_key=element_rect(fill="none", color="none"),
axis_ticks=element_line(color=RULE_STRONG, size=0.3),
axis_line=element_line(color=RULE_STRONG, size=0.4),
legend_position="none",
plot_margin=0.022, # keep right-aligned captions off the edge
)
)
def save(p, name):
path = f"{OUT}/{name}.svg"
p.save(path, verbose=False)
print(f" wrote {path}")
# ----------------------------------------------------------------------------
# 1. The data. Lognormal latencies; tuned so the mean drifts UP a little, the
# median drops a lot, and the tail blows up -- so every summary statistic
# ends up telling a different story.
# ----------------------------------------------------------------------------
def generate():
rng = np.random.default_rng(42)
# (median_ms, sigma) for each path. Tuned so the two after-populations are
# far enough apart (and the miss share big enough) to read as two peaks.
BASE = (100.0, 0.50) # day-0 world: one path
HIT = (45.0, 0.40) # new tier, cache hit -> much faster, tight
MISS = (300.0, 0.48) # new tier, cache miss -> extra hop, slower
# Every request also has a response size. The cache holds small hot objects,
# so the chance of a *miss* climbs with size (logistic in log-size). This is
# what makes the slow tail: the misses are the big objects.
SIZE_MED, SIZE_SIG = 18.0, 0.95 # KB, lognormal
MISS_MID, MISS_WIDTH = 45.0, 0.60 # KB midpoint, logistic width (ln units)
frac = [0.0, 0.15, 0.30, 0.50, 0.70, 0.85, 1.0] # rollout % by day
N = 12_000
def lognorm(median, sigma, n):
return rng.lognormal(mean=np.log(median), sigma=sigma, size=n)
rows = []
for day, f in enumerate(frac):
size = lognorm(SIZE_MED, SIZE_SIG, N)
on_new = rng.random(N) < f
p_miss = 1.0 / (1.0 + np.exp(-(np.log(size) - np.log(MISS_MID)) / MISS_WIDTH))
is_miss = rng.random(N) < p_miss
lat = np.empty(N)
m_base = ~on_new
m_miss = on_new & is_miss
m_hit = on_new & ~is_miss
lat[m_base] = lognorm(*BASE, m_base.sum())
lat[m_hit] = lognorm(*HIT, m_hit.sum())
lat[m_miss] = lognorm(*MISS, m_miss.sum())
path = np.where(m_base, "baseline", np.where(m_hit, "hit", "miss"))
rows.append(pd.DataFrame({"day": day, "rollout": f, "latency": lat,
"size_kb": size, "path": path}))
return pd.concat(rows, ignore_index=True)
df = generate()
before = df[df.day == 0].latency.to_numpy() # 0% rolled out
after = df[df.day == 6].latency.to_numpy() # 100% rolled out
def summary(x):
return dict(mean=x.mean(), p50=np.percentile(x, 50),
p95=np.percentile(x, 95), p99=np.percentile(x, 99))
sb, sa = summary(before), summary(after)
print("\n stat before after delta")
for k in ("mean", "p50", "p95", "p99"):
d = (sa[k] - sb[k]) / sb[k] * 100
print(f" {k:>5} {sb[k]:7.0f} {sa[k]:7.0f} {d:+5.0f}%")
# crossing point of the two ECDFs
grid = np.linspace(1, 800, 4000)
Fb = np.searchsorted(np.sort(before), grid) / before.size
Fa = np.searchsorted(np.sort(after), grid) / after.size
xcross = grid[np.argmin(np.abs(Fa - Fb) + (grid < 40) * 9)] # ignore the origin tie
pcross = np.interp(xcross, grid, Fb)
print(f"\n ECDFs cross near x={xcross:.0f} ms (p={pcross:.2f})\n")
XCAP = 750
paired = pd.DataFrame({
"latency": np.concatenate([before, after]),
"period": ["before"] * before.size + ["after"] * after.size,
})
PERIOD_C = {"before": INK, "after": ACCENT}
# ----------------------------------------------------------------------------
# FIG 1 -- the trap: the dashboard shows one number (the mean), and it moved
# the wrong way.
# ----------------------------------------------------------------------------
def fig_mean():
d = pd.DataFrame({"period": ["before", "after"],
"mean": [sb["mean"], sa["mean"]]})
d["period"] = pd.Categorical(d["period"], ["before", "after"])
# 95% CI of the mean (tiny at N=12k) -- makes "no significant change" explicit
def ci(x):
return 1.96 * x.std(ddof=1) / np.sqrt(x.size)
d["ci"] = [ci(before), ci(after)]
p = (
ggplot(d, aes("period", "mean", fill="period"))
+ geom_col(width=0.55, show_legend=False)
+ geom_errorbar(aes(ymin="mean-ci", ymax="mean+ci"), width=0.14,
color=INK, size=0.5)
+ geom_text(aes(label="mean.round(0).astype(int).astype(str) + ' ms'"),
nudge_y=6, family=MONO, size=9, color=INK)
+ scale_fill_manual(values=PERIOD_C)
+ scale_y_continuous(expand=(0, 0, 0.12, 0))
+ labs(title="The dashboard says you made it worse",
subtitle="Mean request latency before vs. after the rollout (± 95% CI).",
x="", y="mean latency (ms)", caption="synthetic · N=12,000 each")
+ base_theme(3.6)
+ theme(panel_grid_major_x=element_blank())
)
save(p, "latency_1_mean")
# ----------------------------------------------------------------------------
# FIG 2 -- density: the "after" is not one population, it is two.
# ----------------------------------------------------------------------------
def fig_density():
d = paired[paired.latency <= XCAP]
p = (
ggplot(d, aes("latency", fill="period", color="period"))
+ geom_density(aes(y=after_stat("density")), alpha=0.35, size=0.9)
+ scale_fill_manual(values=PERIOD_C)
+ scale_color_manual(values=PERIOD_C)
+ scale_x_continuous(expand=(0, 0))
+ scale_y_continuous(expand=(0, 0, 0.05, 0))
+ annotate("text", x=70, y=0.0122, label="after", color=ACCENT,
family=SERIF, size=11, ha="left")
+ annotate("text", x=150, y=0.0075, label="before", color=INK,
family=SERIF, size=11, ha="left")
+ annotate("text", x=300, y=0.0022, label="a second hump\nappears",
color=ACCENT, family=MONO, size=7.5, ha="left", lineheight=0.9)
+ labs(title="Same average, a very different shape",
subtitle="Distribution of latency. The rollout split the traffic in two.",
x="latency (ms)", y="density",
caption=f"tail beyond {XCAP} ms not shown")
+ base_theme()
)
save(p, "latency_2_density")
# ----------------------------------------------------------------------------
# FIG 3 -- the hero: two ECDFs that CROSS. Read every percentile off one panel.
# ----------------------------------------------------------------------------
def ecdf(x):
xs = np.sort(x)
ys = np.arange(1, xs.size + 1) / xs.size
return xs, ys
def fig_cdf():
parts = []
for name in ("before", "after"):
xs, ys = ecdf(before if name == "before" else after)
parts.append(pd.DataFrame({"latency": xs, "F": ys, "period": name}))
d = pd.concat(parts)
d = d[d.latency <= XCAP]
p = (
ggplot(d, aes("latency", "F", color="period"))
+ geom_vline(xintercept=xcross, color=INK_FAINT, size=0.4, linetype="dotted")
+ geom_step(size=1.0)
+ scale_color_manual(values=PERIOD_C)
+ scale_x_continuous(expand=(0, 0))
+ scale_y_continuous(labels=lambda b: [f"{int(v*100)}%" for v in b],
expand=(0.01, 0))
+ annotate("text", x=88, y=0.94, label="after", color=ACCENT,
family=SERIF, size=11, ha="left")
+ annotate("text", x=300, y=0.58, label="before", color=INK,
family=SERIF, size=11, ha="left")
+ annotate("text", x=xcross + 14, y=0.18,
label=f"they cross at\n~{xcross:.0f} ms (p{pcross*100:.0f})",
color=INK_MUTED, family=MONO, size=7.5, ha="left", lineheight=0.9)
+ labs(title="One panel, every percentile",
subtitle="Empirical CDF. Left of the crossing the change wins; right of it, it loses.",
x="latency (ms)", y="share of requests ≤ x",
caption=f"tail beyond {XCAP} ms not shown")
+ base_theme(4.6)
)
save(p, "latency_3_cdf")
# ----------------------------------------------------------------------------
# FIG 4 -- the shift function: Q_after(p) - Q_before(p) at every quantile, with
# a bootstrap 95% band. Quantifies "who won, and by how much, where."
# ----------------------------------------------------------------------------
def fig_shift():
ps = np.linspace(0.02, 0.99, 98)
delta = np.percentile(after, ps * 100) - np.percentile(before, ps * 100)
rng = np.random.default_rng(7)
B = 400
boot = np.empty((B, ps.size))
for i in range(B):
ba = rng.choice(after, after.size, replace=True)
bb = rng.choice(before, before.size, replace=True)
boot[i] = np.percentile(ba, ps * 100) - np.percentile(bb, ps * 100)
lo, hi = np.percentile(boot, [2.5, 97.5], axis=0)
d = pd.DataFrame({"p": ps, "delta": delta, "lo": lo, "hi": hi})
d["sign"] = np.where(d.delta <= 0, "faster", "slower")
zc = float(np.interp(0, delta, ps)) # p where delta crosses 0
p = (
ggplot(d, aes("p", "delta"))
+ geom_hline(yintercept=0, color=INK, size=0.5)
+ geom_ribbon(aes(ymin="lo", ymax="hi"), fill=INK_FAINT, alpha=0.18)
+ geom_line(size=1.1, color=ACCENT)
+ geom_point(aes(x=zc, y=0), color=INK, size=2.2)
+ scale_x_continuous(labels=lambda b: [f"{int(v*100)}%" for v in b],
expand=(0.01, 0))
+ annotate("text", x=0.06, y=-30, label="faster\n(below zero)", color=INK_MUTED,
family=MONO, size=7.5, ha="left", lineheight=0.9)
+ annotate("text", x=0.97, y=d.hi.max() * 0.7, label="slower\n(above zero)",
color=ACCENT, family=MONO, size=7.5, ha="right", lineheight=0.9)
+ annotate("text", x=zc + 0.01, y=28, label=f"breaks even\nat p{zc*100:.0f}",
color=INK, family=MONO, size=7.5, ha="left", lineheight=0.9)
+ labs(title="Who won, and by how much, at every percentile",
subtitle="Shift function: after(p) − before(p) in ms, with a 95% bootstrap band.",
x="percentile", y="change in latency (ms)",
caption="400 bootstrap resamples")
+ base_theme(4.4)
)
save(p, "latency_4_shift")
# ----------------------------------------------------------------------------
# FIG 5 -- ridgeline: the miss-hump being BORN as the rollout ramps day by day.
# ----------------------------------------------------------------------------
def fig_ridge():
# Latency is lognormal, so we estimate and draw on a LOG axis: two lognormal
# humps become two clearly separated bumps instead of one tall spike beside
# an invisible shoulder. This is the honest way to look at latency.
days = sorted(df.day.unique())
LO, HI = 25, XCAP
glog = np.linspace(np.log10(LO), np.log10(HI), 400)
spacing = 0.9
scale = 2.0
rib, crest = [], []
bases, ylabels = [], []
for day in days:
x = df[(df.day == day) & (df.latency.between(LO, HI))].latency.to_numpy()
kde = gaussian_kde(np.log10(x), bw_method=0.16)
dens = kde(glog)
dens = dens / dens.max() * scale
base = (len(days) - 1 - day) * spacing # day 0 on top
f = df[df.day == day].rollout.iloc[0]
rib.append(pd.DataFrame({"x": 10 ** glog, "ymin": base, "ymax": base + dens,
"day": day}))
crest.append(pd.DataFrame({"x": 10 ** glog, "y": base + dens, "day": day}))
bases.append(base)
ylabels.append(f"day {day}\n{int(f*100)}%")
R = pd.concat(rib)
C = pd.concat(crest)
R["day"] = pd.Categorical(R.day, days)
fill_map = {d: RAMP7[d] for d in days}
p = (
ggplot()
+ geom_ribbon(R, aes("x", ymin="ymin", ymax="ymax", fill="day", group="day"),
alpha=0.92)
+ geom_line(C, aes("x", "y", group="day"), color=INK, size=0.35)
+ scale_fill_manual(values=fill_map)
+ scale_x_log10(breaks=[30, 60, 100, 200, 400, 700], expand=(0, 0))
+ scale_y_continuous(breaks=bases, labels=ylabels,
expand=(0.01, 0, 0.04, 0))
+ labs(title="Watch the second peak be born",
subtitle="Each day's latency (log scale) as the new tier ramps 0 → 100%.",
x="latency (ms, log scale)", y="",
caption="log x · tails outside 25–750 ms not shown")
+ base_theme(5.6)
+ theme(panel_grid_major_y=element_blank(),
axis_text_y=element_text(family=MONO, color=INK_MUTED, size=7,
lineheight=0.9),
panel_grid_major_x=element_line(color=RULE, size=0.3))
)
save(p, "latency_5_ridge")
# ----------------------------------------------------------------------------
# FIG 5b -- heatmap: rollout day x latency, colour = per-day density. The second
# population appears as a distinct band splitting off, day by day.
# ----------------------------------------------------------------------------
def fig_heatmap():
days = sorted(df.day.unique())
LO, HI = 25, XCAP
# smooth per-day density in log space (not raw histograms -> no striping)
glog = np.linspace(np.log10(LO), np.log10(HI), 140)
edges10 = 10 ** np.concatenate([[glog[0] - (glog[1] - glog[0]) / 2],
(glog[:-1] + glog[1:]) / 2,
[glog[-1] + (glog[1] - glog[0]) / 2]])
cells = []
for day in days:
x = df[(df.day == day) & (df.latency.between(LO, HI))].latency.to_numpy()
dens = gaussian_kde(np.log10(x), bw_method=0.16)(glog)
dens = (dens / dens.max()) ** 0.7 # per-column peak, gamma-lifted
f = df[df.day == day].rollout.iloc[0]
cells.append(pd.DataFrame({
"day": day, "rollout": f, "dens": dens,
"ymin": edges10[:-1], "ymax": edges10[1:],
"xmin": day - 0.5, "xmax": day + 0.5,
}))
C = pd.concat(cells)
xlabels = [f"day {d}\n{int(df[df.day==d].rollout.iloc[0]*100)}%" for d in days]
p = (
ggplot(C)
+ geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax",
fill="dens"))
+ scale_fill_gradientn(
colors=["#efe7db", "#e0b49a", "#c8855c", "#9e3413", "#5a1c0b"],
guide=None)
+ scale_y_log10(breaks=[30, 60, 100, 200, 400, 700], expand=(0, 0))
+ scale_x_continuous(breaks=days, labels=xlabels, expand=(0, 0))
+ labs(title="Watch the second band appear",
subtitle="Latency density per rollout day (each column scaled to its own peak).",
x="", y="latency (ms, log scale)",
caption="log y · tails outside 25–750 ms not shown")
+ base_theme(4.8)
+ theme(panel_grid_major=element_blank(),
axis_text_x=element_text(family=MONO, color=INK_MUTED, size=7,
lineheight=0.9))
)
save(p, "latency_5b_heatmap")
# ----------------------------------------------------------------------------
# FIG 6 -- filtered CDF: split the mystery by cache path and it resolves into
# two clean, unimodal CDFs. The bimodality had a cause all along.
# ----------------------------------------------------------------------------
def fig_filtered():
parts = []
# baseline reference from day 0
xs, ys = ecdf(before)
parts.append(pd.DataFrame({"latency": xs, "F": ys, "grp": "baseline (before)"}))
for path in ("hit", "miss"):
x = df[(df.day == 6) & (df.path == path)].latency.to_numpy()
xs, ys = ecdf(x)
parts.append(pd.DataFrame({"latency": xs, "F": ys,
"grp": f"cache {path}"}))
d = pd.concat(parts)
d = d[d.latency <= XCAP]
cmap = {"cache hit": INK, "cache miss": ACCENT, "baseline (before)": INK_FAINT}
lmap = {"cache hit": "solid", "cache miss": "solid", "baseline (before)": "dashed"}
d["grp"] = pd.Categorical(d.grp, ["cache hit", "baseline (before)", "cache miss"])
d6 = df[df.day == 6]
hit_pct = round((d6.path == "hit").mean() * 100)
p = (
ggplot(d, aes("latency", "F", color="grp", linetype="grp"))
+ geom_step(size=1.0)
+ scale_color_manual(values=cmap)
+ scale_linetype_manual(values=lmap)
+ scale_x_continuous(expand=(0, 0))
+ scale_y_continuous(labels=lambda b: [f"{int(v*100)}%" for v in b],
expand=(0.01, 0))
+ annotate("text", x=62, y=0.55, label=f"cache hit\n({hit_pct:.0f}% of traffic)",
color=INK, family=MONO, size=7.5, ha="left", lineheight=0.9)
+ annotate("text", x=330, y=0.45, label="cache miss\n(the slow tail)",
color=ACCENT, family=MONO, size=7.5, ha="left", lineheight=0.9)
+ annotate("text", x=175, y=0.90, label="baseline", color=INK_FAINT,
family=MONO, size=7.5, ha="left")
+ labs(title="The bimodality had a cause",
subtitle="The same 'after' requests, split by cache outcome: two clean populations.",
x="latency (ms)", y="share of requests ≤ x",
caption=f"tail beyond {XCAP} ms not shown")
+ base_theme(4.6)
)
save(p, "latency_6_filtered")
# ----------------------------------------------------------------------------
# FIG 7 -- the jointplot: WHY the slow requests are slow. Latency vs response
# size, coloured by cache outcome, with a density on each margin. The
# misses are the big objects; that is the whole mechanism.
# Built in bare matplotlib because a jointplot needs marginal axes,
# which the grammar of graphics has no vocabulary for.
# ----------------------------------------------------------------------------
def fig_joint():
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
d6 = df[(df.day == 6) & (df.latency <= XCAP)]
rng = np.random.default_rng(11)
grp = {"hit": d6[d6.path == "hit"], "miss": d6[d6.path == "miss"]}
col = {"hit": INK, "miss": ACCENT}
fig = plt.figure(figsize=(W, 5.4))
gs = GridSpec(2, 2, width_ratios=[5, 1], height_ratios=[1, 4],
hspace=0.04, wspace=0.04,
left=0.10, right=0.975, top=0.86, bottom=0.10)
ax = fig.add_subplot(gs[1, 0])
axt = fig.add_subplot(gs[0, 0], sharex=ax)
axr = fig.add_subplot(gs[1, 1], sharey=ax)
for a in (fig.gca(), ax, axt, axr):
a.set_facecolor("none")
fig.patch.set_alpha(0)
for name, g in grp.items():
s = g.sample(min(2500, len(g)), random_state=rng.integers(1 << 30))
ax.scatter(s.size_kb, s.latency, s=5, c=col[name], alpha=0.16,
edgecolors="none", rasterized=True)
# marginal densities in log space, drawn as light fills
def marg(a, values, color, horizontal):
lo, hi = np.log10(values.min()), np.log10(values.max())
grid = np.linspace(lo, hi, 200)
dens = gaussian_kde(np.log10(values), bw_method=0.2)(grid)
dens = dens / dens.max()
x10 = 10 ** grid
if horizontal:
a.fill_betweenx(x10, 0, dens, color=color, alpha=0.35, lw=0)
a.plot(dens, x10, color=color, lw=1.0)
else:
a.fill_between(x10, 0, dens, color=color, alpha=0.35, lw=0)
a.plot(x10, dens, color=color, lw=1.0)
for name, g in grp.items():
marg(axt, g.size_kb, col[name], horizontal=False)
marg(axr, g.latency, col[name], horizontal=True)
ax.set_xscale("log"); ax.set_yscale("log")
ax.set_xlim(2, d6.size_kb.quantile(0.995))
ax.set_ylim(20, XCAP)
ax.set_xticks([2, 5, 10, 20, 50, 100, 200])
ax.set_yticks([30, 60, 100, 200, 400])
from matplotlib.ticker import ScalarFormatter
for axis in (ax.xaxis, ax.yaxis):
axis.set_major_formatter(ScalarFormatter())
ax.set_xlabel("response size (KB, log scale)", family=SERIF, color=INK, size=11)
ax.set_ylabel("latency (ms, log scale)", family=SERIF, color=INK, size=11)
ax.tick_params(labelsize=8, colors=INK_MUTED)
for lbl in ax.get_xticklabels() + ax.get_yticklabels():
lbl.set_family(MONO)
ax.grid(True, color=RULE, lw=0.4)
ax.text(0.04, 0.10, "cache hit", transform=ax.transAxes, color=INK,
family=SERIF, size=12, ha="left")
ax.text(0.78, 0.88, "cache miss", transform=ax.transAxes, color=ACCENT,
family=SERIF, size=12, ha="left")
for a in (axt, axr):
for spine in a.spines.values():
spine.set_visible(False)
a.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
axt.margins(y=0); axr.margins(x=0)
axt.set_ylim(0, 1.08); axr.set_xlim(0, 1.08)
fig.text(0.10, 0.955, "The slow requests are the big ones",
family=SERIF, color=INK, size=13, weight="bold", ha="left")
fig.text(0.10, 0.905,
"Latency vs. response size (after traffic). Each margin is a density.",
family=SERIF, color=INK_MUTED, size=10, ha="left")
fig.text(0.975, 0.012, "day 6 · sampled points · tail beyond 750 ms not shown",
family=MONO, color=INK_FAINT, size=7, ha="right")
fig.savefig("latency_7_joint.svg", transparent=True)
plt.close(fig)
print(" wrote ./latency_7_joint.svg")
if __name__ == "__main__":
print("generating figures...")
fig_mean()
fig_density()
fig_cdf()
fig_shift()
fig_ridge()
fig_heatmap()
fig_filtered()
fig_joint()
print("done.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment