{"spec_id":"dumbbell-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// dumbbell-basic: Basic Dumbbell Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-06-30\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 95, right: 130, bottom: 90, left: 175 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Employee satisfaction scores (0–100) before and after flexible work policy\nconst rawData = [\n  { dept: \"Research\",         before: 57, after: 79 },\n  { dept: \"Engineering\",      before: 72, after: 91 },\n  { dept: \"Product\",          before: 63, after: 81 },\n  { dept: \"IT Support\",       before: 65, after: 82 },\n  { dept: \"Design\",           before: 68, after: 84 },\n  { dept: \"Operations\",       before: 71, after: 83 },\n  { dept: \"Marketing\",        before: 60, after: 71 },\n  { dept: \"Customer Success\", before: 66, after: 76 },\n  { dept: \"HR\",               before: 75, after: 83 },\n  { dept: \"Finance\",          before: 62, after: 69 },\n];\n\n// Sort by improvement (largest gap at top)\nconst data = rawData.slice().sort((a, b) => (b.after - b.before) - (a.after - a.before));\ndata.forEach(d => { d.delta = d.after - d.before; });\n\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// --- D3 SVG defs: linear gradient for connecting lines (green → lavender) ---\n// Defined in SVG user-space so position tracks the data scale exactly\nconst defs = svg.append(\"defs\");\nconst grad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"line-grad\")\n  .attr(\"gradientUnits\", \"userSpaceOnUse\")\n  .attr(\"x1\", margin.left)\n  .attr(\"x2\", margin.left + iw)\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0);\ngrad.append(\"stop\").attr(\"offset\", \"0%\")\n  .attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.65);\ngrad.append(\"stop\").attr(\"offset\", \"100%\")\n  .attr(\"stop-color\", t.palette[1]).attr(\"stop-opacity\", 0.65);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Scales\nconst x = d3.scaleLinear().domain([50, 100]).range([0, iw]);\nconst y = d3.scaleBand()\n  .domain(data.map(d => d.dept))\n  .range([0, ih])\n  .padding(0.4);\n\n// Vertical gridlines\ng.selectAll(\".vgrid\")\n  .data(x.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", d => x(d))\n  .attr(\"x2\", d => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// X-axis — d3.format(\"d\") produces clean integers with no decimal noise\nconst xAxisG = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickSize(0).tickFormat(d3.format(\"d\")));\n\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\nxAxisG.selectAll(\".tick text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .attr(\"dy\", \"1.3em\");\nxAxisG.selectAll(\".tick line\").remove();\n\n// Y-axis (no domain line — categories self-label)\nconst yAxisG = g.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(0));\n\nyAxisG.select(\".domain\").remove();\nyAxisG.selectAll(\".tick text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .attr(\"dx\", \"-0.6em\");\n\n// Connecting lines — gradient stroke encodes before→after direction\ng.selectAll(\".dumbbell-line\")\n  .data(data)\n  .join(\"line\")\n  .attr(\"x1\", d => x(d.before))\n  .attr(\"x2\", d => x(d.after))\n  .attr(\"y1\", d => y(d.dept) + y.bandwidth() / 2)\n  .attr(\"y2\", d => y(d.dept) + y.bandwidth() / 2)\n  .attr(\"stroke\", \"url(#line-grad)\")\n  .attr(\"stroke-width\", 3);\n\n// Before dots (Imprint palette[0] — brand green)\ng.selectAll(\".dot-before\")\n  .data(data)\n  .join(\"circle\")\n  .attr(\"cx\", d => x(d.before))\n  .attr(\"cy\", d => y(d.dept) + y.bandwidth() / 2)\n  .attr(\"r\", 10)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// After dots (Imprint palette[1] — lavender)\ng.selectAll(\".dot-after\")\n  .data(data)\n  .join(\"circle\")\n  .attr(\"cx\", d => x(d.after))\n  .attr(\"cy\", d => y(d.dept) + y.bandwidth() / 2)\n  .attr(\"r\", 10)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// Delta labels — d3.format(\"+d\") shows signed improvement for each row\nconst deltaFmt = d3.format(\"+d\");\ng.selectAll(\".delta-label\")\n  .data(data)\n  .join(\"text\")\n  .attr(\"x\", iw + 14)\n  .attr(\"y\", d => y(d.dept) + y.bandwidth() / 2 + 5)\n  .attr(\"fill\", (d, i) => i < 2 ? t.palette[1] : t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", (d, i) => i < 2 ? \"700\" : \"400\")\n  .text(d => deltaFmt(d.delta) + \" pts\");\n\n// Callout annotations for top 2 improvements — placed in the gap below each band\nconst annLabels = [\"▲ Highest gain\", \"▲ Runner-up\"];\ndata.slice(0, 2).forEach((d, i) => {\n  const xMid = (x(d.before) + x(d.after)) / 2;\n  const yAnn = y(d.dept) + y.bandwidth() + 6;\n  g.append(\"text\")\n    .attr(\"x\", xMid)\n    .attr(\"y\", yAnn + 10)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.amber)\n    .style(\"font-size\", \"12px\")\n    .style(\"font-weight\", \"600\")\n    .text(annLabels[i]);\n});\n\n// X-axis label\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Satisfaction Score (0–100)\");\n\n// Legend (horizontal, centered below title in top margin)\nconst legendItems = [\n  { label: \"Before Policy\", color: t.palette[0] },\n  { label: \"After Policy\",  color: t.palette[1] },\n];\nconst legendSpacing = 170;\nconst legendStartX = (width - legendItems.length * legendSpacing) / 2;\nconst legendY = 72;\n\nlegendItems.forEach((item, i) => {\n  const lx = legendStartX + i * legendSpacing;\n  svg.append(\"circle\")\n    .attr(\"cx\", lx + 10).attr(\"cy\", legendY)\n    .attr(\"r\", 10)\n    .attr(\"fill\", item.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n  svg.append(\"text\")\n    .attr(\"x\", lx + 26).attr(\"y\", legendY + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(item.label);\n});\n\n// Title — font-size scaled for long descriptive title\nconst title = \"Employee Satisfaction · dumbbell-basic · javascript · d3 · anyplot.ai\";\nconst titleFontSize = title.length > 67 ? Math.round(22 * 67 / title.length) : 22;\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 42)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}