{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waterfall",
  "type": "registry:component",
  "title": "Waterfall",
  "description": "Waterfall chart for React, in the Hearth collection (Signal with @chartory/signal).",
  "registryDependencies": [
    "@chartory/core",
    "@chartory/plot-waterfall"
  ],
  "files": [
    {
      "path": "registry/chartory/components/waterfall.tsx",
      "type": "registry:component",
      "target": "components/chartory/waterfall.tsx",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// Waterfall (Chartory chart 042): sizes xs | s | m | l, laid out again from the data at each\n// size; collection \"hearth\" (default) or \"signal\" (with @chartory/signal, Code and Complete). Data in the type's shape (default: the sample). Docs:\n// https://usechartory.com/docs/charts/waterfall\nimport { ChartView, type ChartViewProps } from \"@/lib/chartory/charts/chart-view\"\nimport { registerChart } from \"@/lib/chartory/charts/defs\"\nimport { data as sample } from \"@/lib/chartory/charts/types/042-waterfall/data\"\nimport { def } from \"@/lib/chartory/charts/types/042-waterfall/layout\"\n\nregisterChart(def, sample)\n\nexport type WaterfallData = typeof sample\nexport type WaterfallProps = Omit<ChartViewProps, \"def\" | \"data\" | \"states\"> & {\n  /** the data to draw (default: the sample) */\n  data?: WaterfallData\n  /** further data states: the chart morphs to them (update) or ticks through them (live, with interval) */\n  states?: WaterfallData[]\n}\n\nexport function Waterfall(props: WaterfallProps) {\n  return <ChartView def={def} {...props} />\n}\n"
    },
    {
      "path": "registry/chartory/charts/types/042-waterfall/data.ts",
      "type": "registry:lib",
      "target": "lib/chartory/charts/types/042-waterfall/data.ts",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// 042 Waterfall · sample data (Figma chart/042-waterfall-cash-bridge, fintech: Q3 cash bridge in $M).\nimport type { WaterfallData } from \"../../plots/waterfall\";\n\nexport const data: WaterfallData = {\n  max: 4,\n  ticks: [\"4M\", \"3M\", \"2M\", \"1M\", \"0\"],\n  fmt: (v) => v.toFixed(2) + \"M\",\n  steps: [\n    {  label: \"OPEN\", v: 1.82, total: true  },\n    {  label: \"SALES\", v: 1.24  },\n    {  label: \"REFUND\", v: -0.09  },\n    {  label: \"STAFF\", v: -0.71  },\n    {  label: \"CLOUD\", v: -0.23  },\n    {  label: \"TAX\", v: -0.12  },\n    {  label: \"CLOSE\", v: 1.91, total: true  },\n  ],\n};\n"
    },
    {
      "path": "registry/chartory/charts/types/042-waterfall/layout.ts",
      "type": "registry:lib",
      "target": "lib/chartory/charts/types/042-waterfall/layout.ts",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// 042 Waterfall (Figma chart/042-waterfall-cash-bridge): C.waterfall on the cash bridge sample.\nimport { waterfall } from \"../../plots/waterfall\";\nimport { buildVariant, type ChartDef, type Size, type TooltipSpec } from \"../../core/shell\";\nimport { textWidth } from \"../../core/text\";\nimport { data } from \"./data\";\n\n// A signed value shows only when it fits its step's band (band pitch − 4; at S some do not). A new flow takes the\n// nearest cent whose label shows in exactly the variants where the sample's does.\nconst label = (v: number) => Math.ceil(textWidth((v > 0 ? \"+\" : \"−\") + data.fmt(Math.abs(v)), \"Label/Caps\"));\nlet room: { max: number; shown: Set<string> }[] | undefined;\nconst fits = (name: string, x: number) => {\n  room ??= ([\"xs\", \"s\", \"m\", \"l\"] as Size[]).flatMap((s) => [true, false].map((lg) => {\n    const ms = buildVariant(def, s, lg, data).marks, st = ms.filter((m) => m.name.startsWith(\"step · \"));\n    return { max: st[1].ax - st[0].ax - 4, shown: new Set(ms.filter((m) => m.name.startsWith(\"value · \")).map((m) => m.name.slice(8))) };\n  }));\n  return room.every((r) => (label(x) <= r.max) === r.shown.has(name.toLowerCase()));\n};\nconst near = (name: string, v: number, like: number) => {\n  for (let c = 0; c <= 8; c++) for (const s of [1, -1]) { const x = Math.round((v + s * c * 0.01) * 100) / 100; if (Math.sign(x) === Math.sign(like) && fits(name, x)) return x; }\n  return like;\n};\n\n/** The quarters after the sample, `step` on: each opens at the previous close, every flow moves ±15 % (cents of $M,\n *  signs kept) and the close is the opening balance plus the flows again. */\nfunction later(o: typeof data, step: number): typeof data {\n  let open = o.steps[0].v, steps = o.steps;\n  for (let q = 1; q <= step; q++) {\n    const flows = o.steps.map((s, i) => (s.total ? 0 : near(s.label, s.v * (1 + 0.15 * Math.sin(q * 2.1 + i * 1.3)), s.v)));\n    const close = Math.round((open + flows.reduce((a, v) => a + v, 0)) * 100) / 100;\n    steps = o.steps.map((s, i) => ({ ...s, v: !s.total ? flows[i] : i === 0 ? open : close }));\n    open = close;\n  }\n  return { ...o, steps };\n}\n\n/** Tooltip for one step (group = the step's bar): a flow from the balance before to the balance after; the close\n *  against the open; the open on its own. */\nfunction tipOf(o: typeof data, g: number): TooltipSpec | null {\n  const s = o.steps[g];\n  if (!s) return null;\n  const money = (v: number) => \"$\" + o.fmt(v), signed = (v: number) => (v >= 0 ? \"+$\" : \"−$\") + o.fmt(Math.abs(v));\n  let run = 0;\n  for (let i = 0; i < g; i++) run = o.steps[i].total ? o.steps[i].v : run + o.steps[i].v;\n  if (s.total && g === 0) return { kind: \"value\", text: `${s.label.charAt(0) + s.label.slice(1).toLowerCase()} · ${money(s.v)}` };\n  if (s.total) {\n    const open = o.steps[0].v;\n    return { kind: \"range\", caps: s.label, rows: [{ label: \"Open\", value: money(open), swatch: \"data/primary\" }, { label: \"Net change\", value: signed(s.v - open), swatch: s.v >= open ? \"status/success\" : \"status/danger\" }, { label: \"Close\", value: money(s.v), swatch: \"data/primary\" }] };\n  }\n  return { kind: \"range\", caps: s.label, rows: [{ label: \"Before\", value: money(run), swatch: \"data/primary\" }, { label: \"Change\", value: signed(s.v), swatch: s.v >= 0 ? \"status/success\" : \"status/danger\" }, { label: \"After\", value: money(run + s.v), swatch: \"data/primary\" }] };\n}\n\nexport const def: ChartDef = {\n  no: 42,\n  slug: \"waterfall\",\n  legend: [[\"data/primary\", \"Balance\"], [\"status/success\", \"Inflow\"], [\"status/danger\", \"Outflow\"]],\n  // update: the following quarters (see later)\n  update: (d, step) => later(d as typeof data, step),\n  plot: (K, p, w, d) => waterfall(K, p, w, (d as typeof data | undefined) ?? data),\n  tip: (d, g) => tipOf(d as typeof data, g),\n  describe: {\n    title: \"Cash bridge, Q3\",\n    summary: `Cash from the opening balance of $${data.fmt(data.steps[0].v)} to the closing balance of $${data.fmt(data.steps[data.steps.length - 1].v)}: sales in, refunds, staff, cloud and tax out.`,\n    table: {\n      head: [\"Step\", \"Change\", \"Balance\"],\n      rows: (() => {\n        let run = 0;\n        return data.steps.map((s) => {\n          run = s.total ? s.v : run + s.v;\n          return [s.label.charAt(0) + s.label.slice(1).toLowerCase(), s.total ? \"\" : (s.v > 0 ? \"+$\" : \"−$\") + data.fmt(Math.abs(s.v)), \"$\" + data.fmt(run)];\n        });\n      })(),\n    },\n  },\n};\n"
    }
  ],
  "categories": [
    "charts",
    "flow-between-steps"
  ],
  "meta": {
    "access": "free",
    "no": 42,
    "family": "Flow between steps"
  }
}
