# Line chart

Crisp lines with dithered range bands and point markers, for latency, conversion and period-over-period comparisons.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

<ComponentPreview src="examples/line-chart-demo.tsx" size="lg" controls="lineChartControls">
  <LineChartDemo />
</ComponentPreview>

## Installation [#installation]

<InstallBlock name="line-chart" />

## Usage [#usage]

```tsx
import { LineChart } from "@/components/dithercn/line-chart";

const data = [
  { time: "12:00", p10: 160, p50: 188, p90: 238 },
  { time: "12:30", p10: 164, p50: 184, p90: 230 },
  { time: "13:00", p10: 167, p50: 191, p90: 245 },
];

export function Latency() {
  return (
    <LineChart
      data={data}
      x="time"
      series={[
        { label: "p50", value: "p50", range: { low: "p10", high: "p90" } },
      ]}
    />
  );
}
```

Each series is a 2px line. Give it a `range` and the space between `low` and `high` fills with a light dither, which suits percentiles, forecasts and min/max envelopes. The readout lists the value with its range.

## Examples [#examples]

### This week against last week [#this-week-against-last-week]

A `dashed` series reads as a reference: a previous period or a target. The legend repeats the dash, so the two lines never rely on colour alone. Here this week's `range` runs from last week's value to its own, so the dither fills exactly the gap between the two.

<ComponentPreview src="examples/line-chart-compare.tsx" size="lg">
  <LineChartCompare />
</ComponentPreview>

### Few points, every one marked [#few-points-every-one-marked]

With sparse data, `markers="all"` puts a dot on each point and `curve="linear"` keeps straight segments between them, so nobody reads a curve the data does not have. The band is a sector benchmark in its own `color`, and `zero` starts the axis at 0 because a score's distance from zero matters here.

<ComponentPreview src="examples/line-chart-points.tsx">
  <LineChartPoints />
</ComponentPreview>

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` (required) | `readonly T[]` |  | One entry per point along the x axis, in order. |
| `x` (required) | `keyof T \| ((datum: T) => string \| number)` |  | The field (or function) holding each point's x axis label. |
| `series` (required) | `[LineSeries<T>, ...LineSeries<T>[]]` |  | Each series is { label, value, color?, range?: { low, high, color? }, dashed? }. value, low and high are numeric fields of T or functions. |
| `markers` | `"all" \| "last" \| "none"` | `"last"` | Which points get a dot. |
| `zero` | `boolean` | `false` | Start the value axis at zero. By default it fits the data, so small changes stay visible. |
| `height` | `number` | `240` | Height in px, axes included. |
| `curve` | `"smooth" \| "linear"` | `"smooth"` | smooth is a monotone curve that never overshoots a data point. |
| `fill` | `"gradient" \| "dotted" \| "hatched" \| "solid"` | `"gradient"` | Texture of the range bands. gradient is one even dither level, set by level; solid draws a translucent band. |
| `bloom` | `"off" \| "low" \| "high"` | `"off"` | A soft glow in each series' colour behind its fill. Its strength follows --dither-bloom, lower in light mode. |
| `colors` | `readonly DitherColor[]` | `ink, orange, green, pink, …` | Colours by series position: palette names such as "green" or any CSS colour. A series' own color wins. |
| `interactive` | `boolean` | `true` | Scrubbing, the readout and the legend spotlight. false makes a decorative chart: no focus stop, no readout, just an image with a summary. |
| `animate` | `boolean` | `true` | Entrance motion. Always off under reduced motion. |
| `level` | `number` | `0.25` | Gradient only: coverage of the range bands, from 0 to 1. |
| `matrix` | `"bayer2" \| "bayer4" \| "bayer8" \| "blue-noise"` | `"bayer4"` | Gradient only: the dither texture of the bands. |
| `cell` | `number` | `--dither-cell` | Size of one dither cell in px, rounded to whole device pixels. |
| `axes` | `boolean` | `true` | Show the value and category axes. |
| `tooltip` | `boolean` | `true` | Show the readout while scrubbing. |
| `legend` | `boolean` | `Two or more series` | Show the legend under the chart. Its entries spotlight a series on hover and lock it on click. |
| `format` | `Intl.NumberFormatOptions \| ((value: number) => string)` | `Compact, as in 1.2K` | How values are written on the axis, in the readout and in the table. |

Series colours follow the palette: ink, then orange, green, pink, purple, blue, red and grey. Pass `colors`, or `color` on one series, with a palette name or any CSS colour.

## Notes [#notes]

* The chart is a focusable group with a short summary, and every value, including ranges, is in a visually hidden table. The arrow keys, Home and End move the readout without easing, a polite live region reads out each stop, and Escape clears the readout and any locked series. With `bloom`, each line gets a soft glow in its colour.
* On entrance solid lines draw from left to right, dashed lines fade in, and bands step up through the dither levels. With reduced motion the finished chart appears at once.
* Markers carry a 2px ring in `--dither-paper` so they stay legible where lines cross. Set `--dither-paper` to your card's background if it differs from the page.