Skip to content

Commit 2f18dcc

Browse files
feat(d3): implement venn-labeled-items (#9506)
## Implementation: `venn-labeled-items` - javascript/d3 Implements the **javascript/d3** version of `venn-labeled-items`. **File:** `plots/venn-labeled-items/implementations/javascript/d3.js` **Parent Issue:** #5364 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28167148901)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 9192164 commit 2f18dcc

2 files changed

Lines changed: 387 additions & 0 deletions

File tree

  • plots/venn-labeled-items
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// anyplot.ai
2+
// venn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items
3+
// Library: d3 7.9.0 | JavaScript 22.23.0
4+
// Quality: 87/100 | Created: 2026-06-25
5+
//# anyplot-orientation: square
6+
7+
const t = window.ANYPLOT_TOKENS;
8+
const { width, height } = window.ANYPLOT_SIZE;
9+
const THEME = window.ANYPLOT_THEME || "light";
10+
const inkMuted = THEME === "dark" ? "#A8A79F" : "#6B6A63";
11+
12+
// --- Data ---
13+
const circleData = [
14+
{ name: "Overhyped", cx: 600, cy: 480, r: 270 },
15+
{ name: "Actually Useful", cx: 738, cy: 718, r: 270 },
16+
{ name: "Secretly Loved", cx: 462, cy: 718, r: 270 },
17+
];
18+
19+
// D3 ordinal scale maps category names to Imprint palette in canonical order
20+
const colorScale = d3.scaleOrdinal()
21+
.domain(circleData.map(d => d.name))
22+
.range(t.palette.slice(0, 3));
23+
24+
// Centroid of each Venn zone for label placement
25+
const ZONE_CENTROIDS = {
26+
A: { x: 600, y: 314 },
27+
B: { x: 878, y: 800 },
28+
C: { x: 322, y: 800 },
29+
AB: { x: 730, y: 566 },
30+
AC: { x: 470, y: 566 },
31+
BC: { x: 600, y: 840 },
32+
ABC: { x: 600, y: 638 },
33+
outside: { x: 975, y: 360 },
34+
};
35+
36+
const items = [
37+
{ label: "NFTs", zone: "A" },
38+
{ label: "Metaverse", zone: "A" },
39+
{ label: "Segway", zone: "A" },
40+
{ label: "Google Glass", zone: "A" },
41+
{ label: "Wikipedia", zone: "B" },
42+
{ label: "GPS Navigation", zone: "B" },
43+
{ label: "Cloud Backup", zone: "B" },
44+
{ label: "Password Managers", zone: "B" },
45+
{ label: "Spreadsheets", zone: "C" },
46+
{ label: "Fax Machines", zone: "C" },
47+
{ label: "Cable TV", zone: "C" },
48+
{ label: "ChatGPT", zone: "AB" },
49+
{ label: "Electric Scooters", zone: "AB" },
50+
{ label: "TikTok", zone: "AC" },
51+
{ label: "Gamification", zone: "AC" },
52+
{ label: "Dark Mode", zone: "BC" },
53+
{ label: "RSS Feeds", zone: "BC" },
54+
{ label: "Sourdough", zone: "ABC" },
55+
{ label: "Zoom", zone: "ABC" },
56+
{ label: "Landlines", zone: "outside" },
57+
];
58+
59+
// Use d3.group to partition items by zone, then compute vertical stacking positions
60+
const SPACING = 24;
61+
const itemsByZone = d3.group(items, d => d.zone);
62+
63+
itemsByZone.forEach((zoneItems, zone) => {
64+
const { x: zx, y: zy } = ZONE_CENTROIDS[zone];
65+
const totalH = (zoneItems.length - 1) * SPACING;
66+
zoneItems.forEach((item, i) => {
67+
item.x = zx;
68+
item.y = zy - totalH / 2 + i * SPACING;
69+
});
70+
});
71+
72+
// --- SVG ---
73+
const svg = d3.select("#container").append("svg")
74+
.attr("width", width)
75+
.attr("height", height);
76+
77+
// --- Circles (data join, semi-transparent fills, colored strokes) ---
78+
svg.selectAll("circle.venn-circle").data(circleData).join("circle")
79+
.attr("class", "venn-circle")
80+
.attr("cx", d => d.cx).attr("cy", d => d.cy).attr("r", d => d.r)
81+
.attr("fill", d => colorScale(d.name)).attr("fill-opacity", 0.12)
82+
.attr("stroke", d => colorScale(d.name))
83+
.attr("stroke-width", 2.5).attr("stroke-opacity", 0.65);
84+
85+
// --- Category labels (data join per circle, editorial serif) ---
86+
const catFont = "Georgia, 'Times New Roman', serif";
87+
88+
const catLabelDefs = [
89+
{ name: "Overhyped", lines: ["Overhyped"], x: circleData[0].cx, y: circleData[0].cy - circleData[0].r - 20, anchor: "middle" },
90+
{ name: "Actually Useful", lines: ["Actually", "Useful"], x: circleData[1].cx + circleData[1].r + 26, y: circleData[1].cy - 13, anchor: "start" },
91+
{ name: "Secretly Loved", lines: ["Secretly", "Loved"], x: circleData[2].cx - circleData[2].r - 26, y: circleData[2].cy - 13, anchor: "end" },
92+
];
93+
94+
catLabelDefs.forEach(def => {
95+
const catG = svg.append("g");
96+
catG.selectAll("text").data(def.lines).join("text")
97+
.attr("x", def.x)
98+
.attr("y", (_, i) => def.y + i * 28)
99+
.attr("text-anchor", def.anchor)
100+
.attr("fill", colorScale(def.name))
101+
.style("font-size", "20px").style("font-weight", "700")
102+
.style("font-family", catFont)
103+
.text(d => d);
104+
});
105+
106+
// --- "outside all circles" annotation ---
107+
const outsideItems = items.filter(d => d.zone === "outside");
108+
if (outsideItems.length) {
109+
svg.append("text")
110+
.attr("x", ZONE_CENTROIDS.outside.x)
111+
.attr("y", d3.min(outsideItems, d => d.y) - 22)
112+
.attr("text-anchor", "middle").attr("fill", inkMuted)
113+
.style("font-size", "14px").style("font-style", "italic")
114+
.text("outside all circles");
115+
}
116+
117+
// --- Items (data join over all items, positions from d3.group stacking) ---
118+
svg.selectAll("text.venn-item").data(items).join("text")
119+
.attr("class", "venn-item")
120+
.attr("x", d => d.x)
121+
.attr("y", d => d.y)
122+
.attr("text-anchor", "middle")
123+
.attr("dominant-baseline", "middle")
124+
.attr("fill", d => d.zone === "outside" ? inkMuted : t.inkSoft)
125+
.style("font-size", "14px")
126+
.style("font-style", d => d.zone === "outside" ? "italic" : "normal")
127+
.style("font-family", "system-ui, -apple-system, sans-serif")
128+
.text(d => d.label);
129+
130+
// --- Title ---
131+
svg.append("text")
132+
.attr("x", width / 2).attr("y", 46)
133+
.attr("text-anchor", "middle").attr("fill", t.ink)
134+
.style("font-size", "22px").style("font-weight", "600")
135+
.text("venn-labeled-items · javascript · d3 · anyplot.ai");
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
library: d3
2+
language: javascript
3+
specification_id: venn-labeled-items
4+
created: '2026-06-25T11:44:36Z'
5+
updated: '2026-06-25T12:15:23Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 28167148901
8+
issue: 5364
9+
language_version: 22.23.0
10+
library_version: 7.9.0
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/venn-labeled-items/javascript/d3/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/venn-labeled-items/javascript/d3/plot-dark.png
13+
preview_html_light: https://storage.googleapis.com/anyplot-images/plots/venn-labeled-items/javascript/d3/plot-light.html
14+
preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/venn-labeled-items/javascript/d3/plot-dark.html
15+
quality_score: 87
16+
review:
17+
strengths:
18+
- 'Perfect spec compliance: all seven zones plus outside populated, semi-transparent
19+
fills, category labels outside circles in their correct colors'
20+
- 'Excellent data quality: real witty editorial items with believable zone assignments
21+
that fit the WIRED Chartgeist aesthetic'
22+
- Clean idiomatic D3 using d3.group data joins and ANYPLOT_TOKENS/ANYPLOT_SIZE correctly
23+
- 'Full theme adaptation: both light and dark renders pass readability with no dark-on-dark
24+
failures'
25+
- Serif category labels (Georgia) reinforce the editorial magazine aesthetic called
26+
for in the spec
27+
weaknesses:
28+
- Item labels at 14px are at the lower end for a 2400 px canvas — bumping to 15-16px
29+
would improve mobile readability without crowding zones
30+
- 'No visual hierarchy among items: all render identically (same color, size) —
31+
consider slightly larger or bolder text for ABC intersection items (Sourdough,
32+
Zoom) to make the triple-overlap punchline pop'
33+
- Optional editorial subtitle absent — adding a light subtitle like 'A Cultural
34+
Status Report' in smaller italic serif would elevate the editorial feel
35+
- Zone centroids for outside items (x=975) sit close to the right canvas edge; a
36+
small left-shift would provide more breathing room
37+
image_description: |-
38+
Light render (plot-light.png):
39+
Background: Warm off-white (#FAF8F1) — correct theme surface
40+
Chrome: Title "venn-labeled-items · javascript · d3 · anyplot.ai" at top in dark ink (22px, weight 600) — fully readable. Category labels in bold Georgia serif in their respective palette colors (green/lavender/blue) — clearly visible. No axis labels or tick labels (not applicable for Venn).
41+
Data: Three overlapping circles — green (#009E73) for Overhyped, lavender (#C475FD) for Actually Useful, blue (#4467A3) for Secretly Loved. 12% opacity fills show overlapping regions. Item labels in 14px sans-serif using inkSoft token. Outside items in muted italic.
42+
Legibility verdict: PASS — all text readable against light background; no light-on-light issues
43+
44+
Dark render (plot-dark.png):
45+
Background: Warm near-black (#1A1A17) — correct theme surface
46+
Chrome: Title and item labels switch to light-colored text (inkSoft token — light gray) — clearly readable against the dark surface. Category labels remain in the same palette colors (green/lavender/blue), all readable against dark background. No dark-on-dark failures detected.
47+
Data: Data colors are identical to the light render — same green, lavender, and blue circle strokes and fills. The 12% opacity fills create subtle pools against the near-black background, maintaining overlap visibility.
48+
Legibility verdict: PASS — all text readable; chrome correctly adapts to dark theme
49+
criteria_checklist:
50+
visual_quality:
51+
score: 28
52+
max: 30
53+
items:
54+
- id: VQ-01
55+
name: Text Legibility
56+
score: 7
57+
max: 8
58+
passed: true
59+
comment: 'All font sizes explicitly set (title 22px, category labels 20px
60+
bold, items 14px). Readable in both themes. Minor: item labels at 14px are
61+
at the lower bound for 2400px canvas.'
62+
- id: VQ-02
63+
name: No Overlap
64+
score: 6
65+
max: 6
66+
passed: true
67+
comment: Vertical stacking with 24px spacing prevents any text collision across
68+
all seven zones plus outside.
69+
- id: VQ-03
70+
name: Element Visibility
71+
score: 6
72+
max: 6
73+
passed: true
74+
comment: Circle strokes clearly delineate rings; 12% fills appropriately transparent;
75+
text-only items suit the editorial spec.
76+
- id: VQ-04
77+
name: Color Accessibility
78+
score: 2
79+
max: 2
80+
passed: true
81+
comment: Imprint palette is CVD-safe; semi-transparent fills let background
82+
show through.
83+
- id: VQ-05
84+
name: Layout & Canvas
85+
score: 3
86+
max: 4
87+
passed: true
88+
comment: 'Well-centered diagram filling ~70% of canvas. Minor: upper-right
89+
outside zone leaves some empty space.'
90+
- id: VQ-06
91+
name: Axis Labels & Title
92+
score: 2
93+
max: 2
94+
passed: true
95+
comment: No axes (correct for Venn); title in required format; category names
96+
as clear zone labels.
97+
- id: VQ-07
98+
name: Palette Compliance
99+
score: 2
100+
max: 2
101+
passed: true
102+
comment: 'First circle #009E73; positions 2-3 canonical Imprint order; background
103+
#FAF8F1 light / #1A1A17 dark; chrome adapts correctly.'
104+
design_excellence:
105+
score: 12
106+
max: 20
107+
items:
108+
- id: DE-01
109+
name: Aesthetic Sophistication
110+
score: 5
111+
max: 8
112+
passed: true
113+
comment: 'Intentional editorial choices: bold Georgia serif for category labels,
114+
italic for outside items, color-matched labels. Above defaults but not publication-ready.'
115+
- id: DE-02
116+
name: Visual Refinement
117+
score: 4
118+
max: 6
119+
passed: true
120+
comment: Gridless, no spines, generous whitespace, semi-transparent fills
121+
with matching strokes. Good refinement.
122+
- id: DE-03
123+
name: Data Storytelling
124+
score: 3
125+
max: 6
126+
passed: false
127+
comment: Good editorial item choices but no visual hierarchy — all items render
128+
identically, missing opportunity to emphasize the ABC triple-overlap punchline.
129+
spec_compliance:
130+
score: 15
131+
max: 15
132+
items:
133+
- id: SC-01
134+
name: Plot Type
135+
score: 5
136+
max: 5
137+
passed: true
138+
comment: Correct three-circle Venn with symmetric layout, equally-sized circles,
139+
clear overlaps.
140+
- id: SC-02
141+
name: Required Features
142+
score: 4
143+
max: 4
144+
passed: true
145+
comment: Semi-transparent fills, category names outside circles, items as
146+
labeled text, all 7 zones + outside populated, gridless.
147+
- id: SC-03
148+
name: Data Mapping
149+
score: 3
150+
max: 3
151+
passed: true
152+
comment: All items placed in correct zones; outside items separated from circles.
153+
- id: SC-04
154+
name: Title & Legend
155+
score: 3
156+
max: 3
157+
passed: true
158+
comment: Title exactly 'venn-labeled-items · javascript · d3 · anyplot.ai'.
159+
No series legend needed; category labels fulfill the role.
160+
data_quality:
161+
score: 15
162+
max: 15
163+
items:
164+
- id: DQ-01
165+
name: Feature Coverage
166+
score: 6
167+
max: 6
168+
passed: true
169+
comment: All 7 zones plus outside populated; good distribution across zone
170+
types.
171+
- id: DQ-02
172+
name: Realistic Context
173+
score: 5
174+
max: 5
175+
passed: true
176+
comment: Real pop-culture items with believable editorial judgment; matches
177+
WIRED Chartgeist aesthetic.
178+
- id: DQ-03
179+
name: Appropriate Scale
180+
score: 4
181+
max: 4
182+
passed: true
183+
comment: Zone assignments are plausible and witty; NFTs/Metaverse as Overhyped,
184+
GPS/Wikipedia as Actually Useful, Sourdough in all three.
185+
code_quality:
186+
score: 10
187+
max: 10
188+
items:
189+
- id: CQ-01
190+
name: KISS Structure
191+
score: 3
192+
max: 3
193+
passed: true
194+
comment: Data → SVG → circles → category labels → outside annotation → items
195+
→ title. No functions or classes.
196+
- id: CQ-02
197+
name: Reproducibility
198+
score: 2
199+
max: 2
200+
passed: true
201+
comment: All data hardcoded; fully deterministic.
202+
- id: CQ-03
203+
name: Clean Imports
204+
score: 2
205+
max: 2
206+
passed: true
207+
comment: d3 is the single global; no unused imports.
208+
- id: CQ-04
209+
name: Code Elegance
210+
score: 2
211+
max: 2
212+
passed: true
213+
comment: Uses d3.group for zone stacking, data joins throughout; clean and
214+
idiomatic.
215+
- id: CQ-05
216+
name: Output & API
217+
score: 1
218+
max: 1
219+
passed: true
220+
comment: D3 interactive library; harness emits plot-{theme}.png + plot-{theme}.html
221+
correctly.
222+
library_mastery:
223+
score: 7
224+
max: 10
225+
items:
226+
- id: LM-01
227+
name: Idiomatic Usage
228+
score: 4
229+
max: 5
230+
passed: true
231+
comment: Good use of d3.group (D3 v6+ API), d3.scaleOrdinal, .data().join()
232+
data-join, d3.min, correct ANYPLOT_TOKENS/ANYPLOT_SIZE usage.
233+
- id: LM-02
234+
name: Distinctive Features
235+
score: 3
236+
max: 5
237+
passed: true
238+
comment: d3.group for grouping/stacking is D3-idiomatic, but Venn geometry
239+
is entirely manually positioned rather than algorithmically derived using
240+
D3 math utilities.
241+
verdict: APPROVED
242+
impl_tags:
243+
dependencies: []
244+
techniques:
245+
- annotations
246+
patterns:
247+
- data-generation
248+
- iteration-over-groups
249+
dataprep: []
250+
styling:
251+
- alpha-blending
252+
- minimal-chrome

0 commit comments

Comments
 (0)