Balance Chart
A diverging bar chart React component centred on a zero axis: income grows up and expenses grow down, a net line draws in once the bars land, and hover or keyboard focus shows the period's income, expenses and net.
Plot each period's income and expenses on either side of one zero axis: income bars grow up, expense bars grow down, so the periods that ran at a loss stand out at a glance. On entry the bars grow out of the axis one after another, then the net line draws in. Hover any column, or focus the chart and use the arrow keys, and that column gets a light band while the others fade back; the tooltip lists income, expenses and a net figure that tweens from its previous value. It defaults to a dark panel, and surface="auto" follows the site's light/dark mode instead. Pure hand-drawn SVG, no chart library.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add balance-chart.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<BalanceChart/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/balance-chart.en.jsonOr, once registries are configured in components.json, install it as @webberui/balance-chart.
Usage
import { BalanceChart } from "@/components/ui/balance-chart";
const data = [
{ label: "1月", income: 168000, expense: 142500 },
{ label: "2月", income: 121000, expense: 133800 },
{ label: "3月", income: 152400, expense: 139600 },
];
<BalanceChart
title="本季收支"
data={data}
currency="NT$"
onActiveIndexChange={(i) => console.log(i === null ? "清除" : data[i].label)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | BalanceDatum[] | — | Income and expenses per period, in time order from left to right |
height | number | 280 | Plot height (px); width always fills the container |
surface | "dark" | "auto" | "dark" | dark is always a dark panel; auto is white and follows the site's dark: mode |
showNet | boolean | true | Show the net dots and line |
negativeTone | "rose" | "neutral" | "rose" | Expense bar tone: rose or neutral grey |
currency | string | "NT$" | Currency prefix before amounts; ignored when formatValue is provided |
formatValue | (value: number) => string | — | Amount formatter, called with the signed value; defaults to currency plus a grouped integer |
activeIndex | number | null | — | Controlled active period index; managed internally when omitted |
defaultActiveIndex | number | null | null | Initial active period in uncontrolled mode |
onActiveIndexChange | (index: number | null) => void | — | Called when hover, touch or the arrow keys change the period; null when cleared with Esc |
barGap | number | 0.28 | Fraction of each period column left empty (0–0.9) |
title | string | "Balance" (Chinese build: "收支平衡") | Header title text |
ariaLabel | string | "Income and expense balance chart" (Chinese build: "收支平衡長條圖") | Accessible name of the chart, also the sr-only table caption |
className | string | — | Passed through to the outermost container |
BalanceDatum
| Field | Type | Default | Description |
|---|---|---|---|
label | string | — | Period label, shown on the X axis and in the tooltip |
income | number | — | Income amount; negative values are treated as 0 |
expense | number | — | Expense amount, passed as a positive number and drawn below the zero axis; negative values are treated as 0 |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-balance-chart-positive | #2dd4bf (#0d9488 on auto light) | Income bar colour |
--wb-balance-chart-negative | #fb7185 (#f43f5e on auto light) | Expense bar colour with negativeTone="rose", also used for negative net figures |
--wb-balance-chart-accent | #2dd4bf (#0d9488 on auto light) | Colour of positive net figures |
--wb-balance-chart-duration | 700ms | Growth duration of each bar |
--wb-balance-chart-stagger | 50ms | Delay between neighbouring periods starting to grow |
How it works
- Symmetric domain: the largest income or expense across all periods is rounded up to a multiple of 1, 2, 2.5, 5 or 10, with two grid segments on each side; tick values use the compact notation of
Intl.NumberFormat(for example "200K"). - Bars grow out of the zero axis with
scaleY(income from its bottom edge, expenses from their top edge) oncubic-bezier(0.22,1,0.36,1); the net line draws in over 600ms viapathLengthonce every bar has landed, and the net dots appear along it in order. - The entrance fires once through
useInViewwhen the chart enters the viewport (margin-10% 0px); width is measured withResizeObserver, using a 560px fallback during SSR and snapping to the container after mount. - The active column's band and the tooltip slide between columns on a spring (stiffness 400, damping 32); the tooltip sits on the far side of the column and is clamped inside the container, so it never covers the bars or overflows.
- When columns are too narrow for every X label, labels are thinned out automatically; the active period is always labelled and bolded.
Accessibility
- The plot is a focusable
role="group": ← → switch periods, Home / End jump to the ends, Esc clears; a focus-visible ring shows while focused - The
svgisrole="img"with anaria-labelsummarising the period count, total net and the highest / lowest periods - An sr-only
<table>carries the data (period, income, expenses, net) and is linked to the plot througharia-describedby - Switching periods announces that period's income, expenses and net through an sr-only
aria-live="polite"region; the visual tooltip is hidden from assistive tech to avoid duplication - When the user's system has reduce motion turned on, bar growth and the line draw jump straight to their end state, the band and tooltip stop sliding and the net figure no longer tweens; hover and keyboard readouts still work
Diagram Kit
Eleven data-driven SVG diagrams — hub routing, sequence lanes, cycle, timeline, radial branches, funnel, tier pyramid, anchored callouts, branch reveal, quadrant and Venn — each drawn in with stroke tracing and staggered nodes.
Event Bar Chart
A Gantt-like event bar chart React component with timed segments on swimlanes, panned by drag or arrow keys, an event card that expands on hover or focus, a self-contained SVG time axis and a screen reader data table.