{"spec_id":"waterfall-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// waterfall-basic: Basic Waterfall Chart\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-08-04\n//# anyplot-orientation: landscape\n// anyplot.ai\n// waterfall-basic: Basic Waterfall Chart\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-08-04\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { BarPlot } from \"@mui/x-charts/BarChart\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, sans-serif\";\nconst TITLE = \"waterfall-basic · javascript · muix · anyplot.ai\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Quarterly cash bridge: opening balance -> revenue additions & cost/tax\n// deductions -> closing balance. First and last steps are totals; the\n// middle steps are signed changes (positive = inflow, negative = outflow).\nconst categories = [\n  \"Opening Balance\",\n  \"Product Sales\",\n  \"Service Revenue\",\n  \"Refunds\",\n  \"Operating Costs\",\n  \"Taxes\",\n  \"Closing Balance\",\n];\nconst deltas = [120000, 85000, 42000, -18000, -65000, -21000, 143000];\nconst isEdge = (i) => i === 0 || i === deltas.length - 1;\n\n// Running total after each step (edges are absolute totals, not deltas).\nlet running = 0;\nconst runningTotal = deltas.map((d, i) => {\n  running = isEdge(i) ? d : running + d;\n  return running;\n});\n\n// Floating bars built from a stack of 4 series sharing one `stack` key:\n//  - base:     invisible filler that lifts the visible segment off the axis\n//  - increase: visible segment for positive changes (brand green)\n//  - decrease: visible segment for negative changes (semantic red)\n//  - total:    full-height bar for the opening/closing totals (neutral ink)\nconst baseData = deltas.map((d, i) => (isEdge(i) ? null : d >= 0 ? runningTotal[i - 1] : runningTotal[i]));\nconst increaseData = deltas.map((d, i) => (!isEdge(i) && d >= 0 ? d : null));\nconst decreaseData = deltas.map((d, i) => (!isEdge(i) && d < 0 ? -d : null));\nconst totalData = deltas.map((d, i) => (isEdge(i) ? d : null));\n\nconst formatMoney = (v) => `$${Math.round(v / 1000)}k`;\n\n// Increase/decrease fills stay the same saturated green/red in both themes, so\n// their bar labels need a fixed light ink rather than the theme-flipping PAGE_BG\n// (PAGE_BG in dark mode is near-black, which fails contrast on the matte red).\nconst ON_COLOR_TEXT = \"#FAF8F1\";\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const margin = { top: 96, right: 48, bottom: 96, left: 172 };\n\n  return (\n    <ChartContainer\n      width={W}\n      height={H}\n      margin={margin}\n      skipAnimation\n      series={[\n        {\n          type: \"bar\",\n          id: \"base\",\n          stack: \"waterfall\",\n          data: baseData,\n          color: \"transparent\",\n        },\n        {\n          type: \"bar\",\n          id: \"increase\",\n          stack: \"waterfall\",\n          data: increaseData,\n          color: t.palette[0],\n          label: \"Increase\",\n        },\n        {\n          type: \"bar\",\n          id: \"decrease\",\n          stack: \"waterfall\",\n          data: decreaseData,\n          color: t.palette[4],\n          label: \"Decrease\",\n        },\n        {\n          type: \"bar\",\n          id: \"total\",\n          stack: \"waterfall\",\n          data: totalData,\n          color: t.ink,\n          label: \"Total\",\n        },\n        {\n          type: \"line\",\n          id: \"flow\",\n          data: runningTotal,\n          color: t.inkSoft,\n          curve: \"stepAfter\",\n          showMark: false,\n          disableHighlight: true,\n        },\n      ]}\n      xAxis={[{ scaleType: \"band\", data: categories, categoryGapRatio: 0.35 }]}\n      yAxis={[{ scaleType: \"linear\", min: 0, valueFormatter: formatMoney }]}\n    >\n      {/* Title */}\n      <text x={W / 2} y={40} textAnchor=\"middle\" fontSize={22} fontWeight={600} fill={t.ink} fontFamily={FONT}>\n        {TITLE}\n      </text>\n\n      {/* Legend — small color-coded swatches for the three semantic bar roles */}\n      <g fontFamily={FONT}>\n        {[\n          { label: \"Increase\", color: t.palette[0] },\n          { label: \"Decrease\", color: t.palette[4] },\n          { label: \"Total\", color: t.ink },\n        ].map((item, i) => {\n          const x = W / 2 - 220 + i * 150;\n          return (\n            <g key={item.label}>\n              <rect x={x} y={62} width={16} height={16} rx={3} fill={item.color} />\n              <text x={x + 24} y={74} fontSize={15} fill={t.inkSoft}>\n                {item.label}\n              </text>\n            </g>\n          );\n        })}\n      </g>\n\n      <ChartsGrid horizontal />\n      <BarPlot\n        skipAnimation\n        borderRadius={3}\n        barLabel={(item) =>\n          item.seriesId === \"base\" || item.value == null ? null : formatMoney(runningTotal[item.dataIndex])\n        }\n        slotProps={{\n          barLabel: (ownerState) => ({\n            style: {\n              fontFamily: FONT,\n              fontSize: 15,\n              fontWeight: 700,\n              fill: ownerState.seriesId === \"total\" ? t.pageBg : ON_COLOR_TEXT,\n            },\n          }),\n        }}\n      />\n      <LinePlot skipAnimation strokeDasharray=\"10 8\" strokeWidth={2} />\n\n      <ChartsXAxis\n        tickLabelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: FONT }}\n        stroke={t.inkSoft}\n      />\n      <ChartsYAxis\n        tickLabelStyle={{ fontSize: 14, fill: t.inkSoft, fontFamily: FONT }}\n        stroke={t.inkSoft}\n      />\n\n      {/* Y-axis title, drawn by hand: ChartsYAxis's built-in `label` uses a fixed\n          small offset from the axis line that doesn't grow with wide \"$Xk\" tick\n          text, so it collides with the ticks — positioning it ourselves avoids\n          that. */}\n      <text\n        x={26}\n        y={margin.top + (H - margin.top - margin.bottom) / 2}\n        textAnchor=\"middle\"\n        transform={`rotate(-90, 26, ${margin.top + (H - margin.top - margin.bottom) / 2})`}\n        fontSize={16}\n        fill={t.ink}\n        fontFamily={FONT}\n      >\n        Cash Balance\n      </text>\n    </ChartContainer>\n  );\n}\n"}