Chart
Three plain SVG shapes, each drawn from numbers the page already shows: a line over the period, an ordered split as one bar, and a few months as bars. A chart never disagrees with the figure above it.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { LineChart } from '@/ui/chart';<LineChart name="CVs sent by day" />Line chart
One metric over the period's buckets. This period is the 2px brand line, the previous one a 1px dashed grey line on the same scale. Only the first and last bucket are labelled. Hover a bucket: it tints and the tooltip gives its value and the previous one.
import { LineChart } from '@/ui/chart';
const weeks = ['7 Jul', '14 Jul', '21 Jul', '28 Jul', '4 Aug', '11 Aug', '18 Aug', '25 Aug', '1 Sep', '8 Sep', '15 Sep', '29 Sep'];
const series = (values: number[]) => values.map((value, i) => ({ label: weeks[i]!, value, text: String(value) }));
export default function ChartLine() {
return (
<LineChart
name="CVs sent by week"
current={series([4, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16])}
previous={series([3, 5, 6, 5, 7, 6, 8, 7, 9, 8, 10, 9])}
currentLabel="This quarter"
previousLabel="Previous quarter"
className="w-full max-w-130"
/>
);
}Stage bar
An ordered split as one bar, one hue from light to dark in stage order. The legend carries every name and count, so nothing is read from colour alone.
import { StageBar } from '@/ui/chart';
export default function ChartStageBar() {
return (
<StageBar
name="Applications by stage"
segments={[
{ label: '1st round', count: 6 },
{ label: '2nd round', count: 4 },
{ label: 'Final', count: 3 },
{ label: 'Test', count: 1 },
{ label: 'Offer', count: 2 },
]}
className="w-full max-w-xl"
/>
);
}Month bars
A few months side by side, the current one in brand green and the rest a light step of it. Every bar's value is its tooltip.
import { MonthBars } from '@/ui/chart';
export default function ChartMonthBars() {
return (
<MonthBars
name="Invoiced fee by month"
months={[
{ label: 'May', value: 28000, text: '£28,000' },
{ label: 'Jun', value: 36700, text: '£36,700' },
{ label: 'Jul', value: 25300, text: '£25,300' },
{ label: 'Aug', value: 40700, text: '£40,700' },
{ label: 'Sep', value: 32000, text: '£32,000' },
{ label: 'Oct', value: 48000, text: '£48,000', current: true },
]}
className="w-full max-w-xl"
/>
);
}Empty
A period with nothing in it says so in one line at the chart's height. No axis over flat zero lines.
Nothing invoiced in these months
import { MonthBars } from '@/ui/chart';
export default function ChartEmpty() {
return (
<MonthBars
name="Invoiced fee by month"
empty="Nothing invoiced in these months"
months={['May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'].map((label) => ({ label, value: 0, text: '£0' }))}
className="w-full max-w-xl"
/>
);
}Loading
loading holds the chart's box so nothing jumps when the numbers land. Press Replay.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { LineChart } from '@/ui/chart';
const days = ['1 Oct', '3 Oct', '5 Oct', '7 Oct', '9 Oct', '11 Oct', '13 Oct', '15 Oct'];
const series = (values: number[]) => values.map((value, i) => ({ label: days[i]!, value, text: String(value) }));
// Press Replay: the box holds the chart's height while the numbers load, so nothing jumps when they land.
export default function ChartLoading() {
const [loading, setLoading] = useState(true);
const replay = () => {
setLoading(true);
setTimeout(() => setLoading(false), 1500);
};
return (
<div className="flex w-full max-w-130 flex-col items-start gap-200">
<LineChart name="CVs sent by day" current={series([2, 4, 3, 6, 5, 8, 7, 10])} previous={series([3, 2, 4, 3, 5, 4, 6, 5])} loading={loading} className="w-full" />
<Button size="sm" onClick={replay}>
Replay
</Button>
</div>
);
}Edge cases
One bucket draws a flat line. A value that isn't a number draws as zero and its tooltip keeps the page's words, never NaN. A long stage name wraps in the legend and a large count keeps every digit.
import { LineChart, StageBar } from '@/ui/chart';
// One bucket draws a flat line; a value that isn't a number draws as zero, never NaN; a long stage name wraps in the legend.
export default function ChartEdgeCases() {
return (
<div className="grid w-full grid-cols-1 gap-400 md:grid-cols-2">
<LineChart name="CVs sent today" current={[{ label: '15 Oct', value: 6, text: '6' }]} />
<LineChart
name="CVs sent by day"
current={[
{ label: '13 Oct', value: 3, text: '3' },
{ label: '14 Oct', value: Number.NaN, text: 'Not counted' },
{ label: '15 Oct', value: 5, text: '5' },
]}
/>
<StageBar
name="Applications by stage"
segments={[
{ label: 'Second round with the hiring manager', count: 1234 },
{ label: 'Offer', count: 2 },
]}
className="md:col-span-2"
/>
</div>
);
}