# Line
URL: /diagrammatic/docs/charts/line

The default for anything measured over time. One series or several, on linear or log paper, with regions, bands, annotations, and guides.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

```tsx
"use client";

import { Line } from "diagrammatic";
import { Root, Tooltip, getSeriesColor } from "diagrammatic/interactive";

const STEPS = Array.from({ length: 16 }, (_, i) => (i === 0 ? "0" : `${i}k`));
const SERIES = [
  {
    name: "baseline",
    data: [
      220, 244, 271, 301, 334, 369, 405, 441, 476, 509, 539, 566, 589, 608, 622,
      631,
    ],
  },
  {
    name: "fixed penalty",
    data: [
      220, 238, 252, 248, 225, 201, 182, 169, 161, 156, 153, 151, 150, 150, 149,
      149,
    ],
  },
  {
    name: "alternating",
    data: [
      220, 248, 282, 320, 352, 344, 310, 285, 272, 266, 269, 273, 272, 274, 273,
      275,
    ],
  },
];

export default function ResponseLengthChart() {
  return (
    <Root highlightOnHover="series">
      <Line
        title="Mean response length by training recipe"
        summary="Baseline grows to 631 tokens, the fixed penalty falls to 149, and only the alternating recipe settles in the 250 to 300 band."
        aspect={2.2}
        series={SERIES}
        labels={STEPS.map((step, i) => (i % 2 === 0 ? step : ""))}
        yTicks={[
          { at: 200, label: "200" },
          { at: 400, label: "400" },
          { at: 600, label: "600" },
        ]}
        bands={[{ lower: STEPS.map(() => 250), upper: STEPS.map(() => 300) }]}
        regions={[{ from: 4, to: 9, label: "alternating penalty" }]}
        guides={[{ at: 275, label: "target 275" }]}
        marks={[
          { series: "alternating", at: 4, label: "penalty on" },
          { series: "alternating", at: 9, label: "off" },
          { series: "baseline", at: 15, label: "631" },
          { series: "fixed penalty", at: 15, label: "149" },
        ]}
      />
      <Tooltip side="top">
        {({ datum }) => {
          const i = datum.index;
          if (i === undefined) return null;
          return (
            <div className="bg-popover text-popover-foreground w-max rounded-lg border px-2.5 py-1.5 text-xs shadow-md">
              <div className="text-muted-foreground mb-1">step {STEPS[i]}</div>
              {SERIES.map((s) => (
                <div key={s.name} className="flex items-center gap-2">
                  <span
                    className="size-2 rounded-full"
                    style={{
                      background: getSeriesColor(datum.element, s.name),
                    }}
                  />
                  <span className="flex-1">{s.name}</span>
                  <span className="tabular-nums">{s.data[i]}</span>
                </div>
              ))}
            </div>
          );
        }}
      </Tooltip>
    </Root>
  );
}
```

## Usage

```
import { Line } from "diagrammatic";

<Line labels={["Jan", "Feb", "Mar"]} data={[34, 46, 40]} />;
```

`Line` renders on the server. Pass `data` for one series or `series` for several; `labels` names the points on the x axis.

## Examples

### One series

A single series draws in ink with its last value printed at the end of the line.

```tsx
import { Line } from "diagrammatic";

export default function ActiveUsersChart() {
  return (
    <Line
      title="Monthly active users"
      labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep"]}
      data={[34, 46, 40, 58, 52, 66, 60, 76, 90]}
      format={(v) => `${v}k`}
    />
  );
}
```

### Several series

Two or more named series take the categorical palette in order and get a legend. Past four lines, consider [small multiples](/diagrammatic/docs/small-multiples).

```tsx
import { Line } from "diagrammatic";

export default function LatencyByRegionChart() {
  return (
    <Line
      title="p95 latency by region, ms"
      labels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
      series={[
        { name: "us-east", data: [182, 176, 191, 204, 188, 160, 152] },
        { name: "eu-west", data: [214, 220, 209, 231, 246, 198, 190] },
        { name: "ap-south", data: [268, 281, 274, 259, 262, 240, 236] },
      ]}
      yTicks={4}
    />
  );
}
```

