Wasserfalldiagramm
Wie aus einem Anfangswert ein Endwert wird, Schritt für Schritt. Als Brücke, als Rechnung mit Zwischensummen, in Zeilen für lange Namen oder als Geld rein und raus mit dem Kontostand. Auf dem Telefon werden die Säulen zu Zeilen.
Eine von 5 Components in Vergleichs-Charts
Variants
Details
Was Wasserfalldiagramm mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Vergleichs-Charts
- Bereich
- Elements
- Variants
- 4
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/waterfall-chart.tsx
- Bringt mit
- utilskiticonchartchart-figure
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 11 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle WaterfallChartProps aus components/ui/waterfall-chart.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<ChartA11y, 'label'>, ChartFigureProps an.
labeloptionalstringAccessible name of the chart. Default: the title.
variantoptionalWaterfallVariantbridgesubtotalshorizontalcashflow
stepsoptionalWaterfallStep[]bridge, subtotals, horizontal: the steps in order
monthsoptionalCashflowMonth[]cashflow: money in and out per month
openingoptionalnumbercashflow: balance before the first month
formatoptionalIntl.NumberFormatOptionsValue format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }
heightoptionalnumberPlot height for the columns in px. Default 300.
Eingebaute Texte
Diese Texte schreibt Wasserfalldiagramm selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 11 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| change | Englisch Change | Deutsch Veränderung |
| running | Englisch Running total | Deutsch Zwischenstand |
| total | Englisch Total | Deutsch Summe |
| step | Englisch Step | Deutsch Schritt |
| in | Englisch Money in | Deutsch Einnahmen |
| out | Englisch Money out | Deutsch Ausgaben |
| balance | Englisch Balance | Deutsch Kontostand |
| month | Englisch Month | Deutsch Monat |
| gain | Englisch Gain | Deutsch Plus |
| loss | Englisch Loss | Deutsch Minus |
| totals | Englisch Totals | Deutsch Summen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Wasserfalldiagramm reagiert selbst auf die Tasten Escape, Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts, Pos1, Ende und Leertaste.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 380 Zeilen
'use client';
import { useState } from 'react';
import { cn, round, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { ChartRoot, ChartSurface, ChartTable, ChartTooltip, Swatch, barPath, fill, niceTicks, numberFormatter, textWidth, tickFormatter, useEntrance, useIntlLocale, useShown, useWidth, xFormatters, type ChartA11y, type TooltipRow } from '@/components/ui/chart';
import { ChartFigure, PlotAxes, linear, type ChartFigureProps } from '@/components/ui/chart-figure';
/*
* How a start value becomes an end value, step by step.
* bridge start, the changes, the end: columns that float on the running total
* subtotals a statement with subtotals between the groups (profit and loss)
* horizontal the same as rows, long names on the left (also the phone layout of the two above)
* cashflow money in and out per month around the zero line, the balance as a line
*
* Totals stand in grey on the baseline, gains and losses float in the
* success and destructive tones, always with a sign and an arrow in the
* label, so red and green are never the only clue. Thin connectors carry
* the running total from one column to the next.
*/
export type WaterfallVariant = 'bridge' | 'subtotals' | 'horizontal' | 'cashflow';
export interface WaterfallStep {
label: string;
/** A change (positive or negative). For kind total: the value itself. For subtotal: ignored, the running total is used. */
value: number;
/** delta (default), total (stands on the baseline) or subtotal (the running total so far) */
kind?: 'delta' | 'total' | 'subtotal';
}
export interface CashflowMonth {
/** "2026-03" is read as a month and formatted per language */
month: string;
in: number;
out: number;
}
export interface WaterfallChartProps extends Omit<ChartA11y, 'label'>, ChartFigureProps {
/** Accessible name of the chart. Default: the title. */
label?: string;
variant?: WaterfallVariant;
/** bridge, subtotals, horizontal: the steps in order */
steps?: WaterfallStep[];
/** cashflow: money in and out per month */
months?: CashflowMonth[];
/** cashflow: balance before the first month */
opening?: number;
/** Value format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 } */
format?: Intl.NumberFormatOptions;
/** Plot height for the columns in px. Default 300. */
height?: number;
className?: string;
}
const TEXTS = {
en: { change: 'Change', running: 'Running total', total: 'Total', step: 'Step', in: 'Money in', out: 'Money out', balance: 'Balance', month: 'Month', gain: 'Gain', loss: 'Loss', totals: 'Totals' },
de: { change: 'Veränderung', running: 'Zwischenstand', total: 'Summe', step: 'Schritt', in: 'Einnahmen', out: 'Ausgaben', balance: 'Kontostand', month: 'Monat', gain: 'Plus', loss: 'Minus', totals: 'Summen' },
};
const UP = 'var(--kitfg-up)';
const DOWN = 'var(--kitfg-down)';
const TOTAL = 'var(--muted-foreground)';
interface Bar {
label: string;
kind: 'delta' | 'total' | 'subtotal';
change: number;
from: number;
to: number;
}
function bars(steps: WaterfallStep[]): Bar[] {
let run = 0;
return steps.map((s, i) => {
const kind = s.kind ?? (i === 0 ? 'total' : 'delta');
if (kind === 'total') {
run = s.value;
return { label: s.label, kind, change: s.value, from: 0, to: s.value };
}
if (kind === 'subtotal') return { label: s.label, kind, change: run, from: 0, to: run };
const from = run;
run += s.value;
return { label: s.label, kind, change: s.value, from, to: run };
});
}
/**
* Start to end in steps: a bridge, a statement with subtotals, rows, or cash in and out with the balance.
* <WaterfallChart variant="bridge" steps={[{ label: '2025', value: 412000, kind: 'total' }, { label: 'New customers', value: 86000 }, …]} />
*/
export function WaterfallChart(props: WaterfallChartProps) {
const { variant = 'bridge', steps = [], months = [], opening = 0, format, height: heightProp, label, summary, labels, animate = true, title, value, delta, meta, source, framed, className } = props;
const t = useTexts(TEXTS, undefined);
const intl = useIntlLocale();
const fmt = numberFormatter(intl, format);
const tick = tickFormatter(intl, format);
const signed = (n: number) => (n > 0 ? '+' : n < 0 ? '−' : '') + fmt(Math.abs(n));
const [wrapRef, width] = useWidth<HTMLDivElement>(720);
const short = (n: number) => {
const c = tick(n);
return /\p{L}/u.test(c.replace(/[€$£]|ms|GB|kW/g, '')) ? c : fmt(n);
};
const signedShort = (n: number) => (n > 0 ? '+' : n < 0 ? '\u2212' : '') + short(Math.abs(n));
const [rootRef, anim] = useEntrance<HTMLDivElement>(animate);
const [active, setActive] = useState<number | null>(null);
const { shown, open, jump } = useShown(active);
const name = label ?? title ?? '';
const phone = width < 520;
const cash = variant === 'cashflow';
// Columns only while every name fits its column (two lines allowed), else rows: tablets get rows too when names are long
const colBand = (width - 60) / Math.max(1, steps.length);
const fitsColumn = (s: string) => {
const w = s.split(' ');
const h = Math.ceil(w.length / 2);
const lw = w.length < 2 ? textWidth(s, 11) : Math.max(textWidth(w.slice(0, h).join(' '), 11), textWidth(w.slice(h).join(' '), 11));
return lw <= colBand - 6;
};
const rows = variant === 'horizontal' || (!cash && (phone || !steps.every((st) => fitsColumn(st.label))));
const list = bars(steps);
// Start above zero when every value sits far from it, else the changes shrink to slivers. Totals then carry a break mark.
const runs = list.flatMap((b) => (b.kind === 'delta' ? [b.from, b.to] : [b.to]));
const minRun = Math.min(...runs);
const maxRun = Math.max(...runs);
const cut = !cash && minRun > 0 && minRun > maxRun * 0.45;
const floor = cut ? niceTicks(minRun - (maxRun - minRun) * 0.55, maxRun, 5).min : 0;
// Cashflow model
const balance: number[] = [];
months.reduce((b, m) => {
const n = b + m.in - m.out;
balance.push(n);
return n;
}, opening);
const xf = xFormatters(intl, months[0]?.month);
const n = cash ? months.length : list.length;
const onKeyDown = (e: React.KeyboardEvent) => {
const cur = active ?? -1;
let next: number | null = cur;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = Math.min(n - 1, cur + 1);
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = Math.max(0, cur - 1);
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = n - 1;
else if (e.key === 'Escape') next = null;
else return;
e.preventDefault();
setActive(next);
};
const tipRows = (i: number): TooltipRow[] => {
if (cash) {
const m = months[i];
return [
{ key: 'in', label: t.in, value: fmt(m.in), color: UP, shape: 'rect' },
{ key: 'out', label: t.out, value: fmt(m.out), color: DOWN, shape: 'rect' },
{ key: 'bal', label: t.balance, value: fmt(balance[i]), color: 'var(--primary)', shape: 'line' },
];
}
const b = list[i];
return b.kind === 'delta'
? [
{ key: 'c', label: t.change, value: signed(b.change), color: b.change >= 0 ? UP : DOWN, shape: 'rect' },
{ key: 'r', label: t.running, value: fmt(b.to) },
]
: [{ key: 't', label: b.kind === 'subtotal' ? t.running : t.total, value: fmt(b.to), color: TOTAL, shape: 'rect' }];
};
const titleOf = (i: number) => (cash ? xf.tooltip(months[i]?.month) : list[i]?.label ?? '');
const live = active !== null && active < n ? `${titleOf(active)}. ${tipRows(active).map((r) => `${r.label} ${r.value}`).join(', ')}` : '';
const auto = cash
? `${t.balance} ${fmt(opening)} → ${fmt(balance[balance.length - 1] ?? opening)}`
: list.length
? `${list[0].label} ${fmt(list[0].to)}, ${list[list.length - 1].label} ${fmt(list[list.length - 1].to)}`
: '';
const table = cash ? (
<ChartTable caption={name} head={[t.month, t.in, t.out, t.balance]} rows={months.map((m, i) => [xf.tooltip(m.month), fmt(m.in), fmt(m.out), fmt(balance[i])])} />
) : (
<ChartTable caption={name} head={[t.step, t.change, t.running]} rows={list.map((b) => [b.label, b.kind === 'delta' ? signed(b.change) : fmt(b.to), fmt(b.to)])} />
);
const legend = (
<ul className="mb-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12px] text-muted-foreground">
{cash ? (
<>
<li className="flex items-center gap-1.5"><Swatch color={UP} />{t.in}</li>
<li className="flex items-center gap-1.5"><Swatch color={DOWN} />{t.out}</li>
<li className="flex items-center gap-1.5"><Swatch color="var(--primary)" shape="line" />{t.balance}</li>
</>
) : (
<>
<li className="flex items-center gap-1.5"><Swatch color={TOTAL} />{t.totals}</li>
<li className="flex items-center gap-1.5"><Swatch color={UP} /><Icon name="arrow-up-right" size={13} strokeWidth={2} />{t.gain}</li>
<li className="flex items-center gap-1.5"><Swatch color={DOWN} /><Icon name="arrow-down-right" size={13} strokeWidth={2} />{t.loss}</li>
</>
)}
</ul>
);
const delayOf = (i: number) => (anim ? ({ '--kitch-d': `${round(100 + i * 110, 0)}ms` } as React.CSSProperties) : undefined);
/* ---------- rows ---------- */
if (rows) {
const rowH = 38;
const height = n * rowH + 28;
const all = list.flatMap((b) => [b.from, b.to]);
const nice = niceTicks(Math.min(floor, ...all.filter((v) => v >= floor)), Math.max(0, ...all), phone ? 3 : 5);
const labelW = Math.min(Math.round(width * (phone ? 0.36 : 0.28)), Math.max(...list.map((b) => textWidth(b.label, 12))) + 12);
const valueW = Math.max(...list.map((b) => textWidth(b.kind === 'delta' ? signedShort(b.change) : short(b.to), 12))) + 14;
const left = labelW + 8;
const right = width - 4;
const X = linear(nice.min, nice.max, left, right - valueW);
const top = 4;
const thick = 18;
const pick = (clientY: number, el: HTMLElement) => {
const r = el.getBoundingClientRect();
setActive(Math.max(0, Math.min(n - 1, Math.floor((clientY - r.top - top) / rowH))));
};
return (
<ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} className={className}>
<ChartRoot anim={anim} rootRef={rootRef}>
{legend}
<div ref={wrapRef}>
<ChartSurface label={name} summary={summary ?? auto} labels={labels} live={live} table={table} onKeyDown={onKeyDown} onFocus={(e) => e.target === e.currentTarget && active === null && e.currentTarget.matches(':focus-visible') && setActive(0)} onBlur={() => setActive(null)} onPointerMove={(e) => pick(e.clientY, e.currentTarget)} onPointerDown={(e) => pick(e.clientY, e.currentTarget)} onPointerLeave={(e) => e.pointerType === 'mouse' && setActive(null)}>
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden className="block overflow-visible">
{nice.ticks.map((v, i) => {
const gap = nice.ticks.length > 1 ? X(nice.ticks[1]) - X(nice.ticks[0]) : width;
const every = Math.max(1, Math.ceil((Math.max(...nice.ticks.map((u) => textWidth(tick(u), 11))) + 10) / gap));
return (
<g key={v}>
<line x1={X(v)} x2={X(v)} y1={top} y2={top + n * rowH} stroke={v === 0 || (cut && i === 0) ? 'var(--kitch-axis)' : 'var(--kitch-grid)'} strokeWidth="1" shapeRendering="crispEdges" />
{i % every === 0 && (
<text x={X(v)} y={height - 6} textAnchor="middle" className="text-[11px] tabular-nums" fill="var(--muted-foreground)">
{tick(v)}
</text>
)}
</g>
);
})}
{list.map((b, i) => {
const cy = top + i * rowH + rowH / 2;
const a = X(Math.max(nice.min, Math.min(b.from, b.to)));
const z = X(Math.max(b.from, b.to));
const color = b.kind === 'delta' ? (b.change >= 0 ? UP : DOWN) : TOTAL;
const isTotal = b.kind !== 'delta';
const next = list[i + 1];
return (
<g key={i} className="transition-opacity duration-200" opacity={active !== null && active !== i ? 0.5 : 1}>
{active === i && <rect x={0} y={cy - rowH / 2 + 2} width={width} height={rowH - 4} rx="6" fill="var(--foreground)" opacity="0.05" />}
<text x={0} y={cy} dy="0.34em" className={cn('text-[12px]', isTotal ? 'font-semibold' : '')} fill={isTotal || active === i ? 'var(--foreground)' : 'var(--muted-foreground)'}>
{b.label}
</text>
{next && <line x1={X(b.to)} x2={X(b.to)} y1={cy + thick / 2} y2={cy + rowH - thick / 2} stroke="var(--kitch-axis)" strokeWidth="1" strokeDasharray="2 2" className={anim ? 'kitch-fade' : undefined} style={delayOf(i + 1)} />}
{isTotal && cut && <path d={`M${a + 9} ${cy - thick / 2 - 2}l-4 ${thick + 4}M${a + 14} ${cy - thick / 2 - 2}l-4 ${thick + 4}`} stroke="var(--kitch-surface)" strokeWidth="2.5" className="pointer-events-none" style={{ zIndex: 1 }} />}
<path d={barPath(a, cy - thick / 2, Math.max(1, z - a), thick, 3, b.change >= 0 || isTotal ? 'right' : 'left')} fill={color} className={anim ? 'kitch-grow-x' : undefined} style={{ ...delayOf(i), transformOrigin: b.change >= 0 || isTotal ? `${a}px ${cy}px` : `${z}px ${cy}px`, transformBox: 'view-box' }} />
<text x={z + 8} y={cy} dy="0.34em" className={cn('text-[12px] tabular-nums', anim && 'kitch-rise')} style={delayOf(i + 2)} fill="var(--foreground)" fontWeight={isTotal ? 600 : 500}>
{b.kind === 'delta' ? signedShort(b.change) : short(b.to)}
</text>
</g>
);
})}
</svg>
</ChartSurface>
</div>
</ChartRoot>
</ChartFigure>
);
}
/* ---------- columns ---------- */
const height = heightProp ?? 320;
const all = cash ? [0, ...months.flatMap((m) => [m.in, -m.out]), ...balance, opening] : [floor, ...runs];
const nice = niceTicks(Math.min(...all), Math.max(...all), 5);
const yLabels = nice.ticks.map(tick);
const left = Math.max(...yLabels.map((s) => textWidth(s, 11)), 14) + 14;
const right = width - 6;
const top = 26;
const bottom = height - (cash ? 28 : 46);
const Y = linear(nice.min, nice.max, bottom, top);
const band = (right - left) / Math.max(1, n);
const cx = (i: number) => round(left + band * (i + 0.5));
const thick = Math.min(cash ? 14 : 36, band * (cash ? 0.3 : 0.56));
const pick = (clientX: number, el: HTMLElement) => {
const r = el.getBoundingClientRect();
const x = (clientX - r.left) * (width / (r.width || width));
setActive(Math.max(0, Math.min(n - 1, Math.floor((x - left) / band))));
};
const catLabels = cash ? months.map((m) => xf.axis(m.month)) : list.map((b) => b.label);
// Labels wrap to two lines before any is skipped
const lineW = (s: string) => {
const w = s.split(' ');
if (cash || w.length < 2) return textWidth(s, 11);
const h = Math.ceil(w.length / 2);
return Math.max(textWidth(w.slice(0, h).join(' '), 11), textWidth(w.slice(h).join(' '), 11));
};
const every = Math.max(1, Math.ceil((Math.max(...catLabels.map(lineW)) + 8) / band));
return (
<ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} className={className}>
<ChartRoot anim={anim} rootRef={rootRef}>
{legend}
<div ref={wrapRef}>
<ChartSurface label={name} summary={summary ?? auto} labels={labels} live={live} table={table} onKeyDown={onKeyDown} onFocus={(e) => e.target === e.currentTarget && active === null && e.currentTarget.matches(':focus-visible') && setActive(0)} onBlur={() => setActive(null)} onPointerMove={(e) => pick(e.clientX, e.currentTarget)} onPointerDown={(e) => pick(e.clientX, e.currentTarget)} onPointerLeave={(e) => e.pointerType === 'mouse' && setActive(null)}>
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden className="block overflow-visible">
<PlotAxes left={left} right={right} top={top} bottom={bottom} height={height} yTicks={nice.ticks.map((v, i) => ({ at: Y(v), label: yLabels[i], strong: v === 0 || (cut && i === 0) }))} />
{shown !== null && <rect x={0} y={top - 8} width={round(band - 4)} height={bottom - top + 8} rx="6" fill="var(--foreground)" className="kitch-glide" data-jump={jump ? '' : undefined} style={{ transform: `translateX(${round(left + band * shown + 2)}px)`, opacity: open ? 0.045 : 0 }} />}
{/* Category labels, wrapped on two lines where needed */}
<g className="text-[11px]" fill="var(--muted-foreground)">
{catLabels.map((s, i) => {
if (i % every) return null;
const words = s.split(' ');
const two = !cash && textWidth(s, 11) > band - 6 && words.length > 1;
const half = Math.ceil(words.length / 2);
return (
<text key={i} x={cx(i)} y={bottom + 18} textAnchor="middle" className={cn(active === i && 'fill-foreground', !cash && list[i]?.kind !== 'delta' && 'font-semibold')}>
{two ? (
<>
<tspan x={cx(i)}>{words.slice(0, half).join(' ')}</tspan>
<tspan x={cx(i)} dy="13">
{words.slice(half).join(' ')}
</tspan>
</>
) : (
s
)}
</text>
);
})}
</g>
{cash ? (
<>
{months.map((m, i) => (
<g key={m.month} className="transition-opacity duration-200" opacity={active !== null && active !== i ? 0.55 : 1}>
<path d={barPath(cx(i) - thick - 1, Y(m.in), thick, Y(0) - Y(m.in), 3, 'up')} fill={UP} className={anim ? 'kitch-grow-y' : undefined} style={delayOf(i * 0.5)} />
<path d={barPath(cx(i) + 1, Y(0), thick, Y(-m.out) - Y(0), 3, 'down')} fill={DOWN} className={anim ? 'kitch-grow-y-down' : undefined} style={delayOf(i * 0.5)} />
</g>
))}
<path d={balance.map((b, i) => `${i ? 'L' : 'M'}${cx(i)} ${Y(b)}`).join('')} pathLength={1} fill="none" stroke="var(--primary)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" className={anim ? 'kitch-draw' : undefined} style={anim ? ({ '--kitch-d': '700ms', '--kitch-t': '1.2s' } as React.CSSProperties) : undefined} />
{balance.map((b, i) => (
<circle key={i} cx={cx(i)} cy={Y(b)} r={active === i || i === balance.length - 1 ? 4.5 : 0} fill="var(--primary)" stroke="var(--kitch-surface)" strokeWidth="2" className="transition-[r] duration-200" />
))}
{balance.length > 0 && (
<text x={cx(balance.length - 1)} y={Y(balance[balance.length - 1]) - 12} textAnchor="end" className={cn('text-[12px] font-semibold tabular-nums', anim && 'kitch-rise')} fill="var(--foreground)" style={anim ? ({ '--kitch-d': '1700ms', paintOrder: 'stroke', stroke: 'var(--kitch-surface)', strokeWidth: 4 } as React.CSSProperties) : { paintOrder: 'stroke', stroke: 'var(--kitch-surface)', strokeWidth: 4 }}>
{tick(balance[balance.length - 1])}
</text>
)}
</>
) : (
list.map((b, i) => {
const isTotal = b.kind !== 'delta';
const color = isTotal ? TOTAL : b.change >= 0 ? UP : DOWN;
const yTop = Y(Math.max(b.from, b.to));
const yBot = Y(Math.max(nice.min, Math.min(b.from, b.to)));
const h = Math.max(1, yBot - yTop);
const up = isTotal ? b.to >= 0 : b.change >= 0;
const next = list[i + 1];
const text = isTotal ? short(b.to) : signedShort(b.change);
const fits = textWidth(text, 11) <= band - 4;
return (
<g key={i} className="transition-opacity duration-200" opacity={active !== null && active !== i ? 0.55 : 1}>
{next && <line x1={cx(i) + thick / 2} x2={cx(i + 1) - thick / 2} y1={Y(b.to)} y2={Y(b.to)} stroke="var(--kitch-axis)" strokeWidth="1" strokeDasharray="2 2" className={anim ? 'kitch-fade' : undefined} style={delayOf(i + 1)} />}
<path d={barPath(cx(i) - thick / 2, yTop, thick, h, 4, up ? 'up' : 'down')} fill={color} className={anim ? (up ? 'kitch-grow-y' : 'kitch-grow-y-down') : undefined} style={{ ...delayOf(i), ...(isTotal ? {} : { transformOrigin: up ? `50% 100%` : `50% 0` }) }} />
{isTotal && cut && <path d={`M${cx(i) - thick / 2 - 2} ${yBot - 8}l${thick + 4} -5M${cx(i) - thick / 2 - 2} ${yBot - 13}l${thick + 4} -5`} stroke="var(--kitch-surface)" strokeWidth="2.5" className={anim ? 'kitch-fade' : undefined} style={delayOf(i + 2)} />}
{fits && (
<text x={cx(i)} y={up ? yTop - 7 : yBot + 15} textAnchor="middle" className={cn('text-[11px] tabular-nums', anim && 'kitch-rise')} style={delayOf(i + 2)} fill="var(--foreground)" fontWeight={isTotal ? 600 : 500}>
{text}
</text>
)}
</g>
);
})
)}
</svg>
<ChartTooltip open={open} jump={jump} x={shown !== null ? cx(shown) : 0} y={top} width={width} offset={Math.min(band / 2, 40) + 6} title={shown !== null ? titleOf(shown) : ''} rows={shown !== null ? tipRows(shown) : []} />
</ChartSurface>
</div>
</ChartRoot>
</ChartFigure>
);
}Häufige Fragen
Wie installiere ich Wasserfalldiagramm?
Mit aiw add @ai-website/waterfall-chart. Der Befehl legt die Datei components/ui/waterfall-chart.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, chart und chart-figure. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Wasserfalldiagramm?
4 Varianten, Brücke, Mit Zwischensummen, Zeilen und Zahlungsfluss. Du wählst sie mit der Prop variant, zum Beispiel variant="subtotals".
Kann ich die Texte von Wasserfalldiagramm übersetzen?
Ja. Wasserfalldiagramm bringt 11 eigene Texte auf Deutsch und Englisch mit, etwa „Veränderung“, „Zwischenstand“ und „Summe“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Wasserfalldiagramm einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Wasserfalldiagramm mit aiw_components und installieren sie mit aiw_add @ai-website/waterfall-chart. Nenn im Prompt die Variante, etwa variant="bridge", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Wasserfalldiagramm ohne Code anpassen?
Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden WaterfallChart-Code zum Kopieren.