# Installation

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

Install the package, draw a first chart and size it in its container. Read this once before the component pages; everything after it assumes it.

```sh
npm install @umriss-ui/charts
```

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

The command installs the package alone: React 19 or newer is its only peer and stays the application's own, and the package depends on nothing else. The stylesheet comes with the JavaScript, so there is nothing to import - `@umriss-ui/charts/styles.css` is there for setups that link stylesheets by hand. Its colours, type and sizes are `--uc-` tokens that fall back onto core's; [core's Theming page](https://romanhaendler.github.io/umriss-ui/core/theming/#charts-tokens) lists them all.

A chart takes its container's width and follows it; give it a `height` (300 px without one). `useChart(rows)` hands out `Chart`, its axes and its series, typed at the row; each reads its value from a row through `value`, a field name or a function - a value that is `null`, `undefined` or not finite is a gap, never a zero.

An instant is a `Date`, milliseconds since 1970, or - where the x axis says `time` - the ISO string an API sends ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)). On a time axis the ticks stand on the viewer's local clock, so a day begins at local midnight. The words are English; German comes from `@umriss-ui/charts/wording/de`.

The charts take German per chart: `wording={GERMAN_CHARTS_WORDING}` from `@umriss-ui/charts/wording/de`, as [the keyboard and screen reader example](https://romanhaendler.github.io/umriss-ui/charts/chart/#keyboard-and-screen-reader) shows. They read no language provider, so this demo has no EN/DE switch in its header.

## Examples

### Draw a first chart

Hand [`useChart`](https://romanhaendler.github.io/umriss-ui/charts/api/#useChart) the rows as they come, then give its `Chart` an axis per direction and a series; the x axis and the series read a row through `value`. With `time` the axis reads the instants an API sends as text.

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

/* Sign-ins per hour, as the API sends them: the hour's start in local time. */
const SIGN_INS = [
  { t: "2026-03-17T00:00", signIns: 310 },
  { t: "2026-03-17T01:00", signIns: 220 },
  { t: "2026-03-17T02:00", signIns: 180 },
  { t: "2026-03-17T03:00", signIns: 170 },
  { t: "2026-03-17T04:00", signIns: 205 },
  { t: "2026-03-17T05:00", signIns: 340 },
  { t: "2026-03-17T06:00", signIns: 820 },
  { t: "2026-03-17T07:00", signIns: 1460 },
  { t: "2026-03-17T08:00", signIns: 2130 },
  { t: "2026-03-17T09:00", signIns: 2480 },
  { t: "2026-03-17T10:00", signIns: 2390 },
];

export default function FirstChart() {
  const { Chart, XAxis, YAxis, Line } = useChart(SIGN_INS);
  return (
    <Chart height={240} ariaLabel="Sign-ins per hour today">
      <XAxis value="t" time />
      <YAxis label="Sign-ins per hour" />
      <Line value="signIns" name="Sign-ins" />
      <Tooltip />
    </Chart>
  );
}
```

### Size a chart by its container

Without `width` a chart takes its container's width and follows it; `height` is always yours. Drag the corner, or collapse the pair.

```tsx
import { useState } from "react";
import { 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 SIGN_IN = metrics("sign-in");

function Requests({ width, height }: { width?: number; height: number }) {
  const { Chart, XAxis, YAxis, Line } = useChart(SIGN_IN);
  return (
    <Chart width={width} height={height} ariaLabel="Sign-in requests per minute today">
      <XAxis value="t" time />
      <YAxis />
      <Line value="requests" name="Requests per minute" />
    </Chart>
  );
}

export default function InItsContainer() {
  const [collapsed, setCollapsed] = useState(false);
  return (
    <>
      {/* `resize: horizontal` on the host - the chart measures what it finds. */}
      <div className="resizable">
        <Requests height={200} />
      </div>
      <button className="demo-button" onClick={() => setCollapsed((v) => !v)}>
        {collapsed ? "Expand" : "Collapse"}
      </button>
      {!collapsed && (
        <div className="side-by-side">
          <Requests width={420} height={180} />
          <div className="narrow">
            <Requests height={180} />
          </div>
        </div>
      )}
    </>
  );
}
```

## 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 |
|---|---|---|---|
| [`width`](https://romanhaendler.github.io/umriss-ui/charts/chart/#ChartProps-width) | `number \| "100%"` | `"100%"` | Width in CSS pixels, or `"100%"` for the host's: the chart measures its host and follows it through a ResizeObserver. |
| [`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. |

#### 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" />`. |
| [`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. |
| [`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 |
|---|---|---|---|
| [`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. |
| [`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. |
