Installation

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.

npm install @umriss-ui/charts

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 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). 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 shows. They read no language provider, so this demo has no EN/DE switch in its header.

Examples

Draw a first chart

Hand 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.

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.

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 apply here.

Props on this page

From ChartProps — the full table stands on Chart.

NameTypeDefaultDescription
widthnumber | "100%""100%"Width in CSS pixels, or "100%" for the host's: the chart measures its host and follows it through a ResizeObserver.
heightnumberfills the frame, at least 300 pxHeight 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).
childrenReactNode—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

NameTypeDefaultDescription
ariaLabelstring—Required in DEV through a warning.

From XAxisProps — the full table stands on Axis.

NameTypeDefaultDescription
valuerequiredAxisField<T> | ((d: T, index: number) => 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). 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" />.
timeboolean—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.
labelstring—Axis title.

From YAxisProps — the full table stands on Axis.

NameTypeDefaultDescription
labelstring—Axis title.

From LineProps — the full table stands on Line.

NameTypeDefaultDescription
valuerequiredValue<T>—Y value - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap.
namestring"Series n", after its place in the chartThe name in legend and tooltip. Without one a warning stands in DEV - an unnamed series is a colour nobody can look up.

Every page of @umriss-ui/charts

Getting started

Chart

Series

Limits and alarms

Around the chart

API index