# Area

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

A filled course (an area chart): down to zero for a quantity, between two edges for a range, stacked for parts of a whole.

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

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

Without `baseline` the foot is 0 and stays in the y extent. A range's lower edge is the `baseline` of the same series, not a second series - where either edge is missing, the fill has a hole.

## Examples

### Fill down to zero

Without `baseline` an area is filled down to 0, and 0 stays in the y extent - a quantity is read from its foot.

```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 Filled() {
  const { Chart, XAxis, YAxis, Area } = useChart(CHECKOUT);
  return (
    <Chart height={260} ariaLabel="Checkout's requests per minute today">
      <XAxis value="t" time />
      <YAxis label="Requests/min" />
      <Area value="requests" name="Requests" fillOpacity={0.35} strokeWidth={2} />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

### Fill a range between two edges

Give `baseline` the lower edge and the area becomes a range; where either edge is missing, the fill has a hole. `dash` breaks the outline, never the fill.

```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 HOUR = 60 * MINUTE;

interface ExpectedPoint {
  t: number;
  /** The range the requests per minute are expected in; `null` where the
      job that computes it did not run. */
  low: number | null;
  high: number | null;
}

/** The range a service's requests per minute are expected in today, every
    five minutes - the day's usual curve, give or take 15 %. The job that
    computes it skipped 03:00 to 04:00. */
function expected(serviceId: string): ExpectedPoint[] {
  const service = SERVICES.find((one) => one.id === serviceId);
  if (service === undefined) throw new Error(`No service "${serviceId}".`);
  const points: ExpectedPoint[] = [];
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / HOUR;
    const usual = VOLUME[service.tier] * (0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16)));
    const skipped = hour >= 3 && hour < 4;
    points.push({ t, low: skipped ? null : Math.round(usual * 0.85), high: skipped ? null : Math.round(usual * 1.15) });
  }
  return points;
}

const CHECKOUT = metrics("checkout");
const EXPECTED = expected("checkout");

