Quarterly cash contribution flow (USD thousands) Step Kind Value ($k) From ($k) Balance ($k) Opening start 120 0 120 Sales delta 75 120 195 Refunds delta -15 195 180 Subtotal subtotal 180 0 180 Costs delta -65 180 115 Tax delta -20 115 95 Closing end 95 0 95
"use client" ;
import * as Chart from "@kind-ui/charts" ;
import "@kind-ui/charts/styles.css" ;
// Values are USD thousands. Checkpoints are absolute; deltas are signed changes.
const entries = [
{ id: "opening" , label: "Opening" , kind: "start" , value: 120 },
{ id: "sales" , label: "Sales" , kind: "delta" , value: 75 },
{ id: "refunds" , label: "Refunds" , kind: "delta" , value: - 15 },
{ id: "operating" , label: "Subtotal" , kind: "subtotal" },
{ id: "costs" , label: "Costs" , kind: "delta" , value: - 65 },
{ id: "tax" , label: "Tax" , kind: "delta" , value: - 20 },
{ id: "closing" , label: "Closing" , kind: "end" , value: 95 },
] satisfies Chart . WaterfallEntry [];
const data = Chart. computeWaterfallData (entries);
const labels = new Map (data. map (( row ) => [row.id, row.label]));
const money = ( value : number | null ) => (value === null ? "Unknown" : `$${ value }k` );
const config = {
range: { label: "Cash balance" , color: "#733bff" },
} satisfies Chart . SeriesConfig ;
export function CashFlowChart () {
return (
< Chart.Root config = {config}>
< div style = {{ display: "flex" , gap: 16 , flexWrap: "wrap" , fontSize: 12 }}>
< span >
< span aria-hidden = "true" style = {{ color: "#733bff" }}>
●
</ span >{ " " }
Checkpoint
</ span >
< span >
< span aria-hidden = "true" style = {{ color: "#15803d" }}>
●
</ span >{ " " }
Increase
</ span >
< span >
< span aria-hidden = "true" style = {{ color: "#be123c" }}>
●
</ span >{ " " }
Decrease
</ span >
</ div >
< Chart.ResponsiveContainer width = "100%" height = { 280 }>
< Chart.WaterfallChart
data = {data}
layout = "vertical"
animate
accessibilityLayer
aria-label = "Quarterly cash contribution flow in USD thousands"
margin = {{ top: 12 , right: 24 , bottom: 4 , left: 0 }}
barCategoryGap = "32%"
>
< Chart.CartesianGrid horizontal = { false } strokeDasharray = "3 3" />
< Chart.XAxis
type = "number"
domain = {[ 0 , "auto" ]}
axisLine = { false }
tickLine = { false }
tickFormatter = {( value ) => money ( Number (value))}
tickMargin = { 10 }
height = { 36 }
/>
< Chart.YAxis
type = "category"
dataKey = "id"
width = { 76 }
axisLine = { false }
tickLine = { false }
tickFormatter = {( id ) => labels. get ( String (id)) ?? String (id)}
tickMargin = { 8 }
/>
< Chart.ReferenceLine x = { 0 } stroke = "currentColor" strokeOpacity = { 0.3 } />
< Chart.WaterfallConnectors
data = {data}
position = "middle"
stroke = "currentColor"
strokeOpacity = { 0.35 }
/>
< Chart.WaterfallSeries radius = { 3 }>
{data. map (( row ) => (
< Chart.Cell
key = {row.id}
fill = {
row.kind !== "delta" ? "#733bff" : (row.value ?? 0 ) < 0 ? "#be123c" : "#15803d"
}
/>
))}
</ Chart.WaterfallSeries >
< Chart.Tooltip
filterNull = { false }
maxWidth = { 180 }
content = {( tooltip ) => {
const row = data. find (( row ) => row.id === String (tooltip.label));
if ( ! tooltip.active || ! row) return null ;
return (
< div
role = "status"
aria-live = "polite"
style = {{
padding: 12 ,
borderRadius: 10 ,
background: "var(--kind-ui-chart-popover, var(--popover, Canvas))" ,
color:
"var(--kind-ui-chart-popover-foreground, var(--popover-foreground, CanvasText))" ,
border: "1px solid #8886" ,
fontSize: 12 ,
}}
>
< strong >{row.label}</ strong >
< div >
{row.kind === "delta"
? "Change"
: row.kind === "subtotal"
? "Subtotal"
: "Checkpoint" }
: {row.kind === "delta" && row.value !== null && row.value > 0 ? "+" : "" }
{ money (row.value)}
</ div >
< div >From: { money (row.start)}</ div >
< div >Balance: { money (row.balance)}</ div >
</ div >
);
}}
/>
</ Chart.WaterfallChart >
</ Chart.ResponsiveContainer >
</ Chart.Root >
);
}
Use computeWaterfallData to prepare rows for the bars and connectors.
< Chart.Root config = {config}>
< Chart.ResponsiveContainer width = "100%" height = { 280 }>
< Chart.WaterfallChart data = {data} layout = "vertical" animate accessibilityLayer >
< Chart.XAxis type = "number" />
< Chart.YAxis type = "category" dataKey = "id" />
< Chart.WaterfallConnectors data = {data} position = "middle" />
< Chart.WaterfallSeries >
{data. map (( row ) => < Chart.Cell key = {row.id} fill = {row.kind !== "delta" ? "#733bff" : (row.value ?? 0 ) < 0 ? "#be123c" : "#15803d" } />)}
</ Chart.WaterfallSeries >
< Chart.Tooltip filterNull = { false } content = {( tooltip ) => {
const row = data. find (( row ) => row.id === String (tooltip.label));
return tooltip.active && row ? < div >{row.label}: {row.value ?? "Unknown" } ($k); balance {row.balance ?? "Unknown" } ($k)</ div > : null ;
}} />
</ Chart.WaterfallChart >
</ Chart.ResponsiveContainer >
</ Chart.Root >
Use one unstacked series. layout="vertical" produces horizontal bars. Label the category axis from id; the color key describes step roles rather than toggling contributions.
computeWaterfallData(entries, initialBalance = 0) returns new ordered rows. These values are USD thousands.
Opening $120k, sales and refunds produce a $180k subtotal; costs and tax leave $95k. Checkpoints supply explicit balances and may intentionally differ from the preceding result.
Connectors join adjacent known, matching balances. Place them before bars; subset chart and connector rows together. Missing geometry or disagreeing checkpoints break the connection.
A null change/checkpoint makes the balance unknown until a known checkpoint restores it. To start unknown, call computeWaterfallData(entries, null).
Missing cash change and checkpoint recovery (USD thousands) Step Kind Value ($k) From ($k) Balance ($k) Opening start 80 0 80 Sales delta 40 80 120 Pending delta Unknown 120 Unknown Fees delta -10 Unknown Unknown Audited total 105 0 105 No change delta 0 105 105 Closing end 105 0 105
"use client" ;
import * as Chart from "@kind-ui/charts" ;
import "@kind-ui/charts/styles.css" ;
// Values are USD thousands. Checkpoints are absolute; deltas are signed changes.
const entries = [
{ id: "opening" , label: "Opening" , kind: "start" , value: 80 },
{ id: "sales" , label: "Sales" , kind: "delta" , value: 40 },
{ id: "pending" , label: "Pending" , kind: "delta" , value: null },
{ id: "fees" , label: "Fees" , kind: "delta" , value: - 10 },
{ id: "audited" , label: "Audited" , kind: "total" , value: 105 },
{ id: "adjustment" , label: "No change" , kind: "delta" , value: 0 },
{ id: "closing" , label: "Closing" , kind: "end" , value: 105 },
] satisfies Chart . WaterfallEntry [];
const data = Chart. computeWaterfallData (entries);
const labels = new Map (data. map (( row ) => [row.id, row.label]));
const money = ( value : number | null ) => (value === null ? "Unknown" : `$${ value }k` );
const config = {
range: { label: "Cash balance" , color: "#733bff" },
} satisfies Chart . SeriesConfig ;
export function MissingCashFlowChart () {
return (
< Chart.Root config = {config}>
< div style = {{ display: "flex" , gap: 16 , flexWrap: "wrap" , fontSize: 12 }}>
< span >
< span aria-hidden = "true" style = {{ color: "#733bff" }}>
●
</ span >{ " " }
Checkpoint
</ span >
< span >
< span aria-hidden = "true" style = {{ color: "#15803d" }}>
●
</ span >{ " " }
Increase
</ span >
< span >
< span aria-hidden = "true" style = {{ color: "#be123c" }}>
●
</ span >{ " " }
Decrease
</ span >
</ div >
< Chart.ResponsiveContainer width = "100%" height = { 280 }>
< Chart.WaterfallChart
data = {data}
layout = "vertical"
animate
accessibilityLayer
aria-label = "Missing cash change and checkpoint recovery in USD thousands"
margin = {{ top: 12 , right: 24 , bottom: 4 , left: 0 }}
barCategoryGap = "32%"
>
< Chart.CartesianGrid horizontal = { false } strokeDasharray = "3 3" />
< Chart.XAxis
type = "number"
domain = {[ 0 , "auto" ]}
axisLine = { false }
tickLine = { false }
tickFormatter = {( value ) => money ( Number (value))}
tickMargin = { 10 }
height = { 36 }
/>
< Chart.YAxis
type = "category"
dataKey = "id"
width = { 76 }
axisLine = { false }
tickLine = { false }
tickFormatter = {( id ) => labels. get ( String (id)) ?? String (id)}
tickMargin = { 8 }
/>
< Chart.ReferenceLine x = { 0 } stroke = "currentColor" strokeOpacity = { 0.3 } />
< Chart.WaterfallConnectors
data = {data}
position = "middle"
stroke = "currentColor"
strokeOpacity = { 0.35 }
/>
< Chart.WaterfallSeries radius = { 3 }>
{data. map (( row ) => (
< Chart.Cell
key = {row.id}
fill = {
row.kind !== "delta" ? "#733bff" : (row.value ?? 0 ) < 0 ? "#be123c" : "#15803d"
}
/>
))}
</ Chart.WaterfallSeries >
< Chart.Tooltip
filterNull = { false }
maxWidth = { 180 }
content = {( tooltip ) => {
const row = data. find (( row ) => row.id === String (tooltip.label));
if ( ! tooltip.active || ! row) return null ;
return (
< div
role = "status"
aria-live = "polite"
style = {{
padding: 12 ,
borderRadius: 10 ,
background: "var(--kind-ui-chart-popover, var(--popover, Canvas))" ,
color:
"var(--kind-ui-chart-popover-foreground, var(--popover-foreground, CanvasText))" ,
border: "1px solid #8886" ,
fontSize: 12 ,
}}
>
< strong >{row.label}</ strong >
< div >
{row.kind === "delta"
? "Change"
: row.kind === "subtotal"
? "Subtotal"
: "Checkpoint" }
: {row.kind === "delta" && row.value !== null && row.value > 0 ? "+" : "" }
{ money (row.value)}
</ div >
< div >From: { money (row.start)}</ div >
< div >Balance: { money (row.balance)}</ div >
</ div >
);
}}
/>
</ Chart.WaterfallChart >
</ Chart.ResponsiveContainer >
</ Chart.Root >
);
}
Pending leaves Fees’ −$10k value known but its geometry unknown; Audited restores $105k. Zero changes retain zero-width geometry, with no fabricated minimum bar.
Values must be finite or null; subtotals must omit value. Invalid kinds/IDs, duplicate IDs, omitted change/checkpoint values, nonfinite values and arithmetic overflow throw.
Choose paper, plain, clay or glow. Custom shape or filter owns the finish; arithmetic and connectors stay unchanged.
Cash flow with a selected bar material (USD thousands) Step Kind Value ($k) From ($k) Balance ($k) Opening start 120 0 120 Sales delta 75 120 195 Refunds delta -15 195 180 Subtotal subtotal 180 0 180 Costs delta -65 180 115 Tax delta -20 115 95 Closing end 95 0 95
"use client" ;
import * as Chart from "@kind-ui/charts" ;
import "@kind-ui/charts/styles.css" ;
// Values are USD thousands. Checkpoints are absolute; deltas are signed changes.
const entries = [
{ id: "opening" , label: "Opening" , kind: "start" , value: 120 },
{ id: "sales" , label: "Sales" , kind: "delta" , value: 75 },
{ id: "refunds" , label: "Refunds" , kind: "delta" , value: - 15 },
{ id: "operating" , label: "Subtotal" , kind: "subtotal" },
{ id: "costs" , label: "Costs" , kind: "delta" , value: - 65 },
{ id: "tax" , label: "Tax" , kind: "delta" , value: - 20 },
{ id: "closing" , label: "Closing" , kind: "end" , value: 95 },
] satisfies Chart . WaterfallEntry [];
const data = Chart. computeWaterfallData (entries);
const labels = new Map (data. map (( row ) => [row.id, row.label]));
const money = ( value : number | null ) => (value === null ? "Unknown" : `$${ value }k` );
const config = {
range: { label: "Cash balance" , color: "#733bff" },
} satisfies Chart . SeriesConfig ;
export function MaterialCashFlowChart ({ material = "paper" } : { material ?: Chart . BarMaterial }) {
return (
< Chart.Root config = {config}>
< div style = {{ display: "flex" , gap: 16 , flexWrap: "wrap" , fontSize: 12 }}>
< span >
< span aria-hidden = "true" style = {{ color: "#733bff" }}>
●
</ span >{ " " }
Checkpoint
</ span >
< span >
< span aria-hidden = "true" style = {{ color: "#15803d" }}>
●
</ span >{ " " }
Increase
</ span >
< span >
< span aria-hidden = "true" style = {{ color: "#be123c" }}>
●
</ span >{ " " }
Decrease
</ span >
</ div >
< Chart.ResponsiveContainer width = "100%" height = { 280 }>
< Chart.WaterfallChart
data = {data}
layout = "vertical"
animate
accessibilityLayer
aria-label = "Cash flow with a selected bar material in USD thousands"
margin = {{ top: 12 , right: 24 , bottom: 4 , left: 0 }}
barCategoryGap = "32%"
>
< Chart.CartesianGrid horizontal = { false } strokeDasharray = "3 3" />
< Chart.XAxis
type = "number"
domain = {[ 0 , "auto" ]}
axisLine = { false }
tickLine = { false }
tickFormatter = {( value ) => money ( Number (value))}
tickMargin = { 10 }
height = { 36 }
/>
< Chart.YAxis
type = "category"
dataKey = "id"
width = { 76 }
axisLine = { false }
tickLine = { false }
tickFormatter = {( id ) => labels. get ( String (id)) ?? String (id)}
tickMargin = { 8 }
/>
< Chart.ReferenceLine x = { 0 } stroke = "currentColor" strokeOpacity = { 0.3 } />
< Chart.WaterfallConnectors
data = {data}
position = "middle"
stroke = "currentColor"
strokeOpacity = { 0.35 }
/>
< Chart.WaterfallSeries material = {material} radius = { 3 }>
{data. map (( row ) => (
< Chart.Cell
key = {row.id}
fill = {
row.kind !== "delta" ? "#733bff" : (row.value ?? 0 ) < 0 ? "#be123c" : "#15803d"
}
/>
))}
</ Chart.WaterfallSeries >
< Chart.Tooltip
filterNull = { false }
maxWidth = { 180 }
content = {( tooltip ) => {
const row = data. find (( row ) => row.id === String (tooltip.label));
if ( ! tooltip.active || ! row) return null ;
return (
< div
role = "status"
aria-live = "polite"
style = {{
padding: 12 ,
borderRadius: 10 ,
background: "var(--kind-ui-chart-popover, var(--popover, Canvas))" ,
color:
"var(--kind-ui-chart-popover-foreground, var(--popover-foreground, CanvasText))" ,
border: "1px solid #8886" ,
fontSize: 12 ,
}}
>
< strong >{row.label}</ strong >
< div >
{row.kind === "delta"
? "Change"
: row.kind === "subtotal"
? "Subtotal"
: "Checkpoint" }
: {row.kind === "delta" && row.value !== null && row.value > 0 ? "+" : "" }
{ money (row.value)}
</ div >
< div >From: { money (row.start)}</ div >
< div >Balance: { money (row.balance)}</ div >
</ div >
);
}}
/>
</ Chart.WaterfallChart >
</ Chart.ResponsiveContainer >
</ Chart.Root >
);
}
WaterfallChart aliases BarChart. Entrance reveals ranges from numeric zero, not sequential arithmetic. Interaction finishes it; reduced motion skips it. Native per-series animation stays disabled.
Use value, start and balance in tooltip content; native range payloads contain endpoints. filterNull={false} permits unknown-step inspection. Range bars cannot use scalar category emphasis.
Focused props for ordered data and explicit Recharts composition; native axes, Cells and reference lines retain their geometry.
Returns WaterfallDatum[] with IDs, labels, kinds, original/computed values, endpoints and balances. range sorts endpoints or becomes null; use value for direction because sorting loses sign.
BarAnimation shares LineAnimation: default duration 1000, easing [0.25, 0.1, 0.25, 1], and supported inspection hoverTransition. See Recharts BarChart .
Binds range with zero minimum size; rejects data, dataKey, stackId and minPointSize. Native shapes/events remain available. See Recharts Bar and Cell .
Native ReferenceLine segments without pointer capture; x, y, segment and children are excluded. See Recharts ReferenceLine .
Root , SeriesConfig , Tooltip , Axes and grid .