### Steps

`step` holds each value until the next one, for quantities that change in discrete jumps: seats, prices, replica counts.

```tsx
import { Line } from "diagrammatic";

export default function SeatCountChart() {
  return (
    <Line
      step
      title="Paid seats"
      labels={["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8", "W9", "W10"]}
      data={[40, 40, 55, 55, 55, 80, 80, 80, 120, 120]}
      yTicks={4}
    />
  );
}
```

### Regions and annotations

`regions` shades a span of the x axis in index units, and `marks` pins a label to a point. Together they tell the reader why the line moved.

```tsx
import { Line } from "diagrammatic";

const HOURS = Array.from(
  { length: 13 },
  (_, i) => `${String(i * 2).padStart(2, "0")}:00`,
);

export default function ErrorRateChart() {
  return (
    <Line
      title="Checkout error rate, %"
      labels={HOURS.map((h, i) => (i % 2 === 0 ? h : ""))}
      data={[0.4, 0.5, 0.4, 0.6, 2.8, 3.4, 1.1, 0.7, 0.5, 0.4, 0.5, 0.4, 0.4]}
      regions={[{ from: 3.5, to: 6, label: "incident" }]}
      marks={[
        { at: 4, label: "deploy 4812" },
        { at: 6, label: "rollback" },
      ]}
      yTicks={3}
    />
  );
}
```

### Uncertainty bands

`bands` takes a `lower` and `upper` array per series and draws the range under the line, for forecasts and confidence intervals.

```tsx
import { Line } from "diagrammatic";

const MONTHS = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
];
const FORECAST = [120, 128, 134, 141, 150, 158, 163, 171, 180, 188, 195, 204];

export default function DemandForecastChart() {
  return (
    <Line
      title="GPU hours forecast, thousands"
      labels={MONTHS}
      series={[{ name: "forecast", data: FORECAST }]}
      bands={[
        {
          lower: FORECAST.map((v, i) => v - 4 - i * 2.2),
          upper: FORECAST.map((v, i) => v + 4 + i * 2.2),
        },
      ]}
      yTicks={4}
    />
  );
}
```

### Thresholds

`guides` draws reference lines on either axis: an SLO on the value axis, a deploy on the x axis.

```tsx
import { Line } from "diagrammatic";

const HOURS = Array.from(
  { length: 12 },
  (_, i) => `${String(i * 2).padStart(2, "0")}:00`,
);

export default function SloChart() {
  return (
    <Line
      title="p95 latency, ms"
      labels={HOURS}
      data={[212, 220, 231, 226, 240, 238, 251, 296, 318, 284, 262, 248]}
      yTicks={4}
      guides={[
        { at: 300, label: "SLO" },
        { at: 7, axis: "x", label: "deploy" },
      ]}
    />
  );
}
```

### Log scale

`yScale="log"` puts growth that spans decades on one frame. A tick count on log paper counts decades.

```tsx
import { Line, formatCompact } from "diagrammatic";

export default function StarsChart() {
  return (
    <Line
      title="Cumulative GitHub stars"
      labels={[
        "M1",
        "M2",
        "M3",
        "M4",
        "M5",
        "M6",
        "M7",
        "M8",
        "M9",
        "M10",
        "M11",
        "M12",
      ]}
      data={[
        180, 420, 760, 1500, 2600, 4100, 6900, 9800, 14200, 21500, 30100, 41800,
      ]}
      yScale="log"
      yTicks={3}
      format={formatCompact}
    />
  );
}
```

### Uneven x positions

`xs` places points at numeric x positions instead of evenly, and `xScale="log"` spaces them logarithmically, as a scaling curve needs.

