{"spec_id":"bar-stacked-labeled","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// bar-stacked-labeled: Stacked Bar Chart with Total Labels\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT_STACK = \"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\";\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Quarterly revenue by product line, in $M. Segments deliberately diverge\n// instead of growing in lockstep: Hardware dips in Q3, Services plateaus in\n// Q4, and Software accelerates — giving the chart a real point of view.\nconst quarters = [\"Q1\", \"Q2\", \"Q3\", \"Q4\"];\nconst productLines = [\"Hardware\", \"Software\", \"Services\"];\nconst revenueByProduct = [\n  [4.2, 4.6, 4.5, 5.0], // Hardware - dips in Q3\n  [3.1, 3.8, 4.6, 5.6], // Software - accelerates, fastest-growing overall\n  [1.8, 2.0, 2.3, 2.2], // Services - plateaus/dips in Q4\n];\nconst totals = quarters.map((_, i) => revenueByProduct.reduce((sum, series) => sum + series[i], 0));\n\n// Identify the fastest-growing segment (Q1 -> Q4) for a subtle legend cue.\nconst growthRates = revenueByProduct.map((series) => (series[series.length - 1] - series[0]) / series[0]);\nconst fastestGrowingIndex = growthRates.indexOf(Math.max(...growthRates));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nfunction drawRoundedRect(ctx, x, y, w, h, r) {\n  ctx.beginPath();\n  ctx.moveTo(x + r, y);\n  ctx.arcTo(x + w, y, x + w, y + h, r);\n  ctx.arcTo(x + w, y + h, x, y + h, r);\n  ctx.arcTo(x, y + h, x, y, r);\n  ctx.arcTo(x, y, x + w, y, r);\n  ctx.closePath();\n}\n\n// --- Total-label plugin ------------------------------------------------------\n// Core Chart.js has no built-in data-label rendering; drawing text in an\n// `afterDatasetsDraw` hook is the idiomatic Chart.js way to annotate bars\n// (documented pattern, not a third-party plugin). A softly-elevated pill\n// behind each total lifts it above library-default text-only labels.\nconst totalLabelsPlugin = {\n  id: \"totalLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const meta = chart.getDatasetMeta(chart.data.datasets.length - 1);\n    ctx.save();\n    ctx.font = `700 17px ${FONT_STACK}`;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    meta.data.forEach((bar, i) => {\n      const label = `$${totals[i].toFixed(1)}M`;\n      const textWidth = ctx.measureText(label).width;\n      const cx = bar.x;\n      const cy = scales.y.getPixelForValue(totals[i]) - 22;\n      const paddingX = 10;\n      const paddingY = 6;\n      ctx.fillStyle = t.elevatedBg;\n      ctx.strokeStyle = t.grid;\n      ctx.lineWidth = 1;\n      drawRoundedRect(ctx, cx - textWidth / 2 - paddingX, cy - paddingY - 8, textWidth + paddingX * 2, paddingY * 2 + 16, 8);\n      ctx.fill();\n      ctx.stroke();\n      ctx.fillStyle = t.ink;\n      ctx.fillText(label, cx, cy);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: quarters,\n    datasets: productLines.map((name, i) => {\n      const isTopSegment = i === productLines.length - 1;\n      return {\n        label: name,\n        data: revenueByProduct[i],\n        backgroundColor: t.palette[i],\n        borderWidth: 0,\n        // Default borderSkipped (\"start\" = bottom) keeps only the exposed\n        // top of the stack rounded, so the seam with the segment below stays flush.\n        borderRadius: isTopSegment ? 6 : 0,\n        hoverBorderWidth: 2,\n        hoverBorderColor: t.ink,\n        stack: \"revenue\",\n      };\n    }),\n  },\n  plugins: [totalLabelsPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    interaction: { mode: \"index\", intersect: false },\n    layout: {\n      padding: { top: 48 },\n    },\n    datasets: {\n      bar: { categoryPercentage: 0.55, barPercentage: 0.85 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"bar-stacked-labeled · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: 700, family: FONT_STACK },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"top\",\n        align: \"end\",\n        labels: {\n          color: t.ink,\n          font: { size: 15, weight: 500, family: FONT_STACK },\n          boxWidth: 18,\n          generateLabels(chart) {\n            const items = Chart.defaults.plugins.legend.labels.generateLabels(chart);\n            const fastest = items[fastestGrowingIndex];\n            if (fastest) fastest.text = `${fastest.text}  ▲ fastest-growing`;\n            return items;\n          },\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label: (ctx) => `${ctx.dataset.label}: $${ctx.parsed.y.toFixed(1)}M`,\n        },\n      },\n    },\n    scales: {\n      x: {\n        stacked: true,\n        ticks: { color: t.inkSoft, font: { size: 14, weight: 600, family: FONT_STACK } },\n        grid: { display: false },\n        title: { display: true, text: \"Quarter\", color: t.ink, font: { size: 15, weight: 500, family: FONT_STACK } },\n      },\n      y: {\n        stacked: true,\n        beginAtZero: true,\n        suggestedMax: Math.max(...totals) * 1.2,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14, weight: 600, family: FONT_STACK },\n          callback: (value) => `$${value}M`,\n        },\n        grid: { color: t.grid, lineWidth: 1, borderDash: [3, 4] },\n        title: { display: true, text: \"Revenue ($M)\", color: t.ink, font: { size: 15, weight: 500, family: FONT_STACK } },\n      },\n    },\n  },\n});\n"}