Skip to content

Commit 61e2c46

Browse files
github-actions[bot]claudeMarkusNeusinger
authored
feat(d3): implement dumbbell-basic (#9573)
## Implementation: `dumbbell-basic` - javascript/d3 Implements the **javascript/d3** version of `dumbbell-basic`. **File:** `plots/dumbbell-basic/implementations/javascript/d3.js` **Parent Issue:** #945 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28481348050)* --------- 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: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 39a5796 commit 61e2c46

2 files changed

Lines changed: 456 additions & 0 deletions

File tree

  • plots/dumbbell-basic
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// anyplot.ai
2+
// dumbbell-basic: Basic Dumbbell Chart
3+
// Library: d3 7.9.0 | JavaScript 22.23.1
4+
// Quality: 91/100 | Created: 2026-06-30
5+
6+
const t = window.ANYPLOT_TOKENS;
7+
const { width, height } = window.ANYPLOT_SIZE;
8+
9+
const margin = { top: 95, right: 130, bottom: 90, left: 175 };
10+
const iw = width - margin.left - margin.right;
11+
const ih = height - margin.top - margin.bottom;
12+
13+
// Employee satisfaction scores (0–100) before and after flexible work policy
14+
const rawData = [
15+
{ dept: "Research", before: 57, after: 79 },
16+
{ dept: "Engineering", before: 72, after: 91 },
17+
{ dept: "Product", before: 63, after: 81 },
18+
{ dept: "IT Support", before: 65, after: 82 },
19+
{ dept: "Design", before: 68, after: 84 },
20+
{ dept: "Operations", before: 71, after: 83 },
21+
{ dept: "Marketing", before: 60, after: 71 },
22+
{ dept: "Customer Success", before: 66, after: 76 },
23+
{ dept: "HR", before: 75, after: 83 },
24+
{ dept: "Finance", before: 62, after: 69 },
25+
];
26+
27+
// Sort by improvement (largest gap at top)
28+
const data = rawData.slice().sort((a, b) => (b.after - b.before) - (a.after - a.before));
29+
data.forEach(d => { d.delta = d.after - d.before; });
30+
31+
const svg = d3.select("#container").append("svg")
32+
.attr("width", width)
33+
.attr("height", height);
34+
35+
// --- D3 SVG defs: linear gradient for connecting lines (green → lavender) ---
36+
// Defined in SVG user-space so position tracks the data scale exactly
37+
const defs = svg.append("defs");
38+
const grad = defs.append("linearGradient")
39+
.attr("id", "line-grad")
40+
.attr("gradientUnits", "userSpaceOnUse")
41+
.attr("x1", margin.left)
42+
.attr("x2", margin.left + iw)
43+
.attr("y1", 0)
44+
.attr("y2", 0);
45+
grad.append("stop").attr("offset", "0%")
46+
.attr("stop-color", t.palette[0]).attr("stop-opacity", 0.65);
47+
grad.append("stop").attr("offset", "100%")
48+
.attr("stop-color", t.palette[1]).attr("stop-opacity", 0.65);
49+
50+
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
51+
52+
// Scales
53+
const x = d3.scaleLinear().domain([50, 100]).range([0, iw]);
54+
const y = d3.scaleBand()
55+
.domain(data.map(d => d.dept))
56+
.range([0, ih])
57+
.padding(0.4);
58+
59+
// Vertical gridlines
60+
g.selectAll(".vgrid")
61+
.data(x.ticks(6))
62+
.join("line")
63+
.attr("x1", d => x(d))
64+
.attr("x2", d => x(d))
65+
.attr("y1", 0)
66+
.attr("y2", ih)
67+
.attr("stroke", t.grid)
68+
.attr("stroke-width", 1);
69+
70+
// X-axis — d3.format("d") produces clean integers with no decimal noise
71+
const xAxisG = g.append("g")
72+
.attr("transform", `translate(0,${ih})`)
73+
.call(d3.axisBottom(x).ticks(6).tickSize(0).tickFormat(d3.format("d")));
74+
75+
xAxisG.select(".domain").attr("stroke", t.inkSoft);
76+
xAxisG.selectAll(".tick text")
77+
.attr("fill", t.inkSoft)
78+
.style("font-size", "14px")
79+
.attr("dy", "1.3em");
80+
xAxisG.selectAll(".tick line").remove();
81+
82+
// Y-axis (no domain line — categories self-label)
83+
const yAxisG = g.append("g")
84+
.call(d3.axisLeft(y).tickSize(0));
85+
86+
yAxisG.select(".domain").remove();
87+
yAxisG.selectAll(".tick text")
88+
.attr("fill", t.inkSoft)
89+
.style("font-size", "14px")
90+
.attr("dx", "-0.6em");
91+
92+
// Connecting lines — gradient stroke encodes before→after direction
93+
g.selectAll(".dumbbell-line")
94+
.data(data)
95+
.join("line")
96+
.attr("x1", d => x(d.before))
97+
.attr("x2", d => x(d.after))
98+
.attr("y1", d => y(d.dept) + y.bandwidth() / 2)
99+
.attr("y2", d => y(d.dept) + y.bandwidth() / 2)
100+
.attr("stroke", "url(#line-grad)")
101+
.attr("stroke-width", 3);
102+
103+
// Before dots (Imprint palette[0] — brand green)
104+
g.selectAll(".dot-before")
105+
.data(data)
106+
.join("circle")
107+
.attr("cx", d => x(d.before))
108+
.attr("cy", d => y(d.dept) + y.bandwidth() / 2)
109+
.attr("r", 10)
110+
.attr("fill", t.palette[0])
111+
.attr("stroke", t.pageBg)
112+
.attr("stroke-width", 2);
113+
114+
// After dots (Imprint palette[1] — lavender)
115+
g.selectAll(".dot-after")
116+
.data(data)
117+
.join("circle")
118+
.attr("cx", d => x(d.after))
119+
.attr("cy", d => y(d.dept) + y.bandwidth() / 2)
120+
.attr("r", 10)
121+
.attr("fill", t.palette[1])
122+
.attr("stroke", t.pageBg)
123+
.attr("stroke-width", 2);
124+
125+
// Delta labels — d3.format("+d") shows signed improvement for each row
126+
const deltaFmt = d3.format("+d");
127+
g.selectAll(".delta-label")
128+
.data(data)
129+
.join("text")
130+
.attr("x", iw + 14)
131+
.attr("y", d => y(d.dept) + y.bandwidth() / 2 + 5)
132+
.attr("fill", (d, i) => i < 2 ? t.palette[1] : t.inkSoft)
133+
.style("font-size", "14px")
134+
.style("font-weight", (d, i) => i < 2 ? "700" : "400")
135+
.text(d => deltaFmt(d.delta) + " pts");
136+
137+
// Callout annotations for top 2 improvements — placed in the gap below each band
138+
const annLabels = ["▲ Highest gain", "▲ Runner-up"];
139+
data.slice(0, 2).forEach((d, i) => {
140+
const xMid = (x(d.before) + x(d.after)) / 2;
141+
const yAnn = y(d.dept) + y.bandwidth() + 6;
142+
g.append("text")
143+
.attr("x", xMid)
144+
.attr("y", yAnn + 10)
145+
.attr("text-anchor", "middle")
146+
.attr("fill", t.amber)
147+
.style("font-size", "12px")
148+
.style("font-weight", "600")
149+
.text(annLabels[i]);
150+
});
151+
152+
// X-axis label
153+
g.append("text")
154+
.attr("x", iw / 2)
155+
.attr("y", ih + 62)
156+
.attr("text-anchor", "middle")
157+
.attr("fill", t.inkSoft)
158+
.style("font-size", "14px")
159+
.text("Satisfaction Score (0–100)");
160+
161+
// Legend (horizontal, centered below title in top margin)
162+
const legendItems = [
163+
{ label: "Before Policy", color: t.palette[0] },
164+
{ label: "After Policy", color: t.palette[1] },
165+
];
166+
const legendSpacing = 170;
167+
const legendStartX = (width - legendItems.length * legendSpacing) / 2;
168+
const legendY = 72;
169+
170+
legendItems.forEach((item, i) => {
171+
const lx = legendStartX + i * legendSpacing;
172+
svg.append("circle")
173+
.attr("cx", lx + 10).attr("cy", legendY)
174+
.attr("r", 10)
175+
.attr("fill", item.color)
176+
.attr("stroke", t.pageBg)
177+
.attr("stroke-width", 1.5);
178+
svg.append("text")
179+
.attr("x", lx + 26).attr("y", legendY + 5)
180+
.attr("fill", t.inkSoft)
181+
.style("font-size", "13px")
182+
.text(item.label);
183+
});
184+
185+
// Title — font-size scaled for long descriptive title
186+
const title = "Employee Satisfaction · dumbbell-basic · javascript · d3 · anyplot.ai";
187+
const titleFontSize = title.length > 67 ? Math.round(22 * 67 / title.length) : 22;
188+
189+
svg.append("text")
190+
.attr("x", width / 2)
191+
.attr("y", 42)
192+
.attr("text-anchor", "middle")
193+
.attr("fill", t.ink)
194+
.style("font-size", `${titleFontSize}px`)
195+
.style("font-weight", "600")
196+
.text(title);

0 commit comments

Comments
 (0)