Skip to content

Commit 6e72d6a

Browse files
feat(bokeh): implement ecdf-basic (#5358)
## Implementation: `ecdf-basic` - python/bokeh Implements the **python/bokeh** version of `ecdf-basic`. **File:** `plots/ecdf-basic/implementations/python/bokeh.py` **Parent Issue:** #976 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24890383540)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 4c72632 commit 6e72d6a

2 files changed

Lines changed: 265 additions & 157 deletions

File tree

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,107 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
ecdf-basic: Basic ECDF Plot
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: bokeh 3.9.0 | Python 3.14.4
4+
Quality: 87/100 | Updated: 2026-04-24
55
"""
66

7+
import os
8+
79
import numpy as np
8-
from bokeh.io import export_png, save
9-
from bokeh.models import ColumnDataSource
10+
from bokeh.io import export_png, output_file, save
11+
from bokeh.models import ColumnDataSource, HoverTool, Label, Span
1012
from bokeh.plotting import figure
11-
from bokeh.resources import CDN
1213

1314

14-
# Data
15-
np.random.seed(42)
16-
values = np.random.randn(200) * 15 + 50 # Normal distribution: mean=50, std=15
15+
# Theme tokens
16+
THEME = os.getenv("ANYPLOT_THEME", "light")
17+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
18+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
19+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
20+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
21+
BRAND = "#009E73" # Okabe-Ito position 1 — always first series
1722

18-
# Compute ECDF
19-
sorted_values = np.sort(values)
20-
ecdf_y = np.arange(1, len(sorted_values) + 1) / len(sorted_values)
23+
# Data: marathon finish times (minutes) for 300 recreational runners
24+
np.random.seed(42)
25+
n_runners = 300
26+
finish_times = np.random.normal(loc=240, scale=32, size=n_runners)
2127

22-
# Create step function data
23-
# For proper ECDF step visualization, duplicate points for horizontal/vertical steps
24-
x_step = np.repeat(sorted_values, 2)[1:]
25-
y_step = np.repeat(ecdf_y, 2)[:-1]
28+
# ECDF
29+
sorted_times = np.sort(finish_times)
30+
cumulative = np.arange(1, n_runners + 1) / n_runners
2631

27-
# Add starting point at first value with y=0
28-
x_step = np.concatenate([[sorted_values[0]], x_step])
29-
y_step = np.concatenate([[0], y_step])
32+
# Key percentiles for storytelling
33+
q25, q50, q75 = np.percentile(sorted_times, [25, 50, 75])
3034

31-
source = ColumnDataSource(data={"x": x_step, "y": y_step})
35+
source = ColumnDataSource(data={"x": sorted_times, "y": cumulative})
3236

3337
# Plot
3438
p = figure(
3539
width=4800,
3640
height=2700,
37-
title="ecdf-basic · bokeh · pyplots.ai",
38-
x_axis_label="Value",
39-
y_axis_label="Cumulative Proportion",
40-
y_range=(0, 1.05),
41+
title="Marathon Finish Times · ecdf-basic · bokeh · anyplot.ai",
42+
x_axis_label="Finish Time (minutes)",
43+
y_axis_label="Cumulative Proportion of Runners",
44+
y_range=(0, 1.02),
45+
background_fill_color=PAGE_BG,
46+
border_fill_color=PAGE_BG,
47+
toolbar_location=None,
4148
)
4249

43-
# Draw step line
44-
p.line(x="x", y="y", source=source, line_width=4, line_color="#306998", alpha=0.9)
50+
# ECDF step line (step-after matches the 1/n jump at each data point)
51+
step_renderer = p.step(x="x", y="y", source=source, line_width=4, line_color=BRAND, mode="after")
52+
53+
# Percentile reference lines (25th, 50th, 75th)
54+
for q_val, q_lbl in [(q25, "25th"), (q50, "50th (median)"), (q75, "75th")]:
55+
p.add_layout(
56+
Span(location=q_val, dimension="height", line_color=INK_SOFT, line_dash="dashed", line_width=2, line_alpha=0.55)
57+
)
58+
p.add_layout(
59+
Label(
60+
x=q_val,
61+
y=0.03,
62+
text=f"{q_lbl}: {q_val:.0f} min",
63+
text_font_size="20pt",
64+
text_color=INK_SOFT,
65+
text_font_style="italic",
66+
x_offset=14,
67+
)
68+
)
4569

46-
# Style
70+
# Hover tool — bokeh's interactive strength
71+
p.add_tools(
72+
HoverTool(
73+
renderers=[step_renderer], tooltips=[("Finish Time", "@x{0.0} min"), ("Cumulative", "@y{0.0%}")], mode="vline"
74+
)
75+
)
76+
77+
# Typography
4778
p.title.text_font_size = "28pt"
79+
p.title.text_color = INK
80+
p.title.text_font_style = "bold"
4881
p.xaxis.axis_label_text_font_size = "22pt"
4982
p.yaxis.axis_label_text_font_size = "22pt"
5083
p.xaxis.major_label_text_font_size = "18pt"
5184
p.yaxis.major_label_text_font_size = "18pt"
5285

53-
# Grid styling
54-
p.grid.grid_line_alpha = 0.3
55-
p.grid.grid_line_dash = "dashed"
86+
# Chrome colors
87+
p.xaxis.axis_label_text_color = INK
88+
p.yaxis.axis_label_text_color = INK
89+
p.xaxis.major_label_text_color = INK_SOFT
90+
p.yaxis.major_label_text_color = INK_SOFT
91+
p.xaxis.axis_line_color = INK_SOFT
92+
p.yaxis.axis_line_color = INK_SOFT
93+
p.xaxis.major_tick_line_color = INK_SOFT
94+
p.yaxis.major_tick_line_color = INK_SOFT
95+
p.xaxis.minor_tick_line_color = None
96+
p.yaxis.minor_tick_line_color = None
97+
98+
# Grid: y-only, subtle solid lines; remove box outline
99+
p.outline_line_color = None
100+
p.xgrid.grid_line_color = None
101+
p.ygrid.grid_line_color = INK
102+
p.ygrid.grid_line_alpha = 0.10
56103

57104
# Save
58-
export_png(p, filename="plot.png")
59-
save(p, filename="plot.html", resources=CDN, title="ecdf-basic · bokeh · pyplots.ai")
105+
export_png(p, filename=f"plot-{THEME}.png")
106+
output_file(f"plot-{THEME}.html", title="Marathon Finish Times · ecdf-basic · bokeh · anyplot.ai")
107+
save(p)

0 commit comments

Comments
 (0)