Skip to content

Commit b028400

Browse files
feat(pygal): implement stem-basic (#5633)
## Implementation: `stem-basic` - python/pygal Implements the **python/pygal** version of `stem-basic`. **File:** `plots/stem-basic/implementations/python/pygal.py` **Parent Issue:** #972 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25172370989)* --------- 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 7ad1f85 commit b028400

2 files changed

Lines changed: 283 additions & 65 deletions

File tree

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,85 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
stem-basic: Basic Stem Plot
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 86/100 | Created: 2025-12-23
3+
Library: pygal 3.1.0 | Python 3.13.13
4+
Quality: 84/100 | Updated: 2026-04-30
55
"""
66

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

1112

12-
# Data - discrete signal samples demonstrating impulse response
13+
# Pop script dir so this file (pygal.py) doesn't shadow the installed pygal package
14+
_script_dir = sys.path.pop(0)
15+
import pygal # noqa: E402
16+
from pygal.style import Style # noqa: E402
17+
18+
19+
sys.path.insert(0, _script_dir)
20+
21+
# Theme tokens
22+
THEME = os.getenv("ANYPLOT_THEME", "light")
23+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
24+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
25+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
26+
27+
OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442")
28+
29+
# Data - discrete impulse response (decaying oscillation)
1330
np.random.seed(42)
1431
n_points = 30
15-
x = np.arange(n_points)
16-
# Create a decaying oscillation pattern (typical impulse response)
17-
y = np.exp(-x / 8) * np.sin(x * 0.8) * 2 + np.random.randn(n_points) * 0.1
32+
sample_index = np.arange(n_points)
33+
amplitude = np.exp(-sample_index / 8) * np.sin(sample_index * 0.8) * 2 + np.random.randn(n_points) * 0.1
34+
35+
# Build stem data: baseline dot suppressed, marker visible only at data value
36+
stem_data = []
37+
for i in range(n_points):
38+
xi = float(sample_index[i])
39+
yi = float(amplitude[i])
40+
stem_data.append({"value": (xi, 0.0), "node": {"r": 0}}) # baseline anchor, no dot
41+
stem_data.append({"value": (xi, yi), "node": {"r": 14}, "label": f"n={i}, a={yi:.3f}"})
42+
stem_data.append(None) # break between stems
1843

19-
# Custom style for 4800×2700 px canvas with larger fonts
44+
# Custom style
2045
custom_style = Style(
21-
background="white",
22-
plot_background="white",
23-
foreground="#333333",
24-
foreground_strong="#333333",
25-
foreground_subtle="#666666",
26-
colors=("#306998",), # Single color for consistency
27-
title_font_size=84,
28-
label_font_size=56,
29-
major_label_font_size=48,
30-
legend_font_size=48,
46+
background=PAGE_BG,
47+
plot_background=PAGE_BG,
48+
foreground=INK,
49+
foreground_strong=INK,
50+
foreground_subtle=INK_MUTED,
51+
colors=OKABE_ITO,
52+
title_font_size=72,
53+
label_font_size=48,
54+
major_label_font_size=44,
55+
legend_font_size=56,
3156
value_font_size=40,
32-
stroke_width=10, # Thicker stem lines for visibility
57+
stroke_width=3,
58+
opacity="0.18",
59+
opacity_hover="0.9",
3360
)
3461

35-
# Create XY chart for stem plot
62+
# Chart
3663
chart = pygal.XY(
3764
width=4800,
3865
height=2700,
39-
style=custom_style,
40-
title="stem-basic · pygal · pyplots.ai",
66+
title="stem-basic · pygal · anyplot.ai",
4167
x_title="Sample Index (n)",
42-
y_title="Amplitude (a.u.)", # Use parentheses instead of brackets for better rendering
43-
show_legend=False,
68+
y_title="Amplitude (a.u.)",
69+
style=custom_style,
4470
show_dots=True,
71+
dots_size=14,
4572
stroke=True,
46-
dots_size=30, # Larger dots for better marker visibility
47-
stroke_style={"width": 10}, # Thicker stem lines
73+
stroke_style={"width": 8},
4874
show_x_guides=False,
4975
show_y_guides=True,
76+
show_legend=False,
77+
margin=120,
5078
)
5179

52-
# Build all stems as coordinate pairs for a single series
53-
# Each stem goes from (x, 0) to (x, y), with None separator between stems
54-
stem_data = []
55-
for i in range(n_points):
56-
xi = float(x[i])
57-
yi = float(y[i])
58-
stem_data.append((xi, 0))
59-
stem_data.append((xi, yi))
60-
stem_data.append(None) # Separator between stems
61-
62-
# Add all stems as a single series for consistent coloring
63-
chart.add("", stem_data, show_dots=True, dots_size=30, stroke_style={"width": 10})
80+
chart.add("Impulse Response", stem_data)
6481

65-
# Save as PNG and HTML
66-
chart.render_to_png("plot.png")
67-
chart.render_to_file("plot.html")
82+
# Save
83+
chart.render_to_png(f"plot-{THEME}.png")
84+
with open(f"plot-{THEME}.html", "wb") as f:
85+
f.write(chart.render())

plots/stem-basic/metadata/python/pygal.yaml

Lines changed: 222 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,229 @@
11
library: pygal
2+
language: python
23
specification_id: stem-basic
34
created: '2025-12-23T20:46:36Z'
4-
updated: '2025-12-23T21:11:10Z'
5-
generated_by: claude-opus-4-5-20251101
6-
workflow_run: 20471156994
7-
issue: 0
8-
python_version: 3.13.11
5+
updated: '2026-04-30T15:15:43Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 25172370989
8+
issue: 972
9+
python_version: 3.13.13
910
library_version: 3.1.0
10-
preview_url: https://storage.googleapis.com/anyplot-images/plots/stem-basic/pygal/plot.png
11-
preview_html: https://storage.googleapis.com/anyplot-images/plots/stem-basic/pygal/plot.html
12-
quality_score: 86
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/stem-basic/python/pygal/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/stem-basic/python/pygal/plot-dark.png
13+
preview_html_light: https://storage.googleapis.com/anyplot-images/plots/stem-basic/python/pygal/plot-light.html
14+
preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/stem-basic/python/pygal/plot-dark.html
15+
quality_score: 84
16+
review:
17+
strengths:
18+
- Perfect spec compliance via XY-triplet stem trick
19+
- Perfect data quality with realistic impulse-response scenario
20+
- 'Perfect code quality: KISS structure, seed set, dual-format output'
21+
- Correct Okabe-Ito palette with full theme adaptation in both renders
22+
weaknesses:
23+
- 'Design excellence moderate (9/20): no visual emphasis on high-amplitude early
24+
peaks; consider amplitude-based color or opacity fade on stems'
25+
- 'DE-02 refinement limited: outer chart border retained; suppress frame for cleaner
26+
look'
27+
- 'VQ-01 marginal: text may appear slightly undersized at full 4800x2700 resolution;
28+
recalibrate font sizes'
29+
image_description: |-
30+
Light render (plot-light.png):
31+
Background: Warm off-white (#FAF8F1) — correct theme surface
32+
Chrome: Title "stem-basic · pygal · anyplot.ai" in dark ink at top center — clearly readable. Axis labels "Sample Index (n)" and "Amplitude (a.u.)" in dark text — fully legible. Tick labels (0,2,4,...,28 on x; -0.8 to 1.6 on y) in dark muted tone — all readable.
33+
Data: 30 stems in brand green (#009E73); thin vertical lines extend from y=0 baseline to circular markers. Decaying oscillation pattern clearly visible. Y-axis guide lines subtle.
34+
Legibility verdict: PASS
35+
36+
Dark render (plot-dark.png):
37+
Background: Warm near-black (#1A1A17) — correct dark surface
38+
Chrome: Title, axis labels, and tick labels all rendered in light text (#F0EFE8 / muted light) — fully readable. No dark-on-dark failures observed anywhere.
39+
Data: Stem colors identical to light render — same brand green (#009E73) stems and circular markers. Decaying oscillation pattern equally clear.
40+
Legibility verdict: PASS
41+
criteria_checklist:
42+
visual_quality:
43+
score: 28
44+
max: 30
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 7
49+
max: 8
50+
passed: true
51+
comment: Font sizes explicitly set (title=72, label=48, major_label=44); all
52+
text readable in both themes
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 6
56+
max: 6
57+
passed: true
58+
comment: No overlapping elements; stems well-spaced; tick labels clear
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 5
62+
max: 6
63+
passed: true
64+
comment: Stems and markers visible; low-amplitude stems near baseline slightly
65+
thin but distinguishable
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 2
69+
max: 2
70+
passed: true
71+
comment: Single CVD-safe brand green on both warm-off-white and near-black
72+
surfaces
73+
- id: VQ-05
74+
name: Layout & Canvas
75+
score: 4
76+
max: 4
77+
passed: true
78+
comment: Fills ~60-70% of canvas with balanced margins
79+
- id: VQ-06
80+
name: Axis Labels & Title
81+
score: 2
82+
max: 2
83+
passed: true
84+
comment: Sample Index (n) and Amplitude (a.u.) — both descriptive with units
85+
- id: VQ-07
86+
name: Palette Compliance
87+
score: 2
88+
max: 2
89+
passed: true
90+
comment: 'First series #009E73; backgrounds #FAF8F1/#1A1A17; theme-adaptive
91+
chrome'
92+
design_excellence:
93+
score: 9
94+
max: 20
95+
items:
96+
- id: DE-01
97+
name: Aesthetic Sophistication
98+
score: 4
99+
max: 8
100+
passed: true
101+
comment: Well-configured default with Okabe-Ito and theme tokens; no distinctive
102+
design beyond standard
103+
- id: DE-02
104+
name: Visual Refinement
105+
score: 3
106+
max: 6
107+
passed: true
108+
comment: X-guides off, Y-guides subtle, margin=120; some deliberate refinement
109+
but default frame retained
110+
- id: DE-03
111+
name: Data Storytelling
112+
score: 2
113+
max: 6
114+
passed: true
115+
comment: Impulse-response context meaningful but no visual hierarchy or emphasis
116+
on high-amplitude peaks
117+
spec_compliance:
118+
score: 15
119+
max: 15
120+
items:
121+
- id: SC-01
122+
name: Plot Type
123+
score: 5
124+
max: 5
125+
passed: true
126+
comment: pygal.XY with triplet encoding produces correct stem geometry (baseline
127+
anchor r=0, data dot r=14, None break)
128+
- id: SC-02
129+
name: Required Features
130+
score: 4
131+
max: 4
132+
passed: true
133+
comment: Thin stems from y=0, visible circular markers, consistent sizing,
134+
baseline at y=0
135+
- id: SC-03
136+
name: Data Mapping
137+
score: 3
138+
max: 3
139+
passed: true
140+
comment: X=sample index 0-29, Y=amplitude; all 30 points visible
141+
- id: SC-04
142+
name: Title & Legend
143+
score: 3
144+
max: 3
145+
passed: true
146+
comment: Title 'stem-basic · pygal · anyplot.ai'; legend suppressed (appropriate
147+
for single series)
148+
data_quality:
149+
score: 15
150+
max: 15
151+
items:
152+
- id: DQ-01
153+
name: Feature Coverage
154+
score: 6
155+
max: 6
156+
passed: true
157+
comment: 30 stems with positive and negative amplitudes, clear decaying envelope,
158+
full baseline crossing
159+
- id: DQ-02
160+
name: Realistic Context
161+
score: 5
162+
max: 5
163+
passed: true
164+
comment: Decaying sinusoidal impulse response — canonical signal-processing
165+
scenario; neutral science domain
166+
- id: DQ-03
167+
name: Appropriate Scale
168+
score: 4
169+
max: 4
170+
passed: true
171+
comment: Amplitude ~-0.8 to +1.6, decay constant ~8 samples; physically realistic
172+
for damped oscillation
173+
code_quality:
174+
score: 10
175+
max: 10
176+
items:
177+
- id: CQ-01
178+
name: KISS Structure
179+
score: 3
180+
max: 3
181+
passed: true
182+
comment: Linear imports->tokens->data->chart->save; no functions or classes
183+
- id: CQ-02
184+
name: Reproducibility
185+
score: 2
186+
max: 2
187+
passed: true
188+
comment: np.random.seed(42) before data generation
189+
- id: CQ-03
190+
name: Clean Imports
191+
score: 2
192+
max: 2
193+
passed: true
194+
comment: All imports used; sys required for path shadow workaround
195+
- id: CQ-04
196+
name: Code Elegance
197+
score: 2
198+
max: 2
199+
passed: true
200+
comment: sys.path workaround well-handled; loop-based stem construction clean
201+
- id: CQ-05
202+
name: Output & API
203+
score: 1
204+
max: 1
205+
passed: true
206+
comment: Saves plot-{THEME}.png and plot-{THEME}.html; correct for interactive
207+
library
208+
library_mastery:
209+
score: 7
210+
max: 10
211+
items:
212+
- id: LM-01
213+
name: Idiomatic Usage
214+
score: 4
215+
max: 5
216+
passed: true
217+
comment: Style object used correctly; per-node radius via node dict shows
218+
solid library knowledge
219+
- id: LM-02
220+
name: Distinctive Features
221+
score: 3
222+
max: 5
223+
passed: true
224+
comment: Per-node dot radius suppression (r=0), individual point labels, HTML
225+
interactive export
226+
verdict: APPROVED
13227
impl_tags:
14228
dependencies: []
15229
techniques:
@@ -19,17 +233,3 @@ impl_tags:
19233
- iteration-over-groups
20234
dataprep: []
21235
styling: []
22-
review:
23-
strengths:
24-
- Correct stem plot visualization with vertical lines from baseline to data markers
25-
- 'Excellent data choice: decaying oscillation pattern demonstrates realistic signal
26-
processing use case'
27-
- Clean code structure following KISS principles with proper reproducibility (seed=42)
28-
- Proper title format and good overall layout balance
29-
- Uses pygal Style system for customization
30-
weaknesses:
31-
- Axis label units not rendering in output (parentheses in labels may cause issues
32-
with pygal)
33-
- Markers could be slightly larger for better visibility at this data density
34-
- Does not leverage pygal interactive tooltip features that could enhance the stem
35-
plot

0 commit comments

Comments
 (0)