# Benchmark

> Package `@umriss-ui/charts`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/charts/benchmark/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/charts/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/charts/llms-full.txt).

Millions of points, measured in this browser rather than claimed: the time to prepare and draw them, and the frame rate while the pointer moves.

```ts
import { useChart } from "@umriss-ui/charts";
```

Demo page: https://romanhaendler.github.io/umriss-ui/charts/benchmark/

A hover redraws only the overlay layer, so the frame rate stays up over a million points. The figures in `packages/charts/docs/capabilities.md` come from here.

## Examples

### Measure a million points

Load up to a million points per series and hover: the times and the frame rate say what drawing and a hover cost here.

```tsx
/* Lines and the area downsample on their own above two points per pixel
   column; a hover draws the overlay layer only, which the FPS counter shows.
   Live appends 10 points a second in a moving window, and the axis width stays
   calm through its hysteresis. Not photographed: it measures the
   moment it runs. */

import { useCallback, useEffect, useRef, useState } from "react";
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
import type { ChartPerf } from "@umriss-ui/charts";

/* A small LCG - the same numbers on every run. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

interface LoadPoint {
  t: number;
  s1: number;
  s2: number;
  s3: number;
  /** A state code 0..3 that holds for a run of about ten points. */
  st: number;
  /** Column, row and level of the same points laid out as a grid 200 rows high. */
  c: number;
  r: number;
  lvl: number;
}

const STATES = [
  { label: "A", color: "#2f6fb4" },
  { label: "B", color: "#c08a2e" },
  { label: "C", color: "#8a8f98" },
  { label: "D", color: "#6b8e5a" },
] as const;
const ROWS = 200;

/** Three random walks of any length, with a state, and a grid laid over them. */
function load(seed: number, n: number): LoadPoint[] {
  const r = random(seed);
  const points = new Array<LoadPoint>(n);
  let s1 = 100;
  let s2 = 60;
  let s3 = 140;
  let st = 0;
  const columns = Math.max(1, Math.ceil(n / ROWS));
  for (let i = 0; i < n; i++) {
    s1 += r() - 0.5;
    s2 += (r() - 0.5) * 0.8;
    s3 += (r() - 0.5) * 1.2;
    if (r() < 0.1) st = Math.floor(r() * 4);
    const c = i % columns;
    const row = Math.floor(i / columns);
    points[i] = { t: i, s1, s2, s3, st, c, r: row, lvl: 0.5 + 0.5 * Math.sin(c / 40) * Math.cos(row / 15) + (r() - 0.5) * 0.2 };
  }
  return points;
}

const SIZES = [1_000, 100_000, 1_000_000] as const;
const WINDOW = 400;

/** Series kinds of the measuring run. A filled mark and a rectangle per point are
    different work from a stroked path - the measurements in the capability record
    must not hold for lines only. */
type Mode = "lines" | "mixed" | "bars" | "scatter" | "state" | "matrix";
const MODES: readonly Mode[] = ["lines", "mixed", "bars", "scatter", "state", "matrix"];

/** How many points along x (columns for the matrix) a zoom shows. */
const ZOOMED = 400;

export default function Benchmark() {
  const [data, setData] = useState<readonly LoadPoint[]>(() => load(5, 1_000));
  const { Chart, XAxis, YAxis, Line, Bar, Area, Scatter, StateBand, Matrix, setDomain } = useChart(data);
  const [perf, setPerf] = useState<ChartPerf>({
    materializeMs: 0,
    seriesDrawMs: 0,
    points: 0,
  });
  const [fps, setFps] = useState<number | null>(null);
  const [live, setLive] = useState(false);
  const [mode, setMode] = useState<Mode>("lines");
  /* The time the chart's handlers take for one pointer move, the mean of the
     last twenty: the hit test of the matrix is in it. */
  const [hoverMs, setHoverMs] = useState<number | null>(null);
  const moves = useRef<number[]>([]);
  const moveStart = useRef(0);
  const matrix = mode === "matrix";
  const columns = matrix ? Math.max(1, Math.ceil(data.length / ROWS)) : data.length;
  const course = mode === "lines" || mode === "mixed";

  const onPerf = useCallback((p: ChartPerf) => setPerf(p), []);

  /* FPS measurement: runs only while the pointer is over the plot area. What is
     measured is the actual frame rate of the page - if it drops, the series layer
     is drawing along (which it must not do on a hover). */
  const measuring = useRef<number | null>(null);
  const startMeasuring = useCallback(() => {
    if (measuring.current !== null) return;
    let frames = 0;
    let start = performance.now();
    const step = () => {
      frames++;
      const now = performance.now();
      if (now - start >= 400) {
        setFps(Math.round((frames * 1000) / (now - start)));
        frames = 0;
        start = now;
      }
      measuring.current = requestAnimationFrame(step);
    };
    measuring.current = requestAnimationFrame(step);
  }, []);
  const stopMeasuring = useCallback(() => {
    if (measuring.current !== null) cancelAnimationFrame(measuring.current);
    measuring.current = null;
    setFps(null);
  }, []);
  useEffect(() => stopMeasuring, [stopMeasuring]);

  /* Streaming test: 10 points/s, a travelling window. */
  useEffect(() => {
    if (!live) return;
    const r = random(4711);
    const timer = window.setInterval(() => {
      setData((previous) => {
        const last = previous[previous.length - 1];
        if (last === undefined) return previous;
        const next: LoadPoint = {
          t: last.t + 1,
          s1: last.s1 + (r() - 0.5) * 3,
          s2: last.s2 + (r() - 0.5) * 2.4,
          s3: last.s3 + (r() - 0.5) * 3.6,
          st: r() < 0.1 ? Math.floor(r() * 4) : last.st,
          c: last.c + 1,
          r: last.r,
          lvl: last.lvl,
        };
        const grown = [...previous, next];
        return grown.length > WINDOW ? grown.slice(grown.length - WINDOW) : grown;
      });
    }, 100);
    return () => window.clearInterval(timer);
  }, [live]);

  const loadPoints = (n: number) => {
    setLive(false);
    setData(load(5, n));
    setDomain("x", null);
    moves.current = [];
  };
  const pick = (m: Mode) => {
    setMode(m);
    setDomain("x", null);
    moves.current = [];
  };
  /* A span in the middle of the data: a small part of the volume is in view. */
  const zoom = () => {
    const from = Math.floor(columns / 2);
    setDomain("x", [from, from + ZOOMED]);
  };

  return (
    <>
      <div className="demo-actions">
        {SIZES.map((n) => (
          <button key={n} className="demo-button" data-role={`points-${n}`} onClick={() => loadPoints(n)}>
            {n.toLocaleString("en-US")} points
          </button>
        ))}
        <button className="demo-button" onClick={() => setLive((v) => !v)}>{live ? "Stop live" : "Live"}</button>
        {MODES.map((m) => (
          <button key={m} className="demo-button" data-role={`kind-${m}`} aria-pressed={mode === m} onClick={() => pick(m)}>
            {m}
          </button>
        ))}
        <button className="demo-button" data-role="zoom" onClick={zoom}>
          Zoom in
        </button>
        <button className="demo-button" data-role="unzoom" onClick={() => setDomain("x", null)}>
          Whole
        </button>
      </div>
      <dl className="metrics">
        <div>
          <dt>Points</dt>
          <dd data-role="points">{perf.points.toLocaleString("en-US")}</dd>
        </div>
        <div>
          <dt>Materialisation</dt>
          <dd data-role="materialize-ms">{perf.materializeMs.toFixed(1)} ms</dd>
        </div>
        <div>
          <dt>Series draw</dt>
          <dd data-role="series-draw-ms">{perf.seriesDrawMs.toFixed(1)} ms</dd>
        </div>
        <div>
          <dt>FPS (hover)</dt>
          <dd>{fps === null ? "–" : fps}</dd>
        </div>
        <div>
          <dt>Pointer move</dt>
          <dd data-role="hover-ms">{hoverMs === null ? "–" : `${hoverMs.toFixed(2)} ms`}</dd>
        </div>
      </dl>
      <div
        data-role="plot"
        onPointerEnter={startMeasuring}
        onPointerLeave={stopMeasuring}
        onPointerMoveCapture={() => {
          moveStart.current = performance.now();
        }}
        onPointerMove={() => {
          moves.current.push(performance.now() - moveStart.current);
          if (moves.current.length > 20) moves.current.shift();
          setHoverMs(moves.current.reduce((a, b) => a + b, 0) / moves.current.length);
        }}
      >
        <Chart
          height={320}
          ariaLabel="Benchmark with three series"
          onPerf={onPerf}
        >
          <XAxis value={matrix ? "c" : "t"} label="Index" domain="data" zoomable />
          <YAxis domain={mode === "state" ? [0, 1] : undefined} />
          {mode === "scatter" ? <Scatter value="s1" name="S1" /> : null}
          {mode === "state" ? <StateBand value="st" states={STATES} name="State" /> : null}
          {matrix ? <Matrix value="r" level="lvl" name="Level" /> : null}
          {mode === "bars" ? <Bar value="s1" name="S1" /> : null}
          {course ? mode === "lines" ? <Line value="s1" name="S1" /> : <Bar value="s1" name="S1" /> : null}
          {course ? mode === "lines" ? <Line value="s2" name="S2" /> : <Area value="s2" name="S2" /> : null}
          {course ? <Line value="s3" name="S3" /> : null}
          <Legend placement="top" />
          <Tooltip mode="x" />
        </Chart>
      </div>
    </>
  );
}
```

