|
1 | 1 | """ anyplot.ai |
2 | 2 | ecdf-basic: Basic ECDF Plot |
3 | | -Library: letsplot 4.9.0 | Python 3.14.4 |
4 | | -Quality: 87/100 | Updated: 2026-04-24 |
| 3 | +Library: letsplot 4.10.1 | Python 3.13.14 |
| 4 | +Quality: 88/100 | Updated: 2026-06-25 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
|
15 | 15 | element_line, |
16 | 16 | element_rect, |
17 | 17 | element_text, |
| 18 | + geom_hline, |
| 19 | + geom_text, |
| 20 | + geom_vline, |
18 | 21 | ggplot, |
19 | 22 | ggsize, |
20 | 23 | labs, |
| 24 | + layer_tooltips, |
21 | 25 | scale_y_continuous, |
22 | 26 | stat_ecdf, |
23 | 27 | theme, |
|
31 | 35 | # Theme tokens |
32 | 36 | THEME = os.getenv("ANYPLOT_THEME", "light") |
33 | 37 | PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 38 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
34 | 39 | INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
35 | 40 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
36 | | -GRID = "#C9C7C1" if THEME == "light" else "#565551" |
| 41 | +GRID = "#D8D7D0" if THEME == "light" else "#3A3A36" |
37 | 42 | BRAND = "#009E73" |
| 43 | +AMBER = "#DDCC77" |
38 | 44 |
|
39 | | -# Data — Web service response times (ms) with mixed distribution |
| 45 | +# Data — web service response times (ms) with bimodal distribution |
40 | 46 | np.random.seed(42) |
41 | 47 | response_times = np.concatenate( |
42 | 48 | [np.random.exponential(scale=50, size=150), np.random.normal(loc=200, scale=30, size=50)] |
43 | 49 | ) |
44 | 50 | df = pd.DataFrame({"response_time": response_times}) |
45 | 51 |
|
46 | | -# Plot — ECDF using stat_ecdf with step geometry |
| 52 | +# Percentile x-values for storytelling annotations |
| 53 | +p25_x = np.percentile(response_times, 25) |
| 54 | +p50_x = np.percentile(response_times, 50) |
| 55 | +p75_x = np.percentile(response_times, 75) |
| 56 | +pct_df = pd.DataFrame( |
| 57 | + { |
| 58 | + "x": [p25_x, p50_x, p75_x], |
| 59 | + "y": [0.25, 0.5, 0.75], |
| 60 | + "label": [f"P25: {p25_x:.0f} ms", f"P50: {p50_x:.0f} ms", f"P75: {p75_x:.0f} ms"], |
| 61 | + } |
| 62 | +) |
| 63 | +# Inflection annotation data (geom_text instead of annotate) |
| 64 | +inflection_df = pd.DataFrame({"x": [45], "y": [0.1], "label": ["Bimodal inflection: ~40 ms"]}) |
| 65 | + |
| 66 | +# Title — 3-part format |
| 67 | +title = "ecdf-basic · python · letsplot · anyplot.ai" |
| 68 | + |
| 69 | +# Plot — ECDF with percentile reference lines, bimodal inflection marker, and text annotations |
47 | 70 | plot = ( |
48 | 71 | ggplot(df, aes(x="response_time")) |
49 | | - + stat_ecdf(geom="step", color=BRAND, size=2) |
50 | | - + labs( |
51 | | - x="Response Time (ms)", |
52 | | - y="Cumulative Proportion", |
53 | | - title="Web Service Response Times · ecdf-basic · letsplot · anyplot.ai", |
| 72 | + # Background reference lines drawn first so ECDF renders on top |
| 73 | + + geom_hline(yintercept=0.25, color=INK_SOFT, linetype="dashed", size=0.5, alpha=0.5) |
| 74 | + + geom_hline(yintercept=0.5, color=INK_SOFT, linetype="dashed", size=0.5, alpha=0.5) |
| 75 | + + geom_hline(yintercept=0.75, color=INK_SOFT, linetype="dashed", size=0.5, alpha=0.5) |
| 76 | + # Bimodal inflection marker — gap between exponential fast-path and compute cluster |
| 77 | + + geom_vline(xintercept=40, color=AMBER, linetype="dotted", size=0.7, alpha=0.7) |
| 78 | + # Main ECDF step line — drawn on top of reference elements |
| 79 | + + stat_ecdf( |
| 80 | + geom="step", color=BRAND, size=1.5, tooltips=layer_tooltips().line("Response Time: @response_time{,.0f} ms") |
54 | 81 | ) |
| 82 | + # Percentile x-value labels at each reference line |
| 83 | + + geom_text(data=pct_df, mapping=aes(x="x", y="y", label="label"), color=INK_SOFT, size=3.5, hjust=0, vjust=-0.5) |
| 84 | + # Inflection annotation label |
| 85 | + + geom_text(data=inflection_df, mapping=aes(x="x", y="y", label="label"), color=AMBER, size=3.5, hjust=0) |
| 86 | + + labs(x="Response Time (ms)", y="Cumulative Proportion", title=title) |
55 | 87 | + scale_y_continuous(limits=[0, 1], breaks=[0, 0.25, 0.5, 0.75, 1.0]) |
56 | | - + ggsize(1600, 900) |
| 88 | + + ggsize(800, 450) |
57 | 89 | + theme_minimal() |
58 | 90 | + theme( |
59 | 91 | plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
60 | 92 | panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
61 | | - legend_background=element_rect(fill=PAGE_BG, color=PAGE_BG), |
62 | | - panel_grid_major=element_line(color=GRID, size=0.6), |
| 93 | + panel_border=element_rect(color=GRID, fill=None, size=0.5), |
| 94 | + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), |
| 95 | + panel_grid_major=element_line(color=GRID, size=0.5), |
63 | 96 | panel_grid_minor=element_blank(), |
64 | 97 | axis_line=element_line(color=INK_SOFT, size=0.6), |
65 | 98 | axis_ticks=element_line(color=INK_SOFT, size=0.5), |
66 | | - axis_text=element_text(size=16, color=INK_SOFT), |
67 | | - axis_title=element_text(size=20, color=INK), |
68 | | - plot_title=element_text(size=24, color=INK), |
| 99 | + axis_text=element_text(size=10, color=INK_SOFT, family="monospace"), |
| 100 | + axis_title=element_text(size=12, color=INK), |
| 101 | + plot_title=element_text(size=16, color=INK), |
| 102 | + plot_margin=[10, 10, 10, 10], |
69 | 103 | ) |
70 | 104 | ) |
71 | 105 |
|
72 | 106 | # Save |
73 | | -ggsave(plot, filename=f"plot-{THEME}.png", path=".", scale=3) |
| 107 | +ggsave(plot, filename=f"plot-{THEME}.png", path=".", scale=4) |
74 | 108 | ggsave(plot, filename=f"plot-{THEME}.html", path=".") |
0 commit comments