Skip to content

Commit 8500d08

Browse files
feat(d3): implement scatter-basic (#8249)
## Implementation: `scatter-basic` - javascript/d3 Implements the **javascript/d3** version of `scatter-basic`. **File:** `plots/scatter-basic/implementations/javascript/d3.js` **Parent Issue:** #611 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26850510462)* --------- 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 8a746fe commit 8500d08

2 files changed

Lines changed: 398 additions & 0 deletions

File tree

  • plots/scatter-basic
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// anyplot.ai
2+
// scatter-basic: Basic Scatter Plot
3+
// Library: d3 7.9.0 | JavaScript 22.22.3
4+
// Quality: 92/100 | Created: 2026-06-02
5+
6+
const t = window.ANYPLOT_TOKENS;
7+
const { width, height } = window.ANYPLOT_SIZE;
8+
const margin = { top: 80, right: 60, bottom: 100, left: 120 };
9+
const iw = width - margin.left - margin.right;
10+
const ih = height - margin.top - margin.bottom;
11+
12+
// --- Data (deterministic LCG seed=42, marketing spend vs sales revenue) ----
13+
let seed = 42;
14+
function lcgRand() {
15+
seed = (1664525 * seed + 1013904223) >>> 0;
16+
return seed / 4294967296;
17+
}
18+
function lcgRandn() {
19+
const u1 = lcgRand() + 1e-10;
20+
const u2 = lcgRand();
21+
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
22+
}
23+
24+
const data = Array.from({ length: 100 }, () => {
25+
const spend = 10 + lcgRand() * 140;
26+
const sales = Math.max(0.3, 0.8 + spend * 0.035 + lcgRandn() * 1.2);
27+
return { x: spend, y: sales };
28+
});
29+
30+
// --- OLS regression and Pearson r ------------------------------------------
31+
const n = data.length;
32+
const sumX = data.reduce((s, d) => s + d.x, 0);
33+
const sumY = data.reduce((s, d) => s + d.y, 0);
34+
const sumXY = data.reduce((s, d) => s + d.x * d.y, 0);
35+
const sumX2 = data.reduce((s, d) => s + d.x * d.x, 0);
36+
const sumY2 = data.reduce((s, d) => s + d.y * d.y, 0);
37+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
38+
const intercept = (sumY - slope * sumX) / n;
39+
const r = (n * sumXY - sumX * sumY) /
40+
Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
41+
42+
// --- SVG mount -------------------------------------------------------------
43+
const svg = d3.select("#container")
44+
.append("svg")
45+
.attr("width", width)
46+
.attr("height", height);
47+
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
48+
49+
// --- Scales ----------------------------------------------------------------
50+
const x = d3.scaleLinear()
51+
.domain([0, d3.max(data, (d) => d.x) * 1.04]).nice().range([0, iw]);
52+
const y = d3.scaleLinear()
53+
.domain([0, d3.max(data, (d) => d.y) * 1.04]).nice().range([ih, 0]);
54+
55+
// --- Grid lines (both axes, floating-grid — no domain line) ----------------
56+
g.append("g").attr("transform", `translate(0,${ih})`)
57+
.call(d3.axisBottom(x).ticks(8).tickSize(-ih).tickFormat(""))
58+
.call((ax) => ax.select(".domain").remove())
59+
.call((ax) => ax.selectAll("line").attr("stroke", t.grid));
60+
61+
g.append("g")
62+
.call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(""))
63+
.call((ax) => ax.select(".domain").remove())
64+
.call((ax) => ax.selectAll("line").attr("stroke", t.grid));
65+
66+
// --- X axis (floating — domain removed for clean open look) ----------------
67+
g.append("g").attr("transform", `translate(0,${ih})`)
68+
.call(d3.axisBottom(x).ticks(8).tickFormat((d) => `${d}K`))
69+
.call((ax) => ax.select(".domain").remove())
70+
.call((ax) => ax.selectAll(".tick text").attr("fill", t.inkSoft).style("font-size", "18px"))
71+
.call((ax) => ax.selectAll(".tick line").remove());
72+
73+
// --- Y axis (floating — domain removed for clean open look) ----------------
74+
g.append("g")
75+
.call(d3.axisLeft(y).ticks(6).tickFormat((d) => `$${d.toFixed(1)}M`))
76+
.call((ax) => ax.select(".domain").remove())
77+
.call((ax) => ax.selectAll(".tick text").attr("fill", t.inkSoft).style("font-size", "18px"))
78+
.call((ax) => ax.selectAll(".tick line").remove());
79+
80+
// --- OLS trend line --------------------------------------------------------
81+
const xDom = x.domain();
82+
g.append("path")
83+
.datum([
84+
{ x: xDom[0], y: intercept + slope * xDom[0] },
85+
{ x: xDom[1], y: intercept + slope * xDom[1] },
86+
])
87+
.attr("fill", "none")
88+
.attr("stroke", t.palette[0])
89+
.attr("stroke-width", 2.5)
90+
.attr("stroke-opacity", 0.5)
91+
.attr("d", d3.line().x((d) => x(d.x)).y((d) => y(d.y)));
92+
93+
// --- Scatter markers -------------------------------------------------------
94+
g.selectAll("circle").data(data).join("circle")
95+
.attr("cx", (d) => x(d.x))
96+
.attr("cy", (d) => y(d.y))
97+
.attr("r", 8)
98+
.attr("fill", t.palette[0])
99+
.attr("fill-opacity", 0.7)
100+
.attr("stroke", t.pageBg)
101+
.attr("stroke-width", 1.5);
102+
103+
// --- Correlation annotation (upper-right margin) ---------------------------
104+
g.append("text")
105+
.attr("x", iw - 8)
106+
.attr("y", 22)
107+
.attr("text-anchor", "end")
108+
.attr("fill", t.inkSoft)
109+
.style("font-size", "16px")
110+
.text(`r ≈ ${r.toFixed(2)}`);
111+
112+
// --- Axis labels -----------------------------------------------------------
113+
svg.append("text")
114+
.attr("x", margin.left + iw / 2).attr("y", height - 18)
115+
.attr("text-anchor", "middle").attr("fill", t.inkSoft)
116+
.style("font-size", "22px").text("Marketing Spend ($K)");
117+
118+
svg.append("text")
119+
.attr("transform", `translate(22,${margin.top + ih / 2}) rotate(-90)`)
120+
.attr("text-anchor", "middle").attr("fill", t.inkSoft)
121+
.style("font-size", "22px").text("Sales Revenue ($M)");
122+
123+
// --- Title -----------------------------------------------------------------
124+
svg.append("text")
125+
.attr("x", width / 2).attr("y", 48)
126+
.attr("text-anchor", "middle").attr("fill", t.ink)
127+
.style("font-size", "28px").style("font-weight", "600")
128+
.text("scatter-basic · javascript · d3 · anyplot.ai");
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
library: d3
2+
language: javascript
3+
specification_id: scatter-basic
4+
created: '2026-06-02T22:05:57Z'
5+
updated: '2026-06-02T22:24:59Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 26850510462
8+
issue: 611
9+
language_version: 22.22.3
10+
library_version: 7.9.0
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/scatter-basic/javascript/d3/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/scatter-basic/javascript/d3/plot-dark.png
13+
preview_html_light: null
14+
preview_html_dark: null
15+
quality_score: 92
16+
review:
17+
strengths:
18+
- OLS regression trend line with correlation annotation (r≈0.81) creates a clear
19+
analytical narrative — viewer immediately grasps the marketing-spend → sales relationship
20+
- 'Floating grid technique (two separate axis calls: one for gridlines, one for
21+
tick labels with no tick marks) is idiomatic D3 and produces a clean, open layout'
22+
- 'Perfect Imprint palette usage: #009E73 for both markers and trend line, t.pageBg
23+
for marker strokes (theme-adaptive definition ring), t.inkSoft/t.ink/t.grid for
24+
all chrome'
25+
- Deterministic LCG seed (seed=42) with Box-Muller transform satisfies reproducibility
26+
requirement for JS environment without seeded Math.random
27+
- All font sizes explicitly set (title 28px, axis labels 22px, ticks 18px, annotation
28+
16px) with balanced typography hierarchy
29+
- Both HTML and PNG outputs produced as required for D3 (interactive library), and
30+
ANYPLOT_SIZE/ANYPLOT_TOKENS contract correctly observed
31+
weaknesses:
32+
- 'DE-01 not at top tier: design choices (OLS line, annotation, floating grid) are
33+
thoughtful but the overall aesthetic is functional rather than striking — a reference-line
34+
annotation style or subtle confidence-band shading around the OLS line would push
35+
it toward publication-ready'
36+
- DE-02 near-perfect but Y-axis label is positioned at x=22 (very close to left
37+
canvas edge in SVG space) — while not clipped, this is inelegant and could conflict
38+
with longer Y-axis label text in future variants; increase to x=30–35
39+
- DE-03 storytelling is good but not excellent — the r≈0.81 annotation conveys the
40+
correlation but doesn't highlight any notable sub-pattern (e.g., whether diminishing
41+
returns exist at high spend, or whether outlier points distort the trend); visual
42+
emphasis on an insight would elevate this
43+
- 'LM-02: no distinctly D3-exclusive technique beyond the floating grid pattern
44+
— a D3-native enhancement such as a confidence-interval band via d3.area() or
45+
a brushable selection region would demonstrate deeper library mastery'
46+
image_description: |-
47+
Light render (plot-light.png):
48+
Background: Warm off-white (#FAF8F1) — correct light-theme page surface, no pure white
49+
Chrome: Title "scatter-basic · javascript · d3 · anyplot.ai" in dark ink at 28px CSS (56px in PNG) spans ~50% of the 3200px width — well-proportioned. X-axis label "Marketing Spend ($K)" and Y-axis label "Sales Revenue ($M)" in secondary ink (t.inkSoft) at 22px CSS. Tick labels (18px CSS) readable for all values: 0K–160K on X, $0.0M–$8.0M on Y. Correlation annotation "r ≈ 0.81" in upper right, visible.
50+
Data: 100 scatter points in #009E73 (Imprint brand green) with fill-opacity 0.7 and white page-background stroke (1.5px) for clean separation. OLS trend line in same green at 50% opacity, spanning full data range. Grid lines are subtle (t.grid token). Domain/axis lines removed for clean open look. Tick marks removed (labels only).
51+
Legibility verdict: PASS — all text clearly readable; no light-on-light issues; no element clipping at canvas edges.
52+
53+
Dark render (plot-dark.png):
54+
Background: Warm near-black (#1A1A17) — correct dark-theme page surface, no pure black
55+
Chrome: Title and axis labels switch to light ink (t.ink / t.inkSoft = #F0EFE8 / #B8B7B0) — clearly visible against dark background. Tick labels light-colored, all readable. Correlation annotation "r ≈ 0.81" visible in upper right (t.inkSoft adapts to light gray on dark). Grid lines subtle (t.grid token adapts to rgba(240,239,232,0.15)).
56+
Data: Scatter points remain identical #009E73 (Imprint palette is theme-independent) — data colors unchanged from light render as required. Marker strokes use t.pageBg = #1A1A17 (dark), creating a subtle dark ring that still visually separates overlapping points. OLS trend line same #009E73 at 50% opacity, visible on dark background.
57+
Legibility verdict: PASS — no dark-on-dark failures; all text uses light-themed tokens; brand green #009E73 readable on #1A1A17 surface.
58+
criteria_checklist:
59+
visual_quality:
60+
score: 30
61+
max: 30
62+
items:
63+
- id: VQ-01
64+
name: Text Legibility
65+
score: 8
66+
max: 8
67+
passed: true
68+
comment: 'All font sizes explicitly set: title 28px CSS, axis labels 22px,
69+
ticks 18px, annotation 16px. Well-proportioned hierarchy, readable at both
70+
full-res and 400px mobile thumbnail.'
71+
- id: VQ-02
72+
name: No Overlap
73+
score: 6
74+
max: 6
75+
passed: true
76+
comment: No overlapping elements. Correlation annotation placed in upper-right
77+
empty area, tick labels spaced appropriately.
78+
- id: VQ-03
79+
name: Element Visibility
80+
score: 6
81+
max: 6
82+
passed: true
83+
comment: 100 points with r=8 CSS (16px in PNG), fill-opacity=0.7 — optimal
84+
for medium density. White-ring stroke helps define overlapping markers.
85+
OLS line visible.
86+
- id: VQ-04
87+
name: Color Accessibility
88+
score: 2
89+
max: 2
90+
passed: true
91+
comment: 'Single series in #009E73 (colorblind-safe Imprint brand green).
92+
Adequate contrast on both surface colors.'
93+
- id: VQ-05
94+
name: Layout & Canvas
95+
score: 4
96+
max: 4
97+
passed: true
98+
comment: Canvas gate passed (no /tmp/anyplot-canvas-gate.txt). Plot fills
99+
~71% of canvas (inner 1420x720 CSS of 1600x900). Margins balanced. Nothing
100+
cut off.
101+
- id: VQ-06
102+
name: Axis Labels & Title
103+
score: 2
104+
max: 2
105+
passed: true
106+
comment: 'X: ''Marketing Spend ($K)'', Y: ''Sales Revenue ($M)'' — both descriptive
107+
with units.'
108+
- id: VQ-07
109+
name: Palette Compliance
110+
score: 2
111+
max: 2
112+
passed: true
113+
comment: 't.palette[0]=#009E73 used for markers and trend line. Backgrounds
114+
#FAF8F1 light / #1A1A17 dark. All chrome tokens (t.ink, t.inkSoft, t.grid,
115+
t.pageBg) correctly applied. Data colors identical across themes.'
116+
design_excellence:
117+
score: 14
118+
max: 20
119+
items:
120+
- id: DE-01
121+
name: Aesthetic Sophistication
122+
score: 5
123+
max: 8
124+
passed: true
125+
comment: 'Above configured-default (4): OLS trend line, correlation annotation,
126+
floating grid, and marker stroke ring show design thinking. Not yet ''strong
127+
design'' (6): visual is clean and functional but lacks a striking focal
128+
element.'
129+
- id: DE-02
130+
name: Visual Refinement
131+
score: 5
132+
max: 6
133+
passed: true
134+
comment: Domain lines removed, tick marks removed, subtle grid via theme tokens,
135+
generous whitespace, page-bg marker strokes. Y-axis label at x=22 is slightly
136+
inelegant but functional. Nearly perfect.
137+
- id: DE-03
138+
name: Data Storytelling
139+
score: 4
140+
max: 6
141+
passed: true
142+
comment: 'OLS trend line + r≈0.81 annotation create a clear quantitative narrative.
143+
''Good'' (4): visual hierarchy guides the reader to the correlation story,
144+
but not ''Excellent'' (6) — no emphasis on outliers or sub-patterns.'
145+
spec_compliance:
146+
score: 15
147+
max: 15
148+
items:
149+
- id: SC-01
150+
name: Plot Type
151+
score: 5
152+
max: 5
153+
passed: true
154+
comment: Correct 2D scatter plot.
155+
- id: SC-02
156+
name: Required Features
157+
score: 4
158+
max: 4
159+
passed: true
160+
comment: Points plotted, alpha~0.7, grid lines, axis labels, descriptive title
161+
— all spec requirements met.
162+
- id: SC-03
163+
name: Data Mapping
164+
score: 3
165+
max: 3
166+
passed: true
167+
comment: X=Marketing Spend, Y=Sales Revenue. All data visible on axes.
168+
- id: SC-04
169+
name: Title & Legend
170+
score: 3
171+
max: 3
172+
passed: true
173+
comment: Title is 'scatter-basic · javascript · d3 · anyplot.ai'. No legend
174+
needed for single series.
175+
data_quality:
176+
score: 15
177+
max: 15
178+
items:
179+
- id: DQ-01
180+
name: Feature Coverage
181+
score: 6
182+
max: 6
183+
passed: true
184+
comment: 100 points with positive correlation, visible scatter/noise, outliers
185+
present, full range of variation shown.
186+
- id: DQ-02
187+
name: Realistic Context
188+
score: 5
189+
max: 5
190+
passed: true
191+
comment: Marketing spend vs. sales revenue is a real, neutral, comprehensible
192+
business scenario.
193+
- id: DQ-03
194+
name: Appropriate Scale
195+
score: 4
196+
max: 4
197+
passed: true
198+
comment: $10K–$150K spend range, $0–$8M sales range — plausible proportions
199+
for a mid-sized business.
200+
code_quality:
201+
score: 10
202+
max: 10
203+
items:
204+
- id: CQ-01
205+
name: KISS Structure
206+
score: 3
207+
max: 3
208+
passed: true
209+
comment: Data → OLS calc → SVG mount → axes → trend line → markers → annotation
210+
→ labels → title. LCG helpers are minimal and necessary.
211+
- id: CQ-02
212+
name: Reproducibility
213+
score: 2
214+
max: 2
215+
passed: true
216+
comment: Fixed-seed LCG (seed=42) with Box-Muller satisfies JS reproducibility
217+
requirement.
218+
- id: CQ-03
219+
name: Clean Imports
220+
score: 2
221+
max: 2
222+
passed: true
223+
comment: No imports needed; d3 is a global. All code is used.
224+
- id: CQ-04
225+
name: Code Elegance
226+
score: 2
227+
max: 2
228+
passed: true
229+
comment: Clean, well-organized. No fake interactive elements. OLS computation
230+
is minimal and appropriate.
231+
- id: CQ-05
232+
name: Output & API
233+
score: 1
234+
max: 1
235+
passed: true
236+
comment: D3 is an interactive library; harness emits plot-light.png + plot-dark.png
237+
+ plot-light.html + plot-dark.html. Current D3 7.9.0 API used.
238+
library_mastery:
239+
score: 8
240+
max: 10
241+
items:
242+
- id: LM-01
243+
name: Idiomatic Usage
244+
score: 5
245+
max: 5
246+
passed: true
247+
comment: 'Expert D3 idioms: data join (.data().join()), d3.axis generators
248+
with .call() chaining, d3.line() for trend line, floating grid (separate
249+
axis call with tickSize(-ih) and no labels), ANYPLOT_SIZE/ANYPLOT_TOKENS
250+
contract correctly observed.'
251+
- id: LM-02
252+
name: Distinctive Features
253+
score: 3
254+
max: 5
255+
passed: true
256+
comment: Floating grid (dual-axis trick) and data join pattern are distinctly
257+
D3. No D3-exclusive technique beyond this — e.g., no d3.area() confidence
258+
band, no d3.brush(), no hierarchy/force layout features.
259+
verdict: APPROVED
260+
impl_tags:
261+
dependencies: []
262+
techniques:
263+
- annotations
264+
patterns:
265+
- data-generation
266+
dataprep:
267+
- regression
268+
styling:
269+
- alpha-blending
270+
- edge-highlighting

0 commit comments

Comments
 (0)