Skip to content

Commit f922a9f

Browse files
feat(pygal): implement scatter-basic (#5315)
## Implementation: `scatter-basic` - python/pygal Implements the **python/pygal** version of `scatter-basic`. **File:** `plots/scatter-basic/implementations/python/pygal.py` **Parent Issue:** #611 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24853810841)* --------- 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 919d6c6 commit f922a9f

2 files changed

Lines changed: 174 additions & 203 deletions

File tree

Lines changed: 62 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,100 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
scatter-basic: Basic Scatter Plot
3-
Library: pygal 3.1.0 | Python 3.14
4-
Quality: 87/100 | Created: 2025-12-22
3+
Library: pygal 3.1.0 | Python 3.14.4
4+
Quality: 87/100 | Created: 2026-04-23
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 — study hours vs exam scores with realistic positive correlation
13-
np.random.seed(42)
14-
n = 115
15-
study_hours = np.random.uniform(2, 14, n)
16-
exam_scores = study_hours * 4.5 + np.random.normal(0, 5.5, n) + 25
17-
exam_scores = np.clip(exam_scores, 15, 100)
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+
1819

19-
# Add deliberate outliers showcasing scatter diversity (high/low performers)
20-
outlier_hours = np.array([3.0, 12.5, 7.0, 11.0, 4.5])
21-
outlier_scores = np.array([82.0, 42.0, 95.0, 48.0, 78.0])
22-
study_hours = np.concatenate([study_hours, outlier_hours])
23-
exam_scores = np.concatenate([exam_scores, outlier_scores])
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 = "#8A8A82" if THEME == "light" else "#6E6D66"
2426

25-
# Compute trend line (linear regression) for data storytelling
26-
coeffs = np.polyfit(study_hours, exam_scores, 1)
27-
slope, intercept = coeffs
28-
r = np.corrcoef(study_hours, exam_scores)[0, 1]
29-
trend_x = np.linspace(study_hours.min(), study_hours.max(), 50)
30-
trend_y = slope * trend_x + intercept
27+
OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442")
28+
29+
# Data — study hours vs exam scores, moderate positive correlation
30+
np.random.seed(42)
31+
n = 180
32+
study_hours = np.random.uniform(1.5, 13.5, n)
33+
exam_scores = study_hours * 4.8 + np.random.normal(0, 6.5, n) + 26
34+
exam_scores = np.clip(exam_scores, 20, 100)
3135

32-
# Identify notable outliers for annotation
33-
residuals = exam_scores - (slope * study_hours + intercept)
34-
top_outlier_idx = int(np.argmax(residuals))
35-
bottom_outlier_idx = int(np.argmin(residuals))
36+
# Visual hierarchy: split at conventional 70% passing threshold to guide the
37+
# viewer's eye and convey the "hours → outcome" narrative beyond a raw cloud.
38+
PASSING = 70.0
39+
above = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True) if s >= PASSING]
40+
below = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True) if s < PASSING]
3641

37-
# Shared font family
3842
font = "DejaVu Sans, Helvetica, Arial, sans-serif"
3943

40-
# Refined style for 4800x2700 px canvas — subtle, professional palette
4144
custom_style = Style(
42-
background="white",
43-
plot_background="#f7f7f7",
44-
foreground="#2a2a2a",
45-
foreground_strong="#2a2a2a",
46-
foreground_subtle="#e0e0e0",
47-
guide_stroke_color="#e0e0e0",
48-
guide_stroke_dasharray="4, 4",
49-
colors=("#306998", "#d64541", "#e8a838"),
45+
background=PAGE_BG,
46+
plot_background=PAGE_BG,
47+
foreground=INK_SOFT,
48+
foreground_strong=INK,
49+
foreground_subtle=INK_MUTED,
50+
colors=OKABE_ITO,
5051
font_family=font,
5152
title_font_family=font,
52-
title_font_size=56,
53-
label_font_size=42,
54-
major_label_font_size=38,
55-
legend_font_size=34,
53+
label_font_family=font,
54+
major_label_font_family=font,
5655
legend_font_family=font,
57-
value_font_size=28,
58-
tooltip_font_size=28,
5956
tooltip_font_family=font,
60-
opacity=0.65,
57+
title_font_size=52,
58+
label_font_size=40,
59+
major_label_font_size=36,
60+
legend_font_size=34,
61+
tooltip_font_size=28,
62+
value_font_size=26,
63+
opacity=0.7,
6164
opacity_hover=0.95,
6265
stroke_opacity=1,
6366
stroke_opacity_hover=1,
6467
)
6568