```tsx
import { Line } from "diagrammatic";

export default function EvalLossChart() {
  return (
    <Line
      title="Validation loss against training tokens, billions"
      xs={[1, 2, 4, 8, 16, 32, 64, 128]}
      xScale="log"
      xTicks={[
        { at: 1, label: "1B" },
        { at: 8, label: "8B" },
        { at: 64, label: "64B" },
      ]}
      series={[
        { name: "350M", data: [3.9, 3.6, 3.35, 3.16, 3.02, 2.93, 2.88, 2.86] },
        { name: "1.3B", data: [3.8, 3.42, 3.11, 2.86, 2.68, 2.55, 2.47, 2.42] },
      ]}
      yTicks={3}
      format={(v) => v.toFixed(1)}
    />
  );
}
```

### Survival curves

A `step` line with `marks` of `kind: "censor"` is a Kaplan-Meier plot: the ticks mark where observation stopped without an event.

```tsx
import { Line } from "diagrammatic";

export default function RetentionChart() {
  return (
    <Line
      step
      title="Share of trial workspaces still active"
      labels={["0", "", "", "30d", "", "", "60d", "", "", "90d"]}
      series={[
        {
          name: "with onboarding",
          data: [1, 0.94, 0.9, 0.86, 0.84, 0.82, 0.81, 0.8, 0.79, 0.79],
        },
        {
          name: "without",
          data: [1, 0.88, 0.79, 0.71, 0.66, 0.62, 0.6, 0.57, 0.55, 0.54],
        },
      ]}
      marks={[
        { series: "with onboarding", at: 4, kind: "censor" },
        { series: "with onboarding", at: 7, kind: "censor" },
        { series: "without", at: 5, kind: "censor" },
      ]}
      yTicks={[
        { at: 0.5, label: "50%" },
        { at: 0.75, label: "75%" },
        { at: 1, label: "100%" },
      ]}
    />
  );
}
```

## Interaction

These examples wrap the server-rendered chart in `diagrammatic/interactive`. See [Interaction](/diagrammatic/docs/interaction) for the full API.

### Tooltip

The tooltip receives the hovered point's `index` and looks the value up in your data.

```tsx
"use client";

import { Line } from "diagrammatic";
import { Root, Tooltip } from "diagrammatic/interactive";

const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const REQUESTS = [12.4, 13.1, 12.8, 14.6, 15.2, 9.8, 8.9];

export default function RequestsChart() {
  return (
    <Root>
      <Line
        title="Requests per second, thousands"
        labels={DAYS}
        data={REQUESTS}
        yTicks={3}
      />
      <Tooltip side="top">
        {({ datum }) => {
          const i = datum.index;
          if (i === undefined) return null;
          return (
            <div className="bg-popover text-popover-foreground w-max rounded-lg border px-2.5 py-1.5 text-xs shadow-md">
              <span className="text-muted-foreground">{DAYS[i]}</span>{" "}
              <span className="font-medium tabular-nums">
                {REQUESTS[i]}k rps
              </span>
            </div>
          );
        }}
      </Tooltip>
    </Root>
  );
}
```

### Highlight a series

A controlled `highlight` fades every other series; `highlightOnHover="series"` does the same while the pointer rests on a line.

```tsx
"use client";

import { useState } from "react";
import { Line } from "diagrammatic";
import { Root } from "diagrammatic/interactive";

const SERIES = [
  { name: "search", data: [42, 48, 51, 58, 61, 66, 64, 71, 77, 82] },
  { name: "direct", data: [30, 29, 33, 35, 34, 38, 41, 40, 43, 45] },
  { name: "referral", data: [12, 15, 14, 18, 22, 21, 25, 29, 28, 33] },
  { name: "social", data: [8, 9, 13, 12, 11, 15, 14, 18, 21, 19] },
];

export default function ChannelChart() {
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <div>
      <div className="mb-3 flex flex-wrap gap-1.5">
        {SERIES.map((s) => (
          <button
            key={s.name}
            type="button"
            aria-pressed={selected === s.name}
            onClick={() => setSelected(selected === s.name ? null : s.name)}
            className="aria-pressed:bg-foreground aria-pressed:text-background rounded-md border px-2 py-1 text-xs"
          >
            {s.name}
          </button>
        ))}
      </div>
      <Root
        {...(selected && { highlight: { series: selected } })}
        highlightOnHover="series"
      >
        <Line
          title="Weekly sessions by channel, thousands"
          labels={["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8", "W9", "W10"]}
          series={SERIES}
          yTicks={4}
        />
      </Root>
    </div>
  );
}
```

