Skip to content

Commit c729120

Browse files
feat(matplotlib): implement ecdf-basic (#9484)
## Implementation: `ecdf-basic` - python/matplotlib Implements the **python/matplotlib** version of `ecdf-basic`. **File:** `plots/ecdf-basic/implementations/python/matplotlib.py` **Parent Issue:** #976 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28160110432)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent a2518d7 commit c729120

2 files changed

Lines changed: 138 additions & 116 deletions

File tree

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
ecdf-basic: Basic ECDF Plot
3-
Library: matplotlib 3.10.9 | Python 3.14.4
4-
Quality: 89/100 | Updated: 2026-04-24
3+
Library: matplotlib 3.11.0 | Python 3.13.14
4+
Quality: 91/100 | Updated: 2026-06-25
55
"""
66

77
import os
@@ -10,52 +10,65 @@
1010
import numpy as np
1111

1212

13-
# Theme
1413
THEME = os.getenv("ANYPLOT_THEME", "light")
1514
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
1615
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
1716
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1817
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
1918
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
20-
BRAND = "#009E73"
19+
BRAND = "#009E73" # Imprint palette position 1
2120

22-
# Data: API response times (ms) — log-normal distribution with a long tail
23-
np.random.seed(42)
24-
response_times_ms = np.random.lognormal(mean=4.6, sigma=0.55, size=250)
21+
# Data: IoT sensor temperature readings (°C) — bimodal mix of offices and server rooms
22+
np.random.seed(7)
23+
office = np.random.normal(loc=22.0, scale=1.8, size=160)
24+
servers = np.random.normal(loc=18.5, scale=0.9, size=90)
25+
temps = np.concatenate([office, servers])
26+
t_min = temps.min()
2527

26-
# Plot
27-
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
28+
# Canvas — 3200 × 1800 px landscape (figsize × dpi, no bbox_inches='tight')
29+
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
2830
ax.set_facecolor(PAGE_BG)
2931

30-
ax.ecdf(response_times_ms, color=BRAND, linewidth=3.5)
32+
# Subtle fill under the ECDF curve for visual weight
33+
sorted_temps = np.sort(temps)
34+
ecdf_y = np.arange(1, len(sorted_temps) + 1) / len(sorted_temps)
35+
ax.fill_between(sorted_temps, 0, ecdf_y, step="post", alpha=0.10, color=BRAND)
3136

32-
# Percentile reference lines (p50, p95, p99) — common SRE reading
33-
percentiles = [50, 95, 99]
34-
percentile_values = np.percentile(response_times_ms, percentiles)
35-
for p, v in zip(percentiles, percentile_values, strict=True):
36-
ax.axhline(y=p / 100, color=INK_SOFT, linestyle=":", linewidth=1.5, alpha=0.6)
37-
ax.axvline(x=v, ymax=p / 100, color=INK_SOFT, linestyle=":", linewidth=1.5, alpha=0.6)
37+
# ECDF step function (drawn on top of fill)
38+
ax.ecdf(temps, color=BRAND, linewidth=2.5)
39+
40+
# Bimodal mode markers — highlight the two sensor populations
41+
for mode_temp, label in [(18.5, "Server\nrooms"), (22.0, "Offices")]:
42+
ax.axvline(mode_temp, color=INK_MUTED, linestyle="--", linewidth=0.8, alpha=0.45)
43+
ax.text(mode_temp + 0.2, 0.05, label, fontsize=8, color=INK_MUTED, va="bottom", ha="left")
44+
45+
# Quartile reference guides (Q1, Q2, Q3)
46+
quartiles = [25, 50, 75]
47+
q_values = np.percentile(temps, quartiles)
48+
for q, v in zip(quartiles, q_values, strict=True):
49+
yf = q / 100
50+
ax.plot([t_min - 0.3, v], [yf, yf], color=INK_MUTED, linestyle=":", linewidth=0.8, alpha=0.6)
51+
ax.plot([v, v], [0, yf], color=INK_MUTED, linestyle=":", linewidth=0.8, alpha=0.6)
3852
ax.annotate(
39-
f"p{p} {v:.0f} ms",
40-
xy=(v, p / 100),
41-
xytext=(10, -18 if p == 99 else 8),
42-
textcoords="offset points",
43-
fontsize=14,
44-
color=INK_SOFT,
53+
f"Q{q // 25} {v:.1f}°C", xy=(v, yf), xytext=(5, 5), textcoords="offset points", fontsize=9, color=INK_MUTED
4554
)
4655

47-
# Style
48-
ax.set_xlabel("Response Time (ms)", fontsize=20, color=INK)
49-
ax.set_ylabel("Cumulative Proportion of Requests", fontsize=20, color=INK)
50-
ax.set_title("API Response Times · ecdf-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK)
51-
ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT)
56+
# Chrome
57+
title = "Sensor Temperatures · ecdf-basic · python · matplotlib · anyplot.ai"
58+
title_fs = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12
59+
ax.set_title(title, fontsize=title_fs, fontweight="medium", color=INK)
60+
ax.set_xlabel("Temperature (°C)", fontsize=10, color=INK)
61+
ax.set_ylabel("Cumulative Proportion", fontsize=10, color=INK)
62+
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)
5263
ax.set_ylim(0, 1.02)
53-
ax.set_xlim(left=0)
64+
ax.set_xlim(left=t_min - 0.5)
65+
5466
ax.spines["top"].set_visible(False)
5567
ax.spines["right"].set_visible(False)
5668
for s in ("left", "bottom"):
5769
ax.spines[s].set_color(INK_SOFT)
58-
ax.grid(True, alpha=0.10, linewidth=0.8, color=INK)
5970

60-
plt.tight_layout()
61-
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)
71+
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)
72+
73+
fig.subplots_adjust(left=0.09, right=0.97, top=0.92, bottom=0.12)
74+
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

0 commit comments

Comments
 (0)