{"spec_id":"bar-grouped","library":"d3","language":"javascript","code":"// anyplot.ai\n// bar-grouped: Grouped Bar Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-08-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 260, bottom: 100, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Quarterly revenue (in $M) for three product lines across four fiscal quarters.\nconst categories = [\"Q1\", \"Q2\", \"Q3\", \"Q4\"];\nconst groups = [\"Hardware\", \"Software\", \"Services\"];\nconst data = [\n  { category: \"Q1\", group: \"Hardware\", value: 4.2 },\n  { category: \"Q1\", group: \"Software\", value: 6.1 },\n  { category: \"Q1\", group: \"Services\", value: 3.0 },\n  { category: \"Q2\", group: \"Hardware\", value: 4.6 },\n  { category: \"Q2\", group: \"Software\", value: 6.8 },\n  { category: \"Q2\", group: \"Services\", value: 3.4 },\n  { category: \"Q3\", group: \"Hardware\", value: 4.1 },\n  { category: \"Q3\", group: \"Software\", value: 7.9 },\n  { category: \"Q3\", group: \"Services\", value: 3.9 },\n  { category: \"Q4\", group: \"Hardware\", value: 5.3 },\n  { category: \"Q4\", group: \"Software\", value: 9.2 },\n  { category: \"Q4\", group: \"Services\", value: 4.5 },\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 x0 = d3.scaleBand().domain(categories).range([0, iw]).paddingInner(0.3);\nconst x1 = d3.scaleBand().domain(groups).range([0, x0.bandwidth()]).padding(0.12);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(data, (d) => d.value)])\n  .nice()\n  .range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(groups).range(t.palette);\n\n// --- Defs: per-series sheen gradients + growth-arrow marker ------------------\n// A hand-built path generator + gradient/marker defs is a low-level SVG\n// capability that a high-level charting library would not expose directly.\nconst defs = svg.append(\"defs\");\ngroups.forEach((group, i) => {\n  const base = d3.color(color(group));\n  const grad = defs\n    .append(\"linearGradient\")\n    .attr(\"id\", `bar-sheen-${i}`)\n    .attr(\"x1\", \"0%\")\n    .attr(\"y1\", \"0%\")\n    .attr(\"x2\", \"0%\")\n    .attr(\"y2\", \"100%\");\n  grad.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", base.brighter(0.7));\n  grad.append(\"stop\").attr(\"offset\", \"35%\").attr(\"stop-color\", base);\n  grad.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", base.darker(0.15));\n});\n\ndefs\n  .append(\"marker\")\n  .attr(\"id\", \"growth-arrow\")\n  .attr(\"viewBox\", \"0 0 10 10\")\n  .attr(\"refX\", 8)\n  .attr(\"refY\", 5)\n  .attr(\"markerWidth\", 7)\n  .attr(\"markerHeight\", 7)\n  .attr(\"orient\", \"auto-start-reverse\")\n  .append(\"path\")\n  .attr(\"d\", \"M0,0 L10,5 L0,10 Z\")\n  .attr(\"fill\", t.ink);\n\n// --- Rounded-top bar path generator ------------------------------------------\nfunction topRoundedRectPath(x, y, w, h, radius) {\n  const r = Math.min(radius, w / 2, h);\n  return `M${x},${y + r}\n    A${r},${r} 0 0 1 ${x + r},${y}\n    H${x + w - r}\n    A${r},${r} 0 0 1 ${x + w},${y + r}\n    V${y + h}\n    H${x}\n    Z`;\n}\n\n// --- Gridlines (y-axis only) ------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.select(\".grid .domain\").remove();\n\n// --- Bars -------------------------------------------------------------------\nconst categoryGroups = g\n  .selectAll(\".category-group\")\n  .data(categories)\n  .join(\"g\")\n  .attr(\"class\", \"category-group\")\n  .attr(\"transform\", (d) => `translate(${x0(d)},0)`);\n\ncategoryGroups\n  .selectAll(\"path\")\n  .data((cat) => data.filter((d) => d.category === cat))\n  .join(\"path\")\n  .attr(\"d\", (d) =>\n    topRoundedRectPath(x1(d.group), y(d.value), x1.bandwidth(), ih - y(d.value), 5)\n  )\n  .attr(\"fill\", (d) => `url(#bar-sheen-${groups.indexOf(d.group)})`)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Value labels -------------------------------------------------------------\ncategoryGroups\n  .selectAll(\"text\")\n  .data((cat) => data.filter((d) => d.category === cat))\n  .join(\"text\")\n  .attr(\"x\", (d) => x1(d.group) + x1.bandwidth() / 2)\n  .attr(\"y\", (d) => y(d.value) - 12)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"13px\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => d.value.toFixed(1));\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x0));\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).tickFormat((d) => `$${d}M`));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\"line\").remove();\n\n// --- Axis labels ------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 66)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"18px\")\n  .attr(\"fill\", t.ink)\n  .text(\"Fiscal Quarter (2025)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -96)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"18px\")\n  .attr(\"fill\", t.ink)\n  .text(\"Revenue ($M)\");\n\n// --- Growth annotation (Software Q1 -> Q4) ------------------------------------\n// Data storytelling: Software is the standout trend (6.1 -> 9.2). A bracket\n// spanning the empty headroom above the tallest bars calls it out without\n// touching any bar, value label, or gridline.\nconst softwareQ1 = data.find((d) => d.category === \"Q1\" && d.group === \"Software\");\nconst softwareQ4 = data.find((d) => d.category === \"Q4\" && d.group === \"Software\");\nconst growthPct = Math.round(((softwareQ4.value - softwareQ1.value) / softwareQ1.value) * 100);\nconst bracketY = 22;\nconst qx1 = x0(\"Q1\") + x1(\"Software\") + x1.bandwidth() / 2;\nconst qx4 = x0(\"Q4\") + x1(\"Software\") + x1.bandwidth() / 2;\n\nconst annotation = g.append(\"g\").attr(\"class\", \"growth-annotation\");\n\nannotation\n  .append(\"path\")\n  .attr(\n    \"d\",\n    `M${qx1},${bracketY + 8} V${bracketY} H${qx4}`\n  )\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"5,4\")\n  .attr(\"opacity\", 0.75)\n  .attr(\"marker-end\", \"url(#growth-arrow)\");\n\nannotation\n  .append(\"text\")\n  .attr(\"x\", (qx1 + qx4) / 2)\n  .attr(\"y\", bracketY - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"700\")\n  .attr(\"fill\", t.ink)\n  .text(`Software +${growthPct}% (Q1→Q4)`);\n\n// --- Legend -------------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${width - margin.right + 60},${margin.top + 20})`);\n\ngroups.forEach((group, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 40})`);\n  row\n    .append(\"rect\")\n    .attr(\"width\", 24)\n    .attr(\"height\", 24)\n    .attr(\"rx\", 5)\n    .attr(\"ry\", 5)\n    .attr(\"fill\", `url(#bar-sheen-${i})`)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n  row\n    .append(\"text\")\n    .attr(\"x\", 36)\n    .attr(\"y\", 18)\n    .style(\"font-size\", \"16px\")\n    .attr(\"fill\", t.ink)\n    .text(group);\n});\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Product Line Revenue by Quarter · bar-grouped · javascript · d3 · anyplot.ai\");\n"}