|
1 | 1 | """ anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
|
10 | 10 | import numpy as np |
11 | 11 |
|
12 | 12 |
|
13 | | -# Theme |
14 | 13 | THEME = os.getenv("ANYPLOT_THEME", "light") |
15 | 14 | PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
16 | 15 | ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
17 | 16 | INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
18 | 17 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
19 | 18 | INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
20 | | -BRAND = "#009E73" |
| 19 | +BRAND = "#009E73" # Imprint palette position 1 |
21 | 20 |
|
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() |
25 | 27 |
|
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) |
28 | 30 | ax.set_facecolor(PAGE_BG) |
29 | 31 |
|
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) |
31 | 36 |
|
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) |
38 | 52 | 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 |
45 | 54 | ) |
46 | 55 |
|
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) |
52 | 63 | ax.set_ylim(0, 1.02) |
53 | | -ax.set_xlim(left=0) |
| 64 | +ax.set_xlim(left=t_min - 0.5) |
| 65 | + |
54 | 66 | ax.spines["top"].set_visible(False) |
55 | 67 | ax.spines["right"].set_visible(False) |
56 | 68 | for s in ("left", "bottom"): |
57 | 69 | ax.spines[s].set_color(INK_SOFT) |
58 | | -ax.grid(True, alpha=0.10, linewidth=0.8, color=INK) |
59 | 70 |
|
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