Skip to content

Commit 0f63dba

Browse files
feat(letsplot): implement ecdf-basic (#9491)
## Implementation: `ecdf-basic` - python/letsplot Implements the **python/letsplot** version of `ecdf-basic`. **File:** `plots/ecdf-basic/implementations/python/letsplot.py` **Parent Issue:** #976 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28161022176)* --------- 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 c7bfd98 commit 0f63dba

2 files changed

Lines changed: 161 additions & 112 deletions

File tree

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
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
55
"""
66

77
import os
@@ -15,9 +15,13 @@
1515
element_line,
1616
element_rect,
1717
element_text,
18+
geom_hline,
19+
geom_text,
20+
geom_vline,
1821
ggplot,
1922
ggsize,
2023
labs,
24+
layer_tooltips,
2125
scale_y_continuous,
2226
stat_ecdf,
2327
theme,
@@ -31,44 +35,74 @@
3135
# Theme tokens
3236
THEME = os.getenv("ANYPLOT_THEME", "light")
3337
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
38+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
3439
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
3540
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
36-
GRID = "#C9C7C1" if THEME == "light" else "#565551"
41+
GRID = "#D8D7D0" if THEME == "light" else "#3A3A36"
3742
BRAND = "#009E73"
43+
AMBER = "#DDCC77"
3844

39-
# Data — Web service response times (ms) with mixed distribution
45+
# Data — web service response times (ms) with bimodal distribution
4046
np.random.seed(42)
4147
response_times = np.concatenate(
4248
[np.random.exponential(scale=50, size=150), np.random.normal(loc=200, scale=30, size=50)]
4349
)
4450
df = pd.DataFrame({"response_time": response_times})
4551

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
4770
plot = (
4871
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")
5481
)
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)
5587
+ scale_y_continuous(limits=[0, 1], breaks=[0, 0.25, 0.5, 0.75, 1.0])
56-
+ ggsize(1600, 900)
88+
+ ggsize(800, 450)
5789
+ theme_minimal()
5890
+ theme(
5991
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
6092
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),
6396
panel_grid_minor=element_blank(),
6497
axis_line=element_line(color=INK_SOFT, size=0.6),
6598
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],
69103
)
70104
)
71105

72106
# Save
73-
ggsave(plot, filename=f"plot-{THEME}.png", path=".", scale=3)
107+
ggsave(plot, filename=f"plot-{THEME}.png", path=".", scale=4)
74108
ggsave(plot, filename=f"plot-{THEME}.html", path=".")

0 commit comments

Comments
 (0)