Skip to content

Commit 1ab9619

Browse files
feat(pygal): implement scatter-lag (#5273)
## Implementation: `scatter-lag` - pygal Implements the **pygal** version of `scatter-lag`. **File:** `plots/scatter-lag/implementations/pygal.py` **Parent Issue:** #5251 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/24313009875)* --------- 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 33205a2 commit 1ab9619

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
""" pyplots.ai
2+
scatter-lag: Lag Plot for Time Series Autocorrelation Diagnosis
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 87/100 | Created: 2026-04-12
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data — synthetic AR(1) process with moderate positive autocorrelation
13+
np.random.seed(42)
14+
n = 400
15+
phi = 0.78
16+
noise = np.random.normal(0, 1.0, n)
17+
temperature = np.zeros(n)
18+
temperature[0] = 20.0
19+
for i in range(1, n):
20+
temperature[i] = 20.0 + phi * (temperature[i - 1] - 20.0) + noise[i]
21+
22+
lag = 1
23+
y_t = temperature[:-lag]
24+
y_t_lag = temperature[lag:]
25+
26+
# Temporal quartile masks
27+
time_idx = np.arange(len(y_t))
28+
q_bounds = np.percentile(time_idx, [25, 50, 75])
29+
30+
# Build scatter series with interactive tooltips (pygal dict format)
31+
early = [
32+
{"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Day {i + 1}"}
33+
for i in range(len(y_t))
34+
if time_idx[i] < q_bounds[0]
35+
]
36+
mid_early = [
37+
{"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Day {i + 1}"}
38+
for i in range(len(y_t))
39+
if q_bounds[0] <= time_idx[i] < q_bounds[1]
40+
]
41+
mid_late = [
42+
{"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Day {i + 1}"}
43+
for i in range(len(y_t))
44+
if q_bounds[1] <= time_idx[i] < q_bounds[2]
45+
]
46+
late = [
47+
{"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Day {i + 1}"}
48+
for i in range(len(y_t))
49+
if time_idx[i] >= q_bounds[2]
50+
]
51+
52+
# Correlation coefficient
53+
r = np.corrcoef(y_t, y_t_lag)[0, 1]
54+
55+
# Reference geometry
56+
data_min = float(min(y_t.min(), y_t_lag.min()))
57+
data_max = float(max(y_t.max(), y_t_lag.max()))
58+
pad = (data_max - data_min) * 0.05
59+
ref_start = data_min - pad
60+
ref_end = data_max + pad
61+
ref_line = [(ref_start, ref_start), (ref_end, ref_end)]
62+
63+
# ±1σ envelope around y=x to visualise autocorrelation spread
64+
sigma = float(np.std(y_t_lag - y_t))
65+
upper_env = [(ref_start, ref_start + sigma), (ref_end, ref_end + sigma)]
66+
lower_env = [(ref_start, ref_start - sigma), (ref_end, ref_end - sigma)]
67+
68+
# Warm-to-cool temporal palette: terracotta → amber → teal → navy
69+
font = "DejaVu Sans, Helvetica, Arial, sans-serif"
70+
custom_style = Style(
71+
background="white",
72+
plot_background="#f8f7f5",
73+
foreground="#2a2a2a",
74+
foreground_strong="#1a1a1a",
75+
foreground_subtle="#d5d5d3",
76+
guide_stroke_color="#e0dfdd",
77+
colors=(
78+
"#c25a3c", # Q1 — terracotta
79+
"#d4a028", # Q2 — warm amber
80+
"#2a9d8f", # Q3 — teal
81+
"#264653", # Q4 — deep navy
82+
"#c0bebb", # +1σ envelope
83+
"#c0bebb", # −1σ envelope
84+
"#888886", # y = x reference
85+
),
86+
font_family=font,
87+
title_font_family=font,
88+
title_font_size=56,
89+
label_font_size=42,
90+
major_label_font_size=38,
91+
legend_font_size=32,
92+
legend_font_family=font,
93+
value_font_size=28,
94+
tooltip_font_size=28,
95+
tooltip_font_family=font,
96+
opacity=0.60,
97+
opacity_hover=0.95,
98+
stroke_opacity=0.7,
99+
stroke_opacity_hover=1,
100+
)
101+
102+
# Chart — interactivity enabled for SVG hover tooltips
103+
chart = pygal.XY(
104+
width=4800,
105+
height=2700,
106+
style=custom_style,
107+
title=f"Lag Plot (k={lag}, r={r:.2f}) \u00b7 scatter-lag \u00b7 pygal \u00b7 pyplots.ai",
108+
x_title="y(t)",
109+
y_title=f"y(t+{lag})",
110+
show_legend=True,
111+
legend_at_bottom=True,
112+
legend_at_bottom_columns=5,
113+
legend_box_size=24,
114+
stroke=False,
115+
dots_size=8,
116+
show_x_guides=True,
117+
show_y_guides=True,
118+
x_value_formatter=lambda x: f"{x:.1f}",
119+
value_formatter=lambda y: f"{y:.1f}",
120+
margin_bottom=100,
121+
margin_left=80,
122+
margin_right=30,
123+
margin_top=40,
124+
range=(ref_start, ref_end),
125+
xrange=(ref_start, ref_end),
126+
x_labels_major_count=8,
127+
y_labels_major_count=8,
128+
print_values=False,
129+
print_zeroes=False,
130+
truncate_legend=40,
131+
)
132+
133+
# Temporal quartile scatter series
134+
chart.add("Days 1\u2013100", early, stroke=False, dots_size=9)
135+
chart.add("Days 101\u2013200", mid_early, stroke=False)
136+
chart.add("Days 201\u2013300", mid_late, stroke=False)
137+
chart.add("Days 301\u2013399", late, stroke=False, dots_size=9)
138+
139+
# ±1σ envelope (no legend entry)
140+
env_style = {"width": 3, "dasharray": "6, 8", "linecap": "round"}
141+
chart.add(None, upper_env, stroke=True, show_dots=False, stroke_style=env_style)
142+
chart.add(None, lower_env, stroke=True, show_dots=False, stroke_style=env_style)
143+
144+
# Diagonal reference line y = x
145+
chart.add(
146+
"y = x (\u00b11\u03c3)",
147+
ref_line,
148+
stroke=True,
149+
show_dots=False,
150+
stroke_style={"width": 6, "dasharray": "24, 12", "linecap": "round"},
151+
)
152+
153+
# Save
154+
chart.render_to_png("plot.png")
155+
chart.render_to_file("plot.html")
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
library: pygal
2+
specification_id: scatter-lag
3+
created: '2026-04-12T18:12:36Z'
4+
updated: '2026-04-12T18:32:40Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 24313009875
7+
issue: 5251
8+
python_version: 3.14.3
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-lag/pygal/plot.png
11+
preview_html: https://storage.googleapis.com/pyplots-images/plots/scatter-lag/pygal/plot.html
12+
quality_score: 87
13+
review:
14+
strengths:
15+
- Intentional warm-to-cool temporal palette (terracotta → amber → teal → navy) creates
16+
a compelling visual narrative of time progression within the autocorrelation structure
17+
- All required spec features fully implemented plus value-adding ±1σ envelope around
18+
the diagonal reference line
19+
- Comprehensive explicit font sizing throughout; perfect spec compliance (15/15)
20+
and code quality (10/10)
21+
weaknesses:
22+
- dots_size=8–9 causes overplotting for 400 data points; smaller dots (5–6) with
23+
lower opacity (~0.45) would reduce central cluster overlap
24+
- Terracotta (#c25a3c) and amber (#d4a028) risk confusion for deuteranopic (red-green
25+
colorblind) viewers; palette shift needed for full accessibility
26+
image_description: 'The plot shows a lag scatter plot on a light beige (#f8f7f5)
27+
background. The title reads "Lag Plot (k=1, r=0.75) · scatter-lag · pygal · pyplots.ai"
28+
centered at the top. The X-axis is labeled "y(t)" and the Y-axis "y(t+1)". Four
29+
temporal quartile series are plotted: Days 1–100 in terracotta (brick-red), Days
30+
101–200 in amber/golden-yellow, Days 201–300 in teal/seafoam, and Days 301–399
31+
in deep navy. A dashed gray diagonal reference line (y = x ±1σ) runs from lower-left
32+
to upper-right. Subtle dotted grid lines are visible across the plot. The legend
33+
sits at the bottom with five labeled entries. A clear positive linear autocorrelation
34+
pattern is evident — data points cluster diagonally, confirming the AR(1) process
35+
(r=0.75).'
36+
criteria_checklist:
37+
visual_quality:
38+
score: 25
39+
max: 30
40+
items:
41+
- id: VQ-01
42+
name: Text Legibility
43+
score: 8
44+
max: 8
45+
passed: true
46+
comment: 'All font sizes explicitly set: title=56, labels=42, major=38, legend=32,
47+
value=28; fully readable at 4800x2700 px'
48+
- id: VQ-02
49+
name: No Overlap
50+
score: 5
51+
max: 6
52+
passed: true
53+
comment: Text elements clear; minor dot overlap in dense central cluster expected
54+
for 400 points
55+
- id: VQ-03
56+
name: Element Visibility
57+
score: 4
58+
max: 6
59+
passed: false
60+
comment: dots_size=8-9 for 400 points causes noticeable overplotting; guidelines
61+
suggest size 20-50 with alpha 0.3-0.5 for 300+ points
62+
- id: VQ-04
63+
name: Color Accessibility
64+
score: 3
65+
max: 4
66+
passed: false
67+
comment: Terracotta (#c25a3c) and amber (#d4a028) may be confused by deuteranopic
68+
users; rest of palette is good
69+
- id: VQ-05
70+
name: Layout & Canvas
71+
score: 3
72+
max: 4
73+
passed: true
74+
comment: Good proportions; margins tuned; legend well-anchored at bottom
75+
- id: VQ-06
76+
name: Axis Labels & Title
77+
score: 2
78+
max: 2
79+
passed: true
80+
comment: y(t) / y(t+1) are standard lag-plot notation; title is descriptive
81+
and complete
82+
design_excellence:
83+
score: 15
84+
max: 20
85+
items:
86+
- id: DE-01
87+
name: Aesthetic Sophistication
88+
score: 6
89+
max: 8
90+
passed: true
91+
comment: Intentional warm-to-cool temporal palette with custom beige background;
92+
clearly above defaults, but not quite publication-ready
93+
- id: DE-02
94+
name: Visual Refinement
95+
score: 4
96+
max: 6
97+
passed: true
98+
comment: Subtle guide_stroke_color, beige plot background, legend columns,
99+
margin tuning; limited further by pygal API (no spine removal)
100+
- id: DE-03
101+
name: Data Storytelling
102+
score: 5
103+
max: 6
104+
passed: true
105+
comment: Temporal color gradient is an effective storytelling device; correlation
106+
coefficient in title; ±1σ envelope contextualises autocorrelation spread
107+
spec_compliance:
108+
score: 15
109+
max: 15
110+
items:
111+
- id: SC-01
112+
name: Plot Type
113+
score: 5
114+
max: 5
115+
passed: true
116+
comment: Correct XY scatter for lag plot
117+
- id: SC-02
118+
name: Required Features
119+
score: 4
120+
max: 4
121+
passed: true
122+
comment: Diagonal y=x reference line, temporal point coloring, r-value annotation
123+
in title, configurable lag; bonus ±1σ envelope
124+
- id: SC-03
125+
name: Data Mapping
126+
score: 3
127+
max: 3
128+
passed: true
129+
comment: y(t) on X, y(t+lag) on Y, all data visible
130+
- id: SC-04
131+
name: Title & Legend
132+
score: 3
133+
max: 3
134+
passed: true
135+
comment: Title includes spec-id, library, pyplots.ai, plus informative k and
136+
r values; legend labels match temporal quartiles
137+
data_quality:
138+
score: 14
139+
max: 15
140+
items:
141+
- id: DQ-01
142+
name: Feature Coverage
143+
score: 5
144+
max: 6
145+
passed: true
146+
comment: Clear positive autocorrelation pattern demonstrated; temporal coloring
147+
reveals consistent structure; could include stronger/weaker autocorrelation
148+
variation
149+
- id: DQ-02
150+
name: Realistic Context
151+
score: 5
152+
max: 5
153+
passed: true
154+
comment: Daily temperature readings with AR(1) mean reversion (mu=20C) is
155+
natural and neutral
156+
- id: DQ-03
157+
name: Appropriate Scale
158+
score: 4
159+
max: 4
160+
passed: true
161+
comment: Values 16-25C and phi=0.78 are realistic for daily temperatures
162+
code_quality:
163+
score: 10
164+
max: 10
165+
items:
166+
- id: CQ-01
167+
name: KISS Structure
168+
score: 3
169+
max: 3
170+
passed: true
171+
comment: Clean imports -> data -> plot -> save; no functions or classes
172+
- id: CQ-02
173+
name: Reproducibility
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: np.random.seed(42) set
178+
- id: CQ-03
179+
name: Clean Imports
180+
score: 2
181+
max: 2
182+
passed: true
183+
comment: numpy, pygal, Style; all used
184+
- id: CQ-04
185+
name: Code Elegance
186+
score: 2
187+
max: 2
188+
passed: true
189+
comment: Four separate list comprehensions for quartiles are verbose but clear;
190+
no fake functionality
191+
- id: CQ-05
192+
name: Output & API
193+
score: 1
194+
max: 1
195+
passed: true
196+
comment: render_to_png('plot.png') correct; HTML export is a pygal bonus
197+
library_mastery:
198+
score: 8
199+
max: 10
200+
items:
201+
- id: LM-01
202+
name: Idiomatic Usage
203+
score: 5
204+
max: 5
205+
passed: true
206+
comment: Dict-format data points with label keys for SVG tooltips, stroke_style
207+
for line customisation, legend_at_bottom_columns — all idiomatic pygal
208+
- id: LM-02
209+
name: Distinctive Features
210+
score: 3
211+
max: 5
212+
passed: true
213+
comment: Pygal-specific dict tooltip data and stroke_style dasharray are library-distinctive;
214+
could leverage pygal interactive SVG more explicitly
215+
verdict: REJECTED
216+
impl_tags:
217+
dependencies: []
218+
techniques:
219+
- hover-tooltips
220+
- html-export
221+
patterns:
222+
- data-generation
223+
- iteration-over-groups
224+
dataprep:
225+
- time-series
226+
styling:
227+
- alpha-blending

0 commit comments

Comments
 (0)