Skip to content
cennge-ui
GitHubv0.3.1, changelog

Charts overview

Five charts in the theme's tokens, drawn in SVG by cennge-ui itself: bars, lines, areas, a donut and a sparkline.

CI minutes in October

2,840

up from 1,700 in March

cennge-ui in October

1,610

57% of all the minutes

CI minutes by repository

Per month, stacked · sample data

  • cennge-ui
  • playwright-msi-scraper
  • payload-cache
  • ai-niche-scout
  • spredo-crypto-screener

Arrow keys move between the values.

CI minutes per month by repository
Monthcennge-uiplaywright-msi-scraperpayload-cacheai-niche-scoutspredo-crypto-screener
Mar820310240180150
Apr910290260210140
May1,040340230260170
Jun1,180360280240190
Jul1,320330310220160
Aug1,250380290270180
Sep1,460410330300200
Oct1,610390350280210
  1. Hover a month, tap it, or Tab to the chart and use the arrows: the readout lists every series at that place.
  2. Series take the palette in order and the first slot is the accent: switch it at the top of the page.
  3. The cennge-ui tile gives its sparkline chart-1, the colour of the cennge-ui bars; the other keeps the default grey with the accent on its last value.
Live: hover it, tap it, use the keys. Sample data.

Field marks

Pattern
A named <figure> with a hidden <table> of its data; a Sparkline is role="img"
Keyboard
Arrow left Arrow right Arrow up Arrow down HomeEndEsc
Size
8.4 KB gzip for the whole entry; each chart's page gives its own
Underneath
No chart library: SVG and HTML from cennge-ui, ESM only
Tests
Unit tests

When to use#

cennge-ui/charts is a second entry of the same package, for the charts a product page or a dashboard needs. Pick the chart by the job the data does:

  • Amounts compared across categories: BarChart, side by side, stacked, or from the side for long names.
  • Change over time, a line per series: LineChart.
  • One quantity over time, or a whole split into parts over time: AreaChart.
  • The parts of one whole, six at most: DonutChart.
  • A trend beside the number it belongs to, without axes: Sparkline.
import { BarChart, LineChart, Sparkline } from "cennge-ui/charts";

The entry is ESM only. It needs nothing beyond the setup in Installation: the palette, the animations and the classes the charts use come with the library's stylesheet.

Similar#

  • Table is for exact values a reader looks up, sorts or copies.
  • Progress shows one value against a maximum.
  • Data layers put values on a map, by place: choropleths and clusters.

One data shape#

BarChart, LineChart and AreaChart take the same props, CartesianChartProps: data with one item per category, index naming the field that labels it, and series listing the fields to draw. Switch the chart and the data stays as it is. The numbers in the examples are sample data.

  • Opened
  • Closed

Arrow keys move between the values.

Issues in cennge-ui per month
MonthOpenedClosed
May1812
Jun2422
Jul3126
Aug2730
Sep3533
Oct2934
one-shape.tsx
import { useState } from "react";
import { ToggleGroup, ToggleGroupItem } from "cennge-ui";
import { AreaChart, BarChart, LineChart, type CartesianChartProps } from "cennge-ui/charts";

const months = [
  { month: "May", opened: 18, closed: 12 },
  { month: "Jun", opened: 24, closed: 22 },
  { month: "Jul", opened: 31, closed: 26 },
  { month: "Aug", opened: 27, closed: 30 },
  { month: "Sep", opened: 35, closed: 33 },
  { month: "Oct", opened: 29, closed: 34 },
];

const chart: CartesianChartProps<(typeof months)[number]> = {
  "aria-label": "Issues in cennge-ui per month",
  data: months,
  index: "month",
  indexLabel: "Month",
  series: [
    { key: "opened", label: "Opened" },
    { key: "closed", label: "Closed" },
  ],
  className: "h-64",
};

export default function OneShape() {
  const [kind, setKind] = useState("bar");
  return (
    <div className="flex w-full flex-col gap-4">
      <ToggleGroup aria-label="Chart" size="sm" value={kind} onValueChange={(value) => value && setKind(value)}>
        <ToggleGroupItem value="bar">BarChart</ToggleGroupItem>
        <ToggleGroupItem value="line">LineChart</ToggleGroupItem>
        <ToggleGroupItem value="area">AreaChart</ToggleGroupItem>
      </ToggleGroup>
      {kind === "bar" ? <BarChart {...chart} /> : kind === "line" ? <LineChart {...chart} /> : <AreaChart {...chart} />}
    </div>
  );
}

The palette#

Series take eight slots in order, --cennge-chart-1 to --cennge-chart-8, which Tailwind has as chart-1 to chart-8 for legends and tiles of your own. A ninth series is grey, default, never a new colour. Switch the accent at the top of the page: the first slot follows it, and the order after it changes with the preset.

  • chart-1
  • chart-2
  • chart-3
  • chart-4
  • chart-5
  • chart-6
  • chart-7
  • chart-8
  • default
