# Line

> Package `@umriss-ui/charts`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/charts/line/>. 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).

Joins the readings of a series into a course (a line chart, a trend). Reach for it for anything measured over time: latency, a balance, hours left.

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

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

A missing reading breaks the line; nothing is drawn across a gap. With `step` a value holds until the next one, for things logged only when they change.

## Examples

### One series

A `Line` joins the readings of its `value` in the order of x; the first y axis draws the grid.

```tsx
import { Tooltip, useChart } 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;

/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);

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;

/** A service's latency and error rate every five minutes of today, from
    midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
    climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): 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(1000 + index * 37);
  const points: MetricPoint[] = [];
  let drift = 0;
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / (60 * MINUTE);
    const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
    drift = 0.8 * drift + (r() - 0.5) * 0.08;
    const base = service.latencySlo * (0.45 + 0.2 * load + drift);
    const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
    const p95 = Math.round(base * surge * (1 + r() * 0.08));
    points.push({
      t,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    });
  }
  return points;
}

const CHECKOUT = metrics("checkout");

export default function OneSeries() {
  const { Chart, XAxis, YAxis, Line } = useChart(CHECKOUT);
  return (
    <Chart height={280} ariaLabel="Checkout's 95th percentile latency today">
      <XAxis value="t" time />
      <YAxis label="ms" />
      <Line value="p95" name="Checkout p95" />
      <Tooltip />
    </Chart>
  );
}
```

### Several series and a gap

Each `Line` may bring its own `data`; a `null` reading breaks the line instead of being bridged, and a lone reading keeps its marker.

```tsx
import { Legend, Tooltip, useChart } 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;

/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);

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;

/** A service's latency and error rate every five minutes of today, from
    midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
    climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): 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(1000 + index * 37);
  const points: MetricPoint[] = [];
  let drift = 0;
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / (60 * MINUTE);
    const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
    drift = 0.8 * drift + (r() - 0.5) * 0.08;
    const base = service.latencySlo * (0.45 + 0.2 * load + drift);
    const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
    const p95 = Math.round(base * surge * (1 + r() * 0.08));
    points.push({
      t,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    });
  }
  return points;
}

const CHECKOUT = metrics("checkout");
const BILLING = metrics("billing");
const SIGN_IN = metrics("sign-in");

/* The image service's exporter restarted from 04:25 to 05:45 and sent a
   single reading at 05:05 in between. */
const IMAGES = metrics("images").map((d) => {
  const minutes = new Date(d.t).getHours() * 60 + new Date(d.t).getMinutes();
  const lost = minutes >= 4 * 60 + 25 && minutes <= 5 * 60 + 45 && minutes !== 5 * 60 + 5;
  return lost ? { ...d, p95: null } : d;
});

export default function MultiSeries() {
  const { Chart, XAxis, YAxis, Line } = useChart(CHECKOUT);
  return (
    <Chart height={300} ariaLabel="95th percentile latency of four services today">
      <XAxis value="t" time />
      <YAxis label="ms" />
      <Line value="p95" name="Checkout" />
      <Line data={BILLING} value="p95" name="Billing" />
      <Line data={SIGN_IN} value="p95" name="Sign-in" dash={[4, 4]} />
      <Line data={IMAGES} value="p95" name="Image service" />
      <Legend placement="top" />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

### Hold a value until it changes

A value logged only when it changes holds until the next entry; `step` draws it so, and a gap ends the hold.

```tsx
import { Legend, Tooltip, useChart } 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;

/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);

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;

/** A service's latency and error rate every five minutes of today, from
    midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
    climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): 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(1000 + index * 37);
  const points: MetricPoint[] = [];
  let drift = 0;
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / (60 * MINUTE);
    const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
    drift = 0.8 * drift + (r() - 0.5) * 0.08;
    const base = service.latencySlo * (0.45 + 0.2 * load + drift);
    const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
    const p95 = Math.round(base * surge * (1 + r() * 0.08));
    points.push({
      t,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    });
  }
  return points;
}

interface ReplicaChange {
  t: number;
  /** Checkout's instances from here on; `null` while the autoscaler was paused. */
  replicas: number | null;
}

/** Checkout's instances today, logged only when the autoscaler changes them.
    It was paused for a database upgrade from 03:00 to 04:00; the last entry,
    at now, closes the hold. */
