{"spec_id":"bar-diverging-likert","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-diverging-likert: Likert Scale Diverging Bar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-24\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 70, bottom: 90, left: 460 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic; each row sums to 100%) -----------------\n// Employee engagement survey, 5-point Likert scale\nconst questions = [\n  { question: \"My manager provides clear expectations\", strongly_disagree: 4, disagree: 9, neutral: 12, agree: 46, strongly_agree: 29 },\n  { question: \"I have the tools I need to do my job well\", strongly_disagree: 6, disagree: 11, neutral: 14, agree: 41, strongly_agree: 28 },\n  { question: \"I'd recommend this company to a friend\", strongly_disagree: 5, disagree: 13, neutral: 17, agree: 38, strongly_agree: 27 },\n  { question: \"Leadership communicates our direction clearly\", strongly_disagree: 8, disagree: 15, neutral: 18, agree: 37, strongly_agree: 22 },\n  { question: \"I receive recognition for good work\", strongly_disagree: 9, disagree: 16, neutral: 20, agree: 34, strongly_agree: 21 },\n  { question: \"My workload is manageable\", strongly_disagree: 11, disagree: 19, neutral: 16, agree: 33, strongly_agree: 21 },\n  { question: \"I see a clear path for career growth here\", strongly_disagree: 13, disagree: 21, neutral: 19, agree: 30, strongly_agree: 17 },\n  { question: \"Cross-team collaboration works well\", strongly_disagree: 15, disagree: 22, neutral: 21, agree: 28, strongly_agree: 14 },\n];\n\n// Sort by net agreement (agree + strongly_agree − disagree − strongly_disagree), descending\nconst netAgreement = (d) => d.agree + d.strongly_agree - d.disagree - d.strongly_disagree;\nquestions.sort((a, b) => netAgreement(b) - netAgreement(a));\n\n// --- Categories & diverging colors (Imprint anchors: red <-> muted <-> blue) -\nconst red = t.palette[4]; // #AE3030 — semantic anchor, negative pole\nconst blue = t.palette[2]; // #4467A3 — Imprint blue, positive pole\nconst muted = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\"; // muted semantic anchor (theme-adaptive)\nconst categories = [\n  { key: \"strongly_disagree\", label: \"Strongly Disagree\", color: red },\n  { key: \"disagree\", label: \"Disagree\", color: d3.interpolateRgb(red, muted)(0.5) },\n  { key: \"neutral\", label: \"Neutral\", color: muted },\n  { key: \"agree\", label: \"Agree\", color: d3.interpolateRgb(blue, muted)(0.5) },\n  { key: \"strongly_agree\", label: \"Strongly Agree\", color: blue },\n];\nconst colorOf = new Map(categories.map((c) => [c.key, c.color]));\n\n// --- Diverging segments: neutral splits evenly across the center baseline ---\nconst segmentsFor = (d) => {\n  const half = d.neutral / 2;\n  return [\n    { key: \"strongly_disagree\", x0: -(half + d.disagree + d.strongly_disagree), x1: -(half + d.disagree), value: d.strongly_disagree },\n    { key: \"disagree\", x0: -(half + d.disagree), x1: -half, value: d.disagree },\n    { key: \"neutral\", x0: -half, x1: half, value: d.neutral },\n    { key: \"agree\", x0: half, x1: half + d.agree, value: d.agree },\n    { key: \"strongly_agree\", x0: half + d.agree, x1: half + d.agree + d.strongly_agree, value: d.strongly_agree },\n  ];\n};\nconst rows = questions.map((d) => ({ question: d.question, segments: segmentsFor(d) }));\nconst maxExtent = d3.max(rows, (r) => d3.max(r.segments, (s) => Math.max(Math.abs(s.x0), Math.abs(s.x1))));\nconst roundedMax = Math.ceil(maxExtent / 10) * 10;\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain([-roundedMax, roundedMax]).range([0, iw]);\nconst y = d3.scaleBand().domain(questions.map((d) => d.question)).range([0, ih]).padding(0.35);\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Value-axis gridlines (vertical, subtle) --------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickSize(-ih).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Center baseline (x = 0) ------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(0)).attr(\"x2\", x(0))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5).attr(\"opacity\", 0.6);\n\n// --- Bars ---------------------------------------------------------------\nconst rowG = g.selectAll(\".row\").data(rows).join(\"g\")\n  .attr(\"class\", \"row\")\n  .attr(\"transform\", (d) => `translate(0,${y(d.question)})`);\n\nrowG.selectAll(\"rect\").data((d) => d.segments).join(\"rect\")\n  .attr(\"x\", (s) => x(s.x0))\n  .attr(\"width\", (s) => x(s.x1) - x(s.x0))\n  .attr(\"y\", 0)\n  .attr(\"height\", y.bandwidth())\n  .attr(\"rx\", 2)\n  .attr(\"fill\", (s) => colorOf.get(s.key));\n\n// --- Percentage labels inside segments (where space permits) ---------------\nconst textColorFor = (fill) => {\n  const c = d3.rgb(fill);\n  const lum = 0.2126 * (c.r / 255) + 0.7152 * (c.g / 255) + 0.0722 * (c.b / 255);\n  return lum > 0.55 ? \"#1A1A17\" : \"#FFFDF6\";\n};\nrowG.selectAll(\"text.segment-label\")\n  .data((d) => d.segments.filter((s) => x(s.x1) - x(s.x0) > 34))\n  .join(\"text\")\n  .attr(\"class\", \"segment-label\")\n  .attr(\"x\", (s) => (x(s.x0) + x(s.x1)) / 2)\n  .attr(\"y\", y.bandwidth() / 2)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .attr(\"fill\", (s) => textColorFor(colorOf.get(s.key)))\n  .text((s) => `${Math.round(s.value)}%`);\n\n// --- Y-axis: question labels --------------------------------------------\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0).tickPadding(14));\nyAxis.select(\".domain\").remove();\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n\n// --- X-axis: percentage ticks, mirrored around the center -------------------\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickFormat((d) => `${Math.abs(d)}%`).ticks(6));\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2).attr(\"y\", ih + 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Share of Respondents\");\n\n// --- Legend (measured, then centered) ---------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", \"translate(0,74)\");\nconst swatchSize = 16;\nconst legendItems = legend.selectAll(\"g.legend-item\").data(categories).join(\"g\")\n  .attr(\"class\", \"legend-item\");\nlegendItems.append(\"rect\")\n  .attr(\"width\", swatchSize).attr(\"height\", swatchSize).attr(\"rx\", 3)\n  .attr(\"fill\", (d) => d.color);\nlegendItems.append(\"text\")\n  .attr(\"x\", swatchSize + 8).attr(\"y\", swatchSize - 3)\n  .style(\"font-size\", \"15px\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => d.label);\n\nconst itemGap = 34;\nconst widths = [];\nlegendItems.each(function () { widths.push(this.getBBox().width); });\nconst totalWidth = widths.reduce((a, b) => a + b, 0) + itemGap * (widths.length - 1);\nlet cursor = margin.left + iw / 2 - totalWidth / 2;\nlegendItems.each(function (d, i) {\n  d3.select(this).attr(\"transform\", `translate(${cursor},0)`);\n  cursor += widths[i] + itemGap;\n});\n\n// --- Title ----------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Employee Engagement Survey · bar-diverging-likert · javascript · d3 · anyplot.ai\");\n"}