### Draw a week of seconds

Two series of 604,800 readings each: every pixel column draws first, lowest, highest and last, so a spike stays; zoom in for every reading.

```tsx
/* Not photographed, like the benchmark above it: it measures the series draw. */

import { useCallback, useState } from "react";
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
import type { ChartPerf } from "@umriss-ui/charts";

/* Data from the operations world, written out here so the example runs on its own. */

/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();

const MINUTE = 60_000;

interface Service {
  id: string;
  name: string;
  team: string;
  /** 1 is customer-facing and pages at night; 3 waits for the morning. */
  tier: 1 | 2 | 3;
  /** The latency objective: the 95th percentile stays below this, in ms. */
  latencySlo: number;
  /** The availability promised for a month, in per cent. */
  availabilityTarget: number;
}

const SERVICES: readonly Service[] = [
  { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
  { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
  { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
  { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
  { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
  { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
  { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
  { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];

interface MetricPoint {
  t: number;
  /** Median and 95th percentile latency, in ms. */
  p50: number;
  p95: number;
  /** Failed requests, in per cent. */
  errorRate: number;
  /** Requests per minute. */
  requests: number;
}

/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;

const HOUR = 60 * MINUTE;

const DAY = 24 * HOUR;

/** Monday, 9 March 2026, 00:00 - the start of last week. */
const LAST_WEEK = at(9, 0);

/** Search returned nothing for some regions (INC-1043): Saturday, 18:30 to 19:55. */
const SEARCH_OUTAGE = [at(14, 18, 30) - LAST_WEEK, at(14, 19, 55) - LAST_WEEK] as const;

/** A service's last week, Monday to Monday, one point every `step`
    milliseconds: busy by day, quieter at the weekend. During INC-1043 Search's
    95th percentile more than doubles and its errors climb. The hour is counted,
    not asked of a Date - a week of seconds is 604,800 points. */
function week(serviceId: string, step: number): MetricPoint[] {
  const index = SERVICES.findIndex((one) => one.id === serviceId);
  const service = SERVICES[index];
  if (service === undefined) throw new Error(`No service "${serviceId}".`);
  const r = random(2000 + index * 37);
  const n = Math.floor((7 * DAY) / step);
  const points = new Array<MetricPoint>(n);
  /* The drift wanders at the same pace whatever the step. */
  const keep = Math.pow(0.8, step / (5 * MINUTE));
  const kick = 0.08 * Math.sqrt(1 - keep * keep) / 0.6;
  let drift = 0;
  for (let i = 0; i < n; i++) {
    const offset = i * step;
    const day = Math.floor(offset / DAY);
    const hour = (offset % DAY) / HOUR;
    const load = (day >= 5 ? 0.85 : 1) * (0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16)));
    drift = keep * drift + (r() - 0.5) * kick;
    const outage = serviceId === "search" && offset >= SEARCH_OUTAGE[0] && offset < SEARCH_OUTAGE[1];
    const p95 = Math.round(service.latencySlo * (0.45 + 0.2 * load + drift) * (outage ? 2.3 : 1) * (1 + r() * 0.08));
    points[i] = {
      t: LAST_WEEK + offset,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (outage ? 3 : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    };
  }
  return points;
}

export default function WeekOfSeconds() {
  // Made on first render, not on import: 604,800 readings are no page's
  // business until this one is open.
  const [data] = useState(() => week("search", 1000));
  const { Chart, XAxis, YAxis, Line } = useChart(data);
  const [drawMs, setDrawMs] = useState(0);
  const onPerf = useCallback((p: ChartPerf) => setDrawMs(p.seriesDrawMs), []);
  return (
    <>
      <dl className="metrics">
        <div>
          <dt>Points</dt>
          <dd>{(2 * data.length).toLocaleString("en-US")}</dd>
        </div>
        <div>
          <dt>Series draw</dt>
          <dd>{drawMs.toFixed(1)} ms</dd>
        </div>
      </dl>
      <Chart height={300} ariaLabel="Search latency and requests over last week, a reading a second" onPerf={onPerf}>
        <XAxis value="t" time domain="data" zoomable />
        <YAxis label="ms" domain="visible" />
        <YAxis id="requests" position="right" label="Requests/min" domain="visible" />
        <Line value="p95" name="p95" />
        <Line value="requests" yAxisId="requests" name="Requests" />
        <Legend placement="top" />
        <Tooltip mode="x" />
      </Chart>
    </>
  );
}
```

