Lunia DS

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.

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-chart

Usage

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.

PropTypeDefaultDescription
titlestring"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.
valuestringundefinedChosen period id (controlled).
defaultValuestringfirst periodPeriod id at first when not controlled.
formatIntl.NumberFormatOptions{ style: "currency", currency: "USD", maximumFractionDigits: 0 }Formatting of amounts.
localesstring | string[]"en-US"Locale for formatting.
highlightstringlast barKey of the bar drawn in the brand color.
EventDescription
valueChangeAnother 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
BC1Chart first showsbarsheight0 → valueduration-5xl (640)ease-outdelay stagger-list (28)Shows at once
BC2Chart first showstotalnumber roll$0 → $48,320duration-6xl (900)ease-outSwaps at once
BC3Period changesperiod pilltranslate xold option → new optionduration-3xl (460)ease-throwMoves at once
BC4Period changesperiod pillwidthold width → new widthduration-xl (380)ease-outResizes at once
BC5Period changesbars in both periodstranslate x, heightold place and height → new place and heightduration-5xl (640)ease-outdelay stagger-list (28)Moves at once
BC6Period changesbars that leavewidth, heightvalue → 0, on their sideduration-5xl (640)ease-outHides at once
BC7Period changesnew barsheight0 → valueduration-5xl (640)ease-outdelay stagger-list (28)Shows at once
BC8Period changesy axis labelsnumber rollold scale → new scaleduration-5xl (640)ease-outSwaps at once
BC9Period changestotalnumber roll$48,320 → $91,310duration-6xl (900)ease-outSwaps at once
BC10Change first showschange chipwidth (grid columns)0fr → 1frduration-2xl (420)ease-outShows at once
BC20Change first showschange chipopacity0 → 1duration-md (260)ease-outShows at once
BC11Change goes up or downchange chipbackground color, text colorsuccess → errorduration-lg (320)ease-outSwaps at once
BC12Change goes up or downchip arrowrotation0deg → 180degduration-2xl (420)ease-throwSwaps at once
BC13Pointer enters or leaveshover column, readoutopacity0 → 1duration-sm (200)ease-outShows at once
BC14Pointer or keyboard moves to a barhover columntranslate xlast bar → new barduration-xl (380)ease-throwMoves at once
BC15Pointer or keyboard moves to a barhover columnwidthlast width → new widthduration-xl (380)ease-outResizes at once
BC16Pointer or keyboard moves to a barreadouttranslate xlast place → follows the columnduration-xl (380)ease-throwMoves at once
BC17Pointer or keyboard moves to a barreadout monthtext morphAugust 2026 → September 2026duration-3xl (460)ease-outSwaps at once
BC18Pointer or keyboard moves to a barreadout amountnumber roll$8,320 → $9,140duration-5xl (640)ease-outSwaps at once
BC19Highlighted bar changesbarbackground colorutility-brand-200 → bg-brand-solidduration-lg (320)ease-outSwaps 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.