Skip to content
cennge-ui
GitHubv0.3.1, changelog

LineChart

Change over time, a line per series, read at any point with a crosshair.

CI run time

Median minutes per run in October · sample data

  • cennge-ui
  • payload-cache

Arrow keys move between the values.

Median CI run time per day in October
Daycennge-uipayload-cache
Oct 111.2 min4.1 min
Oct 210.8 min4.3 min
Oct 311.5 min3.9 min
Oct 410.9 min4.6 min
Oct 59.8 min3.2 min
Oct 69.6 min3.1 min
Oct 711.9 min4.4 min
Oct 812.4 min4.2 min
Oct 912.1 min4.8 min
Oct 1011.6 min4.5 min
Oct 1112.8 min4.7 min
Oct 1210.2 min3.4 min
Oct 139.9 min–
Oct 1412.5 min–
Oct 1512.9 min–
Oct 1613.4 min5.1 min
Oct 1712.7 min4.9 min
Oct 1813.1 min5.3 min
Oct 1910.6 min3.8 min
Oct 2010.4 min3.6 min
Oct 2112.2 min4.6 min
Oct 2211.8 min4.4 min
Oct 2311.4 min4.9 min
Oct 2411.9 min4.7 min
Oct 2512.3 min4.2 min
Oct 2610.1 min3.3 min
Oct 279.7 min3.5 min
Oct 2811.3 min4.1 min
Oct 2910.9 min3.9 min
Oct 3010.6 min4.3 min
Oct 3110.8 min4 min

payload-cache response time

95th percentile by hour · sample data

Arrow keys move between the values.

payload-cache response time, 95th percentile
Hourp95
00:00212 ms
04:00198 ms
08:00246 ms
12:00301 ms
16:00287 ms
20:00233 ms
  1. Hover a day or tap it: the crosshair snaps to the nearest day and reads every series there.
  2. A missing value breaks its line, and reads as a dash in the readout and the table.
  3. With startAtZero off the axis fits the data, so a change small next to the values still shows.
Live: hover it, tap it, use the keys.

Field marks

Import
import { LineChart } from "cennge-ui/charts";
Size
5.6 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
Lines
2px, straight or a smooth curve that never overshoots its points
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: line-chart

When to use#

Use a LineChart when the order of the values matters: days, weeks, releases, hours. The line shows the shape of a change across many points, and the crosshair gives the exact values at any one of them. Each series is a line in its own colour, named in the legend beside a sample of its stroke.

Similar#

  • AreaChart is the same chart with the area under each line washed in its colour, and it can stack series into a whole.
  • BarChart takes the same data, index and series and compares amounts by length, from an axis that takes in zero unless you set a domain.
  • Sparkline is a trend without axes or a readout, small enough to sit beside a number.

One line#

A single series has no legend; the aria-label names the chart and its table. The numbers on this page are sample data.

Arrow keys move between the values.

Open issues in ai-niche-scout per week
Week ofOpen issues
Aug 1731
Aug 2434
Aug 3133
Sep 738
Sep 1436
Sep 2129
Sep 2827
Oct 530
Oct 1224
Oct 1922
Oct 2623
Nov 219
basic.tsx
import { LineChart } from "cennge-ui/charts";

const weeks = [
  { week: "Aug 17", open: 31 },
  { week: "Aug 24", open: 34 },
  { week: "Aug 31", open: 33 },
  { week: "Sep 7", open: 38 },
  { week: "Sep 14", open: 36 },
  { week: "Sep 21", open: 29 },
  { week: "Sep 28", open: 27 },
  { week: "Oct 5", open: 30 },
  { week: "Oct 12", open: 24 },
  { week: "Oct 19", open: 22 },
  { week: "Oct 26", open: 23 },
  { week: "Nov 2", open: 19 },
];

export default function OpenIssues() {
  return (
    <LineChart
      aria-label="Open issues in ai-niche-scout per week"
      data={weeks}
      index="week"
      indexLabel="Week of"
      series={[{ key: "open", label: "Open issues" }]}
      className="h-56"
    />
  );
}

Several series#

Series take the palette's slots in the order of series, the first in the accent, so a series added at the end never repaints the others. The readout lists them in the same order.

  • Opened
  • Merged
  • Closed unmerged

Arrow keys move between the values.

Pull requests in cennge-ui per month
MonthOpenedMergedClosed unmerged
May26194
Jun31273
Jul29306
Aug38295
Sep42402
Oct36414
series.tsx
import { LineChart } from "cennge-ui/charts";

const months = [
  { month: "May", opened: 26, merged: 19, closed: 4 },
  { month: "Jun", opened: 31, merged: 27, closed: 3 },
  { month: "Jul", opened: 29, merged: 30, closed: 6 },
  { month: "Aug", opened: 38, merged: 29, closed: 5 },
  { month: "Sep", opened: 42, merged: 40, closed: 2 },
  { month: "Oct", opened: 36, merged: 41, closed: 4 },
];

export default function PullRequests() {
  return (
    <LineChart
      aria-label="Pull requests in cennge-ui per month"
      data={months}
      index="month"
      indexLabel="Month"
      series={[
        { key: "opened", label: "Opened" },
        { key: "merged", label: "Merged" },
        { key: "closed", label: "Closed unmerged" },
      ]}
      className="h-64"
    />
  );
}

