{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waterfall-chart",
  "type": "registry:component",
  "title": "Waterfall Chart",
  "description": "How a run of changes carried one total to another, with each step floating on the running balance.",
  "dependencies": [
    "@base-ui/react",
    "@tanstack/charts",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/waterfall-chart/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/waterfall-chart/index.tsx",
      "content": "'use client';\n\n/**\n * WaterfallChart — how a run of changes carried one total to another.\n *\n * ```tsx\n * <WaterfallChart data={bridge}>\n *   <WaterfallChart.Grid />\n *   <WaterfallChart.Connectors />\n *   <WaterfallChart.Bars />\n *   <WaterfallChart.XAxis />\n *   <WaterfallChart.Tooltip />\n * </WaterfallChart>\n * ```\n *\n * ## The same parts as the app\n *\n * The parts and their props are the app library's WaterfallChart, so a chart\n * written for one reads the same in the other. What differs is what the web\n * changes: a colour is named by the theme's role, a step is read by hovering,\n * tapping or dragging, a click pins the reading, and the arrow keys walk the run.\n *\n * ## Every bar but a total floats\n *\n * A step's bar starts where the previous one ended, so the gap under it is the\n * running total it acts on. A total is a reading rather than a change, so it is\n * measured from the baseline. Each bar grows from its own start towards its end,\n * because a step is a movement between two balances.\n *\n * ## The connectors are the sequence\n *\n * Without them the bars are a row of rectangles at unexplained heights. Each runs\n * from the middle of one band to the middle of the next, and the bars are drawn\n * over them, so the line reads as passing behind the run.\n *\n * ## The parts describe, the root draws\n *\n * `Grid`, `Bars`, `Connectors` and the rest draw nothing themselves. Each one\n * tells the root what it wants, and the root builds one chart definition from\n * them. They are found by context, so a part can be wrapped in a component of\n * your own. It also means the chart draws in the browser, in a box its\n * `aspectRatio` has reserved.\n */\nimport {\n  barX,\n  barY,\n  defineChart,\n  link,\n  text,\n  type ChartInteractionController,\n  type ChartMotionContext,\n  type ChartMark,\n  type ChartPoint,\n  type SceneNode,\n} from '@tanstack/charts';\nimport { motion } from '@tanstack/charts/motion';\nimport { Chart } from '@tanstack/charts/react/core';\nimport { scaleBand } from '@tanstack/charts/scales/band';\nimport { scaleLinear } from '@tanstack/charts/scales/linear';\nimport { tooltip as chartTooltip } from '@tanstack/charts/tooltip';\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\nimport {\n  waterfallExtent,\n  waterfallKinds,\n  waterfallReaches,\n  waterfallSteps as resolveSteps,\n  type WaterfallDatum as StepDatum,\n  type WaterfallKind,\n  type WaterfallStep as ResolvedStep,\n} from './waterfall-steps';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type WaterfallChartStatus = 'loading' | 'ready';\nexport type WaterfallChartOrientation = 'vertical' | 'horizontal';\nexport type { WaterfallKind };\n\n/** Which of the theme's roles a bar is drawn in. */\nexport type WaterfallChartColor =\n  | 'primary'\n  | 'info'\n  | 'success'\n  | 'warning'\n  | 'destructive'\n  | 'chart-1'\n  | 'chart-2'\n  | 'chart-3'\n  | 'chart-4'\n  | 'chart-5';\n\nexport type WaterfallDatum = StepDatum<WaterfallChartColor>;\nexport type WaterfallStep = ResolvedStep<WaterfallChartColor>;\n\n/** The running totals every step sits on, for a header or a table beside the chart. */\nexport const waterfallSteps = (data: readonly WaterfallDatum[]): WaterfallStep[] => resolveSteps(data);\n\nconst COLOR_VAR: Record<WaterfallChartColor, string> = {\n  primary: 'var(--primary)',\n  info: 'var(--info)',\n  success: 'var(--success)',\n  warning: 'var(--warning)',\n  destructive: 'var(--destructive)',\n  'chart-1': 'var(--chart-1)',\n  'chart-2': 'var(--chart-2)',\n  'chart-3': 'var(--chart-3)',\n  'chart-4': 'var(--chart-4)',\n  'chart-5': 'var(--chart-5)',\n};\n\nconst THEME = {\n  foreground: 'var(--foreground)',\n  muted: 'var(--muted-foreground)',\n  grid: 'var(--border)',\n  background: 'transparent',\n  palette: ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'],\n};\n\nconst waterfallChartVariants = tv({\n  slots: {\n    root: 'relative grid w-full grid-cols-1 text-foreground',\n    plot: 'relative col-start-1 row-start-2 w-full',\n    // `pan-y` hands a vertical drag to the page and a sideways one to the chart.\n    chart:\n      'absolute! inset-0 touch-pan-y rounded-lg text-xs [--ts-chart-tooltip-background:var(--popover)] [--ts-chart-tooltip-border-radius:var(--radius-xl)] [--ts-chart-tooltip-border:1px_solid_var(--border)] [--ts-chart-tooltip-color:var(--popover-foreground)] [--ts-chart-tooltip-shadow:var(--shadow-lg)] [&_svg]:rounded-lg [&_svg]:outline-none [&_svg:focus-visible]:ring-2 [&_svg:focus-visible]:ring-ring',\n    sweepClip: 'pointer-events-none absolute inset-0 overflow-hidden',\n    sweep:\n      'absolute inset-y-0 left-0 w-2/5 bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--chart-1)_55%,transparent),transparent)] motion-reduce:hidden',\n    legend:\n      'pointer-events-none absolute right-2 top-1 col-start-1 row-start-2 flex gap-3 text-xs text-muted-foreground',\n    legendItem: 'flex items-center gap-1.5',\n    legendSwatch: 'size-2 shrink-0 rounded-full',\n    header: 'col-start-1 row-start-1 flex items-start justify-between gap-3 pb-3',\n    headerText: 'flex min-w-0 flex-1 flex-col gap-0.5',\n    headerMuted: 'text-xs text-muted-foreground',\n    headerValue: 'text-xl font-bold tabular-nums tracking-tight',\n    headerTrailing: 'flex shrink flex-wrap items-center justify-end gap-x-3 gap-y-1 pt-1 text-xs text-muted-foreground',\n  },\n});\n\n/** The value-axis gutter held while loading, when there are no labels to size it from. */\nconst LOADING_GUTTER = 44;\n\n/** How much of the value axis a placeholder bar takes. */\nconst SKELETON_LENGTH = 0.18;\n\n/** Bands to draw when there is no data yet to count them from. */\nconst SKELETON_BARS = 6;\n\nconst SKELETON_MARK = 'waterfall-chart-skeleton';\n\n/** How much of the entrance goes to staggering the bands. */\nconst STAGGER_SHARE = 0.45;\n\n/** How long a connector takes to appear, once the bar it leaves has finished growing. */\nconst CONNECTOR_FADE = 120;\n\n/** How far a written value sits off the end of its bar, in pixels. */\nconst VALUE_OFFSET = { above: -6, below: 15 };\n\n/** What a stub stands on: no step yet, and a length that is a share of the axis. */\nconst STUB_STEP: WaterfallStep = {\n  datum: { label: '', value: 0 },\n  label: '',\n  value: SKELETON_LENGTH,\n  kind: 'total',\n  start: 0,\n  end: SKELETON_LENGTH,\n};\n\nconst KEYFRAMES = '@keyframes panelui-waterfall-sweep{from{transform:translateX(-100%)}to{transform:translateX(250%)}}';\n\nconst COMPACT = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 });\nconst COMPACT_SMALL = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 });\n\n/** Below one, a single decimal would round most values down to nothing. */\nconst compactNumber = (value: number) => (Math.abs(value) < 1 ? COMPACT_SMALL : COMPACT).format(value);\n\n/** A total is a reading, so it has no sign. On a change the sign is the reading. */\nconst signedNumber = (step: WaterfallStep) =>\n  step.kind === 'total' ? compactNumber(step.value) : `${step.value > 0 ? '+' : ''}${compactNumber(step.value)}`;\n\nconst KIND_NAMES: Record<WaterfallKind, string> = { rise: 'Increase', fall: 'Decrease', total: 'Total' };\n\n/** One bar: a resolved step and the band it stands in. */\ninterface StepRow {\n  index: number;\n  step: WaterfallStep;\n}\n\n/**\n * What the parts have asked for. An axis formatter is part of the drawing, so a\n * new one redraws it. The readout's are read through `latest` when it opens, so a\n * new one costs nothing.\n */\ninterface Slots {\n  grid?: { rows: number; dashArray: string; opacity: number };\n  bars?: { cornerRadius: number | undefined };\n  connectors?: { dashArray: string; strokeWidth: number; opacity: number };\n  xAxis?: Pick<WaterfallChartXAxisProps, 'ticks' | 'format'>;\n  yAxis?: { ticks: number; format: WaterfallChartYAxisProps['format'] };\n  values?: { format: WaterfallChartValuesProps['format'] };\n  tooltip?: {\n    className: string | undefined;\n    latest: { current: Pick<WaterfallChartTooltipProps, 'formatValue' | 'formatTotal' | 'labels'> };\n  };\n  skeleton?: { bars: number | undefined; duration: number };\n}\n\ninterface WaterfallChartContextValue {\n  steps: readonly WaterfallStep[];\n  colors: Record<WaterfallKind, string>;\n  setSlot: <K extends keyof Slots>(slot: K, value: Slots[K]) => void;\n  activeIndex: number;\n}\n\nconst WaterfallChartContext = createContext<WaterfallChartContextValue | null>(null);\n\nfunction useChart(component: string): WaterfallChartContextValue {\n  const context = useContext(WaterfallChartContext);\n  if (!context) {\n    throw new Error(`${component} must be used within a <WaterfallChart>`);\n  }\n  return context;\n}\n\n/**\n * The step being read, for something rendered inside the chart. A readout outside\n * it takes `onActiveIndexChange` instead.\n */\nexport function useWaterfallChart() {\n  const { steps, activeIndex } = useChart('useWaterfallChart');\n  return {\n    activeIndex,\n    activeStep: activeIndex >= 0 ? (steps[activeIndex] ?? null) : null,\n  };\n}\n\n/**\n * Holds a non-zero step at the smallest length it may be drawn. A step that rounds\n * to nothing still happened. The floor is in pixels, so it is applied once the\n * scales have placed the bar, and the bar's far end is the end that moves. A step\n * wholly past a cropped `domain` stays at nothing, because its change is off the plot.\n */\nfunction withMinimumLength<TMark extends ChartMark<StepRow, number, number, number, number>>(\n  mark: TMark,\n  floor: number,\n  horizontal: boolean,\n  domain: readonly [number, number] | undefined\n): TMark {\n  const adjust = (node: SceneNode): SceneNode => {\n    if (node.kind === 'group') return { ...node, children: node.children.map(adjust) };\n    if (node.kind !== 'rect' || !node.interaction?.point) return node;\n    const { step } = node.interaction.point.datum as StepRow;\n    const length = horizontal ? node.width : node.height;\n    if (step.end === step.start || length >= floor || !waterfallReaches(step, domain)) return node;\n    const forward = step.end > step.start;\n    return horizontal\n      ? { ...node, width: floor, x: forward ? node.x : node.x + length - floor }\n      : { ...node, height: floor, y: forward ? node.y + length - floor : node.y };\n  };\n\n  return {\n    ...mark,\n    initialize(context) {\n      const initialized = mark.initialize(context);\n      return {\n        ...initialized,\n        postDomain(scene) {\n          const base = initialized.postDomain ? initialized.postDomain(scene) : scene;\n          return { ...base, nodes: base.nodes.map(adjust) };\n        },\n      };\n    },\n  };\n}\n\n/**\n * Takes a mark out of the reading. The chart counts each connector and written\n * value as a point, so the arrow keys would stop on them and never reach the last band.\n */\nfunction withoutPoints<TMark extends ChartMark<StepRow, number, number, number, number>>(mark: TMark): TMark {\n  return {\n    ...mark,\n    initialize(context) {\n      const initialized = mark.initialize(context);\n      return {\n        ...initialized,\n        postDomain(scene) {\n          const base = initialized.postDomain ? initialized.postDomain(scene) : scene;\n          return { ...base, points: [] };\n        },\n      };\n    },\n  };\n}\n\n/** The stubs' outlines from the drawn scene, for the sweep to be clipped to. */\nfunction stubOutlines(nodes: readonly SceneNode[], x = 0, y = 0, inside = false): string {\n  return nodes\n    .map((node) => {\n      if (node.kind === 'group') {\n        return stubOutlines(\n          node.children,\n          x + (node.translateX ?? 0),\n          y + (node.translateY ?? 0),\n          inside || node.key.includes(SKELETON_MARK)\n        );\n      }\n      if (node.kind !== 'rect' || !(inside || node.key.includes(SKELETON_MARK))) return '';\n      const left = node.x + x;\n      const top = node.y + y;\n      return `M${left},${top}h${node.width}v${node.height}h${-node.width}Z`;\n    })\n    .join('');\n}\n\nexport interface WaterfallChartProps extends Omit<ComponentProps<'div'>, 'ref'> {\n  /** The steps, in the order they happen. The chart never sorts them. */\n  data: readonly WaterfallDatum[];\n  /**\n   * `loading` holds the bars back and grows them in when it turns `ready`. Add a\n   * `WaterfallChart.Skeleton` for something to stand in the plot meanwhile.\n   */\n  status?: WaterfallChartStatus;\n  /** The plot's width ÷ height. `2` is the wide card shape. */\n  aspectRatio?: number;\n  /** Milliseconds for the bars to grow in. */\n  animationDuration?: number;\n  /** Milliseconds for the bars and the axis to settle after the data changes. */\n  domainDuration?: number;\n  /**\n   * Fix the value axis instead of deriving it. The derived axis always includes\n   * zero, and bar lengths cannot be compared on one that does not.\n   */\n  yDomain?: [number, number];\n  /** `vertical` stands the bars up; `horizontal` lays the run down the side. */\n  orientation?: WaterfallChartOrientation;\n  /** Fraction of each band left empty, `0` to `1`. */\n  barGap?: number;\n  /** Largest bar thickness in pixels. Derived from the band when omitted. */\n  barWidth?: number;\n  /** Corner radius on the ends of a bar. */\n  cornerRadius?: number;\n  /** Smallest length a non-zero bar is drawn at, in pixels. */\n  minBarLength?: number;\n  /** Opacity of the bars that are not being read. */\n  fadedOpacity?: number;\n  /** Which of the theme's roles a step that adds is drawn in. */\n  riseColor?: WaterfallChartColor;\n  /** Which of the theme's roles a step that subtracts is drawn in. */\n  fallColor?: WaterfallChartColor;\n  /** Which of the theme's roles a `total` step is drawn in. */\n  totalColor?: WaterfallChartColor;\n  /**\n   * The step being read, and `-1`/`null` when there is none. Fires when the index\n   * changes, not per frame.\n   */\n  onActiveIndexChange?: (index: number, step: WaterfallStep | null) => void;\n  /** Drop the axis padding, for a run with no axis or readout. */\n  compact?: boolean;\n  /** What a screen reader calls the chart. */\n  label?: string;\n  /** What a screen reader says for one step. Defaults to its name, change and balance. */\n  labelForDatum?: (step: WaterfallStep, index: number) => string;\n}\n\n/** Imperative handle: re-run the grow-in, for a \"replay\" control. */\nexport interface WaterfallChartHandle {\n  replay: () => void;\n}\n\nconst WaterfallChartRoot = forwardRef<WaterfallChartHandle, WaterfallChartProps & RenderProp<'div'>>(function WaterfallChartRoot(\n  {\n    data,\n    status = 'ready',\n    aspectRatio = 2,\n    animationDuration = 700,\n    domainDuration = 500,\n    yDomain,\n    orientation = 'vertical',\n    barGap = 0.34,\n    barWidth,\n    cornerRadius = 4,\n    minBarLength = 2,\n    fadedOpacity = 0.3,\n    riseColor = 'success',\n    fallColor = 'destructive',\n    totalColor = 'chart-1',\n    onActiveIndexChange,\n    compact = false,\n    label,\n    labelForDatum,\n    className,\n    children,\n    render,\n    ...props\n  },\n  ref\n) {\n  const slotClasses = waterfallChartVariants();\n  const plotRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  const [slots, setSlots] = useState<Slots>({});\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const [revision, setRevision] = useState(0);\n  const [stubs, setStubs] = useState('');\n  const interactionRef = useRef<ChartInteractionController<StepRow, number, number> | null>(null);\n  const dragRelease = useRef<AbortController | null>(null);\n\n  /*\n   * The chart mounts once its width is known. Mounted at a guess, the first\n   * measurement is a resize, and a resize ends the entrance before it is seen.\n   */\n  useEffect(() => {\n    const node = plotRef.current;\n    if (!node) return;\n    const read = () => setWidth(node.clientWidth);\n    read();\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  const setSlot = useCallback<WaterfallChartContextValue['setSlot']>(\n    (slot, value) => setSlots((current) => ({ ...current, [slot]: value })),\n    []\n  );\n\n  useImperativeHandle(ref, () => ({ replay: () => setRevision((current) => current + 1) }), []);\n\n  const loading = status === 'loading';\n  const horizontal = orientation === 'horizontal';\n  const { grid, bars: barsSlot, connectors, xAxis, yAxis, values, tooltip, skeleton } = slots;\n\n  const steps = useMemo(() => waterfallSteps(data), [data]);\n  const colors = useMemo<Record<WaterfallKind, string>>(\n    () => ({ rise: COLOR_VAR[riseColor], fall: COLOR_VAR[fallColor], total: COLOR_VAR[totalColor] }),\n    [riseColor, fallColor, totalColor]\n  );\n\n  const definition = useMemo(() => {\n    const rows: StepRow[] = steps.map((step, index) => ({ index, step }));\n    const links = rows.slice(0, -1);\n\n    const showStubs = loading && skeleton !== undefined;\n    const bands = loading ? Math.max(1, skeleton?.bars ?? (steps.length || SKELETON_BARS)) : steps.length;\n    const indexes = Array.from({ length: bands }, (_unused, index) => index);\n    const stubRows: StepRow[] = showStubs ? indexes.map((index) => ({ index, step: STUB_STEP })) : [];\n\n    const fillOf = ({ datum, kind }: WaterfallStep) => (datum.color ? COLOR_VAR[datum.color] : colors[kind]);\n\n    const shared = {\n      key: (row: StepRow) => row.index,\n      fill: (row: StepRow) => fillOf(row.step),\n      radius: barsSlot?.cornerRadius ?? cornerRadius,\n      maxThickness: barWidth,\n      states: [\n        {\n          when: { focus: 'unmatched' as const },\n          style: { fillOpacity: fadedOpacity },\n          transition: { type: 'tween' as const, duration: 120, easing: 'ease-out' as const },\n        },\n      ],\n    };\n    const drawn = horizontal\n      ? barX(rows, { ...shared, y: 'index', x1: (row: StepRow) => row.step.start, x2: (row: StepRow) => row.step.end })\n      : barY(rows, { ...shared, x: 'index', y1: (row: StepRow) => row.step.start, y2: (row: StepRow) => row.step.end });\n    const shaped = minBarLength > 0 ? withMinimumLength(drawn, minBarLength, horizontal, yDomain) : drawn;\n\n    const stubShared = {\n      id: SKELETON_MARK,\n      key: (row: StepRow) => row.index,\n      fill: 'var(--muted)',\n      radius: cornerRadius,\n      maxThickness: barWidth,\n      motion: false as const,\n    };\n    const stubMark = horizontal\n      ? barX(stubRows, { ...stubShared, y: 'index', x1: 0, x2: SKELETON_LENGTH })\n      : barY(stubRows, { ...stubShared, x: 'index', y1: 0, y2: SKELETON_LENGTH });\n\n    const connectorStyle = connectors\n      ? {\n          key: (row: StepRow) => row.index,\n          stroke: 'var(--muted-foreground)',\n          strokeOpacity: connectors.opacity,\n          strokeWidth: connectors.strokeWidth,\n          strokeDasharray: connectors.dashArray,\n        }\n      : null;\n    const connectorMark = connectorStyle\n      ? horizontal\n        ? withoutPoints(\n            link(links, {\n              ...connectorStyle,\n              y1: 'index',\n              y2: (row: StepRow) => row.index + 1,\n              x1: (row: StepRow) => row.step.end,\n              x2: (row: StepRow) => row.step.end,\n            })\n          )\n        : withoutPoints(\n            link(links, {\n              ...connectorStyle,\n              x1: 'index',\n              x2: (row: StepRow) => row.index + 1,\n              y1: (row: StepRow) => row.step.end,\n              y2: (row: StepRow) => row.step.end,\n            })\n          )\n      : null;\n\n    // Sideways the bars run across the plot, and a number at the end of one lands on the next band's name.\n    const valueMark =\n      values && !horizontal\n        ? withoutPoints(\n            text(rows, {\n              key: (row: StepRow) => row.index,\n              x: 'index',\n              y: (row: StepRow) => row.step.end,\n              text: (row: StepRow) => (values.format ? values.format(row.step, row.index) : signedNumber(row.step)),\n              fill: 'var(--foreground)',\n              fontSize: 12,\n              fontWeight: 500,\n              anchor: 'middle',\n              dy: (row: StepRow) => (row.step.end >= row.step.start ? VALUE_OFFSET.above : VALUE_OFFSET.below),\n              motion: false as const,\n            })\n          )\n        : null;\n\n    const categoryLabel = (index: number) => {\n      const step = steps[index];\n      if (!step) return '';\n      return xAxis?.format ? xAxis.format(step, index) : step.label;\n    };\n\n    // A band owns a slice of the axis, with `barGap` of it left empty and split\n    // evenly round the bar, so the end bands sit half a gap from the edge.\n    const categoryScale = scaleBand(indexes, [0, 1]).paddingInner(barGap).paddingOuter(barGap / 2);\n    const categoryStride = xAxis?.ticks ? Math.ceil(bands / Math.max(1, xAxis.ticks)) : 1;\n    const categoryAxisShown = horizontal ? yAxis !== undefined : xAxis !== undefined;\n    const categoryOptions = {\n      scale: categoryScale,\n      axis: categoryAxisShown\n        ? {\n            line: false as const,\n            tickLabels: { opacity: 1 },\n            ticks: {\n              size: 0,\n              format: categoryLabel,\n              ...(xAxis?.ticks && !horizontal ? { values: indexes.filter((index) => index % categoryStride === 0) } : {}),\n            },\n          }\n        : (false as const),\n    };\n\n    const valueTicks = yAxis?.ticks ?? grid?.rows;\n    const valueFormat = (value: number) => {\n      if (loading) return '';\n      return yAxis?.format ? yAxis.format(value) : compactNumber(value);\n    };\n    const valueAxisShown = !horizontal && yAxis !== undefined;\n    const valueOptions = {\n      // While loading the stubs need a scale of their own: their length is a\n      // share of the axis, and there may be no values yet to derive one from.\n      // Clamped so a bar past a cropped `yDomain` stops at the plot's edge.\n      scale: scaleLinear(loading ? [0, 1] : waterfallExtent(steps, yDomain), [0, 1]).clamp(true),\n      nice: !loading && !yDomain,\n      grid: grid ? { stroke: 'var(--border)', strokeOpacity: grid.opacity, strokeDasharray: grid.dashArray } : false,\n      axis: valueAxisShown\n        ? {\n            line: false as const,\n            tickLabels: { opacity: 1 },\n            ticks: { size: 0, format: valueFormat, ...(valueTicks ? { count: valueTicks } : {}) },\n          }\n        : horizontal && valueTicks\n          ? { line: false as const, ticks: { size: 0, count: valueTicks, format: () => '' } }\n          : (false as const),\n    };\n\n    const interactive = tooltip !== undefined && barsSlot !== undefined && !loading && rows.length > 0;\n    const readout = tooltip\n      ? {\n          use: chartTooltip,\n          className: tooltip.className,\n          content(points: readonly ChartPoint<StepRow, number, number>[]) {\n            const { formatValue, formatTotal, labels } = tooltip.latest.current;\n            const point = points[0];\n            if (!point) return { title: '', rows: [] };\n            const { step } = point.datum;\n            const balance = formatTotal\n              ? formatTotal(step)\n              : step.kind === 'total'\n                ? null\n                : compactNumber(step.end);\n            return {\n              title: step.label,\n              rows: [\n                {\n                  label: labels?.[step.kind] ?? KIND_NAMES[step.kind],\n                  value: formatValue ? formatValue(step) : signedNumber(step),\n                  color: fillOf(step),\n                },\n                ...(balance === null ? [] : [{ label: labels?.balance ?? 'Balance', value: balance }]),\n              ],\n            };\n          },\n        }\n      : undefined;\n\n    return defineChart({\n      theme: THEME,\n      marks: loading\n        ? [stubMark]\n        : [...(connectorMark ? [connectorMark] : []), ...(barsSlot ? [shaped] : []), ...(valueMark ? [valueMark] : [])],\n      scales: horizontal ? { x: valueOptions, y: categoryOptions } : { x: categoryOptions, y: valueOptions },\n      ...(compact ? { guides: false as const, margin: 0 } : {}),\n      ...(loading && !horizontal && yAxis ? { margin: { left: LOADING_GUTTER } } : {}),\n      focusRing: false,\n      /*\n       * Bars grow in one after another and all finish inside the one duration: the\n       * window each gets is what is left after the last one's delay. A connector\n       * waits for the bar it leaves, because a line to a bar that is not there yet\n       * points at nothing. Only these arrive, so a replay does not fade the axis in.\n       */\n      motion: (context: ChartMotionContext<StepRow>) => {\n        if (context.phase !== 'enter') {\n          return { transition: { type: 'tween' as const, duration: domainDuration, easing: 'ease-out' as const } };\n        }\n        if (context.role !== 'bar' && context.role !== 'link') return false as const;\n        const band = context.datum?.index ?? 0;\n        const stagger = bands > 1 ? STAGGER_SHARE : 0;\n        const delay = bands > 1 ? (band / bands) * animationDuration * STAGGER_SHARE : 0;\n        const duration = animationDuration * (1 - stagger);\n        return context.role === 'bar'\n          ? { delay, transition: { type: 'tween' as const, duration, easing: 'ease-out' as const } }\n          : {\n              delay: delay + duration,\n              transition: { type: 'tween' as const, duration: CONNECTOR_FADE, easing: 'ease-out' as const },\n            };\n      },\n      keyboard: interactive,\n      pointer: interactive,\n      ...(interactive && readout\n        ? {\n            focus: horizontal ? ('group-y' as const) : ('group-x' as const),\n            maxFocusDistance: Number.POSITIVE_INFINITY,\n            tooltip: readout,\n          }\n        : {}),\n    });\n  }, [\n    steps,\n    colors,\n    loading,\n    horizontal,\n    barGap,\n    barWidth,\n    cornerRadius,\n    minBarLength,\n    fadedOpacity,\n    yDomain?.[0],\n    yDomain?.[1],\n    compact,\n    animationDuration,\n    domainDuration,\n    grid,\n    barsSlot,\n    connectors,\n    xAxis,\n    yAxis,\n    values,\n    tooltip,\n    skeleton,\n  ]);\n\n  // Marks take their timing from the definition. This one is what the readout\n  // moves with, and it should keep up with the pointer.\n  const renderer = useMemo(() => motion({ transition: { type: 'tween', duration: 160, easing: 'ease-out' } }), []);\n\n  const summary = label ?? `Waterfall chart, ${steps.length} ${steps.length === 1 ? 'step' : 'steps'}`;\n\n  const context = useMemo<WaterfallChartContextValue>(\n    () => ({ steps, colors, setSlot, activeIndex }),\n    [steps, colors, setSlot, activeIndex]\n  );\n\n  const spokenRow = (step: WaterfallStep, index: number) =>\n    labelForDatum?.(step, index) ??\n    (step.kind === 'total'\n      ? `${step.label}. Total, ${step.value}.`\n      : `${step.label}. ${KIND_NAMES[step.kind]} of ${Math.abs(step.value)}, from ${step.start} to ${step.end}.`);\n\n  // The chart also reports a band again when the data is rebuilt under it. Only a\n  // change of band is news, and repeating it can loop a caller that keeps the step.\n  const reportedIndex = useRef(-1);\n  const reportActive = (index: number) => {\n    if (reportedIndex.current === index) return;\n    reportedIndex.current = index;\n    setActiveIndex(index);\n    onActiveIndexChange?.(index, index >= 0 ? (steps[index] ?? null) : null);\n  };\n  const onFocusChange = (point: ChartPoint<StepRow, number, number> | null) =>\n    reportActive(point ? point.datum.index : -1);\n\n  // A remounted chart says nothing about the focus it lost.\n  useEffect(() => reportActive(-1), [revision, status]);\n\n  // Without a readout nothing can read a step, and the chart keeps a keyboard or drag focus it holds.\n  const readable = tooltip !== undefined && barsSlot !== undefined;\n  useEffect(() => {\n    if (readable) return;\n    interactionRef.current?.setControlledFocus(null);\n    reportActive(-1);\n  }, [readable]);\n\n  /*\n   * The chart follows a mouse on its own but not a finger, which it treats as a\n   * tap. A sideways drag reads the bands it crosses, and the last one stays until\n   * the next press, the same as a tap's. A vertical drag never gets here: `pan-y`\n   * gives it to the page, and the browser cancels the pointer.\n   */\n  // Cleared as well as aborted: a controller left in the ref after the chart is\n  // hidden mid-drag (by Activity, say) would stop any later drag from releasing.\n  useEffect(\n    () => () => {\n      dragRelease.current?.abort();\n      dragRelease.current = null;\n    },\n    []\n  );\n  const readDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (event.pointerType !== 'touch' || !readable || loading) return;\n    const interaction = interactionRef.current;\n    const target = interaction?.resolvePointer(event.clientX, event.clientY);\n    if (!interaction || !target) return;\n    interaction.setControlledFocus(target);\n    if (dragRelease.current) return;\n    const release = new AbortController();\n    dragRelease.current = release;\n    document.addEventListener(\n      'pointerdown',\n      () => {\n        dragRelease.current = null;\n        interactionRef.current?.setControlledFocus(null);\n      },\n      { capture: true, once: true, signal: release.signal }\n    );\n  };\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'waterfall-chart',\n        className: slotClasses.root({ className }),\n        children: (\n          <>\n            {loading ? null : (\n              <ul aria-label={summary} className=\"sr-only\">\n                {steps.map((step, index) => (\n                  <li key={index}>{spokenRow(step, index)}</li>\n                ))}\n              </ul>\n            )}\n            <div\n              ref={plotRef}\n              data-slot=\"waterfall-chart-plot\"\n              className={slotClasses.plot()}\n              style={{ aspectRatio }}\n              onKeyDown={(event) => {\n                if (event.key === 'Escape') interactionRef.current?.setControlledFocus(null);\n              }}\n              onPointerMove={readDrag}\n              onPointerCancel={() => interactionRef.current?.setControlledFocus(null)}\n            >\n              {width > 0 ? (\n                <Chart\n                  // An entrance only grows on a chart's first paint, so the bars\n                  // arriving after a load, or a replay, are a new chart.\n                  key={`${revision}:${status}`}\n                  definition={definition}\n                  renderer={renderer}\n                  ariaLabel={summary}\n                  aspectRatio={aspectRatio}\n                  initialWidth={width}\n                  className={slotClasses.chart()}\n                  onFocusChange={onFocusChange}\n                  onRender={({ scene, interaction }) => {\n                    interactionRef.current = interaction;\n                    setStubs(stubOutlines(scene.nodes));\n                  }}\n                />\n              ) : null}\n              {loading && skeleton && stubs ? (\n                <div\n                  data-slot=\"waterfall-chart-skeleton\"\n                  aria-hidden\n                  className={slotClasses.sweepClip()}\n                  style={{ clipPath: `path('${stubs}')` }}\n                >\n                  <style>{KEYFRAMES}</style>\n                  <div\n                    className={slotClasses.sweep()}\n                    style={{ animation: `panelui-waterfall-sweep ${skeleton.duration}ms linear infinite` }}\n                  />\n                </div>\n              ) : null}\n            </div>\n            {children}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n\n  return <WaterfallChartContext.Provider value={context}>{element}</WaterfallChartContext.Provider>;\n});\nWaterfallChartRoot.displayName = 'WaterfallChart';\n\n/** Hands a part's formatters to the root without a new function rebuilding the chart. */\nfunction useLatest<T>(value: T) {\n  const [latest] = useState(() => ({ current: value }));\n  useEffect(() => {\n    latest.current = value;\n  });\n  return latest;\n}\n\nexport interface WaterfallChartGridProps {\n  /** Roughly how many lines to draw across the value axis. */\n  rows?: number;\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** Opacity of the lines. */\n  opacity?: number;\n}\n\n/** Lines across the value axis, so a bar can be read against a number. */\nfunction WaterfallChartGrid({\n  rows = 4,\n  dashArray = '4 6',\n  opacity = 1,\n}: WaterfallChartGridProps) {\n  const { setSlot } = useChart('WaterfallChart.Grid');\n  useEffect(() => {\n    setSlot('grid', { rows, dashArray, opacity });\n    return () => setSlot('grid', undefined);\n  }, [rows, dashArray, opacity, setSlot]);\n  return null;\n}\nWaterfallChartGrid.displayName = 'WaterfallChart.Grid';\n\nexport interface WaterfallChartBarsProps {\n  /** Corner radius, overriding the chart's. */\n  cornerRadius?: number;\n}\n\n/** The bars. Each is coloured by its kind, or by the step's own `color`. */\nfunction WaterfallChartBars({\n  cornerRadius,\n}: WaterfallChartBarsProps) {\n  const { setSlot } = useChart('WaterfallChart.Bars');\n  useEffect(() => {\n    setSlot('bars', { cornerRadius });\n    return () => setSlot('bars', undefined);\n  }, [cornerRadius, setSlot]);\n  return null;\n}\nWaterfallChartBars.displayName = 'WaterfallChart.Bars';\n\nexport interface WaterfallChartConnectorsProps {\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** The stroke's width in pixels. */\n  strokeWidth?: number;\n  /** Opacity of the lines. */\n  opacity?: number;\n}\n\n/** The lines from each bar's end to the next band. They always pass behind the bars. */\nfunction WaterfallChartConnectors({\n  dashArray = '3 4',\n  strokeWidth = 1,\n  opacity = 1,\n}: WaterfallChartConnectorsProps) {\n  const { setSlot } = useChart('WaterfallChart.Connectors');\n  useEffect(() => {\n    setSlot('connectors', { dashArray, strokeWidth, opacity });\n    return () => setSlot('connectors', undefined);\n  }, [dashArray, strokeWidth, opacity, setSlot]);\n  return null;\n}\nWaterfallChartConnectors.displayName = 'WaterfallChart.Connectors';\n\nexport interface WaterfallChartSkeletonProps {\n  /**\n   * How many placeholder bars to draw. Defaults to one per step, and to six when\n   * the data has not arrived.\n   */\n  bars?: number;\n  /** Milliseconds for one pass of the sweep. */\n  duration?: number;\n}\n\n/**\n * The loading state: a row of short, equal stubs on the baseline, with a highlight\n * travelling across them. Floating them would invent a set of running totals.\n *\n * The highlight is a child translated inside a clip of the stubs' outlines, so\n * the loop runs on the compositor and repaints nothing.\n */\nfunction WaterfallChartSkeleton({\n  bars,\n  duration = 1400,\n}: WaterfallChartSkeletonProps) {\n  const { setSlot } = useChart('WaterfallChart.Skeleton');\n  useEffect(() => {\n    setSlot('skeleton', { bars, duration });\n    return () => setSlot('skeleton', undefined);\n  }, [bars, duration, setSlot]);\n  return null;\n}\nWaterfallChartSkeleton.displayName = 'WaterfallChart.Skeleton';\n\nexport interface WaterfallChartXAxisProps {\n  /**\n   * How many labels to show. Every step by default, thinned only when the bands\n   * get too narrow to read. Pass a number to force it lower.\n   */\n  ticks?: number;\n  /** Turn a step into its label. Defaults to its `label`. */\n  format?: (step: WaterfallStep, index: number) => string;\n}\n\n/** The step names under the bands. Sideways, `YAxis` has them. */\nfunction WaterfallChartXAxis({\n  ticks,\n  format,\n}: WaterfallChartXAxisProps) {\n  const { setSlot } = useChart('WaterfallChart.XAxis');\n  useEffect(() => {\n    setSlot('xAxis', { ticks, format });\n    return () => setSlot('xAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nWaterfallChartXAxis.displayName = 'WaterfallChart.XAxis';\n\nexport interface WaterfallChartYAxisProps {\n  /** Roughly how many labels to show along the value axis. */\n  ticks?: number;\n  /** Format a value for its label. Defaults to a compact number. */\n  format?: (value: number) => string;\n}\n\n/** Value labels down the side. Sideways, the side is the step axis, so the names go there. */\nfunction WaterfallChartYAxis({\n  ticks = 4,\n  format,\n}: WaterfallChartYAxisProps) {\n  const { setSlot } = useChart('WaterfallChart.YAxis');\n  useEffect(() => {\n    setSlot('yAxis', { ticks, format });\n    return () => setSlot('yAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nWaterfallChartYAxis.displayName = 'WaterfallChart.YAxis';\n\nexport interface WaterfallChartValuesProps {\n  /** Format a step's number. Defaults to a signed compact number. */\n  format?: (step: WaterfallStep, index: number) => string;\n}\n\n/**\n * Each step's change, written just past the end of its bar. Upright only: sideways\n * a number at the end of a bar lands on the next band's name.\n */\nfunction WaterfallChartValues({\n  format,\n}: WaterfallChartValuesProps) {\n  const { setSlot } = useChart('WaterfallChart.Values');\n  useEffect(() => {\n    setSlot('values', { format });\n    return () => setSlot('values', undefined);\n  }, [format, setSlot]);\n  return null;\n}\nWaterfallChartValues.displayName = 'WaterfallChart.Values';\n\nexport interface WaterfallChartTooltipProps {\n  /** Format the step's change. Defaults to a signed compact number. */\n  formatValue?: (step: WaterfallStep) => string;\n  /** Format the running total line. Return `null` to drop it. */\n  formatTotal?: (step: WaterfallStep) => string | null;\n  /** Names for the three kinds and for the running total line. */\n  labels?: Partial<Record<WaterfallKind | 'balance', string>>;\n  /** Added to the readout's surface. */\n  className?: string;\n}\n\n/**\n * The readout, and what drives it: hovering a band, pressing or dragging across\n * them, or the arrow keys. A click or a tap pins the reading until the next press\n * elsewhere or Escape. It reports what the step changed by and what the balance\n * stood at afterwards, and the whole plot is the hit area.\n */\nfunction WaterfallChartTooltip({\n  formatValue,\n  formatTotal,\n  labels,\n  className,\n}: WaterfallChartTooltipProps) {\n  const { setSlot } = useChart('WaterfallChart.Tooltip');\n  const latest = useLatest({ formatValue, formatTotal, labels });\n  useEffect(() => {\n    setSlot('tooltip', { className, latest });\n    return () => setSlot('tooltip', undefined);\n  }, [className, latest, setSlot]);\n  return null;\n}\nWaterfallChartTooltip.displayName = 'WaterfallChart.Tooltip';\n\nexport interface WaterfallChartLegendProps extends ComponentProps<'div'> {\n  /** Names for the three kinds. Defaults to Increase, Decrease and Total. */\n  labels?: Partial<Record<WaterfallKind, string>>;\n}\n\n/** A swatch and a name per kind the run contains, so a run with no totals lists no colour for them. */\nfunction WaterfallChartLegend({\n  labels,\n  className,\n  render,\n  ...props\n}: WaterfallChartLegendProps & RenderProp<'div'>) {\n  const { steps, colors } = useChart('WaterfallChart.Legend');\n  const slots = waterfallChartVariants();\n  const kinds = waterfallKinds(steps);\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'waterfall-chart-legend',\n        'aria-hidden': true,\n        className: slots.legend({ className }),\n        children: kinds.map((kind) => (\n          <span key={kind} className={slots.legendItem()}>\n            <span className={slots.legendSwatch()} style={{ background: colors[kind] }} />\n            {labels?.[kind] ?? KIND_NAMES[kind]}\n          </span>\n        )),\n      },\n      props\n    ),\n  });\n\n  return kinds.length ? element : null;\n}\nWaterfallChartLegend.displayName = 'WaterfallChart.Legend';\n\nexport interface WaterfallChartHeaderProps extends Omit<ComponentProps<'div'>, 'title'> {\n  /** Small line above the value: what the run is of. */\n  title?: ReactNode;\n  /** The readout. The largest thing on the card, and the first thing read. */\n  value?: ReactNode;\n  /** One muted line under the value: a period, a comparison, a total. */\n  caption?: ReactNode;\n  /** Names for the three kinds, as the legend takes. */\n  labels?: Partial<Record<WaterfallKind, string>>;\n  /**\n   * Draw a swatch and a name per kind along the trailing edge. Prefer this to\n   * `WaterfallChart.Legend` on a chart that has a header: the legend sits over the\n   * plot, in the corner the bars also want.\n   */\n  legend?: boolean;\n  /** Trailing slot: a control, a badge, a range picker. Wins over `legend`. */\n  children?: ReactNode;\n}\n\n/**\n * The strip above the plot: what the run is of, what it reads and what the colours\n * mean. The value is not derived here. A readout that follows the pointer takes it\n * from `onActiveIndexChange` and passes the string down, so one header can show the\n * closing balance at rest and a step's change while one is read.\n */\nfunction WaterfallChartHeader({\n  title,\n  value,\n  caption,\n  labels,\n  legend = false,\n  className,\n  children,\n  render,\n  ...props\n}: WaterfallChartHeaderProps & RenderProp<'div'>) {\n  const { steps, colors } = useChart('WaterfallChart.Header');\n  const slots = waterfallChartVariants();\n  const kinds = waterfallKinds(steps);\n  const trailing =\n    children ??\n    (legend && kinds.length\n      ? kinds.map((kind) => (\n          <span key={kind} className={slots.legendItem()}>\n            <span className={slots.legendSwatch()} style={{ background: colors[kind] }} />\n            {labels?.[kind] ?? KIND_NAMES[kind]}\n          </span>\n        ))\n      : null);\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'waterfall-chart-header',\n        className: slots.header({ className }),\n        children: (\n          <>\n            <div className={slots.headerText()}>\n              {title ? <div className={slots.headerMuted()}>{title}</div> : null}\n              {value ? <div className={slots.headerValue()}>{value}</div> : null}\n              {caption ? <div className={slots.headerMuted()}>{caption}</div> : null}\n            </div>\n            {trailing ? <div className={slots.headerTrailing()}>{trailing}</div> : null}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nWaterfallChartHeader.displayName = 'WaterfallChart.Header';\n\nexport const WaterfallChart = Object.assign(WaterfallChartRoot, {\n  Header: WaterfallChartHeader,\n  Grid: WaterfallChartGrid,\n  Connectors: WaterfallChartConnectors,\n  Bars: WaterfallChartBars,\n  Skeleton: WaterfallChartSkeleton,\n  XAxis: WaterfallChartXAxis,\n  YAxis: WaterfallChartYAxis,\n  Values: WaterfallChartValues,\n  Tooltip: WaterfallChartTooltip,\n  Legend: WaterfallChartLegend,\n});\n\n/* By name as well, for a Server Component, which cannot dot into a client\n   module. */\nexport {\n  WaterfallChartHeader,\n  WaterfallChartGrid,\n  WaterfallChartConnectors,\n  WaterfallChartBars,\n  WaterfallChartSkeleton,\n  WaterfallChartXAxis,\n  WaterfallChartYAxis,\n  WaterfallChartValues,\n  WaterfallChartTooltip,\n  WaterfallChartLegend,\n};\n"
    },
    {
      "path": "components/panelui/waterfall-chart/waterfall-steps.ts",
      "type": "registry:component",
      "target": "@components/panelui/waterfall-chart/waterfall-steps.ts",
      "content": "/**\n * The running totals a waterfall sits on, kept apart from the drawing so that\n * the bars, the connectors, the labels and the readout all get the same answer.\n */\n\n/** Which of the three roles a step's bar is drawn in. */\nexport type WaterfallKind = 'rise' | 'fall' | 'total';\n\nexport interface WaterfallDatum<TColor extends string = string> {\n  /** Name of the step, as the axis and the readout show it. */\n  label: string;\n  /**\n   * The change this step makes to the running total. On a `total` step it is\n   * added before the bar is drawn, so `0` reads the balance as it stands and a\n   * non-zero one opens the run at a starting balance.\n   */\n  value: number;\n  /** Draw this step as a reading: measured from the baseline, in the total's colour. */\n  total?: boolean;\n  /** Which of the theme's roles to draw this one bar in, over its kind's. */\n  color?: TColor;\n}\n\n/** One step, resolved against the running total it acts on. */\nexport interface WaterfallStep<TColor extends string = string> {\n  datum: WaterfallDatum<TColor>;\n  label: string;\n  /** The change, or on a total the balance it reads. */\n  value: number;\n  kind: WaterfallKind;\n  /** Where the bar is measured from: the running total before this step. */\n  start: number;\n  /** Where the bar reaches: the running total after it. */\n  end: number;\n}\n\n/**\n * Resolves the data into steps. A value that is not a finite number counts as\n * zero, so the step keeps its place and one bad row does not poison every total\n * after it.\n */\nexport function waterfallSteps<TColor extends string = string>(\n  data: readonly WaterfallDatum<TColor>[]\n): WaterfallStep<TColor>[] {\n  let running = 0;\n  return data.map((datum) => {\n    const value = Number.isFinite(datum.value) ? datum.value : 0;\n    const start = running;\n    running += value;\n    if (datum.total) return { datum, label: datum.label, value: running, kind: 'total', start: 0, end: running };\n    return { datum, label: datum.label, value, kind: value < 0 ? 'fall' : 'rise', start, end: running };\n  });\n}\n\n/**\n * The value axis. It takes both ends of every bar, because a domain from the\n * changes alone would be a fraction of the height a climbing run needs, and it\n * always includes zero. Headroom goes past the furthest bar only, so the run\n * stays on its baseline. A `domain` with both ends finite wins.\n */\nexport function waterfallExtent(\n  steps: readonly Pick<WaterfallStep, 'start' | 'end'>[],\n  domain?: readonly [number, number]\n): [number, number] {\n  if (domain && Number.isFinite(domain[0]) && Number.isFinite(domain[1])) return [domain[0], domain[1]];\n  let min = 0;\n  let max = 0;\n  for (const { start, end } of steps) {\n    min = Math.min(min, start, end);\n    max = Math.max(max, start, end);\n  }\n  if (min === 0 && max === 0) return [0, 1];\n  return [min === 0 ? 0 : min * 1.1, max === 0 ? 0 : max * 1.1];\n}\n\n/**\n * Whether any of a step's bar lies inside a fixed `domain`. Past either end the\n * scale clamps it to nothing. A step that only touches an end also has no length there.\n */\nexport function waterfallReaches(\n  { start, end }: Pick<WaterfallStep, 'start' | 'end'>,\n  domain?: readonly [number, number]\n): boolean {\n  if (!domain || !domain.every(Number.isFinite)) return true;\n  const low = Math.min(domain[0], domain[1]);\n  const high = Math.max(domain[0], domain[1]);\n  return Math.max(start, end) > low && Math.min(start, end) < high;\n}\n\n/** The kinds a run actually contains, in the order a legend lists them. */\nexport function waterfallKinds(steps: readonly Pick<WaterfallStep, 'kind'>[]): WaterfallKind[] {\n  const seen = new Set(steps.map(({ kind }) => kind));\n  return (['rise', 'fall', 'total'] as const).filter((kind) => seen.has(kind));\n}\n"
    }
  ]
}
