{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waterfall-chart",
  "title": "Waterfall Chart",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A waterfall or bridge chart from a start to an end: floating bars for increases and decreases with a + or − sign as well as colour, totals on the baseline, connectors, real HTML labels and a table of the numbers for screen readers.",
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/waterfall-chart.tsx",
      "content": "// Ballmac UI: Waterfall Chart. https://ui.ballmac.com/components/waterfall-chart\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { useMessages } from \"@/lib/ballmac/i18n\"\nimport { cn } from \"@/lib/utils\"\n\ntype WaterfallStep = {\n  id: string\n  label: string\n  /** For a change, how much it adds (positive) or takes away (negative). For a total, the running total at that point. */\n  value: number\n  /** A total stands on the baseline and resets the running total. A change floats from where the last step ended. */\n  kind?: \"change\" | \"total\"\n}\n\ntype WaterfallChartProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  steps: WaterfallStep[]\n  /** Turns a value into text. */\n  format?: (value: number) => string\n  /** Height of the plot in pixels. */\n  height?: number\n  /** A heading read by assistive technology and shown above the chart. */\n  title?: string\n}\n\ntype Bar = { step: WaterfallStep; from: number; to: number; running: number }\n\n/** The running total, the start and end of every bar. Pure, so it can be tested and reused. */\nfunction buildBars(steps: WaterfallStep[]): Bar[] {\n  let running = 0\n  return steps.map((step) => {\n    if (step.kind === \"total\") {\n      running = step.value\n      return { step, from: 0, to: step.value, running }\n    }\n    const from = running\n    running += step.value\n    return { step, from, to: running, running }\n  })\n}\n\nconst sign = (n: number) => (n > 0 ? \"+\" : n < 0 ? \"−\" : \"\")\n\n/**\n * A waterfall (bridge) chart: a start, the changes that add to it or take away from it, and where it ends. Built from HTML, so labels are real text that wraps\n * and scales, and a table of the same numbers is there for screen readers. Increases and decreases carry a + or − sign as well as their colour.\n * Runs on the server.\n */\nfunction WaterfallChart({ steps, format = (v) => v.toLocaleString(\"en-US\"), height = 260, title, className, ...props }: WaterfallChartProps) {\n  const id = React.useId()\n  const msg = useMessages()\n  const bars = buildBars(steps)\n  const all = bars.flatMap((b) => [b.from, b.to, 0])\n  const lo = Math.min(...all)\n  const hi = Math.max(...all)\n  const span = hi - lo || 1\n  const y = (v: number) => ((hi - v) / span) * 100\n  const zero = y(0)\n  const last = bars[bars.length - 1]\n\n  return (\n    <figure data-slot=\"waterfall-chart\" aria-labelledby={title ? `${id}-t` : undefined} className={cn(\"grid w-full min-w-0 gap-3\", className)} {...props}>\n      {title && (\n        <figcaption id={`${id}-t`} className=\"text-sm font-medium text-foreground\">\n          {title}\n        </figcaption>\n      )}\n      <div className=\"rounded-xl border bg-card p-3 pb-2 sm:p-4\" dir=\"ltr\">\n        <div className=\"relative flex gap-1.5 sm:gap-2.5\" style={{ height }} aria-hidden=\"true\">\n          <span className=\"absolute inset-x-0 border-t border-dashed border-border\" style={{ top: `${zero}%` }} />\n          {bars.map((b, i) => {\n            const top = y(Math.max(b.from, b.to))\n            const h = Math.max(Math.abs(y(b.to) - y(b.from)), 0.8)\n            const total = b.step.kind === \"total\"\n            const up = b.step.value >= 0\n            const next = bars[i + 1]\n            return (\n              <div key={b.step.id} className=\"relative min-w-0 flex-1\">\n                <span\n                  className={cn(\"absolute inset-x-0 rounded-[5px] transition-[height,top] duration-500 motion-reduce:transition-none\", total ? \"bg-foreground\" : up ? \"bg-chart-2\" : \"bg-destructive\")}\n                  style={{ top: `${top}%`, height: `${h}%` }}\n                />\n                <span className=\"absolute inset-x-0 -translate-y-full pb-1 text-center text-[11px] font-medium tabular-nums text-foreground sm:text-xs\" style={{ top: `${top}%` }}>\n                  {total ? format(b.step.value) : `${sign(b.step.value)}${format(Math.abs(b.step.value))}`}\n                </span>\n                {next && <span className=\"absolute h-px bg-border\" style={{ top: `${y(b.to)}%`, insetInlineStart: \"100%\", width: \"0.4rem\" }} />}\n              </div>\n            )\n          })}\n        </div>\n        <div className=\"mt-2 flex gap-1.5 sm:gap-2.5\" aria-hidden=\"true\">\n          {bars.map((b) => (\n            <p key={b.step.id} className=\"min-w-0 flex-1 text-center text-[11px] leading-tight text-muted-foreground sm:text-xs\">\n              {b.step.label}\n            </p>\n          ))}\n        </div>\n      </div>\n      <table className=\"sr-only\">\n        <caption>{title ?? \"Changes from the start to the end\"}</caption>\n        <thead>\n          <tr>\n            <th scope=\"col\">{msg(\"waterfall-chart.step\", \"Step\")}</th>\n            <th scope=\"col\">{msg(\"waterfall-chart.change\", \"Change\")}</th>\n            <th scope=\"col\">{msg(\"waterfall-chart.after\", \"Total after\")}</th>\n          </tr>\n        </thead>\n        <tbody>\n          {bars.map((b) => (\n            <tr key={b.step.id}>\n              <th scope=\"row\">{b.step.label}</th>\n              <td>{b.step.kind === \"total\" ? \"Total\" : `${sign(b.step.value)}${format(Math.abs(b.step.value))}${b.step.value >= 0 ? \", increase\" : \", decrease\"}`}</td>\n              <td>{format(b.running)}</td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n      {last && (\n        <p className=\"sr-only\">\n          {msg(\"waterfall-chart.ends\", \"Ends at {value}.\", { value: format(last.running) })}\n        </p>\n      )}\n    </figure>\n  )\n}\n\nexport { WaterfallChart, buildBars, type WaterfallChartProps, type WaterfallStep }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/waterfall-chart.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "waterfall",
      "bridge",
      "chart",
      "variance",
      "revenue",
      "finance",
      "cost",
      "change"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "steps is [{ id, label, value, kind?: 'change' | 'total' }]. A change floats from where the last step ended (positive is an increase, negative a decrease); a total sits on the baseline and resets the running total. format(value), height, title. Exports buildBars(steps). Server-rendered, no client code.",
      "whenToUse": [
        "Explaining how a number moved from one value to another: revenue, headcount, cost",
        "A variance or bridge between two periods"
      ],
      "whenNotToUse": [
        "Showing a trend; use chart-line",
        "Parts of a whole; use treemap or chart-pie",
        "Steps with drop-off between stages; use flow-sankey"
      ],
      "composesWith": [
        "stat-card",
        "bullet-chart",
        "chart-frame",
        "run-ledger"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "The bars are decoration; a table lists each step with its change (+ or − and increase or decrease) and the total after it"
        },
        {
          "keys": "Signs",
          "action": "Increases and decreases are written with + and − on the bars, so colour is never the only signal"
        }
      ],
      "customization": [
        "height",
        "title",
        "format",
        "totals in the middle for sub-totals"
      ]
    },
    "statesNone": "A presentational chart: you pass the steps in, so it has nothing of its own to load or fail.",
    "examples": [
      "waterfall-chart-demo",
      "waterfall-chart-negative"
    ],
    "url": "https://ui.ballmac.com/components/waterfall-chart"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}