A gap in the data#

playwright-msi-scraper did not run over the weekend. Give null where there is no value: the line stops and starts again after it instead of diving to zero, and the readout and the table show a dash. A value alone between two gaps is drawn as a dot.

Arrow keys move between the values.

Pages scraped per day
DayPages
Oct 271,840
Oct 281,920
Oct 292,310
Oct 302,240
Oct 31–
Nov 1–
Nov 22,405
Nov 32,120
Nov 42,260
Nov 52,380
gaps.tsx
import { LineChart } from "cennge-ui/charts";

// The scraper did not run over the weekend: null, not 0.
const days = [
  { day: "Oct 27", pages: 1840 },
  { day: "Oct 28", pages: 1920 },
  { day: "Oct 29", pages: 2310 },
  { day: "Oct 30", pages: 2240 },
  { day: "Oct 31", pages: null },
  { day: "Nov 1", pages: null },
  { day: "Nov 2", pages: 2405 },
  { day: "Nov 3", pages: 2120 },
  { day: "Nov 4", pages: 2260 },
  { day: "Nov 5", pages: 2380 },
];

export default function PagesScraped() {
  return (
    <LineChart
      aria-label="Pages scraped per day"
      data={days}
      index="day"
      indexLabel="Day"
      series={[{ key: "pages", label: "Pages" }]}
      className="h-56"
    />
  );
}

Small changes, fitted axis#

The value axis starts at zero by default. With startAtZero={false} it fits round ends around the data, which suits a rate that never leaves the nineties. showDots marks every value, and axisFormat writes the axis as valueFormat writes the readout.

Each dot sits in a 2px ring of the colour behind the chart, so it stays clear of the line. The ring takes --cennge-chart-surface, which falls back to a Card's surface; set it when the chart sits on another one, as here.

Arrow keys move between the values.

payload-cache hit rate per day
DayHit rate
Mon94.2%
Tue95.1%
Wed93.8%
Thu96%
Fri96.4%
Sat92.7%
Sun93.5%
fit.tsx
import { LineChart } from "cennge-ui/charts";

const days = [
  { day: "Mon", hits: 94.2 },
  { day: "Tue", hits: 95.1 },
  { day: "Wed", hits: 93.8 },
  { day: "Thu", hits: 96 },
  { day: "Fri", hits: 96.4 },
  { day: "Sat", hits: 92.7 },
  { day: "Sun", hits: 93.5 },
];

const percent = (value: number) => `${value}%`;

export default function CacheHitRate() {
  return (
    <LineChart
      aria-label="payload-cache hit rate per day"
      data={days}
      index="day"
      indexLabel="Day"
      series={[{ key: "hits", label: "Hit rate" }]}
      startAtZero={false}
      showDots
      valueFormat={percent}
      axisFormat={percent}
      // The dots sit in a ring of the colour behind the chart.
      className="h-56 [--cennge-chart-surface:var(--cennge-surface-2)]"
    />
  );
}

A smooth curve#

curve="smooth" bends through the points without overshooting them: between two equal values the curve stays flat, and a count of zero never dips below the axis.

Arrow keys move between the values.

Failed CI runs per day
DayFailed runs
Mon0
Tue3
Wed0
Thu0
Fri5
Sat1
Sun0
smooth.tsx
import { LineChart } from "cennge-ui/charts";

const days = [
  { day: "Mon", failed: 0 },
  { day: "Tue", failed: 3 },
  { day: "Wed", failed: 0 },
  { day: "Thu", failed: 0 },
  { day: "Fri", failed: 5 },
  { day: "Sat", failed: 1 },
  { day: "Sun", failed: 0 },
];

export default function FailedRuns() {
  return (
    <LineChart
      aria-label="Failed CI runs per day"
      data={days}
      index="day"
      indexLabel="Day"
      curve="smooth"
      series={[{ key: "failed", label: "Failed runs", color: "danger" }]}
      className="h-48"
    />
  );
}

Accessibility#

  • The figure and its plot are named by aria-label; the plot is a focusable group described by labels.keyboardHint.
  • Focus from the keyboard puts the crosshair on the first point. The arrows move it, Home and End jump to the ends, Escape hides it, and a polite live region reads each place, as "Oct 2: cennge-ui 10.8 min, payload-cache 4.3 min".
  • The readout is for the eye only: screen readers get every value in a table captioned with the chart's name, a missing value written as a dash.
  • A finger scrubs along the chart while the page still scrolls up and down; a tap leaves the readout up until a tap elsewhere.

Server rendering#

The legend, the axes, the grid and the data table come from the server. The lines are drawn once the browser has measured the plot and draw in from the left; with reduced motion they appear at once.

Props#

Each entry of series is { key, label, color }: the field that holds its values, its name (the key without one), and a palette slot from chart-1 to chart-8, a theme colour or any CSS colour.

PropTypeDefault
curve
"linear""smooth""smooth" bends through the points without overshooting them: a count never dips below zero.
"linear"
showDots
booleanA dot on every value, not only where the readout is.
false
startAtZero
booleanThe value axis starts at zero. Off, it fits the data, for changes small next to the values.
true
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.
–