export default function Corridor() {
  const { Chart, XAxis, YAxis, Area, Line } = useChart(CHECKOUT);
  return (
    <Chart height={280} ariaLabel="Checkout's requests inside the range they are expected in">
      <XAxis value="t" time />
      <YAxis label="Requests/min" />
      {/* The outline runs along the upper edge; dashed, it reads as expected, not measured. */}
      <Area
        data={EXPECTED}
        value="high"
        baseline="low"
        name="Expected range"
        strokeWidth={1}
        dash={[4, 3]}
      />
      <Line value="requests" name="Requests" strokeWidth={1.75} />
      <Legend placement="top" />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

### Stack areas

Areas with the same `stack` stand on the ones before them: the top edge is the whole, each band one depot's part. Hide a depot from the legend, and the stack closes over the gap.

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

/* Data from the logistics 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 MINUTE = 60_000;

const on = (month: number, day: number, hours = 0) => new Date(2026, month - 1, day, hours).getTime();

const HOUR = 60 * MINUTE;

interface DepotHour {
  t: number;
  /** Parcels loaded per hour at each depot. */
  north: number;
  river: number;
  east: number;
}

/** Monday 06:00 to Tuesday 06:00, hour by hour. East Gate closes at night and
    loads nothing - 0, not nothing. */
const PARCELS_PER_HOUR: readonly DepotHour[] = (() => {
  const r = random(906);
  return Array.from({ length: 25 }, (_, i) => {
    const hour = (6 + i) % 24;
    const night = hour >= 22 || hour < 6;
    return {
      t: on(3, 16, 6) + i * HOUR,
      north: Math.round(120 + r() * 30),
      river: Math.round((night ? 60 : 90) + r() * 25),
      east: night ? 0 : Math.round(70 + r() * 30),
    };
  });
})();

export default function StackedAreas() {
  const { Chart, XAxis, YAxis, Area } = useChart(PARCELS_PER_HOUR);

  return (
    <Chart height={280} ariaLabel="Parcels loaded per hour at three depots, stacked">
      <XAxis value="t" time domain="data" />
      <YAxis label="Parcels per hour" />
      <Area value="north" name="North" stack="depots" fillOpacity={0.5} strokeWidth={1} />
      <Area value="river" name="Riverside" stack="depots" fillOpacity={0.5} strokeWidth={1} />
      <Area value="east" name="East Gate" stack="depots" fillOpacity={0.5} strokeWidth={1} />
      <Legend placement="top" />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

### Show shares of the whole over time

`normalize` on a stack makes every hour sum to 100 %: the axis reads in per cent, and `format` still writes the tooltip's readings in tonnes.

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

/* Data from the logistics 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 HOUR = 3_600_000;

const on = (month: number, day: number, hours = 0) => new Date(2026, month - 1, day, hours).getTime();

interface DepotLoad {
  t: number;
  /** Freight loaded per hour at each depot, in tonnes. */
  north: number;
  river: number;
  east: number;
}

/** Monday 06:00 to Tuesday 06:00, hour by hour. East Gate closes at night, and
    Riverside's night trucks take over its share. */
const TONNES_PER_HOUR: readonly DepotLoad[] = (() => {
  const r = random(911);
  return Array.from({ length: 25 }, (_, i) => {
    const hour = (6 + i) % 24;
    const night = hour >= 22 || hour < 6;
    return {
      t: on(3, 16, 6) + i * HOUR,
      north: Math.round((6 + r() * 2) * 10) / 10,
      river: Math.round(((night ? 7 : 4) + r() * 1.5) * 10) / 10,
      east: night ? 0 : Math.round((3.5 + r() * 2) * 10) / 10,
    };
  });
})();

const tonnes = (v: number) => `${v.toFixed(1)} t`;

export default function Shares() {
  const { Chart, XAxis, YAxis, Area } = useChart(TONNES_PER_HOUR);
  return (
    <Chart height={280} ariaLabel="Each depot's share of the freight loaded per hour">
      <XAxis value="t" time domain="data" />
      <YAxis label="Share of the hour" />
      <Area value="north" name="North" stack="depots" normalize format={tonnes} fillOpacity={0.5} strokeWidth={1} />
      <Area value="river" name="Riverside" stack="depots" normalize format={tonnes} fillOpacity={0.5} strokeWidth={1} />
      <Area value="east" name="East Gate" stack="depots" normalize format={tonnes} fillOpacity={0.5} strokeWidth={1} />
      <Legend placement="top" />
      <Tooltip mode="x" />
    </Chart>
  );
}
```

## When to use something else

- A value without a natural zero → Line

## Keyboard

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

## API

### `AreaProps<T>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `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.<br>Shown in: [Fill down to zero](https://romanhaendler.github.io/umriss-ui/charts/area/#filled), [Fill a range between two edges](https://romanhaendler.github.io/umriss-ui/charts/area/#corridor), [Stack areas](https://romanhaendler.github.io/umriss-ui/charts/area/#stacked) and 5 more |
| `baseline` | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/area/#type-Value)`<T>` | a fixed baseline at 0 | Lower edge - a number field of the row or a function of it. In a `stack` the stack below is the lower edge, and this is not read.<br>Shown in: [Fill a range between two edges](https://romanhaendler.github.io/umriss-ui/charts/area/#corridor), [Mix series kinds (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#mixed), [Mark every kind (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#marks-on-every-kind) |
| `xAxisId` | `string` | `"x"` | Binding to an x axis.<br>Shown in: [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: [Bind every kind of series to a second axis (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#every-kind-on-a-second-axis) |
| `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Fill a range between two edges](https://romanhaendler.github.io/umriss-ui/charts/area/#corridor), [Bind every kind of series to a second axis (Axis)](https://romanhaendler.github.io/umriss-ui/charts/axis/#every-kind-on-a-second-axis) |
| `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: [Fill down to zero](https://romanhaendler.github.io/umriss-ui/charts/area/#filled), [Fill a range between two edges](https://romanhaendler.github.io/umriss-ui/charts/area/#corridor), [Stack areas](https://romanhaendler.github.io/umriss-ui/charts/area/#stacked) and 5 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: [Show shares of the whole over time](https://romanhaendler.github.io/umriss-ui/charts/area/#shares) |
| `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value. |
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it. |
| `stack` | `string` | — | The stack this series stands in: every Bar and Area with the same id on the same x and y axis stands on the ones registered before it. A gap stacks as zero; negative values stack downward from zero. The tooltip names each series' own value and the stack's total.<br>Shown in: [Stack areas](https://romanhaendler.github.io/umriss-ui/charts/area/#stacked), [Show shares of the whole over time](https://romanhaendler.github.io/umriss-ui/charts/area/#shares) |
| `normalize` | `boolean` | — | On any member of a stack, every x of the stack sums to 100 %: each value becomes its share, and the y axis reads in percent unless it has a `tickFormat`. The total stays the readings' own sum, written in the series' `format`.<br>Shown in: [Show shares of the whole over time](https://romanhaendler.github.io/umriss-ui/charts/area/#shares) |
| `fillOpacity` | `number` | `0.18` | Opacity of the fill, from 0 to 1; the outline stays fully opaque.<br>Shown in: [Fill down to zero](https://romanhaendler.github.io/umriss-ui/charts/area/#filled), [Stack areas](https://romanhaendler.github.io/umriss-ui/charts/area/#stacked), [Show shares of the whole over time](https://romanhaendler.github.io/umriss-ui/charts/area/#shares) |
| `strokeWidth` | `number` | `1.5` | Width of the outline along the upper edge in CSS pixels; 0 leaves it out.<br>Shown in: [Fill down to zero](https://romanhaendler.github.io/umriss-ui/charts/area/#filled), [Fill a range between two edges](https://romanhaendler.github.io/umriss-ui/charts/area/#corridor), [Stack areas](https://romanhaendler.github.io/umriss-ui/charts/area/#stacked) and 2 more |
| `dash` | `readonly number[]` | a solid outline | Dash pattern of the outline as a run of lengths in CSS pixels. The fill stays whole.<br>Shown in: [Fill a range between two edges](https://romanhaendler.github.io/umriss-ui/charts/area/#corridor) |

### 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;
```