const REPLICAS: readonly ReplicaChange[] = [
  { t: at(17, 0), replicas: 4 },
  { t: at(17, 3), replicas: null },
  { t: at(17, 4), replicas: 3 },
  { t: at(17, 6, 30), replicas: 5 },
  { t: at(17, 8), replicas: 8 },
  { t: at(17, 9, 45), replicas: 12 },
  { t: at(17, 10, 15), replicas: 9 },
  { t: NOW, replicas: 9 },
];

const CHECKOUT = metrics("checkout");

export default function StepLine() {
  const { Chart, XAxis, YAxis, Line } = useChart(CHECKOUT);
  return (
    <Chart height={260} ariaLabel="Checkout's requests and the instances serving them today">
      <XAxis value="t" time />
      <YAxis label="Requests/min" />
      <YAxis id="instances" position="right" domain={[0, 15]} label="Instances" />
      <Line value="requests" name="Requests" />
      <Line data={REPLICAS} value="replicas" yAxisId="instances" name="Instances" step strokeWidth={2} />
      <Legend />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

### Compare progress with a plan

A sprint's burn-down: the ideal as a quiet dashed line, the hours left as the course, which stops where the days to come begin.

```tsx
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";

/* Data from the planning world, written out here so the example runs on its own. */
const at = (day: number, hours = 9, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();

/** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
interface WorkItem {
  id: string;
  /** The project. */
  task: string;
  /** The person. */
  lane: string;
  from: number;
  to: number;
  name: string;
  sprint: string;
  /** Hours, as estimated at planning. */
  estimate: number;
  status: "to do" | "in progress" | "in review" | "done";
}

const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
  ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });

const WORK: readonly WorkItem[] = [
  item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
  item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
  item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
  item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
  item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
  item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
  item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
  item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
  item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
  item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
  item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
  item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
  item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
  item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
  item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
  item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
];

interface BurndownPoint {
  /** The working day, at 17:00. */
  t: number;
  /** Where the line would be, burning evenly. */
  ideal: number;
  /** Hours left, `null` for the days still to come. */
  remaining: number | null;
}

/** Sprint 14's ten working days. The team fell behind in the first week, when
    two were away, and is catching up. */
const BURNDOWN: readonly BurndownPoint[] = (() => {
  const total = WORK.reduce((sum, one) => sum + one.estimate, 0);
  const days = [9, 10, 11, 12, 13, 16, 17, 18, 19, 20];
  const burnt = [0, 22, 38, 60, 71, 98];
  return days.map((day, i) => ({
    t: at(day, 17),
    ideal: Math.round(total * (1 - i / (days.length - 1))),
    remaining: burnt[i] === undefined ? null : total - burnt[i]!,
  }));
})();

const day = (v: number) => new Date(v).toLocaleDateString("en-GB", { weekday: "short", day: "numeric" });

export default function BurnDown() {
  const { Chart, XAxis, YAxis, Line } = useChart(BURNDOWN);
  return (
    <Chart height={260} ariaLabel="Sprint 14: hours left against the ideal">
      <XAxis value="t" ticks={BURNDOWN.map((d) => d.t)} tickFormat={day} />
      <YAxis label="Hours left" />
      <Line value="ideal" name="Ideal" color="var(--uc-color-text)" dash={[4, 4]} />
      <Line value="remaining" name="Remaining" markers="always" strokeWidth={2} />
      <Legend />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

## When to use something else

- A quantity read from its foot → Area
- Single samples with nothing measured between them → Scatter

## Keyboard

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

## API

### `LineProps<T>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `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.<br>Shown in: [One series](https://romanhaendler.github.io/umriss-ui/charts/line/#one-series), [Several series and a gap](https://romanhaendler.github.io/umriss-ui/charts/line/#multi-series), [Hold a value until it changes](https://romanhaendler.github.io/umriss-ui/charts/line/#step) and 47 more |
| `xAxisId` | `string` | `"x"` | Binding to an x axis.<br>Shown in: [Add axes for other magnitudes (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#axes), [Bind every kind of series to a second axis (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#every-kind-on-a-second-axis) |
| `yAxisId` | `string` | `"y"` | Binding to a y axis.<br>Shown in: [Hold a value until it changes](https://romanhaendler.github.io/umriss-ui/charts/line/#step), [Add axes for other magnitudes (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#axes), [Align a second axis to the grid (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#aligned-ticks) and 4 more |
| `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Several series and a gap](https://romanhaendler.github.io/umriss-ui/charts/line/#multi-series), [Hold a value until it changes](https://romanhaendler.github.io/umriss-ui/charts/line/#step), [Tell series apart without colour (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#told-apart-without-colour) and 6 more |
| `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.<br>Shown in: [One series](https://romanhaendler.github.io/umriss-ui/charts/line/#one-series), [Several series and a gap](https://romanhaendler.github.io/umriss-ui/charts/line/#multi-series), [Hold a value until it changes](https://romanhaendler.github.io/umriss-ui/charts/line/#step) and 47 more |
| `format` | `(value: number) => string` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it.<br>Shown in: [Write each value in its unit (Tooltip)](https://romanhaendler.github.io/umriss-ui/charts/tooltip/#value-format), [Show the values as a table (Tooltip)](https://romanhaendler.github.io/umriss-ui/charts/tooltip/#data-table), [Table a long series (Tooltip)](https://romanhaendler.github.io/umriss-ui/charts/tooltip/#a-week-as-a-table) and 1 more |
| `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value.<br>Shown in: [Compare progress with a plan](https://romanhaendler.github.io/umriss-ui/charts/line/#burn-down), [Fix the domain and name the ticks (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#configuration), [Watch latency against its objective (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#watch-latency-against-its-objective) and 1 more |
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it.<br>Shown in: [Compare the budget with the forecast (Scenarios)](https://romanhaendler.github.io/umriss-ui/charts/#compare-budget-with-forecast) |
| `strokeWidth` | `number` | `1.5` | Line width in CSS pixels.<br>Shown in: [Hold a value until it changes](https://romanhaendler.github.io/umriss-ui/charts/line/#step), [Compare progress with a plan](https://romanhaendler.github.io/umriss-ui/charts/line/#burn-down), [Mix series kinds (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#mixed) and 9 more |
| `dash` | `readonly number[]` | a solid line | Dash pattern as a run of lengths in CSS pixels.<br>Shown in: [Several series and a gap](https://romanhaendler.github.io/umriss-ui/charts/line/#multi-series), [Compare progress with a plan](https://romanhaendler.github.io/umriss-ui/charts/line/#burn-down), [Add axes for other magnitudes (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#axes) and 1 more |
| `markers` | `"auto" \| "always" \| "never"` | `"auto"` | When the individual points are drawn as marks: `auto` every point up to 60 points and above that only a point between two gaps - it has no line to be seen by - `always`, or `never`, not even that one.<br>Shown in: [Compare progress with a plan](https://romanhaendler.github.io/umriss-ui/charts/line/#burn-down), [Draw a series per group of long rows (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#series-from-long-rows), [Fix the domain and name the ticks (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#configuration) and 4 more |
| `step` | `boolean` | — | Sample-and-hold: each value holds as a horizontal until the next sample and jumps there - a target, a digital signal. A gap ends the hold at its x. The tooltip reports the sample the hold began with.<br>Shown in: [Hold a value until it changes](https://romanhaendler.github.io/umriss-ui/charts/line/#step) |

### Types on this page

#### `Value<T>`

A value read from the row ([ADR-0048](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0048-charts-are-bound-to-their-rows-by-the-hook.md)): the name of a number field, compared
by its name, or a function, compared by its source text (`ValueFunction`).

```ts
type Value<T> = NumberField<T> | ValueFunction<T>;
```

#### `NumberField<T>`

The fields of a row whose value is a number, or absent.

```ts
type NumberField<T> = NoInfer<
  {
    [K in keyof T & string]-?: T[K] extends number | null | undefined ? K : never;
  }[keyof T & string]
>;
```

#### `ValueFunction<T>`

A **value** computed from the row - the function form of `Value`.
Compared by its source text, not its identity - an inline function is new
on every render. Known limit: one that reads a
changed closure variable under the same text is not run again; pass a new
data reference then. A native or bound function is compared by identity.

```ts
type ValueFunction<T> = (d: T, index: number) => number | null | undefined;
```

## Known limits

- No smoothing: a curve between two readings would invent values nobody measured ([ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/)).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
