GrChartWaterfall
Overview
When to take it
- movement from balance to balance — subscriptions, liabilities, a warehouse: not only every change is visible but also how the end of the month came out of its beginning;
- checking whether the balance adds up —
kind: 'total'steps put the real values at the start and at the end, and a discrepancy with the sum of the movements becomes visible to the eye rather than in one’s head; - breaking down the delta between two numbers — “revenue grew by 12 %” and the four reasons for that growth read as one story;
- increases and decreases mixed together — the colour follows the sign, and the direction of every step is visible before the labels are read;
- there are few steps — up to fifteen or twenty; beyond that the labels of the axis run into one another, and the bridge is worth folding down to large items.
When to take something else
| Need | Take |
|---|---|
| Compare quantities across categories, with no succession | GrChartBar |
| Show the composition of one whole at one moment | GrChartPie |
| Show the course of a quantity over time | GrChartLine |
| Show where users are lost between stages | GrChartFunnel |
| Show one number with a trend | GrStatistic |
Examples 4 each with a live demo and its code
How September's MRR was made
The opening MRR is a
total, drawn from zero; new business, expansion, contraction and churn accumulate on it, green up and red down.show-totalcloses the bridge with a named column. Thetooltipslot gets each step with the running total before and after it, andstep-clicklists the accounts behind a movement.MRR bridge · SeptemberFrom €184.0k to €205.6k · click a step to see its accounts Chart data Step Change Running total before Running total after Start of Sep 184.0 0.0 184.0 New 22.4 184.0 206.4 Expansion 9.1 206.4 215.5 Reactivation 1.1 215.5 216.6 Contraction -3.2 216.6 213.4 Churn -7.8 213.4 205.6 End of Sep 205.6 205.6 205.6 Click a movement to list the accounts behind it.A stock check that does not add up
Both ends are real numbers — the opening balance and the physical count — so both are
totalsteps, and when the movements do not add up the last bar stops short of where they end: 32 lamps are missing, and the picture says so. A zero step is drawn as a mark, andorientation="horizontal"gives each label its own line.Desk lamp, black · stock check32 units are not accounted for Chart data Step Change Running total before Running total after Opening stock 1,200 0 1,200 Received 480 1,200 1,680 Shipped -910 1,680 770 Returns 36 770 806 Written off -14 806 792 Transfers 0 792 792 Physical count 760 792 760 Where the growth came from
«+€142k» and four reasons for it read as one story. Next to the €1.2m of Q2 a €19k currency effect would be a hairline, and an axis cut above zero would lie about the sizes — so the bridge explains the change itself, from zero, and
show-totalnames where it lands.Revenue Q2 → Q3: +€142kVolume did most of it; mix and the dollar took some back Chart data Step Change Running total before Running total after Price 64 0 64 Volume 118 64 182 Mix -21 182 161 FX -19 161 142 Change 142 142 142 An income statement with its subtotals
Gross and operating profit are lines of the report, not deltas:
kind: "total"draws them from zero and restarts the accumulation on them, so the reader never adds columns up to find a number that already has a name.show-connectorsjoins the movements and stops at every total.Income statement · Q3 2026Thousands of eurosNet margin 15.7%Chart data Step Change Running total before Running total after Revenue 1,240 0 1,240 Cost of sales -410 1,240 830 Gross profit 830 830 830 Salaries -390 830 440 Ads -120 440 320 Rent -60 320 260 Operating profit 260 260 260 Tax -65 260 195 Net profit 195 195 195
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
height | number | undefined | undefined | The canvas height in pixels; 256 by default. The data, the loading state and the empty state all take up the same height. |
width | number | undefined | 640 | Declared width: the first render starts from it, after that the width is measured. |
yDomain | readonly [number | null, number | null] | undefined | undefined | The bounds of the value axis `[min, max]`; `null` in place of a bound — it is taken from the data. |
yTickCount | number | undefined | 5 | The desired number of ticks on the value axis; 5 by default. The step stays "round", so there may be one tick fewer. |
yTickFormat | ((value: number) => string) | undefined | undefined | The tick label of the value axis, for example `value => value + '%'`. The space for the axis is measured from the formatted label. |
valueFormat | GrChartNumberFormat | undefined | undefined | The value format in the tooltip, the hidden table and announcements: decimal places and separators. |
showGrid | "y" | "both" | "x" | "none" | undefined | undefined | Which grid is needed. The axes are named after the data: in the horizontal layout they swap places by themselves. |
tooltip | boolean | undefined | undefined | The panel with the values under the cursor; `true` by default. `false` hides only the panel: the cursor, the active marks, `update:activeIndex` and `pointHover` keep working while the chart is interactive. |
activeIndexv-model | number | null | undefined | undefined | The cursor — `v-model:activeIndex`. |
loading | boolean | undefined | false | Loading: a skeleton of the same height stands in place of the plot area, and the space for the axes is already reserved. |
empty | boolean | undefined | undefined | The empty state, set explicitly. Unset — the chart is empty when there is nothing to draw. |
emptyText | string | undefined | undefined | The placeholder text of an empty chart. Unset — "No data" from the package translations. |
dataTable | "hidden" | "visible" | "off" | undefined | undefined | The data table: `hidden` (the default) — for the screen reader only, `visible` — under the chart, `off` — no table. |
interactive | boolean | undefined | true | `false` — the chart becomes a picture: no focus, no tooltip, no keyboard. |
size | "md" | "xs" | "sm" | "lg" | undefined | undefined | The type size of the axis and legend labels on the core control scale; by default — the size from `GrConfigProvider`. |
locale | string | undefined | undefined | The BCP-47 locale for numbers and dates. Unset — the language of the application translations, otherwise `en`. |
ariaLabel | string | undefined | undefined | The accessible name of the chart. Unset — a summary the component assembles: the chart type and the amount of data. |
ariaDescription | string | undefined | undefined | A description for the screen reader — the conclusion the chart is shown for; it adds to the summary rather than replacing it. |
dataTableMaxRows | number | "auto" | undefined | undefined | The row cap of the hidden data table. `'auto'` (the default) — as many rows as can be read; above the cap the table takes an even sample and says so with a note, while point-by-point completeness is left to the keyboard. A number sets its own cap, `Infinity` lifts it entirely, `dataTable: 'off'` removes the table altogether — which of these is needed is up to the application. |
orientation | "vertical" | "horizontal" | undefined | undefined | The direction: `vertical` (the default) or `horizontal` — long step names read as a single line. |
barRadius | number | undefined | undefined | The corner rounding of the bars in pixels. |
baseline | number | undefined | 0 | The starting accumulation. |
showTotal | string | boolean | undefined | false | Draw a total bar on the right. A string sets its label. |
showConnectors | boolean | undefined | undefined | Connectors between the bars — they show that the next step starts where the previous one ended; `true` by default. |
stepsrequired | readonly GrChartWaterfallStep[] | — | The steps of the bridge: `{ label, value, kind?, color? }`. `kind: 'total'` is an absolute value: the bar starts from zero, and the running total is reset to it. |
Slots
| Slot | Type | Description |
|---|---|---|
tooltip |
| Custom content for the tooltip panel instead of the standard one; the data of the active position comes in the slot props. |
empty | any | A custom placeholder for an empty chart instead of "No data"; it takes up the same `height`. |
header | any | A header above the chart, inside its root — above a legend placed on top. |
Events
| Event | Signature | Description |
|---|---|---|
update:activeIndex | (event: "update:activeIndex", value: number | null): void | — |
stepClick | (event: "stepClick", value: GrChartWaterfallActiveStep): void | — |
stepHover | (event: "stepHover", value: GrChartWaterfallActiveStep | null): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
element | HTMLElement | null | The root element — for measuring and scrolling in the consumer. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrChartWaterfallConfigurableProps, GrChartSize, GrChartWaterfallActiveStep } from '@feugene/granularity-charts/components/GrChartWaterfall'Filedist/types/components/GrChartWaterfall/config.d.ts
const grChartWaterfallConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChartWaterfall">;Filedist/types/components/GrChartWaterfall/defaults.d.ts
interface GrChartWaterfallConfigurableProps {
size: GrChartSize;
height: number;
barRadius: number;
showConnectors: boolean;
orientation: 'vertical' | 'horizontal';
showGrid: 'both' | 'x' | 'y' | 'none';
tooltip: boolean;
dataTable: 'hidden' | 'visible' | 'off';
dataTableMaxRows: number | 'auto';
}Filedist/types/components/GrChartFrame/chartFrameStyles.d.ts
type GrChartSize = GrComponentSize;Filedist/types/components/GrChartWaterfall/safelist.d.ts
const grChartWaterfallSafelist: string[];Filedist/types/components/GrChartWaterfall/GrChartWaterfall.vue.d.ts
interface GrChartWaterfallActiveStep {
index: number;
label: string;
value: number;
before: number;
after: number;
kind: 'delta' | 'total';
}interface GrChartWaterfallProps {
steps: readonly GrChartWaterfallStep[];
baseline?: number;
showTotal?: boolean | string;
showConnectors?: boolean;
barRadius?: number;
orientation?: 'vertical' | 'horizontal';
height?: number;
width?: number;
yDomain?: readonly [number | null, number | null];
yTickCount?: number;
yTickFormat?: (value: number) => string;
valueFormat?: GrChartNumberFormat;
showGrid?: 'both' | 'x' | 'y' | 'none';
tooltip?: boolean;
activeIndex?: number | null;
loading?: boolean;
empty?: boolean;
emptyText?: string;
dataTable?: 'hidden' | 'visible' | 'off';
interactive?: boolean;
size?: GrChartSize;
locale?: string;
ariaLabel?: string;
ariaDescription?: string;
dataTableMaxRows?: number | 'auto';
}
export interface GrChartWaterfallEmits {
(e: 'update:activeIndex', value: number | null): void;
(e: 'stepClick', value: GrChartWaterfallActiveStep): void;
(e: 'stepHover', value: GrChartWaterfallActiveStep | null): void;
}
type __VLS_Slots = {
tooltip?: (props: {
active: GrChartActivePoint;
step: GrChartWaterfallActiveStep | null;
formatValue: (value: number | null) => string;interface GrChartWaterfallEmits {
(e: 'update:activeIndex', value: number | null): void;
(e: 'stepClick', value: GrChartWaterfallActiveStep): void;
(e: 'stepHover', value: GrChartWaterfallActiveStep | null): void;
}Accessibility
APG pattern
При orientation: 'horizontal' шаги идут сверху вниз, и по ним ходят ↓/↑
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
A total step declares the accumulation rather than adding to it
kind: 'delta' (the default) is a change from the current accumulation, and kind: 'total' an
absolute value: the column is drawn from zero, and the accumulation is reset to it. That is what
makes it possible to put “At the start” and “At the end” as real numbers from the backend: if the
bridge does not add up, the last column will not match the top of the one before it, and the error in
the data will be visible.
A connector to such a step is not drawn. It does not continue the accumulation but declares it, and a “from here to here” line would lie about the succession.
The colour follows the sign rather than the index of the series
A bridge is a single series, and the palette of series distinguishes nothing here: what has to be
distinguished is an increase (--gr-success) from a decrease (--gr-danger). The total and the
declared accumulation are coloured neutrally (--gr-chart-1) — that is not a movement. An explicit
color on a step is stronger than any of those rules.
A zero step is drawn as a rule
“There was no movement” is a fact rather than an absence of data. A column of zero height would
disappear from the drawing, and the reader would see a story in which such a step does not exist at
all. Instead a rule of --gr-chart-waterfall-zero-step thickness stands at the level of the
accumulation.
The table carries the accumulation rather than a single delta
The hidden table gives three numbers per step: the change, the accumulation before and the accumulation after. From a single delta the bridge cannot be reconstructed — the height of a column is set precisely by the accumulation, and a reader without sight is obliged to see the same as a sighted one sees from the position of the band.
The category of a step is its index rather than its label
Two “Adjustments” in a row in a bridge are an ordinary matter, and normalisation collapses identical categories inside a series. The index of the step therefore goes inside the frame, and the real labels arrive on the axis separately: otherwise there would be fewer positions than steps, and the cursor and the keyboard would travel over the wrong data.
The value axis follows the accumulation
The domain of the axis is computed from the bases and tops of the bars together with zero, rather
than from the values of the steps: a delta of −410 on an accumulation of 1240 does not pull the axis
below zero, where not a single bar stands. yDomain takes precedence: if its bottom is above zero, a
bar that starts from zero is cut off at the edge of the area — the cut itself says that the axis does
not start at zero — and connectors and zero steps outside the domain are not drawn.
Every step is named
Step labels are not thinned out the way bar categories are: a bridge cannot be read without the
name of a step. A label that is short of room wraps by words onto two lines, and beyond that ends
with an ellipsis, with the full name in <title>; neighbouring labels do not run into each other.
If even after wrapping some label is left with fewer than four characters — “Pr…” instead of
“Price” on a narrow canvas — the labels are staggered: neighbours go into different rows, and each
gets the width of two steps. A truncated label is never shorter than four characters.
In the horizontal layout the names stand in a column on the left, up to 40% of the width, the value axis leaves room for the last tick label, and those of its labels that are short of room are thinned out to every other one — the first and the last stay.
Limits
The bridge does not compute what its steps consist of — they are given by the consumer. It does not group small steps into “Other” either: only the caller knows what counts as small. There are no nested (two-level) bridges.
Props in this example