palette.tsx
const slots = [
  { name: "chart-1", swatch: "bg-chart-1" },
  { name: "chart-2", swatch: "bg-chart-2" },
  { name: "chart-3", swatch: "bg-chart-3" },
  { name: "chart-4", swatch: "bg-chart-4" },
  { name: "chart-5", swatch: "bg-chart-5" },
  { name: "chart-6", swatch: "bg-chart-6" },
  { name: "chart-7", swatch: "bg-chart-7" },
  { name: "chart-8", swatch: "bg-chart-8" },
  { name: "default", swatch: "bg-default-500" },
];

export default function Palette() {
  return (
    <ul className="grid w-full grid-cols-3 gap-3 sm:grid-cols-9">
      {slots.map((slot) => (
        <li key={slot.name} className="flex flex-col gap-1.5">
          <span className={`h-10 rounded-medium ${slot.swatch}`} />
          <span className="font-code text-xs text-muted">{slot.name}</span>
        </li>
      ))}
    </ul>
  );
}

The slots were checked by a script, not picked by eye:

  • each sits in the lightness band charts read best in (OKLCH lightness 0.43 to 0.77 in the light theme, 0.48 to 0.67 in the dark), with a chroma of at least 0.1;
  • each keeps a contrast of 3:1 on every surface;
  • slots next to each other stay apart for colour-blind readers: their OKLab distance (×100) is at least 8.5 under simulated protanopia and deuteranopia, and at least 16.7 with full colour vision;
  • after the accent, each preset has the order whose closest neighbours stay furthest apart. With the violet accent, orange comes second and blue fourth, because violet next to blue blurs.

Give a series a theme colour where the colour means something (failures in danger), default for the grey, or any CSS colour.

Numbers and words in your language#

Values follow the locale of CenngeProvider: in full in the readout and the table, short on the axis. valueFormat and axisFormat replace either, and labels replaces the words the charts say themselves: the keyboard hint, the empty state and, on a DonutChart, the total and the share. The provider sets no language on the page, so lang on an element around the chart tells screen readers to read it in French.

Les flèches passent d’une valeur à l’autre.

Minutes de CI par mois
MoisMinutes
juil.2 340
août2 370
sept.2 700
oct.2 840
locale.tsx
import { CenngeProvider } from "cennge-ui";
import { BarChart } from "cennge-ui/charts";

const months = [
  { month: "juil.", minutes: 2340 },
  { month: "août", minutes: 2370 },
  { month: "sept.", minutes: 2700 },
  { month: "oct.", minutes: 2840 },
];

export default function FrenchChart() {
  return (
    <CenngeProvider locale="fr-FR">
      <div lang="fr" className="w-full">
        <BarChart
          aria-label="Minutes de CI par mois"
          data={months}
          index="month"
          indexLabel="Mois"
          series={[{ key: "minutes", label: "Minutes" }]}
          labels={{ keyboardHint: "Les flèches passent d’une valeur à l’autre.", empty: "Aucune donnée" }}
          className="h-56"
        />
      </div>
    </CenngeProvider>
  );
}

Accessibility#

  • BarChart, LineChart, AreaChart and DonutChart are each a <figure> named by its aria-label. Each holds a focusable group, described by labels.keyboardHint, that answers to the arrow keys, Home, End and Escape; a polite live region reads each place the keys reach.
  • The readout is drawn for the eye only. Screen readers get the same data as a table captioned with the chart's name; in BarChart, LineChart and AreaChart a missing value is written as a dash.
  • Colour never names a series alone: the legend puts each name beside a sample of its mark, a box for bars and areas, a stroke for lines.
  • A Sparkline has no readout. It is one picture, role="img", and its aria-label is all a screen reader hears.
  • The end-to-end tests run axe over every kind of chart in both themes, and again with a readout open on BarChart, LineChart and DonutChart.

Server rendering#

The axes are HTML and the grid an SVG, both placed in percent, so the server sends them where they will stay, with the legend and the data table. Bars, lines and areas are drawn in pixels once the browser has measured the plot. DonutChart and Sparkline draw in a fixed viewBox and are whole on the server.

Styling#

  • Size a chart with className. The default h-72 of BarChart, LineChart and AreaChart takes in the axes and the legend; a Sparkline is h-8 w-24 until you say otherwise.
  • A dot on a line sits in a ring of the colour behind the chart, --cennge-chart-surface, which falls back to a Card's surface. Set it when the chart sits on another one.
  • Bars grow, lines and areas draw in and the donut pops in as they appear; with reduced motion nothing moves.

Props#

Shared by BarChart, LineChart and AreaChart#

Each entry of series is { key, label, color }: the field that holds its values, its name in the legend, the readout and the table (the key without one), and its colour.

PropTypeDefault
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.
–