{"spec_id":"waterfall-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// waterfall-basic: Basic Waterfall Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-08-04\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Warehouse inventory levels through a week of restocks, orders, and losses.\nconst categories = [\n  \"Starting Stock\",\n  \"Restock A\",\n  \"Restock B\",\n  \"Online Orders\",\n  \"Retail Orders\",\n  \"Returns\",\n  \"Damaged Stock\",\n  \"Ending Stock\",\n];\nconst changes = [1200, 450, 300, -520, -380, 65, -45, null]; // null: last bar is a computed total\n\n// --- Cumulative running total + per-bar [low, high] span --------------------\nconst cumulative = [];\nlet running = 0;\nfor (let i = 0; i < changes.length - 1; i++) {\n  running += changes[i];\n  cumulative.push(running);\n}\nconst endingTotal = running;\ncumulative.push(endingTotal);\n\nconst isTotal = (i) => i === 0 || i === categories.length - 1;\nconst bars = categories.map((_, i) => {\n  if (isTotal(i)) return { lo: 0, hi: cumulative[i], kind: \"total\", after: cumulative[i] };\n  const before = cumulative[i - 1];\n  const after = cumulative[i];\n  return { lo: Math.min(before, after), hi: Math.max(before, after), kind: changes[i] >= 0 ? \"increase\" : \"decrease\", after };\n});\n\nconst barColor = (kind) => (kind === \"total\" ? t.ink : kind === \"increase\" ? t.palette[0] : t.palette[4]);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: categories,\n    datasets: [\n      {\n        label: \"Stock level\",\n        data: bars.map((b) => [b.lo, b.hi]),\n        backgroundColor: bars.map((b) => barColor(b.kind)),\n        borderWidth: 0,\n        barPercentage: 0.7,\n        categoryPercentage: 0.75,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 40 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"waterfall-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 24 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          generateLabels: () => [\n            { text: \"Increase\", fillStyle: t.palette[0], strokeStyle: t.palette[0], lineWidth: 0 },\n            { text: \"Decrease\", fillStyle: t.palette[4], strokeStyle: t.palette[4], lineWidth: 0 },\n            { text: \"Total\", fillStyle: t.ink, strokeStyle: t.ink, lineWidth: 0 },\n          ],\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label: (ctx) => `Running total: ${bars[ctx.dataIndex].after.toLocaleString()} units`,\n        },\n      },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: { display: true, text: \"Inventory Event\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Units in Stock\", color: t.ink, font: { size: 16 } },\n        beginAtZero: true,\n      },\n    },\n  },\n  plugins: [\n    {\n      id: \"waterfallAnnotations\",\n      afterDatasetsDraw(chart) {\n        const { ctx, scales } = chart;\n        const meta = chart.getDatasetMeta(0);\n\n        // Connecting lines between the running-total level of consecutive bars\n        ctx.save();\n        ctx.strokeStyle = t.inkSoft;\n        ctx.lineWidth = 1.5;\n        ctx.setLineDash([5, 5]);\n        for (let i = 0; i < bars.length - 1; i++) {\n          const levelY = scales.y.getPixelForValue(cumulative[i]);\n          const xStart = meta.data[i].x + meta.data[i].width / 2;\n          const xEnd = meta.data[i + 1].x - meta.data[i + 1].width / 2;\n          ctx.beginPath();\n          ctx.moveTo(xStart, levelY);\n          ctx.lineTo(xEnd, levelY);\n          ctx.stroke();\n        }\n        ctx.restore();\n\n        // Running-total labels above/below each bar\n        ctx.save();\n        ctx.font = \"600 14px sans-serif\";\n        ctx.fillStyle = t.ink;\n        ctx.textAlign = \"center\";\n        bars.forEach((bar, i) => {\n          const x = meta.data[i].x;\n          const afterIsTop = bar.after === bar.hi;\n          if (afterIsTop) {\n            ctx.textBaseline = \"bottom\";\n            ctx.fillText(bar.after.toLocaleString(), x, scales.y.getPixelForValue(bar.hi) - 10);\n          } else {\n            ctx.textBaseline = \"top\";\n            ctx.fillText(bar.after.toLocaleString(), x, scales.y.getPixelForValue(bar.lo) + 10);\n          }\n        });\n        ctx.restore();\n      },\n    },\n  ],\n});\n"}