Skip to content

Commit c16bec6

Browse files
feat(letsplot): implement scatter-lag (#5268)
## Implementation: `scatter-lag` - letsplot Implements the **letsplot** version of `scatter-lag`. **File:** `plots/scatter-lag/implementations/letsplot.py` **Parent Issue:** #5251 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/24313009924)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 2b4a96e commit c16bec6

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
""" pyplots.ai
2+
scatter-lag: Lag Plot for Time Series Autocorrelation Diagnosis
3+
Library: letsplot 4.9.0 | Python 3.14.3
4+
Quality: 87/100 | Created: 2026-04-12
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from lets_plot import * # noqa: F403
10+
from lets_plot.export import ggsave as export_ggsave
11+
12+
13+
LetsPlot.setup_html() # noqa: F405
14+
15+
# Data - Synthetic AR(1) process with phi=0.85 (strong positive autocorrelation)
16+
np.random.seed(42)
17+
n = 400
18+
lag = 1
19+
phi = 0.85
20+
innovations = np.random.randn(n) * 2.0
21+
22+
temperature = np.zeros(n)
23+
temperature[0] = 20.0
24+
for i in range(1, n):
25+
temperature[i] = phi * temperature[i - 1] + (1 - phi) * 20.0 + innovations[i]
26+
27+
# Build lag plot data: y(t) vs y(t+lag)
28+
value_t = temperature[:-lag]
29+
value_t_lag = temperature[lag:]
30+
time_index = np.arange(len(value_t))
31+
32+
df = pd.DataFrame({"value_t": value_t, "value_t_lag": value_t_lag, "day": time_index})
33+
34+
# Compute autocorrelation at this lag
35+
r = np.corrcoef(value_t, value_t_lag)[0, 1]
36+
37+
# Reference line data (y = x diagonal)
38+
ref_min = min(value_t.min(), value_t_lag.min()) - 1
39+
ref_max = max(value_t.max(), value_t_lag.max()) + 1
40+
ref_df = pd.DataFrame({"x": [ref_min, ref_max], "y": [ref_min, ref_max]})
41+
42+
# Annotation data
43+
anno_df = pd.DataFrame({"x": [ref_max - 1.5], "y": [ref_min + 1.5], "label": [f"r = {r:.2f}"]})
44+
45+
# Plot
46+
plot = (
47+
ggplot(df, aes(x="value_t", y="value_t_lag", color="day")) # noqa: F405
48+
+ geom_line( # noqa: F405
49+
aes(x="x", y="y"), # noqa: F405
50+
data=ref_df,
51+
color="#CCCCCC",
52+
size=1.0,
53+
linetype="dashed",
54+
inherit_aes=False,
55+
)
56+
+ geom_point( # noqa: F405
57+
size=4,
58+
alpha=0.6,
59+
shape=16,
60+
tooltips=layer_tooltips() # noqa: F405
61+
.line("Day|@day")
62+
.line("y(t)|@{value_t}{.2f}")
63+
.line("y(t+1)|@{value_t_lag}{.2f}"),
64+
)
65+
+ geom_text( # noqa: F405
66+
aes(x="x", y="y", label="label"), # noqa: F405
67+
data=anno_df,
68+
size=14,
69+
color="#444444",
70+
family="monospace",
71+
hjust=1.0,
72+
inherit_aes=False,
73+
)
74+
+ scale_color_gradient( # noqa: F405
75+
low="#306998", high="#E3882D", name="Day"
76+
)
77+
+ labs( # noqa: F405
78+
x="y(t)",
79+
y=f"y(t + {lag})",
80+
title="scatter-lag · letsplot · pyplots.ai",
81+
caption="AR(1) simulated daily temperature · dashed line = y(t+1) = y(t)",
82+
)
83+
+ ggsize(1600, 900) # noqa: F405
84+
+ theme_minimal() # noqa: F405
85+
+ theme( # noqa: F405
86+
axis_text=element_text(size=16, color="#555555"), # noqa: F405
87+
axis_title=element_text(size=20, color="#333333"), # noqa: F405
88+
plot_title=element_text(size=24, color="#222222", face="bold"), # noqa: F405
89+
plot_caption=element_text(size=13, color="#999999", face="italic"), # noqa: F405
90+
panel_grid_major=element_line(color="#E8E8E8", size=0.35), # noqa: F405
91+
panel_grid_minor=element_blank(), # noqa: F405
92+
legend_title=element_text(size=16, color="#444444"), # noqa: F405
93+
legend_text=element_text(size=14, color="#555555"), # noqa: F405
94+
axis_ticks=element_line(color="#CCCCCC", size=0.3), # noqa: F405
95+
plot_margin=[30, 40, 20, 20],
96+
)
97+
)
98+
99+
# Save
100+
export_ggsave(plot, filename="plot.png", path=".", scale=3)
101+
export_ggsave(plot, filename="plot.html", path=".")
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
library: letsplot
2+
specification_id: scatter-lag
3+
created: '2026-04-12T18:09:58Z'
4+
updated: '2026-04-12T18:15:02Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 24313009924
7+
issue: 5251
8+
python_version: 3.14.3
9+
library_version: 4.9.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-lag/letsplot/plot.png
11+
preview_html: https://storage.googleapis.com/pyplots-images/plots/scatter-lag/letsplot/plot.html
12+
quality_score: 87
13+
review:
14+
strengths:
15+
- 'Perfect spec compliance: diagonal reference line, temporal color encoding, r
16+
annotation, and configurable lag all implemented correctly'
17+
- 'Strong data quality: realistic AR(1) temperature simulation with phi=0.85 shows
18+
clear autocorrelation pattern in plausible range'
19+
- 'Excellent code quality: clean KISS structure, reproducible, fully idiomatic letsplot
20+
grammar-of-graphics pattern'
21+
- 'Good data storytelling: temporal gradient plus r annotation effectively communicates
22+
autocorrelation structure to the viewer'
23+
weaknesses:
24+
- 'DE-01: Design is clean but not publication-quality; needs more intentional visual
25+
hierarchy beyond temporal gradient'
26+
- 'VQ-04: Custom blue-orange gradient is not a standard colorblind-safe palette;
27+
viridis or plasma preferred for perceptual uniformity'
28+
- 'VQ-03: Alpha=0.6 slightly high for 399 points; central cluster has visual density;
29+
reduce to 0.4-0.5'
30+
- 'LM-02: layer_tooltips() underutilized; could leverage more letsplot-distinctive
31+
capabilities like geom_smooth'
32+
image_description: The plot shows a lag-1 scatter plot of an AR(1) simulated daily
33+
temperature time series (~399 points). Points are colored with a blue (#306998)
34+
to orange (#E3882D) gradient encoding temporal order — early days are dark blue,
35+
later days warm orange. The x-axis is labeled "y(t)" and the y-axis "y(t + 1)".
36+
A dashed light-gray diagonal reference line (y = x) spans the entire plot area.
37+
A large monospace "r = 0.83" annotation appears in the lower-right corner. The
38+
title reads "scatter-lag · letsplot · pyplots.ai" in bold. A "Day" gradient legend
39+
is displayed on the right. The plot uses a minimal theme with very subtle gray
40+
grid lines (major only), no minor grid. An italic gray caption at the bottom reads
41+
"AR(1) simulated daily temperature · dashed line = y(t+1) = y(t)". The overall
42+
layout is 16:9, clean, and professional.
43+
criteria_checklist:
44+
visual_quality:
45+
score: 27
46+
max: 30
47+
items:
48+
- id: VQ-01
49+
name: Text Legibility
50+
score: 8
51+
max: 8
52+
passed: true
53+
comment: 'All font sizes explicitly set: title=24, axis titles=20, axis text=16,
54+
legend=14'
55+
- id: VQ-02
56+
name: No Overlap
57+
score: 6
58+
max: 6
59+
passed: true
60+
comment: No text element overlap; r annotation well-placed in lower right
61+
- id: VQ-03
62+
name: Element Visibility
63+
score: 5
64+
max: 6
65+
passed: true
66+
comment: Points visible but size=4 slightly large for 399 points; alpha=0.6
67+
slightly high causing density in center
68+
- id: VQ-04
69+
name: Color Accessibility
70+
score: 3
71+
max: 4
72+
passed: true
73+
comment: Blue-orange gradient not red-green so accessible, but not a standard
74+
perceptually-uniform colormap like viridis
75+
- id: VQ-05
76+
name: Layout & Canvas
77+
score: 3
78+
max: 4
79+
passed: true
80+
comment: Good 16:9 proportions with balanced legend placement; minor whitespace
81+
on margins
82+
- id: VQ-06
83+
name: Axis Labels & Title
84+
score: 2
85+
max: 2
86+
passed: true
87+
comment: y(t) and y(t+1) are descriptive standard lag plot notation
88+
design_excellence:
89+
score: 12
90+
max: 20
91+
items:
92+
- id: DE-01
93+
name: Aesthetic Sophistication
94+
score: 4
95+
max: 8
96+
passed: false
97+
comment: Custom blue-orange gradient and monospace annotation show design
98+
intent; fundamentally a well-configured minimal default, not publication-ready
99+
- id: DE-02
100+
name: Visual Refinement
101+
score: 4
102+
max: 6
103+
passed: true
104+
comment: Subtle grid (E8E8E8, size=0.35), minor grid removed, custom tick
105+
colors and margins; good attention to detail
106+
- id: DE-03
107+
name: Data Storytelling
108+
score: 4
109+
max: 6
110+
passed: true
111+
comment: Temporal color encoding reveals time series movement through scatter;
112+
r=0.83 annotation immediately communicates autocorrelation strength
113+
spec_compliance:
114+
score: 15
115+
max: 15
116+
items:
117+
- id: SC-01
118+
name: Plot Type
119+
score: 5
120+
max: 5
121+
passed: true
122+
comment: 'Correct lag plot: scatter of y(t) vs y(t+lag)'
123+
- id: SC-02
124+
name: Required Features
125+
score: 4
126+
max: 4
127+
passed: true
128+
comment: Diagonal reference line, color by time index, r correlation annotation,
129+
configurable lag all implemented
130+
- id: SC-03
131+
name: Data Mapping
132+
score: 3
133+
max: 3
134+
passed: true
135+
comment: x=value_t (y(t)), y=value_t_lag (y(t+1)); correct mapping
136+
- id: SC-04
137+
name: Title & Legend
138+
score: 3
139+
max: 3
140+
passed: true
141+
comment: 'Title: scatter-lag · letsplot · pyplots.ai exact format; Day gradient
142+
legend correctly labeled'
143+
data_quality:
144+
score: 15
145+
max: 15
146+
items:
147+
- id: DQ-01
148+
name: Feature Coverage
149+
score: 6
150+
max: 6
151+
passed: true
152+
comment: Shows strong positive autocorrelation, temporal structure via color,
153+
reference diagonal, quantified r value
154+
- id: DQ-02
155+
name: Realistic Context
156+
score: 5
157+
max: 5
158+
passed: true
159+
comment: AR(1) simulated daily temperature; plausible real-world scenario;
160+
non-controversial
161+
- id: DQ-03
162+
name: Appropriate Scale
163+
score: 4
164+
max: 4
165+
passed: true
166+
comment: Temperature range ~12-32°C realistic for daily temp; phi=0.85 gives
167+
realistic autocorrelation
168+
code_quality:
169+
score: 10
170+
max: 10
171+
items:
172+
- id: CQ-01
173+
name: KISS Structure
174+
score: 3
175+
max: 3
176+
passed: true
177+
comment: 'Clean linear flow: imports, data, lag construction, r calc, ref
178+
line, plot, save; no functions or classes'
179+
- id: CQ-02
180+
name: Reproducibility
181+
score: 2
182+
max: 2
183+
passed: true
184+
comment: np.random.seed(42) set
185+
- id: CQ-03
186+
name: Clean Imports
187+
score: 2
188+
max: 2
189+
passed: true
190+
comment: All imports used; noqa comments appropriate for star import pattern
191+
- id: CQ-04
192+
name: Code Elegance
193+
score: 2
194+
max: 2
195+
passed: true
196+
comment: Pythonic, appropriate complexity; AR(1) loop is clearest way to generate
197+
the process
198+
- id: CQ-05
199+
name: Output & API
200+
score: 1
201+
max: 1
202+
passed: true
203+
comment: Saves as plot.png via export_ggsave with scale=3
204+
library_mastery:
205+
score: 8
206+
max: 10
207+
items:
208+
- id: LM-01
209+
name: Idiomatic Usage
210+
score: 5
211+
max: 5
212+
passed: true
213+
comment: 'Expert grammar-of-graphics usage: ggplot/geom/aes/labs/theme composition,
214+
ggsize for sizing, scale_color_gradient — fully idiomatic'
215+
- id: LM-02
216+
name: Distinctive Features
217+
score: 3
218+
max: 5
219+
passed: true
220+
comment: Uses layer_tooltips() (letsplot-specific interactive tooltips) and
221+
HTML export; good but could leverage more letsplot-specific capabilities
222+
verdict: REJECTED
223+
impl_tags:
224+
dependencies: []
225+
techniques:
226+
- annotations
227+
- layer-composition
228+
- hover-tooltips
229+
- html-export
230+
patterns:
231+
- data-generation
232+
dataprep: []
233+
styling:
234+
- alpha-blending
235+
- custom-colormap

0 commit comments

Comments
 (0)