Skip to content
cennge-ui
GitHubv0.3.1, changelog

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.

Issues opened and closed in cennge-ui per month
MonthOpenedClosed
May1812
Jun2422
Jul3126
Aug2730
Sep3533
Oct2934

Open issues by repository

Stacked by label · sample data

  • bug
  • enhancement
  • question

Arrow keys move between the values.

Open issues by repository and label
Repositorybugenhancementquestion
cennge-ui382
ai-niche-scout7113
payload-cache250
spredo-crypto-screener692
playwright-msi-scraper431
  1. Hover a month or tap it: its band lights up and the readout lists every series there.
  2. Tab to a chart: the arrows move between categories, Home and End jump to the ends, Escape hides the readout.
  3. Stacked parts follow the order of the series, with 2px of surface between them; only the outermost is rounded.
Live: hover it, tap it, use the keys.

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

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, index and series and 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.

CI runs of cennge-ui per day
DayRuns
Mon14
Tue22
Wed18
Thu25
Fri19
Sat4
Sun2
basic.tsx
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.

CI runs per week by result
Week ofPassedFailed
Sep 14586
Sep 21644
Sep 28719
Oct 5663
Oct 12805
Oct 19772
Oct 26854
Nov 2903
stacked.tsx
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.

CI minutes in October by repository
RepositoryMinutes
cennge-ui1610 min
playwright-msi-scraper390 min
payload-cache350 min
ai-niche-scout280 min
spredo-crypto-screener210 min
horizontal.tsx
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.

Change in open issues per week
Week ofOpen 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
negative.tsx
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.

No deploys yet this week

Arrow keys move between the values.

Deploys of payload-cache per day
DayDeploys
empty.tsx
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 by labels.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, indexLabel heading 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.

PropTypeDefault
layout
"horizontal""vertical""vertical": columns rising from the bottom; "horizontal": bars from the side, for long names.
"vertical"
isStacked
booleanThe series in one bar, piled up, instead of side by side.
–
barSize
numberThe thickest a bar gets, in pixels: the rest of its band stays open.
24
datarequired
readonly T[]One item per category: a month, a repository…
–
indexrequired
Key<T>The field that names each category, on the axis, in the tooltip and in the table.
–
seriesrequired
readonly ChartSeries<T>[]
–
aria-labelrequired
stringNames the chart for screen readers, and the table that gives them its data.
–
indexLabel
stringWhat the categories are, heading the table's first column, e.g. "Month".
index
indexFormat
(value: T[Key<T>], item: T) => string
–
valueFormat
(value: number) => stringValues in the tooltip and the table.
numbers in the provider's locale
axisFormat
(value: number) => stringValues on the axis.
short numbers in the provider's locale (1.2K)
domain
readonly [number, number]The value axis' ends, instead of round ones around the data.
–
showLegend
boolean
true with two or more series
showGrid
boolean
true
showXAxis
booleanThe axis along the bottom.
true
showYAxis
booleanThe axis up the side.
true
labels
Partial<ChartLabels>
–
emptyContent
React.ReactNodeShown in place of the plot when there is no data.
labels.empty
className
stringSize it here: the default height, h-72, includes the axes and the legend.
–