## Keyboard

The keys of [Chart](https://romanhaendler.github.io/umriss-ui/charts/chart/#keyboard-chart) apply here.

## Props on this page

From `ChartProps` — the full table stands on [Chart](https://romanhaendler.github.io/umriss-ui/charts/chart/#type-ChartProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`height`](https://romanhaendler.github.io/umriss-ui/charts/chart/#ChartProps-height) | `number` | fills the frame, at least 300 px | Height in CSS pixels, fixed. Left out, the chart fills its frame - a stretched grid cell, a card the grid stretches, a container of definite height - and never goes below 300 px, which is also its height in a frame without one. A different floor goes through `style`'s `minHeight` ([ADR-0050](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0050-a-chart-fills-its-frame-never-below-its-floor.md)). |
| [`children`](https://romanhaendler.github.io/umriss-ui/charts/chart/#ChartProps-children) | `ReactNode` | — | The axes, series and companions of this chart. They draw nothing themselves: each registers its configuration and the scene draws it, and the order in the JSX is the drawing order. |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| [`onPerf`](https://romanhaendler.github.io/umriss-ui/charts/chart/#ChartProps-onPerf) | `(perf: `[`ChartPerf`](https://romanhaendler.github.io/umriss-ui/charts/chart/#type-ChartPerf)`) => void` | — | Instrumentation for the benchmark page; not needed otherwise. |

#### Accessibility

| Prop | Type | Default | Description |
|---|---|---|---|
| [`ariaLabel`](https://romanhaendler.github.io/umriss-ui/charts/chart/#ChartProps-ariaLabel) | `string` | — | Required in DEV through a warning. |

From `XAxisProps` — the full table stands on [Axis](https://romanhaendler.github.io/umriss-ui/charts/axis/#type-XAxisProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/axis/#XAxisProps-value) *required* | [`AxisField`](https://romanhaendler.github.io/umriss-ui/charts/axis/#type-AxisField)`<T> \| ((d: T, index: number) => `[`AxisValue`](https://romanhaendler.github.io/umriss-ui/charts/axis/#type-AxisValue)`)` | — | Where a row lies on this axis: a field of the row, compared by its name, or a function of it, compared by its source text as a series' is (`ValueFunction`) - with the same closure limit. A number is a position, a `Date` an instant, a string a category: every distinct one gets a place of its own, in the order the rows bring them, a tick and its name as the label ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)). The type decides, never the content - `"2024"` is a category, `2024` a number - and a row without a value has no place. The axis reads the rows of every series bound to it; where those bring their own `data`, name their row: `<XAxis<HourCount> value="hour" />`. |
| [`domain`](https://romanhaendler.github.io/umriss-ui/charts/axis/#XAxisProps-domain) | `"nice" \| "data" \| readonly [number, number] \| readonly [Date, Date]` | `"nice"` | `"nice"` widens the data's extent to ticks - to named `ticks` where there are any, on a time axis to whole units of a step of hours or longer -, `"data"` keeps it, a pair is fixed: numbers, on a time axis milliseconds or two `Date`s, on a category axis places. On a `zoomable` axis it is the start: what the axis shows while the chart's view names no span for it. |
| [`time`](https://romanhaendler.github.io/umriss-ui/charts/axis/#XAxisProps-time) | `boolean` | — | The values are instants: milliseconds since the epoch, or with `time` said ISO 8601 strings as an API sends them, read by the platform's rule - a date with a time and no offset is local time, a date alone is UTC; a string that is none is not drawn. Rows that hold a `Date` put the axis in time without `time`. Ticks on local boundaries from the minute to the month, labels by level in en-GB with a 24-hour clock - `15:00`, `17 Mar`, `Mar 2026` - and the date on the first tick and on the first of a new day (`17 Mar 00:00`); a day tick there carries the year. The tooltip's x value carries the seconds where the readings lie less than a minute apart. Another language is a `tickFormat`, which is handed the instant in milliseconds. `calendar` implies it. |
| [`zoomable`](https://romanhaendler.github.io/umriss-ui/charts/axis/#XAxisProps-zoomable) | `boolean` | `false` | Zoom and pan: Ctrl or ⌘ with the wheel - and a pinch - zoom around the pointer, a drag, a horizontal wheel or Shift with the wheel pan, a double click goes back to `domain`; with the focus on the chart, + and − zoom, Shift with ← or → pans and 0 goes back. The span shown is part of the chart's view, by this axis' `id` (`useChart`'s `initialView`, `onViewChange`, `setView`, `setDomain`). The plain wheel always scrolls the page. |
| [`id`](https://romanhaendler.github.io/umriss-ui/charts/axis/#XAxisProps-id) | `string` | `"x"` | Axis id, through which series bind themselves. |
| [`label`](https://romanhaendler.github.io/umriss-ui/charts/axis/#XAxisProps-label) | `string` | — | Axis title. |

From `YAxisProps` — the full table stands on [Axis](https://romanhaendler.github.io/umriss-ui/charts/axis/#type-YAxisProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`domain`](https://romanhaendler.github.io/umriss-ui/charts/axis/#YAxisProps-domain) | `"nice" \| "data" \| "visible" \| readonly [number, number]` | `"nice"` | As on the x axis, and `"visible"`: what the series show inside their x axis' domain - a zoomed hour gets the hour's range, not the week's -, widened to ticks as `"nice"` is. Where the x domain is not fixed that is every point. |
| [`position`](https://romanhaendler.github.io/umriss-ui/charts/axis/#YAxisProps-position) | `"left" \| "right"` | `"left"` | Which edge the axis stands at. Several y axes per side are stacked outwards, so that extents of clearly different magnitude stay readable. |
| [`id`](https://romanhaendler.github.io/umriss-ui/charts/axis/#YAxisProps-id) | `string` | `"y"` | Axis id, through which series bind themselves. |
| [`label`](https://romanhaendler.github.io/umriss-ui/charts/axis/#YAxisProps-label) | `string` | — | Axis title. |

From `LineProps` — the full table stands on [Line](https://romanhaendler.github.io/umriss-ui/charts/line/#type-LineProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/line/#LineProps-value) *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/line/#type-Value)`<T>` | — | Y value - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap. |
| [`yAxisId`](https://romanhaendler.github.io/umriss-ui/charts/line/#LineProps-yAxisId) | `string` | `"y"` | Binding to a y axis. |
| [`name`](https://romanhaendler.github.io/umriss-ui/charts/line/#LineProps-name) | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up. |

From `AreaProps` — the full table stands on [Area](https://romanhaendler.github.io/umriss-ui/charts/area/#type-AreaProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/area/#AreaProps-value) *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/area/#type-Value)`<T>` | — | Upper edge - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap. |
| [`name`](https://romanhaendler.github.io/umriss-ui/charts/area/#AreaProps-name) | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up. |

From `BarProps` — the full table stands on [Bar](https://romanhaendler.github.io/umriss-ui/charts/bar/#type-BarProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/bar/#BarProps-value) *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/bar/#type-Value)`<T>` | — | Height - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap. |
| [`name`](https://romanhaendler.github.io/umriss-ui/charts/bar/#BarProps-name) | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up. |

From `ScatterProps` — the full table stands on [Scatter](https://romanhaendler.github.io/umriss-ui/charts/scatter/#type-ScatterProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/scatter/#ScatterProps-value) *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/scatter/#type-Value)`<T>` | — | Y value - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap. |
| [`name`](https://romanhaendler.github.io/umriss-ui/charts/scatter/#ScatterProps-name) | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up. |

From `StateBandProps` — the full table stands on [StateBand](https://romanhaendler.github.io/umriss-ui/charts/stateband/#type-StateBandProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/stateband/#StateBandProps-value) *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/stateband/#type-Value)`<T>` | — | Index of the state in `states` - a number field of the row or a function of it; null/undefined/NaN/±Infinity is a gap - and a gap stays a hole, it gets no colour for "unknown". A colour would be a claim about the interval. |
| [`states`](https://romanhaendler.github.io/umriss-ui/charts/stateband/#StateBandProps-states) *required* | `readonly `[`StateEntry`](https://romanhaendler.github.io/umriss-ui/charts/stateband/#type-StateEntry)`[]` | — | The closed set of states, in the order of their codes. |
| [`name`](https://romanhaendler.github.io/umriss-ui/charts/stateband/#StateBandProps-name) | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up. |

From `MatrixProps` — the full table stands on [Matrix](https://romanhaendler.github.io/umriss-ui/charts/matrix/#type-MatrixProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/charts/matrix/#MatrixProps-value) *required* | [`AxisField`](https://romanhaendler.github.io/umriss-ui/charts/matrix/#type-AxisField)`<T, number \| string \| null \| undefined> \| ((d: T, index: number) => number \| string \| null \| undefined)` | — | The row on the y axis - a field of the data's row or a function of it. A number is a position, as every series' y is; a string is a category: the y axis then has a named row per distinct one, the first at the top, in the order the data bring them or the y axis' `categories` ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)). Never an instant: a `Date` is refused. |
| [`level`](https://romanhaendler.github.io/umriss-ui/charts/matrix/#MatrixProps-level) *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/matrix/#type-Value)`<T>` | — | The level that decides the colour - a number field of the row or a function of it. A missing level is a hole, not a zero. |
| [`name`](https://romanhaendler.github.io/umriss-ui/charts/matrix/#MatrixProps-name) | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up. |

From `TooltipProps` — the full table stands on [Tooltip](https://romanhaendler.github.io/umriss-ui/charts/tooltip/#type-TooltipProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`mode`](https://romanhaendler.github.io/umriss-ui/charts/tooltip/#TooltipProps-mode) | `"x" \| "nearest"` | `"x"` | "x": every series at the x position; "nearest": only the nearest point. |

From `LegendProps` — the full table stands on [Legend](https://romanhaendler.github.io/umriss-ui/charts/legend/#type-LegendProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`placement`](https://romanhaendler.github.io/umriss-ui/charts/legend/#LegendProps-placement) | `"top" \| "bottom"` | `"top"` | Which side of the plot area the legend stands on. |
