Charts
Bar chart
Earnings by month as bars that grow and move instead of being redrawn, so each month can be followed when the period changes.
Live preview
Loading preview
Earnings for 6 months and for 1 year with a change chip. On load the bars grow in; Replay switches between 6M and 1Y and moves the hover column across a few bars.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add earnings-chartUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { type ChartPeriod, EarningsChart } from './earnings-chart';
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const LONG = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
// Monthly earnings from November 2024 to October 2026.
const VALUES = [
5460, 5980, 5120, 5390, 5870, 5640, 6020, 6310, 5950, 6480, 7040, 6720, 7280, 7400, 6600, 7100, 7310, 7300,
6210, 7140, 6820, 8320, 9090, 10740,
];
const bars = VALUES.map((value, i) => {
const month = (10 + i) % 12;
const year = 2024 + Math.floor((10 + i) / 12);
return {
key: `${year}-${String(month + 1).padStart(2, '0')}`,
label: MONTHS[month] as string,
title: `${LONG[month]} ${year}`,
value,
};
});
const PERIODS: ChartPeriod[] = [
{ id: '6m', label: '6M', bars: bars.slice(-6), previous: 42990, comparison: 'vs the 6 months before' },
{ id: '1y', label: '1Y', bars: bars.slice(-12), previous: 94820, comparison: 'vs the year before' },
{ id: 'all', label: 'All', bars, comparison: 'since November 2024' },
];
/** Moves a pretend pointer over the plot, so Replay can show the hover column. */
function pointAt(root: HTMLElement | null, fraction: number | null) {
const plot = root?.querySelector<HTMLElement>('[data-slot="earnings-chart-plot"]');
if (!plot) return;
const box = plot.getBoundingClientRect();
if (fraction === null) {
plot.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false }));
return;
}
plot.dispatchEvent(
new PointerEvent('pointermove', {
bubbles: true,
clientX: box.left + box.width * fraction,
clientY: box.top + 40,
}),
);
}
export default function EarningsChartDemo({ replay = 0 }: { replay?: number }) {
const [period, setPeriod] = React.useState('6m');
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (replay === 0) return;
const steps: [number, () => void][] = [
[0, () => setPeriod('1y')],
[1500, () => pointAt(ref.current, 4.5 / 12)],
[2100, () => pointAt(ref.current, 7.5 / 12)],
[2700, () => pointAt(ref.current, 9.5 / 12)],
[3400, () => pointAt(ref.current, null)],
[3800, () => setPeriod('6m')],
];
const timers = steps.map(([at, run]) => window.setTimeout(run, at));
return () => {
for (const id of timers) window.clearTimeout(id);
};
}, [replay]);
return (
<div ref={ref} className="w-full max-w-[640px]">
<EarningsChart periods={PERIODS} value={period} onValueChange={setPeriod} locales="en-US" />
</div>
);
}
API
Parts: header (title, total, change chip), period switch (segmented, thrown pill), plot (one tab stop), y axis labels, bars, x axis labels, hover column, readout (month and amount), screen reader table.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | "Earnings" | Chart title. |
| periods | { id: string; label: string; bars: { key: string; label: string; value: number; title?: string }[]; previous?: number; comparison?: string }[] | - | Data per period. Bars with the same key in two periods glide instead of popping. |
| value | string | undefined | Chosen period id (controlled). |
| defaultValue | string | first period | Period id at first when not controlled. |
| format | Intl.NumberFormatOptions | { style: "currency", currency: "USD", maximumFractionDigits: 0 } | Formatting of amounts. |
| locales | string | string[] | "en-US" | Locale for formatting. |
| highlight | string | last bar | Key of the bar drawn in the brand color. |
| Event | Description |
|---|---|
| valueChange | Another period was chosen. Payload: its id. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| BC1 | Chart first shows | bars | height | 0 → value | duration-5xl (640)ease-outdelay stagger-list (28) | Shows at once |
| BC2 | Chart first shows | total | number roll | $0 → $48,320 | duration-6xl (900)ease-out | Swaps at once |
| BC3 | Period changes | period pill | translate x | old option → new option | duration-3xl (460)ease-throw | Moves at once |
| BC4 | Period changes | period pill | width | old width → new width | duration-xl (380)ease-out | Resizes at once |
| BC5 | Period changes | bars in both periods | translate x, height | old place and height → new place and height | duration-5xl (640)ease-outdelay stagger-list (28) | Moves at once |
| BC6 | Period changes | bars that leave | width, height | value → 0, on their side | duration-5xl (640)ease-out | Hides at once |
| BC7 | Period changes | new bars | height | 0 → value | duration-5xl (640)ease-outdelay stagger-list (28) | Shows at once |
| BC8 | Period changes | y axis labels | number roll | old scale → new scale | duration-5xl (640)ease-out | Swaps at once |
| BC9 | Period changes | total | number roll | $48,320 → $91,310 | duration-6xl (900)ease-out | Swaps at once |
| BC10 | Change first shows | change chip | width (grid columns) | 0fr → 1fr | duration-2xl (420)ease-out | Shows at once |
| BC20 | Change first shows | change chip | opacity | 0 → 1 | duration-md (260)ease-out | Shows at once |
| BC11 | Change goes up or down | change chip | background color, text color | success → error | duration-lg (320)ease-out | Swaps at once |
| BC12 | Change goes up or down | chip arrow | rotation | 0deg → 180deg | duration-2xl (420)ease-throw | Swaps at once |
| BC13 | Pointer enters or leaves | hover column, readout | opacity | 0 → 1 | duration-sm (200)ease-out | Shows at once |
| BC14 | Pointer or keyboard moves to a bar | hover column | translate x | last bar → new bar | duration-xl (380)ease-throw | Moves at once |
| BC15 | Pointer or keyboard moves to a bar | hover column | width | last width → new width | duration-xl (380)ease-out | Resizes at once |
| BC16 | Pointer or keyboard moves to a bar | readout | translate x | last place → follows the column | duration-xl (380)ease-throw | Moves at once |
| BC17 | Pointer or keyboard moves to a bar | readout month | text morph | August 2026 → September 2026 | duration-3xl (460)ease-out | Swaps at once |
| BC18 | Pointer or keyboard moves to a bar | readout amount | number roll | $8,320 → $9,140 | duration-5xl (640)ease-out | Swaps at once |
| BC19 | Highlighted bar changes | bar | background color | utility-brand-200 → bg-brand-solid | duration-lg (320)ease-out | Swaps at once |
Accessibility
- The period switch is a role="radiogroup" named "Period"; Left and Right arrow keys change the period.
- The plot is one tab stop (role="group") whose name explains the arrow keys; Left, Right, Home and End move between bars.
- The readout of the focused bar is announced through a polite live region.
- Every figure is also in a visually hidden table, so the data does not depend on the drawing.
- The change chip shows a sign and an arrow, not only green or red.