{"spec_id":"bar-diverging","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-diverging: Diverging Bar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 90, bottom: 90, left: 260 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: year-over-year revenue growth by product line (%) ---------------\nconst productLines = [\n  { category: \"Cloud Services\", value: 24 },\n  { category: \"Data Analytics\", value: 18 },\n  { category: \"Cybersecurity\", value: 15 },\n  { category: \"IoT Sensors\", value: 12 },\n  { category: \"Streaming Media\", value: 7 },\n  { category: \"Mobile Devices\", value: 3 },\n  { category: \"Enterprise Software\", value: -2 },\n  { category: \"Consumer Electronics\", value: -6 },\n  { category: \"Retail POS Systems\", value: -9 },\n  { category: \"Desktop Software\", value: -13 },\n  { category: \"Legacy Hardware\", value: -17 },\n  { category: \"Print Media\", value: -21 },\n];\nconst topGrower = productLines[0];\nconst topDecliner = productLines[productLines.length - 1];\n\n// --- Custom per-bar transform: rounded outer corner on the value end only,\n// square on the zero-baseline end (a fine-grained d3.path/arcTo construction\n// a declarative bar-chart API doesn't expose directly). ---------------------\nfunction divergingBarPath(xZero, xValue, yTop, barHeight, radius) {\n  const isPositive = xValue >= xZero;\n  const left = Math.min(xZero, xValue);\n  const right = Math.max(xZero, xValue);\n  const r = Math.min(radius, barHeight / 2, right - left);\n  const path = d3.path();\n  if (isPositive) {\n    path.moveTo(left, yTop);\n    path.lineTo(right - r, yTop);\n    path.arcTo(right, yTop, right, yTop + r, r);\n    path.lineTo(right, yTop + barHeight - r);\n    path.arcTo(right, yTop + barHeight, right - r, yTop + barHeight, r);\n    path.lineTo(left, yTop + barHeight);\n  } else {\n    path.moveTo(right, yTop);\n    path.lineTo(left + r, yTop);\n    path.arcTo(left, yTop, left, yTop + r, r);\n    path.lineTo(left, yTop + barHeight - r);\n    path.arcTo(left, yTop + barHeight, left + r, yTop + barHeight, r);\n    path.lineTo(right, yTop + barHeight);\n  }\n  path.closePath();\n  return path.toString();\n}\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// --- Scales -------------------------------------------------------------------\nconst maxAbs = d3.max(productLines, (d) => Math.abs(d.value));\nconst x = d3.scaleLinear().domain([-maxAbs, maxAbs]).nice().range([0, iw]);\nconst y = d3\n  .scaleBand()\n  .domain(productLines.map((d) => d.category))\n  .range([0, ih])\n  .padding(0.28);\n\n// --- Gridlines (value axis only, trimmed to major ticks) -----------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisBottom(x).ticks(6).tickSize(-ih).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Divider between the growth and decline groups (data is sorted, so the\n// sign change happens once) ------------------------------------------------\nconst growthCount = productLines.filter((d) => d.value >= 0).length;\nif (growthCount > 0 && growthCount < productLines.length) {\n  const lastGrowthY = y(productLines[growthCount - 1].category) + y.bandwidth();\n  const firstDeclineY = y(productLines[growthCount].category);\n  const dividerY = (lastGrowthY + firstDeclineY) / 2;\n  g.append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", iw)\n    .attr(\"y1\", dividerY)\n    .attr(\"y2\", dividerY)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1)\n    .attr(\"stroke-dasharray\", \"4,4\");\n}\n\n// --- Zero baseline --------------------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(0))\n  .attr(\"x2\", x(0))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Bars (rounded on the value-tip end via the custom path generator above) ---\ng.selectAll(\".bar\")\n  .data(productLines)\n  .join(\"path\")\n  .attr(\"class\", \"bar\")\n  .attr(\"d\", (d) => divergingBarPath(x(0), x(d.value), y(d.category), y.bandwidth(), 6))\n  .attr(\"fill\", (d) => (d.value >= 0 ? t.palette[0] : t.palette[4]));\n\n// --- Value labels at bar tips — the strongest grower/decliner is called out\n// with a bolder, larger label and a directional marker ---------------------\ng.selectAll(\".value-label\")\n  .data(productLines)\n  .join(\"text\")\n  .attr(\"class\", \"value-label\")\n  .attr(\"x\", (d) => x(d.value) + (d.value >= 0 ? 10 : -10))\n  .attr(\"y\", (d) => y(d.category) + y.bandwidth() / 2)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", (d) => (d.value >= 0 ? \"start\" : \"end\"))\n  .attr(\"fill\", (d) => (d === topGrower ? t.palette[0] : d === topDecliner ? t.palette[4] : t.inkSoft))\n  .style(\"font-size\", (d) => (d === topGrower || d === topDecliner ? \"17px\" : \"15px\"))\n  .style(\"font-weight\", (d) => (d === topGrower || d === topDecliner ? \"700\" : \"400\"))\n  .text((d) => {\n    const marker = d === topGrower ? \"▲ \" : d === topDecliner ? \"▼ \" : \"\";\n    return `${marker}${d.value > 0 ? \"+\" : \"\"}${d.value}%`;\n  });\n\n// --- Axes -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickFormat((d) => `${d}%`));\nxAxis\n  .selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", (d) => (d === 0 ? \"700\" : \"400\"));\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0));\nyAxis\n  .selectAll(\"text\")\n  .attr(\"fill\", (d) => (d === topGrower.category ? t.palette[0] : d === topDecliner.category ? t.palette[4] : t.inkSoft))\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", (d) => (d === topGrower.category || d === topDecliner.category ? \"700\" : \"400\"));\nyAxis.select(\".domain\").remove();\n\n// --- Axis label ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Year-over-Year Revenue Growth (%)\");\n\n// --- Legend (growth / decline) ----------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${width - margin.right - 210},${margin.top - 56})`);\nconst legendItems = [\n  { label: \"Growth\", color: t.palette[0] },\n  { label: \"Decline\", color: t.palette[4] },\n];\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(${i * 110},0)`);\n  row.append(\"rect\").attr(\"width\", 16).attr(\"height\", 16).attr(\"fill\", item.color);\n  row\n    .append(\"text\")\n    .attr(\"x\", 24)\n    .attr(\"y\", 13)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- Title -------------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"bar-diverging · javascript · d3 · anyplot.ai\");\n"}