BarChart
Amounts compared by length: a bar per category and series, side by side or stacked, upright or from the side.
Issues in cennge-ui
Opened and closed per month · sample data
- Opened
- Closed
Arrow keys move between the values.
| Month | Opened | Closed |
|---|---|---|
| May | 18 | 12 |
| Jun | 24 | 22 |
| Jul | 31 | 26 |
| Aug | 27 | 30 |
| Sep | 35 | 33 |
| Oct | 29 | 34 |
Open issues by repository
Stacked by label · sample data
- bug
- enhancement
- question
Arrow keys move between the values.
| Repository | bug | enhancement | question |
|---|---|---|---|
| cennge-ui | 3 | 8 | 2 |
| ai-niche-scout | 7 | 11 | 3 |
| payload-cache | 2 | 5 | 0 |
| spredo-crypto-screener | 6 | 9 | 2 |
| playwright-msi-scraper | 4 | 3 | 1 |
- Hover a month or tap it: its band lights up and the readout lists every series there.
- Tab to a chart: the arrows move between categories, Home and End jump to the ends, Escape hides the readout.
- Stacked parts follow the order of the series, with 2px of surface between them; only the outermost is rounded.
Field marks
- Import
import { BarChart } from "cennge-ui/charts";- Size
- 5.2 KB gzip for this import, its dependencies aside
- Pattern
- A
<figure>with a focusable group over the plot and a hidden<table>of the data - Keyboard
- Arrow left Arrow right Arrow up Arrow down HomeEndEsc
- SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Bars
- 24px thick at most (
barSize), rounded at the data end, square on the baseline - Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: bar-chart
- Source
- src/charts
When to use#
Use a BarChart to compare amounts across categories: runs per day, minutes per repository, issues per month. Several series stand side by side in each category, or pile up in one bar with isStacked when their sum matters as well as the parts.
With layout="horizontal" the bars grow from the side and the names get a column of their own, up to 40% of the chart's width, which suits repository names that would never fit under a column.
Similar#
- LineChart takes the same
data,indexandseriesand draws change over time as a line, with a crosshair on the nearest point. - DonutChart shows the parts of one whole, six of them at most; values that are close to each other read better as bars.
- Progress is one value against a maximum, not a comparison.
One series#
A single series has no legend: the aria-label names the chart, and names the table screen readers get. The numbers on this page are sample data.
Arrow keys move between the values.
| Day | Runs |
|---|---|
| Mon | 14 |
| Tue | 22 |
| Wed | 18 |
| Thu | 25 |
| Fri | 19 |
| Sat | 4 |
| Sun | 2 |
import { BarChart } from "cennge-ui/charts";
const runs = [
{ day: "Mon", runs: 14 },
{ day: "Tue", runs: 22 },
{ day: "Wed", runs: 18 },
{ day: "Thu", runs: 25 },
{ day: "Fri", runs: 19 },
{ day: "Sat", runs: 4 },
{ day: "Sun", runs: 2 },
];
export default function CiRunsPerDay() {
return (
<BarChart
aria-label="CI runs of cennge-ui per day"
data={runs}
index="day"
indexLabel="Day"
series={[{ key: "runs", label: "Runs" }]}
className="h-56"
/>
);
}Stacked, in colours that mean something#
isStacked piles the series into one bar in the order of series. A series takes the palette slot of its place unless you give it a color: a theme colour where the colour means something, as here with passed runs in success and failed ones in danger.
- Passed
- Failed
Arrow keys move between the values.
| Week of | Passed | Failed |
|---|---|---|
| Sep 14 | 58 | 6 |
| Sep 21 | 64 | 4 |
| Sep 28 | 71 | 9 |
| Oct 5 | 66 | 3 |
| Oct 12 | 80 | 5 |
| Oct 19 | 77 | 2 |
| Oct 26 | 85 | 4 |
| Nov 2 | 90 | 3 |
import { BarChart } from "cennge-ui/charts";
const weeks = [
{ week: "Sep 14", passed: 58, failed: 6 },
{ week: "Sep 21", passed: 64, failed: 4 },
{ week: "Sep 28", passed: 71, failed: 9 },
{ week: "Oct 5", passed: 66, failed: 3 },
{ week: "Oct 12", passed: 80, failed: 5 },
{ week: "Oct 19", passed: 77, failed: 2 },
{ week: "Oct 26", passed: 85, failed: 4 },
{ week: "Nov 2", passed: 90, failed: 3 },
];
export default function CiRunsByResult() {
return (
<BarChart
aria-label="CI runs per week by result"
data={weeks}
index="week"
indexLabel="Week of"
isStacked
series={[
{ key: "passed", label: "Passed", color: "success" },
{ key: "failed", label: "Failed", color: "danger" },
]}
className="h-64"
/>
);
}Long names, from the side#
layout="horizontal" turns the chart on its side. A name longer than its column is cut short with an ellipsis on the axis and stays whole in the readout and the table. valueFormat writes the values in the readout and the table; the axis keeps its short numbers.
Arrow keys move between the values.
| Repository | Minutes |
|---|---|
| cennge-ui | 1610 min |
| playwright-msi-scraper | 390 min |
| payload-cache | 350 min |
| ai-niche-scout | 280 min |
| spredo-crypto-screener | 210 min |
import { BarChart } from "cennge-ui/charts";
// Sorted, so the longest bar leads.
const minutes = [
{ repository: "cennge-ui", minutes: 1610 },
{ repository: "playwright-msi-scraper", minutes: 390 },
{ repository: "payload-cache", minutes: 350 },
{ repository: "ai-niche-scout", minutes: 280 },
{ repository: "spredo-crypto-screener", minutes: 210 },
];
export default function CiMinutesByRepository() {
return (
<BarChart
aria-label="CI minutes in October by repository"
data={minutes}
index="repository"
indexLabel="Repository"
layout="horizontal"
series={[{ key: "minutes", label: "Minutes" }]}
valueFormat={(value) => `${value} min`}
className="h-56"
/>
);
}Below zero#
A negative value hangs below the zero line, rounded at its lower end, so gains and losses read from one baseline: a week below zero closed more issues than it opened. domain sets the ends of the axis, here even on both sides of zero, in place of round ones around the data.
Arrow keys move between the values.
| Week of | Open issues |
|---|---|
| Sep 14 | +5 |
| Sep 21 | -3 |
| Sep 28 | +2 |
| Oct 5 | -8 |
| Oct 12 | -4 |
| Oct 19 | +1 |
| Oct 26 | -6 |
| Nov 2 | -2 |
import { BarChart } from "cennge-ui/charts";
const weeks = [
{ week: "Sep 14", change: 5 },
{ week: "Sep 21", change: -3 },
{ week: "Sep 28", change: 2 },
{ week: "Oct 5", change: -8 },
{ week: "Oct 12", change: -4 },
{ week: "Oct 19", change: 1 },
{ week: "Oct 26", change: -6 },
{ week: "Nov 2", change: -2 },
];
const signed = new Intl.NumberFormat("en-US", { signDisplay: "exceptZero" });
export default function OpenIssuesChange() {
return (
<BarChart
aria-label="Change in open issues per week"
data={weeks}
index="week"
indexLabel="Week of"
series={[{ key: "change", label: "Open issues" }]}
domain={[-10, 10]}
valueFormat={(value) => signed.format(value)}
className="h-56"
/>
);
}Nothing to show#
With an empty data the chart keeps its height and shows emptyContent where the plot would be, or labels.empty ("No data") without it. There is no axis to read and nothing to focus. Switch to last week and the bars grow in.
Arrow keys move between the values.
| Day | Deploys |
|---|
import { useState } from "react";
import { ToggleGroup, ToggleGroupItem } from "cennge-ui";
import { BarChart } from "cennge-ui/charts";
type Deploys = { day: string; deploys: number }[];
const lastWeek: Deploys = [
{ day: "Mon", deploys: 3 },
{ day: "Tue", deploys: 1 },
{ day: "Wed", deploys: 4 },
{ day: "Thu", deploys: 2 },
{ day: "Fri", deploys: 5 },
];
const thisWeek: Deploys = [];
export default function DeploysPerDay() {
const [week, setWeek] = useState("this");
return (
<div className="flex w-full flex-col gap-4">
<ToggleGroup aria-label="Week" size="sm" value={week} onValueChange={(value) => value && setWeek(value)}>
<ToggleGroupItem value="last">Last week</ToggleGroupItem>
<ToggleGroupItem value="this">This week</ToggleGroupItem>
</ToggleGroup>
<BarChart
aria-label="Deploys of payload-cache per day"
data={week === "last" ? lastWeek : thisWeek}
index="day"
indexLabel="Day"
series={[{ key: "deploys", label: "Deploys" }]}
emptyContent="No deploys yet this week"
className="h-48"
/>
</div>
);
}Accessibility#
- The figure and the plot inside it are named by
aria-label. The plot is a focusable group described bylabels.keyboardHint, "Arrow keys move between the values." - Focus from the keyboard shows the first category at once. Each move is read out by a polite live region, as "Jun: Opened 24, Closed 22".
- The readout is drawn for the eye only. Screen readers get every value in a table captioned with the chart's name,
indexLabelheading its first column, a missing value written "–" and never as zero. - Colour never names a series alone: the legend puts each name beside a sample of its bar.
Server rendering#
The legend, the axes, the grid and the data table come from the server; the axes and the grid are placed in percent, so they stay where the server put them. The bars wait until the browser has measured the plot, then grow out of the baseline; with reduced motion they appear at once.
Props#
Each entry of series is { key, label, color }: key is the field that holds its values, label its name in the legend, the readout and the table (the key without one), and color a palette slot from chart-1 to chart-8, a theme colour or any CSS colour.