Charts

LineChart

A multi-series line chart in pure SVG - no d3, no canvas, nothing measured, so it renders complete on the server.

Server safePure SVG0 dependenciesSource

Import

Charts live at their own subpath with their own stylesheet, so an app that never draws one pays nothing for them.

import { LineChart } from '@the_viveksingh/vivek-ui/charts'
import '@the_viveksingh/vivek-ui/charts.css'

A single series

Pass `{ x, y }` points. The axis, grid and labels are derived.

Revenue1500200025003000350040004500JanFebMarAprMayJun
Revenue
CategoryValue
Jan1200
Feb2600
Mar2450
Apr3800
May3200
Jun4600
import { LineChart } from '@the_viveksingh/vivek-ui/charts'
import '@the_viveksingh/vivek-ui/charts.css'

export default function Revenue() {
  return (
    <LineChart
      data={[
        { x: 'Jan', y: 1200 },
        { x: 'Feb', y: 2600 },
        { x: 'Mar', y: 2450 },
        { x: 'Apr', y: 3800 },
      ]}
      title="Revenue"
      height={240}
      showGrid
    />
  )
}

Multiple series

Each series gets its own colour, dash pattern and marker shape, so it stays readable in greyscale and to a colourblind reader.

Revenue vs costs100015002000250030003500JanFebMarApr
Revenue vs costs
CategoryRevenueCosts
Jan1200900
Feb26001400
Mar24501500
Apr38001900
// Each series carries its own data array.
<LineChart
  series={[
    {
      name: 'Revenue',
      data: [
        { x: 'Jan', y: 1200 },
        { x: 'Feb', y: 2600 },
        { x: 'Mar', y: 2450 },
        { x: 'Apr', y: 3800 },
      ],
    },
    {
      name: 'Costs',
      data: [
        { x: 'Jan', y: 900 },
        { x: 'Feb', y: 1400 },
        { x: 'Mar', y: 1500 },
        { x: 'Apr', y: 1900 },
      ],
    },
  ]}
  title="Revenue vs costs"
  height={260}
  showGrid
  showLegend
/>

Mapping from an API

Money arrives as integer cents and dates as ISO strings. One `.map()` is the whole adapter.

Revenue1500200025003000350040004500JanFebMarAprMayJun
Revenue
CategoryValue
Jan1200
Feb2600
Mar2450
Apr3800
May3200
Jun4600
const monthly = await db.revenueByMonth()
// [{ month: '2026-01', total_cents: 120000 }, …]

<LineChart
  data={monthly.map((row) => ({
    x: new Date(row.month).toLocaleDateString('en', { month: 'short' }),
    y: row.total_cents / 100,
  }))}
  title="Revenue"
  curve="smooth"
  height={240}
  showGrid
/>

Accessibility

A chart is not an image

Every chart carries role="img" with a generated accessible name, and renders a real <table> alongside it — visually hidden, never display: none — so a screen-reader user gets the actual numbers instead of "chart". Set accessibleTable={false} only if you have provided the data in a table elsewhere on the page.

No series is ever encoded by colour alone: each carries a distinct dash pattern and marker shape on top of its colour, so the chart survives greyscale printing and every common form of colour blindness.

The palette is verified rather than asserted. Each theme has its own six colours, because one set cannot clear the 3:1 non-text contrast threshold against both a white and a near-black plot surface. Separation is measured under simulated protanopia, deuteranopia and tritanopia: for one to four series — which is nearly every chart — the closest pair sits at ΔE 17, slightly better than the unmodified Okabe-Ito palette it is derived from. Past four series the dash patterns and marker shapes carry the distinction.

Hostile data

Real series contain gaps and rubbish. Every chart is tested against empty arrays, a single point, all-equal values, negatives crossing zero, NaN, ±Infinity, 1e308 and 1e-320, with an assertion that no rendered SVG attribute ever contains NaN. A bad row degrades the chart; it does not blank your page.


Props

Generated from the package's own type declarations, so this table cannot drift from the code.

Props for LineChart
PropTypeDefaultDescription
dataChartDataSingle series. `[{ x, y }]`, or a bare `number[]` where the index is the x.
seriesreadonly ChartSeries[]Multiple series. Takes precedence over `data` when both are given.
heightnumberHeight of the `viewBox`, in px. The chart scales to its container width.
showGridboolean
showAxesboolean
showLegendbooleanDefaults to on for more than one series.
interactiveLegendbooleanTurn each legend entry into a checkbox that shows and hides its series, with a fade. No JavaScript and no client boundary: the entries are real checkboxes and the chart reacts with `:has()`. Off by default, because it makes the legend a set of controls - right for a dashboard, wrong for a figure in a report.
showPointsbooleanPer-point markers. Defaults to on while every series has 24 points or fewer.
curve'linear' | 'smooth'`smooth` draws a Catmull-Rom spline; control points are clamped to the plot.
strokeWidthnumber
tooltipbooleanOpt in to the pointer hover tooltip. Mouse only - the data table is the AT path.