Skip to content

Commit 9cdda7f

Browse files
feat(pygal): implement ecdf-basic (#5360)
## Implementation: `ecdf-basic` - python/pygal Implements the **python/pygal** version of `ecdf-basic`. **File:** `plots/ecdf-basic/implementations/python/pygal.py` **Parent Issue:** #976 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24891020304)* --------- 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 6e72d6a commit 9cdda7f

2 files changed

Lines changed: 248 additions & 167 deletions

File tree

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,116 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
ecdf-basic: Basic ECDF Plot
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 93/100 | Created: 2025-12-17
3+
Library: pygal 3.1.0 | Python 3.14.4
4+
Quality: 87/100 | Created: 2026-04-24
55
"""
66

7-
import numpy as np
8-
import pygal
9-
from pygal.style import Style
7+
import os
8+
import sys
109

1110

12-
# Data
11+
# Script filename shadows the installed `pygal` package when run as `python pygal.py`;
12+
# dropping the script directory from sys.path lets the real package resolve.
13+
sys.path.pop(0)
14+
15+
import numpy as np # noqa: E402
16+
import pygal # noqa: E402
17+
from pygal.style import Style # noqa: E402
18+
19+
20+
# Theme tokens
21+
THEME = os.getenv("ANYPLOT_THEME", "light")
22+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
23+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
24+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
25+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
26+
BRAND = "#009E73"
27+
ACCENT = "#D55E00"
28+
29+
# Data — 120 food-delivery times (minutes); right-skewed gamma is realistic for
30+
# real order-to-door durations (mean ≈ 30 min, long right tail for outliers).
1331
np.random.seed(42)
14-
values = np.random.randn(100)
32+
delivery_times = np.random.gamma(shape=6.0, scale=5.0, size=120)
1533

16-
# Calculate ECDF
17-
sorted_values = np.sort(values)
34+
# ECDF: sorted values on x, cumulative proportion (k/n) on y
35+
sorted_values = np.sort(delivery_times)
1836
n = len(sorted_values)
1937
ecdf_y = np.arange(1, n + 1) / n
2038

21-
# Build step function data for XY chart
22-
# Each point needs to create a horizontal step then a vertical step
23-
xy_data = []
39+
# Step function: horizontal lead-in at y=0, then each observation adds a vertical
40+
# jump followed by a horizontal segment.
41+
x_lead = float(sorted_values[0]) - 2.0
42+
step_points = [(x_lead, 0.0), (float(sorted_values[0]), 0.0)]
2443
for i in range(n):
25-
if i == 0:
26-
# First point: start from (value, 0) to show initial step
27-
xy_data.append((sorted_values[i], 0))
28-
else:
29-
# Horizontal line from previous point to current x
30-
xy_data.append((sorted_values[i], ecdf_y[i - 1]))
31-
# Vertical step up to current y
32-
xy_data.append((sorted_values[i], ecdf_y[i]))
44+
step_points.append((float(sorted_values[i]), float(ecdf_y[i])))
45+
x_next = float(sorted_values[i + 1]) if i + 1 < n else float(sorted_values[-1]) + 2.0
46+
step_points.append((x_next, float(ecdf_y[i])))
47+
48+
# Quartile reference markers: P25, median, P75 — let readers read percentiles directly
49+
p25 = float(np.percentile(delivery_times, 25))
50+
p50 = float(np.percentile(delivery_times, 50))
51+
p75 = float(np.percentile(delivery_times, 75))
3352

34-
# Extend to the right edge for completeness
35-
xy_data.append((sorted_values[-1] + 0.5, ecdf_y[-1]))
53+
font = "DejaVu Sans, Helvetica, Arial, sans-serif"
3654

37-
# Custom style for 4800x2700 px canvas
3855
custom_style = Style(
39-
background="white",
40-
plot_background="white",
41-
foreground="#333",
42-
foreground_strong="#333",
43-
foreground_subtle="#666",
44-
colors=("#306998",),
56+
background=PAGE_BG,
57+
plot_background=PAGE_BG,
58+
foreground=INK_SOFT,
59+
foreground_strong=INK,
60+
foreground_subtle=INK_MUTED,
61+
colors=(BRAND, ACCENT),
62+
font_family=font,
63+
title_font_family=font,
64+
label_font_family=font,
65+
major_label_font_family=font,
66+
legend_font_family=font,
67+
tooltip_font_family=font,
4568
title_font_size=72,
46-
label_font_size=48,
47-
major_label_font_size=42,
48-
legend_font_size=42,
49-
value_font_size=36,
69+
label_font_size=52,
70+
major_label_font_size=44,
71+
legend_font_size=40,
72+
tooltip_font_size=32,
73+
value_font_size=30,
74+
stroke_opacity=1,
75+
stroke_opacity_hover=1,
76+
opacity=1,
77+
opacity_hover=1,
78+
stroke_width=28,
5079
)
5180

52-
# Create XY chart (scatter-like with lines for step function)
5381
chart = pygal.XY(
5482
width=4800,
5583
height=2700,
5684
style=custom_style,
57-
title="ecdf-basic · pygal · pyplots.ai",
58-
x_title="Value",
85+
title="ecdf-basic · pygal · anyplot.ai",
86+
x_title="Delivery Time (minutes)",
5987
y_title="Cumulative Proportion",
6088
show_dots=False,
61-
stroke_style={"width": 4},
6289
show_x_guides=True,
6390
show_y_guides=True,
6491
show_legend=False,
6592
range=(0, 1.05),
93+
x_labels_major_count=9,
94+
y_labels_major_count=6,
95+
value_formatter=lambda v: f"{v:.2f}",
96+
x_value_formatter=lambda v: f"{v:.0f}",
97+
margin=60,
98+
js=[],
6699
)
67100

68-
# Add ECDF data
69-
chart.add("ECDF", xy_data)
101+
chart.add("ECDF", step_points)
102+
chart.add(
103+
"Quartiles",
104+
[
105+
{"value": (p25, 0.25), "label": f"P25 = {p25:.1f} min"},
106+
{"value": (p50, 0.50), "label": f"Median = {p50:.1f} min"},
107+
{"value": (p75, 0.75), "label": f"P75 = {p75:.1f} min"},
108+
],
109+
stroke=False,
110+
show_dots=True,
111+
dots_size=40,
112+
)
70113

71-
# Save outputs
72-
chart.render_to_file("plot.html")
73-
chart.render_to_png("plot.png")
114+
chart.render_to_png(f"plot-{THEME}.png")
115+
with open(f"plot-{THEME}.html", "wb") as f:
116+
f.write(chart.render())

0 commit comments

Comments
 (0)