66-
# Axis range tightened to data bounds for better canvas utilization
67-
x_min, x_max = float(np.floor(study_hours.min())), float(np.ceil(study_hours.max()))
68-
y_min = float(max(0, np.floor(exam_scores.min() / 5) * 5))
69-
y_max = float(min(100, np.ceil(exam_scores.max() / 5) * 5 + 5))
70-
71-
# Create XY chart
7269
chart = pygal.XY(
7370
width=4800,
7471
height=2700,
7572
style=custom_style,
76-
title="scatter-basic \u00b7 pygal \u00b7 pyplots.ai",
73+
title="scatter-basic · pygal · anyplot.ai",
7774
x_title="Study Hours per Week (hrs)",
7875
y_title="Exam Score (%)",
76+
stroke=False,
77+
dots_size=17,
7978
show_legend=True,
8079
legend_at_bottom=True,
81-
legend_at_bottom_columns=3,
82-
legend_box_size=24,
83-
stroke=False,
84-
dots_size=9,
80+
legend_at_bottom_columns=2,
81+
legend_box_size=32,
8582
show_x_guides=True,
8683
show_y_guides=True,
87-
x_value_formatter=lambda x: f"{x:.0f}",
88-
value_formatter=lambda y: f"{y:.0f}%",
89-
margin_bottom=100,
90-
margin_left=60,
91-
margin_right=40,
92-
margin_top=50,
93-
x_label_rotation=0,
94-
truncate_legend=-1,
95-
range=(y_min, y_max),
96-
xrange=(x_min, x_max),
84+
x_value_formatter=lambda v: f"{v:.0f}",
85+
value_formatter=lambda v: f"{v:.0f}%",
86+
range=(15, 100),
87+
xrange=(1, 14),
9788
x_labels_major_count=7,
9889
y_labels_major_count=9,
90+
margin=60,
9991
print_values=False,
100-
print_zeroes=False,
10192
js=[],
10293
)
10394

104-
# Add scatter data as list of (x, y) tuples
105-
points = [(float(h), float(s)) for h, s in zip(study_hours, exam_scores, strict=True)]
106-
chart.add(
107-
f"Students (n={len(points)})",
108-
points,
109-
stroke=False,
110-
formatter=lambda x: f"({x[0]:.1f} hrs, {x[1]:.0f}%)" if isinstance(x, (tuple, list)) else f"{x:.0f}",
111-
)
112-
113-
# Add trend line — dashed stroke for visual contrast
114-
trend_points = [(float(x), float(y)) for x, y in zip(trend_x, trend_y, strict=True)]
115-
chart.add(
116-
f"Trend (r = {r:.2f})",
117-
trend_points,
118-
stroke=True,
119-
show_dots=False,
120-
stroke_style={"width": 14, "dasharray": "32, 14", "linecap": "round", "linejoin": "round"},
121-
)
122-
123-
# Annotate notable outliers — pygal per-point metadata with label styling
124-
oh = float(study_hours[top_outlier_idx])
125-
os_ = float(exam_scores[top_outlier_idx])
126-
bh = float(study_hours[bottom_outlier_idx])
127-
bs = float(exam_scores[bottom_outlier_idx])
128-
chart.add(
129-
"Outliers",
130-
[
131-
{"value": (oh, os_), "label": f"High performer ({oh:.0f}h \u2192 {os_:.0f}%)"},
132-
{"value": (bh, bs), "label": f"Low performer ({bh:.0f}h \u2192 {bs:.0f}%)"},
133-
],
134-
stroke=False,
135-
dots_size=16,
136-
formatter=lambda x: f"{x[1]:.0f}%" if isinstance(x, (tuple, list)) else f"{x:.0f}",
137-
)
95+
chart.add("Passing (≥ 70%)", above)
96+
chart.add("Below 70%", below)
13897

139-
# Save outputs — dual format leverages pygal's SVG-native + PNG capability
140-
chart.render_to_png("plot.png")
141-
chart.render_to_file("plot.html")
98+
chart.render_to_png(f"plot-{THEME}.png")
99+
with open(f"plot-{THEME}.html", "wb") as f:
100+
f.write(chart.render())

0 commit comments

Comments
 (0)