### Select a point

`onMarkClick` reports the clicked point's address, and a controlled highlight keeps the selection visible.

```tsx
"use client";

import { useState } from "react";
import { Line } from "diagrammatic";
import { Root, type MarkDatum } from "diagrammatic/interactive";

const BUILDS = [
  "#4801",
  "#4802",
  "#4803",
  "#4804",
  "#4805",
  "#4806",
  "#4807",
  "#4808",
];
const DURATION = [6.2, 6.4, 6.1, 9.8, 6.3, 6.0, 6.6, 6.2];
const CAUSE = [
  "",
  "",
  "",
  "cold cache after the runner image bump",
  "",
  "",
  "",
  "",
];

export default function BuildTimeChart() {
  const [picked, setPicked] = useState<number>(3);

  return (
    <div>
      <Root
        highlight={{ index: picked }}
        onMarkClick={(datum: MarkDatum) => {
          if (datum.index !== undefined) setPicked(datum.index);
        }}
      >
        <Line
          title="CI duration, minutes"
          labels={BUILDS}
          data={DURATION}
          yTicks={3}
        />
      </Root>
      <p className="text-muted-foreground mt-3 text-sm">
        <span className="text-foreground font-medium">{BUILDS[picked]}</span>{" "}
        took <span className="tabular-nums">{DURATION[picked]} min</span>
        {CAUSE[picked] ? `: ${CAUSE[picked]}.` : "."} Click a point to inspect
        another build.
      </p>
    </div>
  );
}
```

### Live data

The chart is a pure function of its props, so streaming data is a state update. This one appends a reading every second, and `data-dg-motion` pulses the latest point while it runs. See [Motion](/diagrammatic/docs/motion).

```tsx
"use client";

import { useEffect, useState, useSyncExternalStore } from "react";
import { Line } from "diagrammatic";

const WINDOW = 30;
const REDUCED = "(prefers-reduced-motion: reduce)";

function subscribe(onChange: () => void) {
  const query = window.matchMedia(REDUCED);
  query.addEventListener("change", onChange);
  return () => query.removeEventListener("change", onChange);
}

function next(prev: number[], t: number): number[] {
  const last = prev[prev.length - 1] ?? 40;
  const noise = Math.abs(Math.sin(t * 12.9898) * 43758.5453) % 1;
  const drift = Math.sin(t / 6) * 3 + noise * 4 - 2;
  return [...prev.slice(1), Math.max(5, Math.min(95, last + drift))];
}

export default function LiveCpuChart() {
  const [data, setData] = useState(() =>
    Array.from({ length: WINDOW }, (_, i) => 40 + Math.sin(i / 4) * 8),
  );
  const reduced = useSyncExternalStore(
    subscribe,
    () => window.matchMedia(REDUCED).matches,
    () => false,
  );
  const [choice, setChoice] = useState<boolean | null>(null);
  const running = choice ?? !reduced;

  useEffect(() => {
    if (!running) return;
    let t = 0;
    const id = setInterval(() => {
      t += 1;
      setData((prev) => next(prev, t));
    }, 1000);
    return () => clearInterval(id);
  }, [running]);

  return (
    <div data-dg-motion={running ? "" : undefined}>
      <Line
        title="CPU, last 30 seconds"
        data={data}
        yMax={100}
        yTicks={[
          { at: 0, label: "0%" },
          { at: 50, label: "50%" },
          { at: 100, label: "100%" },
        ]}
        format={(v) => `${Math.round(v)}%`}
      />
      <button
        type="button"
        onClick={() => setChoice(!running)}
        className="text-muted-foreground hover:text-foreground mt-3 text-xs"
      >
        {running ? "Pause" : "Resume"}
      </button>
    </div>
  );
}
```

### Fluid width

