Charts
Waterfall Flow
A waterfall: how the opening balance became the closing one. Inflows and outflows float on the running total, the totals stand on zero, and connectors hold the level.
- chart
- waterfall
- finance
- svg
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/chart-018?lang=en
- приход
- расход
- итог
Единица измерения: thousands
| Начало | 420 | накопленный итог 420 |
|---|---|---|
| Продажи | 260 | накопленный итог 680 |
| Продления | 140 | накопленный итог 820 |
| Возвраты | -90 | накопленный итог 730 |
| Расходы | -215 | накопленный итог 515 |
| Итог | 515 | накопленный итог 515 |
Show the full instructions
What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.
# Install and place "chart-018" (Waterfall Flow) from VibeUI
## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/chart-018.json
Registry item: https://vibeui.ru/r/chart-018.json
Installs to: components/vibeui/chart-018.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.
Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.
## 2. What it is
A waterfall: how the opening balance became the closing one. Inflows and outflows float on the running total, the totals stand on zero, and connectors hold the level.
A waterfall chart: total bars stand on zero, inflow and outflow steps float on the running total, dashed connectors join the levels, the sign of the value colours the bar, and the values are printed above the bars. The data is mirrored in a hidden table with the running total. Zero dependencies, one file, no client JS.
## 3. How to use it
import { Chart018 } from "@/components/vibeui/chart-018"
<Chart018
title="Cash movement this quarter"
unit="thousands"
steps={[{ label: "Opening", value: 420, total: true }, { label: "Sales", value: 260 }]}
/>
Read the installed file for the full prop list.
## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.
Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)
## 5. Keep exactly as installed
- the local --vibeui-chart-018-* palette — do not swap it for your theme tokens (bg-card, --chart-1 and the like)
- deriving the colour from the sign of the value: an outflow painted green will eventually mislead
- the dashed connectors between steps — without them the level between bars is lost
- total bars measured from zero: they anchor the whole construction, they are not another change
- the «+» sign on inflows: without it plus and minus differ by colour alone
- the hidden table with the running total and aria-hidden on the svg — the data must be readable as text
## 6. You may change
- the steps array: label, value and the total flag on the anchor bars
- title — the chart heading
- unit — the unit named in the caption under the chart
- the accent through the accent prop — it colours the total bars
- the legend of inflows, outflows and totals
## 7. Rules
- A negative running total is not drawn: the scale runs from zero upwards.
- The total flag must sit on the first and the last step — otherwise the construction floats.
- Beyond eight steps the labels under the bars start colliding.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.
## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.For developers
npx shadcn@latest add https://vibeui.ru/r/chart-018.jsonhttps://vibeui.ru/r/chart-018.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-chart-018-*. Серверный: хуков нет. Накопленная сумма считается заранее: каждому столбцу нужны и низ, и верх, поэтому один проход по шагам превращает значения в пары от-до. Столбцы с флагом total рисуются от нуля, остальные — от предыдущего уровня; цвет берётся из знака значения, а не из данных. Пунктирные перемычки соединяют вершину шага с началом следующего. Данные продублированы визуально скрытой таблицей с накопленным итогом.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Chart018Step = {
label: string
value: number
total?: boolean
}
export type Chart018Props = Omit<
ComponentPropsWithoutRef<"figure">,
"children" | "title"
> & {
title?: string
steps?: Chart018Step[]
unit?: string
accent?: string
}
// Идея компонента: waterfall — как из начального остатка получился конечный.
// Столбцы приходов и расходов висят в воздухе на накопленной сумме, итоговые
// стоят на нуле, а пунктирные перемычки не дают потерять уровень между
// шагами. Знак значения красит столбец, поэтому расход нельзя сделать зелёным.
const STYLES = `
:where([data-vibeui-block="chart-018"]){
--vibeui-chart-018-bg:oklch(1 0 0);
--vibeui-chart-018-fg:oklch(0.22 0.014 265);
--vibeui-chart-018-muted:oklch(0.55 0.014 265);
--vibeui-chart-018-border:oklch(0.91 0.006 265);
--vibeui-chart-018-grid:oklch(0.94 0.005 265);
--vibeui-chart-018-up:oklch(0.62 0.13 155);
--vibeui-chart-018-down:oklch(0.62 0.16 25);
--vibeui-chart-018-total:oklch(0.42 0.03 265);
--vibeui-chart-018-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="chart-018"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:32rem;box-sizing:border-box;margin:0;padding:0.875rem;
background:var(--vibeui-chart-018-bg);
border:1px solid var(--vibeui-chart-018-border);border-radius:0.875rem;
color:var(--vibeui-chart-018-fg);font-family:var(--vibeui-chart-018-font);
}
[data-vibeui-block="chart-018"] [data-part="title"]{margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="chart-018"] svg{display:block;width:100%;height:auto}
[data-vibeui-block="chart-018"] [data-part="grid"]{stroke:var(--vibeui-chart-018-grid);stroke-width:1}
[data-vibeui-block="chart-018"] [data-part="bar"][data-kind="up"]{fill:var(--vibeui-chart-018-up)}
[data-vibeui-block="chart-018"] [data-part="bar"][data-kind="down"]{fill:var(--vibeui-chart-018-down)}
[data-vibeui-block="chart-018"] [data-part="bar"][data-kind="total"]{fill:var(--vibeui-chart-018-total)}
[data-vibeui-block="chart-018"] [data-part="link"]{
stroke:var(--vibeui-chart-018-muted);stroke-width:1;stroke-dasharray:3 3;
}
[data-vibeui-block="chart-018"] [data-part="value"]{
fill:var(--vibeui-chart-018-fg);font-size:8.5px;font-weight:600;text-anchor:middle;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="chart-018"] [data-part="name"]{
fill:var(--vibeui-chart-018-muted);font-size:8px;text-anchor:middle;
}
[data-vibeui-block="chart-018"] [data-part="key"]{
display:flex;flex-wrap:wrap;gap:0.25rem 0.875rem;margin:0;padding:0;list-style:none;
font-size:0.6875rem;color:var(--vibeui-chart-018-muted);
}
[data-vibeui-block="chart-018"] [data-part="key"] li{display:flex;align-items:center;gap:0.375rem}
[data-vibeui-block="chart-018"] [data-part="chip"]{width:0.625rem;height:0.625rem;border-radius:2px}
[data-vibeui-block="chart-018"] [data-part="chip"][data-kind="up"]{background:var(--vibeui-chart-018-up)}
[data-vibeui-block="chart-018"] [data-part="chip"][data-kind="down"]{background:var(--vibeui-chart-018-down)}
[data-vibeui-block="chart-018"] [data-part="chip"][data-kind="total"]{background:var(--vibeui-chart-018-total)}
[data-vibeui-block="chart-018"] [data-part="unit"]{margin:0;font-size:0.75rem;color:var(--vibeui-chart-018-muted)}
[data-vibeui-block="chart-018"] [data-part="data"]{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
clip-path:inset(50%);white-space:nowrap;border:0;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="chart-018"] *{animation:none!important;transition:none!important}}
`
const LEFT = 8
const RIGHT = 332
const TOP = 24
const BASE = 126
const DEFAULT_STEPS: Chart018Step[] = [
{ label: "Начало", value: 420, total: true },
{ label: "Продажи", value: 260 },
{ label: "Продления", value: 140 },
{ label: "Возвраты", value: -90 },
{ label: "Расходы", value: -215 },
{ label: "Итог", value: 515, total: true },
]
/**
* Waterfall: приходы и расходы между двумя итогами.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Chart018({
title = "Движение денег за квартал",
steps = DEFAULT_STEPS,
unit = "тысяч рублей",
accent,
className,
style,
...props
}: Chart018Props) {
// Накопленная сумма считается заранее: каждому столбцу нужны и низ, и верх,
// а не только значение. Список собирается копированием, чтобы во время
// отрисовки не мутировался внешний счётчик.
const bars = steps.reduce<
{ step: Chart018Step; from: number; to: number; kind: string }[]
>((done, step) => {
const previous = done.length > 0 ? done[done.length - 1].to : 0
const from = step.total ? 0 : previous
const to = step.total ? step.value : previous + step.value
return [
...done,
{
step,
from,
to,
kind: step.total ? "total" : step.value >= 0 ? "up" : "down",
},
]
}, [])
const ceiling = Math.max(...bars.map((bar) => Math.max(bar.from, bar.to)), 1)
const band = (RIGHT - LEFT) / Math.max(bars.length, 1)
const width = Math.min(band * 0.58, 40)
const y = (value: number) => BASE - (value / ceiling) * (BASE - TOP)
const palette = {
...(accent ? { "--vibeui-chart-018-total": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-chart-018" precedence="medium">
{STYLES}
</style>
<figure
{...props}
data-vibeui-block="chart-018"
className={className}
style={palette}
>
<figcaption data-part="title">{title}</figcaption>
<svg viewBox="0 0 340 150" aria-hidden="true" focusable="false">
<line data-part="grid" x1={LEFT} y1={BASE} x2={RIGHT} y2={BASE} />
{bars.map((bar, index) => {
const x = LEFT + band * index + (band - width) / 2
const top = Math.min(y(bar.from), y(bar.to))
const height = Math.max(Math.abs(y(bar.from) - y(bar.to)), 2)
const next = bars[index + 1]
return (
<g key={bar.step.label}>
<rect
data-part="bar"
data-kind={bar.kind}
x={x}
y={top}
width={width}
height={height}
rx={2}
/>
<text data-part="value" x={x + width / 2} y={top - 5}>
{bar.step.total
? bar.step.value
: `${bar.step.value > 0 ? "+" : ""}${bar.step.value}`}
</text>
<text data-part="name" x={x + width / 2} y={BASE + 14}>
{bar.step.label}
</text>
{next && !next.step.total ? (
<line
data-part="link"
x1={x + width}
y1={y(bar.to)}
x2={x + band}
y2={y(bar.to)}
/>
) : null}
</g>
)
})}
</svg>
<ul data-part="key">
<li>
<span data-part="chip" data-kind="up" aria-hidden="true" />
приход
</li>
<li>
<span data-part="chip" data-kind="down" aria-hidden="true" />
расход
</li>
<li>
<span data-part="chip" data-kind="total" aria-hidden="true" />
итог
</li>
</ul>
<p data-part="unit">Единица измерения: {unit}</p>
<table data-part="data">
<caption>
{title}, {unit}
</caption>
<tbody>
{bars.map((bar) => (
<tr key={bar.step.label}>
<th scope="row">{bar.step.label}</th>
<td>{bar.step.value}</td>
<td>накопленный итог {bar.to}</td>
</tr>
))}
</tbody>
</table>
</figure>
</>
)
}