`Fit` measures its box and hands the width down, so the frame is laid out again at every width and the type stays at 11px. Resize the window.

```tsx
"use client";

import { Line } from "diagrammatic";
import { Fit } from "diagrammatic/interactive";

export default function FluidChart() {
  return (
    <Fit>
      {({ width }) => (
        <Line
          title="Daily signups"
          width={width}
          aspect={3}
          labels={[
            "1",
            "",
            "",
            "",
            "5",
            "",
            "",
            "",
            "",
            "10",
            "",
            "",
            "",
            "",
            "15",
          ]}
          data={[12, 14, 13, 17, 21, 19, 24, 28, 27, 33, 31, 36, 41, 39, 45]}
          yTicks={3}
        />
      )}
    </Fit>
  );
}
```

## From a spec

The same chart as JSON, for a model to write. See [Charts from a model](/diagrammatic/docs/ai-specs).

```tsx
import { Chart, validateSpec } from "diagrammatic/spec";

const modelOutput = `{
  "v": "dg/1",
  "type": "line",
  "title": "Weekly active users",
  "unit": "compact",
  "labels": ["W1", "W2", "W3", "W4", "W5", "W6"],
  "data": [1200, 1350, 1310, 1580, 1720, 1910],
  "yTicks": 3
}`;

export default function SpecChart() {
  return <Chart spec={validateSpec(JSON.parse(modelOutput))} />;
}
```

## When to use it

- **Use it for** trends, rates, and anything measured on a continuous timeline.
- **Watch out** for more than four lines, which turn to spaghetti; facet into small multiples instead. For a quantity that accumulates, an [area](/diagrammatic/docs/charts/area) reads better.

## Props

- `data?`: `number[]` — One series of values. Use \`series\` for several.
- `series?`: `{ name: string; data: number[] }[]` — Named series, colored in palette order. Two or more get a legend.
- `labels?`: `string[]` — One label per point on the x axis. An empty string skips a label.
- `xs?`: `number[]` — Numeric x positions, one per point, instead of even spacing.
- `xScale`: `"linear" | "log"` (default `"linear"`) — The x scale when \`xs\` is given.
- `yScale`: `"linear" | "log"` (default `"linear"`) — The value scale.
- `yMax?`: `number` — Fixes the top of the value axis, so several charts share a scale.
- `xTicks?`: `number | { at: number; label: string }[]` — A tick count or an explicit ladder on the x axis.
- `yTicks?`: `number | { at: number; label: string }[]` — A tick count or an explicit ladder on the value axis. Unset draws no value axis.
- `step`: `boolean` (default `false`) — Holds each value until the next point.
- `regions?`: `{ from: number; to: number; label?: string }[]` — Shaded spans of the x axis, in index units or \`xs\` units.
- `bands?`: `{ lower: number[]; upper: number[] }[]` — A range under each series, in series order.
- `marks?`: `{ at: number; series?: string; label?: string; kind?: "point" | "censor" }[]` — Annotations pinned to points. \`kind: "censor"\` draws a censoring tick.
- `guides?`: `{ at: number; axis?: "x" | "y"; label?: string }[]` — Reference lines. The value axis by default.
- `legend?`: `boolean` — The in-figure legend. On when there are two or more series.
- `format`: `(value: number) => string` (default `formatCompact`) — Formats printed values and tick labels.
- `title?`: `string` — Names the chart for assistive technology. See [Accessibility](/diagrammatic/docs/accessibility).
- `summary?`: `string` — A sentence read after the title.
- `width`: `number` (default `640`) — Rendered width in CSS pixels. See [Sizing](/diagrammatic/docs/sizing).
- `height?`: `number` — Rendered height in CSS pixels. Defaults to \`width / aspect\`.
- `aspect?`: `number` — Width over height when \`height\` is not given.
- `fontSize`: `number` (default `11`) — The type size in CSS pixels.
- `view?`: `{ x: number; y: number; w: number; h: number }` — A window onto the figure, from \`Zoom\`.

Every other SVG attribute, event handler, `style`, and `ref` passes through to the root `svg`.