@umriss-ui/charts 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/llms-full.md CHANGED
@@ -1,4 +1,4 @@
1
- # @umriss-ui/charts 0.45.0
1
+ # @umriss-ui/charts 0.47.0
2
2
 
3
3
  > React canvas chart library for data-dense dashboards - line, area, bar, box plot, scatter, control and Pareto charts. Few chart kinds, drawn well, fast.
4
4
 
@@ -2210,6 +2210,74 @@ export default function Picture() {
2210
2210
  }
2211
2211
  ```
2212
2212
 
2213
+ ##### Columns instead of rows
2214
+
2215
+ A course of 100,000 readings in two `Float64Array` columns, with no object per row: the rows are an index array, and the axis and the line read `column[i]` at each index. The rows are memoised on the columns, so a new course is read again; a function compared by its text would keep the old values. A column from Arrow comes as `getChild(name).toArray()`. Columns are no input of [`useChart`](#api-index-1) itself, because a hit, `onSelect`, the tooltip and the data table are per row ([ADR-0048](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0048-charts-are-bound-to-their-rows-by-the-hook.md)).
2216
+
2217
+ ```tsx
2218
+ import { useMemo, useState } from "react";
2219
+ import { Button } from "@umriss-ui/core";
2220
+ import { DataTable, Tooltip, bin, useChart } from "@umriss-ui/charts";
2221
+
2222
+ const COUNT = 100_000;
2223
+
2224
+ /** A small LCG - the same numbers on every computer. */
2225
+ function random(seed: number): () => number {
2226
+ let s = seed >>> 0;
2227
+ return () => {
2228
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
2229
+ return s / 4294967296;
2230
+ };
2231
+ }
2232
+
2233
+ /** One reading a minute, and the pressure read at it, as two columns. */
2234
+ function course(seed: number): { minute: Float64Array; pressure: Float64Array } {
2235
+ const r = random(seed);
2236
+ const minute = new Float64Array(COUNT);
2237
+ const pressure = new Float64Array(COUNT);
2238
+ for (let i = 0; i < COUNT; i++) {
2239
+ minute[i] = i;
2240
+ pressure[i] = 6 + 0.8 * Math.sin(i / 500) + (r() - 0.5) * 0.6;
2241
+ }
2242
+ return { minute, pressure };
2243
+ }
2244
+
2245
+ export default function ColumnsInsteadOfRows() {
2246
+ const [seed, setSeed] = useState(2026);
2247
+ const columns = useMemo(() => course(seed), [seed]);
2248
+ // One index per reading: the rows are read again whenever the columns are new.
2249
+ const rows = useMemo(() => Array.from({ length: columns.pressure.length }, (_, i) => i), [columns]);
2250
+ const counts = useMemo(() => bin(columns.pressure, { count: 20 }), [columns]);
2251
+
2252
+ const { Chart, XAxis, YAxis, Line } = useChart(rows);
2253
+ const { Chart: Histogram, XAxis: CountXAxis, YAxis: CountYAxis, Bar } = useChart(counts);
2254
+ return (
2255
+ <>
2256
+ <div className="pair">
2257
+ <div>
2258
+ <Chart height={260} aria-label="Pressure of 100,000 readings, one a minute">
2259
+ <XAxis value={(_, i) => columns.minute[i]} label="Minute" />
2260
+ <YAxis label="bar" />
2261
+ <Line value={(_, i) => columns.pressure[i]} name="Pressure" />
2262
+ <Tooltip mode="x" />
2263
+ <DataTable />
2264
+ </Chart>
2265
+ </div>
2266
+ <div>
2267
+ <Histogram height={260} aria-label="How often each pressure was read, in 20 bins">
2268
+ <CountXAxis value="x" label="bar" />
2269
+ <CountYAxis label="readings" />
2270
+ <Bar value="count" name="Readings" barWidth={0.94} />
2271
+ <Tooltip mode="x" />
2272
+ </Histogram>
2273
+ </div>
2274
+ </div>
2275
+ <Button onClick={() => setSeed((s) => s + 1)}>New course</Button>
2276
+ </>
2277
+ );
2278
+ }
2279
+ ```
2280
+
2213
2281
  #### When to use something else
2214
2282
 
2215
2283
  - A single number with a small trend beside it → `Sparkline` or `Stat` from @umriss-ui/core
@@ -2244,16 +2312,16 @@ Under forced colours the chart draws in the system colours and tells its series
2244
2312
  | Prop | Type | Default | Description |
2245
2313
  |---|---|---|---|
2246
2314
  | `ref` | `Ref<HTMLDivElement>` | — | The root element, the `<div>` that `className`, `style`, `id` and the `data-*` attributes reach. |
2247
- | `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.<br>Shown in: [Size a chart by its container (Installation)](#installation) |
2248
- | `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)).<br>Shown in: [Mix series kinds](#chart-1), [Say there is nothing to show](#chart-1), [Read it by keyboard and screen reader](#chart-1) and 79 more |
2315
+ | `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.<br>Shown in: [Size a chart by its container (Installation)](#installation), [A legend beside the plot (Legend)](#legend) |
2316
+ | `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)).<br>Shown in: [Mix series kinds](#chart-1), [Say there is nothing to show](#chart-1), [Read it by keyboard and screen reader](#chart-1) and 93 more |
2249
2317
  | `padding` | `number \| Partial<``Padding``>` | — | Outer spacing of the plot area in CSS pixels.<br>Shown in: [Fix the domain and name the ticks (Axis)](#axis) |
2250
2318
  | `empty` | `ReactNode` | the wording's `empty` | Shown in the middle of the plot area when no visible series has a point to show - no data, only gaps, or every series hidden. Axes and frame stay. Not while `loading`.<br>Shown in: [Say there is nothing to show](#chart-1) |
2251
2319
  | `loading` | `boolean` | `false` | The rows are on their way, as the table's `loading` says ([ADR-0042](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0042-a-rows-height-never-follows-what-it-shows.md)). A course already drawn stays: it dims after a moment and takes no pointer until the answer is in. Where there is nothing to show, the empty message waits. The plot is `aria-busy` meanwhile.<br>Shown in: [Show that rows are on their way](#chart-1) |
2252
2320
  | `wording` | `Partial<``ChartsWording``>` | `DEFAULT_CHARTS_WORDING` | The chart's words ([ADR-0031](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0031-the-charts-carry-their-own-wording.md)); entries left out fall back to English. Its `locale` also formats every number and every date or time label the chart makes itself (`en-GB`, `de-DE`; the browser's language is not read) - a `tickFormat` or `format` of your own stays in charge of its axis or series. German: `GERMAN_CHARTS_WORDING` from `@umriss-ui/charts/wording/de`.<br>Shown in: [Read it by keyboard and screen reader](#chart-1) |
2253
2321
  | `timeZone` | `string` | the reader's zone | An IANA time zone (`"Europe/Berlin"`) for every time axis of the chart, a calendar axis included: ticks stand on its hours and midnights, and the labels, tooltip, readout, data table and summary write its time, in the wording's `locale`. Where the reader's zone is another, the axis title ends in the zone's name (after mount). An hour a clock change doubles is ticked twice and its labels carry the offset; the hour it removes has no tick. A name Intl does not know throws its `RangeError`. An application on `@umriss-ui/core` passes `useFormats().timeZone`.<br>Shown in: [Name the time zone of the instants (Axis)](#axis) |
2254
2322
  | `encoding` | `"color" \| "marks"` | `"color"` | How series are told apart. `"color"`: by their colour. `"marks"`: by a dash and a marker shape as well - a line, an area and a scatter -, and by a hatch - a bar, a state, a matrix cell, a limit band -, chosen by the same palette place as the colour, so that a reader who does not see the colours still tells them apart; the legend's chips show the same. A caller's own `dash` wins over the place's.<br>Shown in: [Tell series apart without colour](#chart-1), [Mark every kind](#chart-1), [Mark bands, limits and cells](#chart-1) and 1 more |
2255
- | `syncId` | `string` | — | Charts with the same id share the pointer's x position, in domain units: each draws its crosshair there, the tooltip stays with the chart under the pointer. Zoom is not shared this way: charts zoom together by sending each other their view (`useChart`'s `onViewChange` and `setView`).<br>Shown in: [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Watch latency against its objective (Scenarios)](#watch-latency-against-its-objective), [Hold the tile length in control (Scenarios)](#hold-the-tile-length-in-control) |
2256
- | `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.<br>Shown in: [Mix series kinds](#chart-1), [Say there is nothing to show](#chart-1), [Read it by keyboard and screen reader](#chart-1) and 79 more |
2323
+ | `syncId` | `string` | — | Charts with the same id share the pointer's x position, in domain units: each draws its crosshair there, the tooltip stays with the chart under the pointer. Zoom is not shared this way: charts zoom together by sending each other their view (`useChart`'s `onViewChange` and `setView`).<br>Shown in: [Small multiples on one y domain (Axis)](#axis), [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Watch latency against its objective (Scenarios)](#watch-latency-against-its-objective) and 1 more |
2324
+ | `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.<br>Shown in: [Mix series kinds](#chart-1), [Say there is nothing to show](#chart-1), [Read it by keyboard and screen reader](#chart-1) and 93 more |
2257
2325
 
2258
2326
  ###### Events
2259
2327
 
@@ -2324,6 +2392,7 @@ Everything the charts put out as text.
2324
2392
  | `count` *required* | `string` | — | |
2325
2393
  | `outlierList` *required* | `(values: readonly string[], more: number) => string` | — | |
2326
2394
  | `downsampled` *required* | `(readings: number) => string` | — | Added to the caption where the table shows the downsampled course: how many readings its rows stand for. |
2395
+ | `levelRange` *required* | `(from: string, to: string) => string` | — | Added to the name of a matrix' legend entry that has a colour bar: the range its colours span, both ends written in the matrix' format. The bar's own values are hidden from assistive technology. |
2327
2396
 
2328
2397
  ###### `ChartPerf`
2329
2398
 
@@ -2338,9 +2407,9 @@ Measurements of the instrumentation for the benchmark page.
2338
2407
  #### Known limits
2339
2408
 
2340
2409
  - No error state of its own: where the rows could not be loaded, show an `Alert` from @umriss-ui/core. Waiting for them is `loading`.
2341
- - No pie, donut, radar or candlestick, no smoothing, animation or export, no WebGL ([ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md)).
2410
+ - No pie, donut, radar or candlestick, no smoothing, animation or export, no WebGL ([ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md)). The share of a whole is bars on a percent axis: the Pareto page, "Shares, not counts".
2342
2411
 
2343
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
2412
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
2344
2413
 
2345
2414
  ### Axis
2346
2415
 
@@ -3046,6 +3115,204 @@ export default function TimeZone() {
3046
3115
  }
3047
3116
  ```
3048
3117
 
3118
+ ##### Small multiples on one y domain
3119
+
3120
+ Six charts share a `syncId` and the same `domain` on every `YAxis`. Equal domains give equal tick labels, so the y bands are equally wide and the plot areas line up; axes that fit their own data would start each plot at another x. Which domain is common is the application's decision, not a prop.
3121
+
3122
+ ```tsx
3123
+ import { Tooltip, useChart } from "@umriss-ui/charts";
3124
+
3125
+ /* Data from the operations world, written out here so the example runs on its own. */
3126
+
3127
+ /** A small LCG - the same numbers on every computer. */
3128
+ function random(seed: number): () => number {
3129
+ let s = seed >>> 0;
3130
+ return () => {
3131
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
3132
+ return s / 4294967296;
3133
+ };
3134
+ }
3135
+
3136
+ interface Load {
3137
+ hour: number;
3138
+ /** Processor load, in per cent. */
3139
+ cpu: number;
3140
+ }
3141
+
3142
+ /** A day of hourly load per service: busy by day, each at a level of its own. */
3143
+ const SERVICES = ["Checkout", "Billing", "Sign-in", "Search", "Image service", "Notifications"].map((name, index) => {
3144
+ const r = random(40 + index);
3145
+ const level = 25 + index * 11;
3146
+ const rows: Load[] = Array.from({ length: 24 }, (_, hour) => ({
3147
+ hour,
3148
+ cpu: Math.min(100, Math.round(level * (0.5 + 0.5 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16))) + r() * 8)),
3149
+ }));
3150
+ return { name, rows };
3151
+ });
3152
+
3153
+ function Small({ name, rows }: { name: string; rows: readonly Load[] }) {
3154
+ const { Chart, XAxis, YAxis, Line } = useChart(rows);
3155
+ return (
3156
+ <Chart height={140} syncId="services" aria-label={`${name}, processor load over a day`}>
3157
+ <XAxis value="hour" tickCount={4} />
3158
+ <YAxis domain={[0, 100]} tickCount={3} label={`${name} %`} />
3159
+ <Line value="cpu" name={name} />
3160
+ <Tooltip mode="x" />
3161
+ </Chart>
3162
+ );
3163
+ }
3164
+
3165
+ export default function SmallMultiples() {
3166
+ return (
3167
+ <div data-role="small-multiples" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 8 }}>
3168
+ {SERVICES.map((service) => (
3169
+ <Small key={service.name} {...service} />
3170
+ ))}
3171
+ </div>
3172
+ );
3173
+ }
3174
+ ```
3175
+
3176
+ ##### Values over decades
3177
+
3178
+ The chart's scales are straight: a log scale would break the affine mapping its axes rely on ([ADR-0001](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0001-affine-scale-contract.md), [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md)). So the value is `Math.log10` of the reading, `ticks` stand at 1 to 9 times every power of ten, `tickFormat` labels only the whole powers, and the series' `format` writes `10 ** v`, so that the tooltip shows the reading and not its logarithm.
3179
+
3180
+ ```tsx
3181
+ import { Tooltip, useChart } from "@umriss-ui/charts";
3182
+
3183
+ /* Data from the operations world, written out here so the example runs on its own. */
3184
+
3185
+ interface Burst {
3186
+ hour: number;
3187
+ /** The reading: log lines per minute. */
3188
+ reading: number;
3189
+ /** The reading's logarithm to base 10: what the chart is given. */
3190
+ v: number;
3191
+ }
3192
+
3193
+ /** Log lines per minute over two days: a quiet system, then an incident that
3194
+ grows by decades and dies away again. */
3195
+ const BURST: readonly Burst[] = Array.from({ length: 49 }, (_, hour) => {
3196
+ const reading = Math.max(1, Math.round(10 ** (2.2 + 3.6 * Math.sin((Math.PI * hour) / 40) ** 3 * (hour < 28 ? 1 : 1 - (hour - 28) / 30))));
3197
+ return { hour, reading, v: Math.log10(reading) };
3198
+ });
3199
+
3200
+ /** log10(m * 10^k) for m from 1 to 9 in each of six decades, and the top. */
3201
+ const TICKS = [...Array.from({ length: 6 }, (_, k) => Array.from({ length: 9 }, (_, m) => Math.log10((m + 1) * 10 ** k))).flat(), 6];
3202
+
3203
+ const isDecade = (v: number) => Math.abs(v - Math.round(v)) < 1e-9;
3204
+
3205
+ export default function Decades() {
3206
+ const { Chart, XAxis, YAxis, Line } = useChart(BURST);
3207
+ return (
3208
+ <Chart height={280} aria-label="Log lines per minute over two days, on a scale in decades">
3209
+ <XAxis value="hour" label="Hours" />
3210
+ <YAxis domain={[0, 6]} ticks={TICKS} tickFormat={(v) => (isDecade(v) ? `10^${Math.round(v)}` : "")} label="Lines per minute" />
3211
+ <Line value="v" name="Lines per minute" format={(v) => Math.round(10 ** v).toLocaleString("en")} />
3212
+ <Tooltip mode="x" />
3213
+ </Chart>
3214
+ );
3215
+ }
3216
+ ```
3217
+
3218
+ ##### A trend with pens
3219
+
3220
+ Three pens stand on one time axis, each on a scale of its own with its unit as the label; yesterday's course of one pen is one more dashed `Line`. The pen list is core's `Checkbox` bound to `hidden` and `toggleSeries` (an application with the table binds a `Table` the same way). A pen is a tag of the application's historian, which the chart does not know: it draws the rows it is given.
3221
+
3222
+ ```tsx
3223
+ import { Checkbox, Text } from "@umriss-ui/core";
3224
+ import { Tooltip, useChart } from "@umriss-ui/charts";
3225
+
3226
+ /* Data from the operations world, written out here so the example runs on its own. */
3227
+
3228
+ /** A small LCG - the same numbers on every computer. */
3229
+ function random(seed: number): () => number {
3230
+ let s = seed >>> 0;
3231
+ return () => {
3232
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
3233
+ return s / 4294967296;
3234
+ };
3235
+ }
3236
+
3237
+ const MINUTE = 60_000;
3238
+
3239
+ const DAY = 24 * 60 * MINUTE;
3240
+
3241
+ /** Today, 06:00 to 14:00 local, a reading every ten minutes. */
3242
+ const START = new Date(2026, 2, 17, 6).getTime();
3243
+
3244
+ interface Reading {
3245
+ t: number;
3246
+ temperature: number;
3247
+ pressure: number;
3248
+ flow: number;
3249
+ }
3250
+
3251
+ function course(seed: number): Reading[] {
3252
+ const r = random(seed);
3253
+ return Array.from({ length: 49 }, (_, i) => ({
3254
+ t: START + i * 10 * MINUTE,
3255
+ temperature: Math.round((1180 + 25 * Math.sin(i / 7) + (r() - 0.5) * 8) * 10) / 10,
3256
+ pressure: Math.round((4.2 + 0.5 * Math.sin(i / 11 + 1) + (r() - 0.5) * 0.12) * 100) / 100,
3257
+ flow: Math.round(310 + 60 * Math.sin(i / 9 + 2) + (r() - 0.5) * 14),
3258
+ }));
3259
+ }
3260
+
3261
+ const TODAY = course(11);
3262
+
3263
+ /** Yesterday's temperature as the historian returns it, then shifted by a day to stand on today's axis. */
3264
+ const YESTERDAY = course(12).map((row) => ({ t: row.t - DAY, temperature: row.temperature }));
3265
+
3266
+ const YESTERDAY_ON_TODAY = YESTERDAY.map((row) => ({ ...row, t: row.t + DAY }));
3267
+
3268
+ const PENS = [
3269
+ { name: "Furnace temperature", key: "temperature", unit: "°C", digits: 0 },
3270
+ { name: "Steam pressure", key: "pressure", unit: "bar", digits: 2 },
3271
+ { name: "Cooling flow", key: "flow", unit: "m³/h", digits: 0 },
3272
+ ] as const;
3273
+
3274
+ const YESTERDAY_NAME = "Furnace temperature, yesterday";
3275
+
3276
+ export default function Pens() {
3277
+ const { Chart, XAxis, YAxis, Line, hidden, toggleSeries } = useChart(TODAY);
3278
+ const last = TODAY[TODAY.length - 1] as Reading;
3279
+ return (
3280
+ <div style={{ display: "grid", gap: 8 }}>
3281
+ <Chart height={300} aria-label="Furnace temperature, steam pressure and cooling flow today, and yesterday's temperature">
3282
+ <XAxis value="t" time />
3283
+ <YAxis domain={[1100, 1250]} label="°C" />
3284
+ <YAxis id="bar" domain={[3, 6]} label="bar" />
3285
+ <YAxis id="flow" position="right" domain={[200, 400]} label="m³/h" />
3286
+ <Line value="temperature" name="Furnace temperature" markers="never" />
3287
+ <Line value="temperature" data={YESTERDAY_ON_TODAY} name={YESTERDAY_NAME} dash={[5, 4]} color="var(--uc-series-1)" markers="never" />
3288
+ <Line value="pressure" yAxisId="bar" name="Steam pressure" markers="never" />
3289
+ <Line value="flow" yAxisId="flow" name="Cooling flow" markers="never" />
3290
+ <Tooltip mode="x" />
3291
+ </Chart>
3292
+ <div style={{ display: "grid", gap: 4, maxWidth: 420 }}>
3293
+ {PENS.map((pen) => (
3294
+ <Checkbox
3295
+ key={pen.name}
3296
+ checked={!hidden.includes(pen.name)}
3297
+ onChange={() => {
3298
+ toggleSeries(pen.name);
3299
+ if (pen.key === "temperature") toggleSeries(YESTERDAY_NAME);
3300
+ }}
3301
+ label={
3302
+ <span style={{ display: "flex", gap: 8, minWidth: 380, justifyContent: "space-between" }}>
3303
+ <Text as="span">{pen.name}, {pen.unit}</Text>
3304
+ <Text as="span" mono>{last[pen.key].toFixed(pen.digits)}</Text>
3305
+ </span>
3306
+ }
3307
+ />
3308
+ ))}
3309
+ <Text as="p" size="sm" tone="muted" data-role="hidden">{hidden.join(", ")}</Text>
3310
+ </div>
3311
+ </div>
3312
+ );
3313
+ }
3314
+ ```
3315
+
3049
3316
  #### Keyboard
3050
3317
 
3051
3318
  The keys of [Chart](#chart-1) apply here.
@@ -3056,19 +3323,19 @@ The keys of [Chart](#chart-1) apply here.
3056
3323
 
3057
3324
  | Prop | Type | Default | Description |
3058
3325
  |---|---|---|---|
3059
- | `value` *required* | `AxisField``<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](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" />`.<br>Shown in: [Fix the domain and name the ticks](#axis), [Add axes for other magnitudes](#axis), [Leave out the hours nobody works](#axis) and 79 more |
3326
+ | `value` *required* | `AxisField``<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](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" />`.<br>Shown in: [Fix the domain and name the ticks](#axis), [Add axes for other magnitudes](#axis), [Leave out the hours nobody works](#axis) and 93 more |
3060
3327
  | `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.<br>Shown in: [Bind every kind of series to a second axis](#axis), [Read it by keyboard and screen reader (Chart)](#chart-1), [Gestures and keys (Zoom and pan)](#zoom-and-pan) and 12 more |
3061
3328
  | `position` | `"bottom" \| "top"` | `"bottom"` | Which edge the axis stands at. A second x axis on the opposite edge is how a series counts in a unit of its own.<br>Shown in: [Add axes for other magnitudes](#axis), [Bind every kind of series to a second axis](#axis) |
3062
- | `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.<br>Shown in: [Align a second axis to the grid](#axis), [Name the time zone of the instants](#axis), [Draw a first chart (Installation)](#installation) and 39 more |
3329
+ | `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.<br>Shown in: [Align a second axis to the grid](#axis), [Name the time zone of the instants](#axis), [A trend with pens](#axis) and 42 more |
3063
3330
  | `calendar` | `readonly ``WorkingInterval``[]` | — | Working calendar: the intervals in which time counts. With it the axis stands in working time - nights and weekends are out, and every removed span gets a break mark. The scale stays affine; the mapping happens in materialisation ([ADR-0001](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0001-affine-scale-contract.md)).<br>Shown in: [Leave out the hours nobody works](#axis), [Hand over a picture (Chart)](#chart-1) |
3064
- | `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.<br>Shown in: [Read it by keyboard and screen reader (Chart)](#chart-1), [Gestures and keys (Zoom and pan)](#zoom-and-pan), [Limit the zoom (Zoom and pan)](#zoom-and-pan) and 7 more |
3331
+ | `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.<br>Shown in: [Read it by keyboard and screen reader (Chart)](#chart-1), [Gestures and keys (Zoom and pan)](#zoom-and-pan), [Limit the zoom (Zoom and pan)](#zoom-and-pan) and 8 more |
3065
3332
  | `zoomLimits` | `ZoomLimits` | at most the data's extent or this axis' own `domain`, whichever is wider, at least three data steps | The narrowest and the widest span zoom may reach, in the axis' units - milliseconds on a time axis.<br>Shown in: [Limit the zoom (Zoom and pan)](#zoom-and-pan) |
3066
- | `id` | `string` | `"x"` | Axis id, through which series bind themselves.<br>Shown in: [Add axes for other magnitudes](#axis), [Align a second axis to the grid](#axis), [Bind every kind of series to a second axis](#axis) and 7 more |
3067
- | `label` | `string` | — | Axis title.<br>Shown in: [Fix the domain and name the ticks](#axis), [Add axes for other magnitudes](#axis), [Leave out the hours nobody works](#axis) and 71 more |
3068
- | `tickCount` | `number` | — | Target value; the 1-2-5 algorithm may deviate.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Name the time zone of the instants](#axis) and 9 more |
3069
- | `tickFormat` | `ValueFormat` | — | Label of a tick. Compared by its source text: one that reads a changed closure variable under the same text does not relabel the axis - give it a new text or remount. A bound `Intl.NumberFormat#format` is compared by identity. Or `NumberOptions`: Intl's options in the wording's locale, compared by value; the step's places stay unless the options set digits.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Bind every kind of series to a second axis](#axis) and 21 more |
3333
+ | `id` | `string` | `"x"` | Axis id, through which series bind themselves.<br>Shown in: [Add axes for other magnitudes](#axis), [Align a second axis to the grid](#axis), [Bind every kind of series to a second axis](#axis) and 8 more |
3334
+ | `label` | `string` | — | Axis title.<br>Shown in: [Fix the domain and name the ticks](#axis), [Add axes for other magnitudes](#axis), [Leave out the hours nobody works](#axis) and 85 more |
3335
+ | `tickCount` | `number` | — | Target value; the 1-2-5 algorithm may deviate.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Name the time zone of the instants](#axis) and 10 more |
3336
+ | `tickFormat` | `ValueFormat` | — | Label of a tick. Compared by its source text: one that reads a changed closure variable under the same text does not relabel the axis - give it a new text or remount. A bound `Intl.NumberFormat#format` is compared by identity. Or `NumberOptions`: Intl's options in the wording's locale, compared by value; the step's places stay unless the options set digits.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Bind every kind of series to a second axis](#axis) and 24 more |
3070
3337
  | `grid` | `boolean` | only on the first axis of its orientation | Grid lines.<br>Shown in: [One vehicle through the day (StateBand)](#stateband), [Give each one a lane (StateBand)](#stateband), [See where the vans stand idle (Scenarios)](#see-where-the-vans-stand-idle) |
3071
- | `ticks` | `readonly number[]` | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an hour axis' named hours, a lane grid. An axis that reads categories needs none - every category has its tick.<br>Shown in: [Bind every kind of series to a second axis](#axis), [Mark bands, limits and cells (Chart)](#chart-1), [Compare progress with a plan (Line)](#line) and 7 more |
3338
+ | `ticks` | `readonly number[]` | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an hour axis' named hours, a lane grid. An axis that reads categories needs none - every category has its tick.<br>Shown in: [Bind every kind of series to a second axis](#axis), [Values over decades](#axis), [Mark bands, limits and cells (Chart)](#chart-1) and 9 more |
3072
3339
  | `categories` | `readonly string[]` | — | The categories of the axis, in their order. Without it they stand in the order the rows first bring them. With it the list is the order and the extent: a category no row carries keeps its empty place, and a row whose category is not listed is not drawn - DEV names it. It makes the axis a category axis whatever the rows hold ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)).<br>Shown in: [Fix the order and keep an empty place](#axis) |
3073
3340
  | `categoryLabel` | `(category: string) => string` | the category itself | What is written for a category - under its tick, in the tooltip, the readout and the data table - where the rows hold a key and not a name. Two categories may share a label and keep two places. Compared by its source text, as `tickFormat` is.<br>Shown in: [Fix the order and keep an empty place](#axis), [Tell series apart without colour (Chart)](#chart-1), [Legend above or below (Legend)](#legend) and 1 more |
3074
3341
 
@@ -3076,15 +3343,15 @@ The keys of [Chart](#chart-1) apply here.
3076
3343
 
3077
3344
  | Prop | Type | Default | Description |
3078
3345
  |---|---|---|---|
3079
- | `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.<br>Shown in: [Fix the domain and name the ticks](#axis), [Bind every kind of series to a second axis](#axis), [Mark bands, limits and cells (Chart)](#chart-1) and 11 more |
3346
+ | `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.<br>Shown in: [Fix the domain and name the ticks](#axis), [Bind every kind of series to a second axis](#axis), [Small multiples on one y domain](#axis) and 15 more |
3080
3347
  | `alignTicks` | `boolean` | — | On a further y axis: take the first y axis' tick count and widen this axis' domain until its ticks fall on that one's grid lines, the steps still 1-2-5 - one grid serves both. `tickCount` then does not apply; on the first y axis it changes nothing.<br>Shown in: [Align a second axis to the grid](#axis) |
3081
- | `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.<br>Shown in: [Add axes for other magnitudes](#axis), [Align a second axis to the grid](#axis), [Bind every kind of series to a second axis](#axis) and 7 more |
3082
- | `id` | `string` | `"y"` | Axis id, through which series bind themselves.<br>Shown in: [Add axes for other magnitudes](#axis), [Align a second axis to the grid](#axis), [Bind every kind of series to a second axis](#axis) and 7 more |
3083
- | `label` | `string` | — | Axis title.<br>Shown in: [Fix the domain and name the ticks](#axis), [Add axes for other magnitudes](#axis), [Leave out the hours nobody works](#axis) and 71 more |
3084
- | `tickCount` | `number` | — | Target value; the 1-2-5 algorithm may deviate.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Name the time zone of the instants](#axis) and 9 more |
3085
- | `tickFormat` | `ValueFormat` | — | Label of a tick. Compared by its source text: one that reads a changed closure variable under the same text does not relabel the axis - give it a new text or remount. A bound `Intl.NumberFormat#format` is compared by identity. Or `NumberOptions`: Intl's options in the wording's locale, compared by value; the step's places stay unless the options set digits.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Bind every kind of series to a second axis](#axis) and 21 more |
3348
+ | `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.<br>Shown in: [Add axes for other magnitudes](#axis), [Align a second axis to the grid](#axis), [Bind every kind of series to a second axis](#axis) and 8 more |
3349
+ | `id` | `string` | `"y"` | Axis id, through which series bind themselves.<br>Shown in: [Add axes for other magnitudes](#axis), [Align a second axis to the grid](#axis), [Bind every kind of series to a second axis](#axis) and 8 more |
3350
+ | `label` | `string` | — | Axis title.<br>Shown in: [Fix the domain and name the ticks](#axis), [Add axes for other magnitudes](#axis), [Leave out the hours nobody works](#axis) and 85 more |
3351
+ | `tickCount` | `number` | — | Target value; the 1-2-5 algorithm may deviate.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Name the time zone of the instants](#axis) and 10 more |
3352
+ | `tickFormat` | `ValueFormat` | — | Label of a tick. Compared by its source text: one that reads a changed closure variable under the same text does not relabel the axis - give it a new text or remount. A bound `Intl.NumberFormat#format` is compared by identity. Or `NumberOptions`: Intl's options in the wording's locale, compared by value; the step's places stay unless the options set digits.<br>Shown in: [Fix the domain and name the ticks](#axis), [Leave out the hours nobody works](#axis), [Bind every kind of series to a second axis](#axis) and 24 more |
3086
3353
  | `grid` | `boolean` | only on the first axis of its orientation | Grid lines.<br>Shown in: [One vehicle through the day (StateBand)](#stateband), [Give each one a lane (StateBand)](#stateband), [See where the vans stand idle (Scenarios)](#see-where-the-vans-stand-idle) |
3087
- | `ticks` | `readonly number[]` | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an hour axis' named hours, a lane grid. An axis that reads categories needs none - every category has its tick.<br>Shown in: [Bind every kind of series to a second axis](#axis), [Mark bands, limits and cells (Chart)](#chart-1), [Compare progress with a plan (Line)](#line) and 7 more |
3354
+ | `ticks` | `readonly number[]` | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an hour axis' named hours, a lane grid. An axis that reads categories needs none - every category has its tick.<br>Shown in: [Bind every kind of series to a second axis](#axis), [Values over decades](#axis), [Mark bands, limits and cells (Chart)](#chart-1) and 9 more |
3088
3355
  | `categories` | `readonly string[]` | — | The categories of the axis, in their order. Without it they stand in the order the rows first bring them. With it the list is the order and the extent: a category no row carries keeps its empty place, and a row whose category is not listed is not drawn - DEV names it. It makes the axis a category axis whatever the rows hold ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)).<br>Shown in: [Fix the order and keep an empty place](#axis) |
3089
3356
  | `categoryLabel` | `(category: string) => string` | the category itself | What is written for a category - under its tick, in the tooltip, the readout and the data table - where the rows hold a key and not a name. Two categories may share a label and keep two places. Compared by its source text, as `tickFormat` is.<br>Shown in: [Fix the order and keep an empty place](#axis), [Tell series apart without colour (Chart)](#chart-1), [Legend above or below (Legend)](#legend) and 1 more |
3090
3357
 
@@ -3155,14 +3422,14 @@ nothing.
3155
3422
 
3156
3423
  | Prop | Type | Default | Description |
3157
3424
  |---|---|---|---|
3158
- | `decimals` | `number` | — | |
3425
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
3159
3426
 
3160
3427
  #### Known limits
3161
3428
 
3162
- - No logarithmic and no band scale: compute the logarithm in `value`; a category is a place on the linear scale ([ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md), [ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)).
3429
+ - No logarithmic and no band scale: compute the logarithm in `value`; a category is a place on the linear scale ([ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md), [ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)).
3163
3430
  - umriss shows the reader's time ([ADR-0060](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0060-validation-and-a-layout-editor-are-never-time-zones-not-yet.md)).
3164
3431
 
3165
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
3432
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
3166
3433
 
3167
3434
  ### Zoom and pan
3168
3435
 
@@ -3740,6 +4007,81 @@ function Course({ zoomed, onRange }: { zoomed: ChartRange | null; onRange: (rang
3740
4007
  }
3741
4008
  ```
3742
4009
 
4010
+ ##### Follow the live edge, with pause
4011
+
4012
+ `setView` moves the span and is not reported, so the chart's own following is never heard as the reader's; `setDomain` is reported, which is why it fought a wheel zoom. A zoom or a pan stops the following through `onViewChange`, and "Live" puts the view back on the newest reading.
4013
+
4014
+ ```tsx
4015
+ import { Button } from "@umriss-ui/core";
4016
+ import { useEffect, useMemo, useState } from "react";
4017
+ import { Tooltip, useChart } from "@umriss-ui/charts";
4018
+
4019
+ /* A reading a second, derived from the clock: the value of a second never
4020
+ changes, so the rows that arrived before stay where they are. */
4021
+
4022
+ /** A small LCG - the same numbers on every computer. */
4023
+ function random(seed: number): () => number {
4024
+ let s = seed >>> 0;
4025
+ return () => {
4026
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
4027
+ return s / 4294967296;
4028
+ };
4029
+ }
4030
+
4031
+ const SECOND = 1_000;
4032
+ const HOUR = 60 * 60 * SECOND;
4033
+ const WINDOW = 2 * HOUR;
4034
+
4035
+ /** The seed of a second, mixed so that neighbouring seconds draw unrelated
4036
+ numbers: the generator alone is a sawtooth from one second to the next. */
4037
+ function mix(n: number): number {
4038
+ let x = Math.imul(n ^ (n >>> 16), 0x45d9f3b);
4039
+ x = Math.imul(x ^ (x >>> 16), 0x45d9f3b);
4040
+ return (x ^ (x >>> 16)) >>> 0;
4041
+ }
4042
+
4043
+ /** The p95 latency of a second, in ms: a slow drift and a little noise. */
4044
+ function reading(second: number): number {
4045
+ const r = random(mix(second));
4046
+ return Math.round(240 + 40 * Math.sin(second / 900) + 12 * (r() - 0.5));
4047
+ }
4048
+
4049
+ /** Every second of the two hours that end now. */
4050
+ function lastWindow(now: number): { t: number; p95: number }[] {
4051
+ const end = Math.floor(now / SECOND);
4052
+ const first = end - WINDOW / SECOND + 1;
4053
+ return Array.from({ length: WINDOW / SECOND }, (_, i) => ({ t: (first + i) * SECOND, p95: reading(first + i) }));
4054
+ }
4055
+
4056
+ export default function FollowTheLiveEdge() {
4057
+ const [now, setNow] = useState(() => Date.now());
4058
+ const [following, setFollowing] = useState(true);
4059
+ useEffect(() => {
4060
+ const id = setInterval(() => setNow(Date.now()), SECOND);
4061
+ return () => clearInterval(id);
4062
+ }, []);
4063
+ const rows = useMemo(() => lastWindow(now), [now]);
4064
+ const { Chart, XAxis, YAxis, Line, setView } = useChart(rows, {
4065
+ // Only the reader's changes reach this: `setView` below is never reported.
4066
+ onViewChange: () => setFollowing(false),
4067
+ });
4068
+ useEffect(() => {
4069
+ if (following) setView({ domains: { x: [now - HOUR, now] } });
4070
+ }, [now, following, setView]);
4071
+ return (
4072
+ <>
4073
+ <Chart height={260} aria-label="Latency over the last hour, following the newest reading">
4074
+ <XAxis value="t" time zoomable />
4075
+ <YAxis label="ms" />
4076
+ <Line value="p95" name="p95" />
4077
+ <Tooltip mode="x" />
4078
+ </Chart>
4079
+ <Button onClick={() => setFollowing((on) => !on)}>{following ? "Pause" : "Live"}</Button>
4080
+ </>
4081
+ );
4082
+ }
4083
+ ```
4084
+
3743
4085
  #### Keyboard
3744
4086
 
3745
4087
  | Key | Action |
@@ -3755,19 +4097,19 @@ The keys of [Chart](#chart-1) apply here.
3755
4097
 
3756
4098
  | Prop | Type | Default | Description |
3757
4099
  |---|---|---|---|
3758
- | `value` *required* | `AxisField``<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](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" />`.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 79 more |
4100
+ | `value` *required* | `AxisField``<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](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" />`.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 93 more |
3759
4101
  | `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.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 12 more |
3760
4102
  | `position` | `"bottom" \| "top"` | `"bottom"` | Which edge the axis stands at. A second x axis on the opposite edge is how a series counts in a unit of its own.<br>Shown in: [Add axes for other magnitudes (Axis)](#axis), [Bind every kind of series to a second axis (Axis)](#axis) |
3761
- | `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.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 39 more |
4103
+ | `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.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 42 more |
3762
4104
  | `calendar` | `readonly ``WorkingInterval``[]` | — | Working calendar: the intervals in which time counts. With it the axis stands in working time - nights and weekends are out, and every removed span gets a break mark. The scale stays affine; the mapping happens in materialisation ([ADR-0001](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0001-affine-scale-contract.md)).<br>Shown in: [Leave out the hours nobody works (Axis)](#axis), [Hand over a picture (Chart)](#chart-1) |
3763
- | `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.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 7 more |
4105
+ | `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.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 8 more |
3764
4106
  | `zoomLimits` | `ZoomLimits` | at most the data's extent or this axis' own `domain`, whichever is wider, at least three data steps | The narrowest and the widest span zoom may reach, in the axis' units - milliseconds on a time axis.<br>Shown in: [Limit the zoom](#zoom-and-pan) |
3765
- | `id` | `string` | `"x"` | Axis id, through which series bind themselves.<br>Shown in: [Add axes for other magnitudes (Axis)](#axis), [Align a second axis to the grid (Axis)](#axis), [Bind every kind of series to a second axis (Axis)](#axis) and 7 more |
3766
- | `label` | `string` | — | Axis title.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 71 more |
3767
- | `tickCount` | `number` | — | Target value; the 1-2-5 algorithm may deviate.<br>Shown in: [Keep charts in step](#zoom-and-pan), [Fix the domain and name the ticks (Axis)](#axis), [Leave out the hours nobody works (Axis)](#axis) and 9 more |
3768
- | `tickFormat` | `ValueFormat` | — | Label of a tick. Compared by its source text: one that reads a changed closure variable under the same text does not relabel the axis - give it a new text or remount. A bound `Intl.NumberFormat#format` is compared by identity. Or `NumberOptions`: Intl's options in the wording's locale, compared by value; the step's places stay unless the options set digits.<br>Shown in: [Fix the domain and name the ticks (Axis)](#axis), [Leave out the hours nobody works (Axis)](#axis), [Bind every kind of series to a second axis (Axis)](#axis) and 21 more |
4107
+ | `id` | `string` | `"x"` | Axis id, through which series bind themselves.<br>Shown in: [Add axes for other magnitudes (Axis)](#axis), [Align a second axis to the grid (Axis)](#axis), [Bind every kind of series to a second axis (Axis)](#axis) and 8 more |
4108
+ | `label` | `string` | — | Axis title.<br>Shown in: [Gestures and keys](#zoom-and-pan), [Limit the zoom](#zoom-and-pan), [Fit the y axis to what is visible](#zoom-and-pan) and 85 more |
4109
+ | `tickCount` | `number` | — | Target value; the 1-2-5 algorithm may deviate.<br>Shown in: [Keep charts in step](#zoom-and-pan), [Fix the domain and name the ticks (Axis)](#axis), [Leave out the hours nobody works (Axis)](#axis) and 10 more |
4110
+ | `tickFormat` | `ValueFormat` | — | Label of a tick. Compared by its source text: one that reads a changed closure variable under the same text does not relabel the axis - give it a new text or remount. A bound `Intl.NumberFormat#format` is compared by identity. Or `NumberOptions`: Intl's options in the wording's locale, compared by value; the step's places stay unless the options set digits.<br>Shown in: [Fix the domain and name the ticks (Axis)](#axis), [Leave out the hours nobody works (Axis)](#axis), [Bind every kind of series to a second axis (Axis)](#axis) and 24 more |
3769
4111
  | `grid` | `boolean` | only on the first axis of its orientation | Grid lines.<br>Shown in: [One vehicle through the day (StateBand)](#stateband), [Give each one a lane (StateBand)](#stateband), [See where the vans stand idle (Scenarios)](#see-where-the-vans-stand-idle) |
3770
- | `ticks` | `readonly number[]` | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an hour axis' named hours, a lane grid. An axis that reads categories needs none - every category has its tick.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis), [Mark bands, limits and cells (Chart)](#chart-1), [Compare progress with a plan (Line)](#line) and 7 more |
4112
+ | `ticks` | `readonly number[]` | — | Fixed tick values instead of the 1-2-5 algorithm, on a numeric axis: an hour axis' named hours, a lane grid. An axis that reads categories needs none - every category has its tick.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis), [Values over decades (Axis)](#axis), [Mark bands, limits and cells (Chart)](#chart-1) and 9 more |
3771
4113
  | `categories` | `readonly string[]` | — | The categories of the axis, in their order. Without it they stand in the order the rows first bring them. With it the list is the order and the extent: a category no row carries keeps its empty place, and a row whose category is not listed is not drawn - DEV names it. It makes the axis a category axis whatever the rows hold ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)).<br>Shown in: [Fix the order and keep an empty place (Axis)](#axis) |
3772
4114
  | `categoryLabel` | `(category: string) => string` | the category itself | What is written for a category - under its tick, in the tooltip, the readout and the data table - where the rows hold a key and not a name. Two categories may share a label and keep two places. Compared by its source text, as `tickFormat` is.<br>Shown in: [Fix the order and keep an empty place (Axis)](#axis), [Tell series apart without colour (Chart)](#chart-1), [Legend above or below (Legend)](#legend) and 1 more |
3773
4115
 
@@ -3781,7 +4123,7 @@ The keys of [Chart](#chart-1) apply here.
3781
4123
 
3782
4124
  | Prop | Type | Default | Description |
3783
4125
  |---|---|---|---|
3784
- | `onViewChange` | `(view: `[`ChartView`](#view)`) => void` | — | Every change the reader makes, once, always the whole view - the one to keep, or to hand another chart's `setView`. A pan or zoom is reported at most once per frame. Not called for the view the chart starts with, nor for one `setView` applies.<br>Shown in: [Keep charts in step](#zoom-and-pan), [Read it by keyboard and screen reader (Chart)](#chart-1) |
4126
+ | `onViewChange` | `(view: `[`ChartView`](#view)`) => void` | — | Every change the reader makes, once, always the whole view - the one to keep, or to hand another chart's `setView`. A pan or zoom is reported at most once per frame. Not called for the view the chart starts with, nor for one `setView` applies.<br>Shown in: [Keep charts in step](#zoom-and-pan), [Follow the live edge, with pause](#zoom-and-pan), [Read it by keyboard and screen reader (Chart)](#chart-1) |
3785
4127
  | `onSelect` | `(hit: ``TooltipHit``<Z>) => void` | — | The reader chose a point: a click on a hit (a tap too), or Enter on the Active point - the same hit the tooltip's `render` receives. Once per click; the end of a drag does not report, and the second click of a double click does not either. The chart holds no selection and draws none: what is chosen is the application's state. Needs a `<Tooltip>`, which makes the hits; the hit does not travel over `syncId`.<br>Shown in: [Choose a hit (Tooltip)](#tooltip), [Drill down from a year to its days (Tooltip)](#tooltip) |
3786
4128
  | `onRange` | `(range: ``ChartRange``<V>) => void` | — | The reader marked a stretch of an x axis: Alt with a drag where the chart zooms, a plain drag where it does not. Called once, on release - once per x axis -, with `from` not after `to`, in the axis' own values: numbers, milliseconds on a time axis, wall-clock instants on a calendar axis, and on a category axis the first and last category covered, as strings - say so in the handler's parameter, `ChartRange<string>`. A band covers the stretch while the pointer is down; the chart keeps no range and draws none afterwards, and it does not travel over `syncId`. A press that travels 4 px or less is a click and reaches `onSelect`; Escape and a cancelled pointer report nothing.<br>Shown in: [Mark a range](#zoom-and-pan), [Mark a range of bars (Bar)](#bar) |
3787
4129
 
@@ -3877,7 +4219,7 @@ nothing.
3877
4219
 
3878
4220
  | Prop | Type | Default | Description |
3879
4221
  |---|---|---|---|
3880
- | `decimals` | `number` | — | |
4222
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
3881
4223
 
3882
4224
  ###### `TooltipPoint<T = unknown>`
3883
4225
 
@@ -3891,6 +4233,7 @@ One hit per series in the tooltip.
3891
4233
  | `yValue` *required* | `number` | — | The point's y value; in a stack its own value, not its top - in a normalised stack its share in percent.<br>Shown in: [Fix the domain and name the ticks (Axis)](#axis), [Choose a hit (Tooltip)](#tooltip) |
3892
4234
  | `datum` *required* | `T` | — | <br>Shown in: [Write the tooltip yourself (Tooltip)](#tooltip), [Drill down from a year to its days (Tooltip)](#tooltip) |
3893
4235
  | `index` *required* | `number` | — | |
4236
+ | `baseline` | `number` | — | The lower edge, where the series has a baseline of its own - a `Bar` or an `Area` outside a stack; `yValue` is the upper one. A stack's foot is no reading and never stands here. A field of its own, as `level` and `box` have ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
3894
4237
  | `level` | `number` | — | Only for the matrix: its level, out of the level channel. It stands here and not in yValue, because yValue is the position on the y axis - for a matrix therefore the row. Putting both into one field would be exactly the overloading [ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md) is written against. |
3895
4238
  | `segment` | `{ from: number; to: number; label: string }` | — | Only for state series: the section being pointed at. A duration does not stand here - it is the difference of two numbers the entry already carries, and formatting it would presuppose knowing what the x axis means. The caller knows that, not this library. |
3896
4239
  | `box` | `BoxNumbers`` & Partial<``BoxExtras``` > & { /** As the caller gave them; only where the series has `outliers`. */ outliers?: readonly number[]; } `` | — | Only for a box: its further numbers; yValue is its median. Fields of their own for the reason `level` has one ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
@@ -3921,7 +4264,7 @@ The optional numbers of a box by name.
3921
4264
 
3922
4265
  - No zoom along y, no rectangle zoom and no navigator: the x span is the one zoom there is.
3923
4266
 
3924
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
4267
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
3925
4268
 
3926
4269
  ### View
3927
4270
 
@@ -4116,7 +4459,7 @@ The keys of [Chart](#chart-1) apply here.
4116
4459
 
4117
4460
  | Prop | Type | Default | Description |
4118
4461
  |---|---|---|---|
4119
- | `onViewChange` | `(view: ``ChartView``) => void` | — | Every change the reader makes, once, always the whole view - the one to keep, or to hand another chart's `setView`. A pan or zoom is reported at most once per frame. Not called for the view the chart starts with, nor for one `setView` applies.<br>Shown in: [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Read it by keyboard and screen reader (Chart)](#chart-1) |
4462
+ | `onViewChange` | `(view: ``ChartView``) => void` | — | Every change the reader makes, once, always the whole view - the one to keep, or to hand another chart's `setView`. A pan or zoom is reported at most once per frame. Not called for the view the chart starts with, nor for one `setView` applies.<br>Shown in: [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Follow the live edge, with pause (Zoom and pan)](#zoom-and-pan), [Read it by keyboard and screen reader (Chart)](#chart-1) |
4120
4463
  | `onSelect` | `(hit: ``TooltipHit``<Z>) => void` | — | The reader chose a point: a click on a hit (a tap too), or Enter on the Active point - the same hit the tooltip's `render` receives. Once per click; the end of a drag does not report, and the second click of a double click does not either. The chart holds no selection and draws none: what is chosen is the application's state. Needs a `<Tooltip>`, which makes the hits; the hit does not travel over `syncId`.<br>Shown in: [Choose a hit (Tooltip)](#tooltip), [Drill down from a year to its days (Tooltip)](#tooltip) |
4121
4464
  | `onRange` | `(range: ``ChartRange``<V>) => void` | — | The reader marked a stretch of an x axis: Alt with a drag where the chart zooms, a plain drag where it does not. Called once, on release - once per x axis -, with `from` not after `to`, in the axis' own values: numbers, milliseconds on a time axis, wall-clock instants on a calendar axis, and on a category axis the first and last category covered, as strings - say so in the handler's parameter, `ChartRange<string>`. A band covers the stretch while the pointer is down; the chart keeps no range and draws none afterwards, and it does not travel over `syncId`. A press that travels 4 px or less is a click and reaches `onSelect`; Escape and a cancelled pointer report nothing.<br>Shown in: [Mark a range (Zoom and pan)](#zoom-and-pan), [Mark a range of bars (Bar)](#bar) |
4122
4465
 
@@ -4124,7 +4467,7 @@ The keys of [Chart](#chart-1) apply here.
4124
4467
 
4125
4468
  | Prop | Type | Default | Description |
4126
4469
  |---|---|---|---|
4127
- | `domains` | `AxisSpans` | — | The span each zoomable x axis shows, by axis `id` - a lone x axis is `"x"`. An axis absent here shows its own `domain`. An id that names no zoomable x axis falls out.<br>Shown in: [Fit the y axis to what is visible (Zoom and pan)](#zoom-and-pan), [Mark a range (Zoom and pan)](#zoom-and-pan) |
4470
+ | `domains` | `AxisSpans` | — | The span each zoomable x axis shows, by axis `id` - a lone x axis is `"x"`. An axis absent here shows its own `domain`. An id that names no zoomable x axis falls out.<br>Shown in: [Fit the y axis to what is visible (Zoom and pan)](#zoom-and-pan), [Mark a range (Zoom and pan)](#zoom-and-pan), [Follow the live edge, with pause (Zoom and pan)](#zoom-and-pan) |
4128
4471
  | `hidden` | `readonly string[]` | — | The hidden series, by `name`; a series without one cannot be hidden. A name no series carries falls out.<br>Shown in: [Hide a series from the legend (Legend)](#legend) |
4129
4472
 
4130
4473
  ##### Types on this page
@@ -4174,6 +4517,7 @@ One hit per series in the tooltip.
4174
4517
  | `yValue` *required* | `number` | — | The point's y value; in a stack its own value, not its top - in a normalised stack its share in percent.<br>Shown in: [Fix the domain and name the ticks (Axis)](#axis), [Choose a hit (Tooltip)](#tooltip) |
4175
4518
  | `datum` *required* | `T` | — | <br>Shown in: [Write the tooltip yourself (Tooltip)](#tooltip), [Drill down from a year to its days (Tooltip)](#tooltip) |
4176
4519
  | `index` *required* | `number` | — | |
4520
+ | `baseline` | `number` | — | The lower edge, where the series has a baseline of its own - a `Bar` or an `Area` outside a stack; `yValue` is the upper one. A stack's foot is no reading and never stands here. A field of its own, as `level` and `box` have ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
4177
4521
  | `level` | `number` | — | Only for the matrix: its level, out of the level channel. It stands here and not in yValue, because yValue is the position on the y axis - for a matrix therefore the row. Putting both into one field would be exactly the overloading [ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md) is written against. |
4178
4522
  | `segment` | `{ from: number; to: number; label: string }` | — | Only for state series: the section being pointed at. A duration does not stand here - it is the difference of two numbers the entry already carries, and formatting it would presuppose knowing what the x axis means. The caller knows that, not this library. |
4179
4523
  | `box` | `BoxNumbers`` & Partial<``BoxExtras``` > & { /** As the caller gave them; only where the series has `outliers`. */ outliers?: readonly number[]; } `` | — | Only for a box: its further numbers; yValue is its median. Fields of their own for the reason `level` has one ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
@@ -4204,7 +4548,7 @@ The optional numbers of a box by name.
4204
4548
 
4205
4549
  - The active point and the open data table are not part of the view: they are where the reader is, not how the chart is looked at.
4206
4550
 
4207
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
4551
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
4208
4552
 
4209
4553
  ## Series
4210
4554
 
@@ -4657,6 +5001,72 @@ export default function BurnDown() {
4657
5001
  }
4658
5002
  ```
4659
5003
 
5004
+ ##### A silence is a gap
5005
+
5006
+ `maxGap` is the longest step a line is drawn through; the half hour the historian sent nothing is a hole, not a steady climb.
5007
+
5008
+ ```tsx
5009
+ import { Text } from "@umriss-ui/core";
5010
+ import { MINUTE, Tooltip, useChart } from "@umriss-ui/charts";
5011
+
5012
+ /* A historian logs a temperature every minute; it sent nothing from 10:12 to 10:47. */
5013
+ const at = (hours: number, minutes: number) => new Date(2026, 2, 17, hours, minutes).getTime();
5014
+ const READINGS: { t: number; temperature: number }[] = [];
5015
+ for (let t = at(9, 30); t <= at(11, 30); t += MINUTE) {
5016
+ if (t > at(10, 12) && t < at(10, 47)) continue;
5017
+ READINGS.push({ t, temperature: Math.round((74 + 4 * Math.sin(t / (9 * MINUTE)) + (t > at(10, 47) ? 3 : 0)) * 10) / 10 });
5018
+ }
5019
+
5020
+ function Bridged() {
5021
+ const { Chart, XAxis, YAxis, Line } = useChart(READINGS);
5022
+ return (
5023
+ <Chart height={200} aria-label="Reactor temperature, the outage bridged by a straight stroke">
5024
+ <XAxis value="t" time />
5025
+ <YAxis label="°C" domain={[68, 84]} />
5026
+ <Line value="temperature" name="Temperature" markers="never" />
5027
+ <Tooltip />
5028
+ </Chart>
5029
+ );
5030
+ }
5031
+
5032
+ function Broken() {
5033
+ const { Chart, XAxis, YAxis, Line } = useChart(READINGS);
5034
+ return (
5035
+ <Chart height={200} aria-label="Reactor temperature, the outage left as a gap">
5036
+ <XAxis value="t" time />
5037
+ <YAxis label="°C" domain={[68, 84]} />
5038
+ <Line value="temperature" name="Temperature" markers="never" maxGap={5 * MINUTE} />
5039
+ <Tooltip />
5040
+ </Chart>
5041
+ );
5042
+ }
5043
+
5044
+ function Filled() {
5045
+ const { Chart, XAxis, YAxis, Area } = useChart(READINGS);
5046
+ return (
5047
+ <Chart height={200} aria-label="Reactor temperature as an area, closed on both sides of the outage">
5048
+ <XAxis value="t" time />
5049
+ <YAxis label="°C" domain={[60, 84]} />
5050
+ <Area value="temperature" name="Temperature" maxGap={5 * MINUTE} />
5051
+ <Tooltip />
5052
+ </Chart>
5053
+ );
5054
+ }
5055
+
5056
+ export default function Outage() {
5057
+ return (
5058
+ <>
5059
+ <Text as="p">Without maxGap</Text>
5060
+ <Bridged />
5061
+ <Text as="p">With maxGap of five minutes</Text>
5062
+ <Broken />
5063
+ <Text as="p">An area closes on both sides of the gap</Text>
5064
+ <Filled />
5065
+ </>
5066
+ );
5067
+ }
5068
+ ```
5069
+
4660
5070
  #### When to use something else
4661
5071
 
4662
5072
  - A quantity read from its foot → Area
@@ -4672,18 +5082,19 @@ The keys of [Chart](#chart-1) apply here.
4672
5082
 
4673
5083
  | Prop | Type | Default | Description |
4674
5084
  |---|---|---|---|
4675
- | `value` *required* | `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](#line), [Several series and a gap](#line), [Hold a value until it changes](#line) and 50 more |
5085
+ | `value` *required* | `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](#line), [Several series and a gap](#line), [Hold a value until it changes](#line) and 58 more |
4676
5086
  | `xAxisId` | `string` | `"x"` | Binding to an x axis.<br>Shown in: [Add axes for other magnitudes (Axis)](#axis), [Bind every kind of series to a second axis (Axis)](#axis) |
4677
- | `yAxisId` | `string` | `"y"` | Binding to a y axis.<br>Shown in: [Hold a value until it changes](#line), [Add axes for other magnitudes (Axis)](#axis), [Align a second axis to the grid (Axis)](#axis) and 4 more |
4678
- | `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Several series and a gap](#line), [Hold a value until it changes](#line), [Tell series apart without colour (Chart)](#chart-1) and 6 more |
4679
- | `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](#line), [Several series and a gap](#line), [Hold a value until it changes](#line) and 50 more |
4680
- | `format` | `ValueFormat` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it, as a function or `NumberOptions`.<br>Shown in: [Write each value in its unit (Tooltip)](#tooltip), [Show the values as a table (Tooltip)](#tooltip), [Table a long series (Tooltip)](#tooltip) and 1 more |
4681
- | `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value.<br>Shown in: [Compare progress with a plan](#line), [Fix the domain and name the ticks (Axis)](#axis), [Watch latency against its objective (Scenarios)](#watch-latency-against-its-objective) and 1 more |
5087
+ | `yAxisId` | `string` | `"y"` | Binding to a y axis.<br>Shown in: [Hold a value until it changes](#line), [Add axes for other magnitudes (Axis)](#axis), [Align a second axis to the grid (Axis)](#axis) and 5 more |
5088
+ | `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Several series and a gap](#line), [Hold a value until it changes](#line), [Tell series apart without colour (Chart)](#chart-1) and 9 more |
5089
+ | `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](#line), [Several series and a gap](#line), [Hold a value until it changes](#line) and 58 more |
5090
+ | `format` | `ValueFormat` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it, as a function or `NumberOptions`.<br>Shown in: [Values over decades (Axis)](#axis), [Write each value in its unit (Tooltip)](#tooltip), [Show the values as a table (Tooltip)](#tooltip) and 2 more |
5091
+ | `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value.<br>Shown in: [Compare progress with a plan](#line), [Fix the domain and name the ticks (Axis)](#axis), [A trend with pens (Axis)](#axis) and 2 more |
4682
5092
  | `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)](#compare-the-budget-with-the-forecast) |
4683
- | `strokeWidth` | `number` | `1.5` | Line width in CSS pixels.<br>Shown in: [Hold a value until it changes](#line), [Compare progress with a plan](#line), [Mix series kinds (Chart)](#chart-1) and 9 more |
4684
- | `dash` | `readonly number[]` | a solid line | Dash pattern as a run of lengths in CSS pixels.<br>Shown in: [Several series and a gap](#line), [Compare progress with a plan](#line), [Add axes for other magnitudes (Axis)](#axis) and 1 more |
4685
- | `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](#line), [Draw a series per group of long rows (Chart)](#chart-1), [Fix the domain and name the ticks (Axis)](#axis) and 4 more |
5093
+ | `strokeWidth` | `number` | `1.5` | Line width in CSS pixels.<br>Shown in: [Hold a value until it changes](#line), [Compare progress with a plan](#line), [Mix series kinds (Chart)](#chart-1) and 10 more |
5094
+ | `dash` | `readonly number[]` | a solid line | Dash pattern as a run of lengths in CSS pixels.<br>Shown in: [Several series and a gap](#line), [Compare progress with a plan](#line), [Add axes for other magnitudes (Axis)](#axis) and 2 more |
5095
+ | `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](#line), [A silence is a gap](#line), [Draw a series per group of long rows (Chart)](#chart-1) and 7 more |
4686
5096
  | `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](#line) |
5097
+ | `maxGap` | `number` | no limit, a stretch without rows is bridged | The longest step between two readings that is still drawn through, in the x axis' units: milliseconds on a time axis, working time on a calendar axis, places on a category axis. A longer step is a gap: the line breaks and a `step` hold ends `maxGap` after its reading. A pointer farther than this from the nearest reading finds no hit on the series.<br>Shown in: [A silence is a gap](#line) |
4687
5098
 
4688
5099
  ##### Types on this page
4689
5100
 
@@ -4738,13 +5149,13 @@ nothing.
4738
5149
 
4739
5150
  | Prop | Type | Default | Description |
4740
5151
  |---|---|---|---|
4741
- | `decimals` | `number` | — | |
5152
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
4742
5153
 
4743
5154
  #### Known limits
4744
5155
 
4745
- - No smoothing: a curve between two readings would invent values nobody measured ([ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md)).
5156
+ - No smoothing: a curve between two readings would invent values nobody measured ([ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md)).
4746
5157
 
4747
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
5158
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
4748
5159
 
4749
5160
  ### Area
4750
5161
 
@@ -5148,12 +5559,12 @@ The keys of [Chart](#chart-1) apply here.
5148
5559
 
5149
5560
  | Prop | Type | Default | Description |
5150
5561
  |---|---|---|---|
5151
- | `value` *required* | `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](#area), [Fill a range between two edges](#area), [Stack areas](#area) and 5 more |
5152
- | `baseline` | `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](#area), [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1) |
5562
+ | `value` *required* | `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](#area), [Fill a range between two edges](#area), [Stack areas](#area) and 7 more |
5563
+ | `baseline` | `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](#area), [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1) and 1 more |
5153
5564
  | `xAxisId` | `string` | `"x"` | Binding to an x axis.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5154
5565
  | `yAxisId` | `string` | `"y"` | Binding to a y axis.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5155
- | `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Fill a range between two edges](#area), [Bind every kind of series to a second axis (Axis)](#axis) |
5156
- | `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](#area), [Fill a range between two edges](#area), [Stack areas](#area) and 5 more |
5566
+ | `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Fill a range between two edges](#area), [Bind every kind of series to a second axis (Axis)](#axis), [A fit line with its band (Scatter)](#scatter) |
5567
+ | `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](#area), [Fill a range between two edges](#area), [Stack areas](#area) and 7 more |
5157
5568
  | `format` | `ValueFormat` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it, as a function or `NumberOptions`.<br>Shown in: [Show shares of the whole over time](#area) |
5158
5569
  | `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value. |
5159
5570
  | `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it. |
@@ -5162,6 +5573,7 @@ The keys of [Chart](#chart-1) apply here.
5162
5573
  | `fillOpacity` | `number` | `0.18` | Opacity of the fill, from 0 to 1; the outline stays fully opaque.<br>Shown in: [Fill down to zero](#area), [Stack areas](#area), [Show shares of the whole over time](#area) |
5163
5574
  | `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](#area), [Fill a range between two edges](#area), [Stack areas](#area) and 2 more |
5164
5575
  | `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](#area) |
5576
+ | `maxGap` | `number` | no limit, a stretch without rows is bridged | The longest step between two readings that is still filled through, in the x axis' units: milliseconds on a time axis, working time on a calendar axis, places on a category axis. A longer step is a gap: the area closes on both sides of it. A pointer farther than this from the nearest reading finds no hit. In a `stack` this is not read.<br>Shown in: [A silence is a gap (Line)](#line) |
5165
5577
 
5166
5578
  ##### Types on this page
5167
5579
 
@@ -5216,7 +5628,7 @@ nothing.
5216
5628
 
5217
5629
  | Prop | Type | Default | Description |
5218
5630
  |---|---|---|---|
5219
- | `decimals` | `number` | — | |
5631
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
5220
5632
 
5221
5633
  ### Bar
5222
5634
 
@@ -5226,7 +5638,7 @@ Bars along the x axis (a bar or column chart): one per category, grouped side by
5226
5638
  import { useChart, bin } from "@umriss-ui/charts";
5227
5639
  ```
5228
5640
 
5229
- The x axis reads the category from the row - a cost centre, a weekday - and gives each a place, a tick and its name ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)); a number or an instant places a bar as well. A bar's foot is 0, so one series carries both signs. `bin()` counts values into the rows of a histogram: each bin's `from`, `to`, middle `x` and `count`, a value on an edge in the bin that edge starts, the last bin closed, values that are not numbers skipped; a `domain` is taken as given and what lies outside it is left out.
5641
+ The x axis reads the category from the row - a cost centre, a weekday - and gives each a place, a tick and its name ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)); a number or an instant places a bar as well. A bar's foot is 0, so one series carries both signs; a `baseline` floats it from one value to another, as in a waterfall. `bin()` counts values into the rows of a histogram: each bin's `from`, `to`, middle `x` and `count`, a value on an edge in the bin that edge starts, the last bin closed, values that are not numbers skipped; a `domain` is taken as given and what lies outside it is left out.
5230
5642
 
5231
5643
  #### Examples
5232
5644
 
@@ -5483,6 +5895,141 @@ export default function MarkARangeOfBars() {
5483
5895
  }
5484
5896
  ```
5485
5897
 
5898
+ ##### A waterfall
5899
+
5900
+ Keep a running sum beside the changes, give `value` where each step ends and `baseline` where it began: the start and end totals stand on zero, the steps float, and no helper series exists to leak into the tooltip.
5901
+
5902
+ ```tsx
5903
+ import { DataTable, Tooltip, useChart } from "@umriss-ui/charts";
5904
+
5905
+ /* The revenue bridge of a half year in €k: the opening total, what moved it, the closing total. */
5906
+ const CHANGES = [
5907
+ { step: "New customers", change: 310 },
5908
+ { step: "Upsell", change: 140 },
5909
+ { step: "Churn", change: -220 },
5910
+ { step: "Price cut", change: -90 },
5911
+ { step: "Renewals", change: 180 },
5912
+ ];
5913
+
5914
+ const START = 1200;
5915
+
5916
+ /** The running sum: each step begins where the one before ended. */
5917
+ const BRIDGE = (() => {
5918
+ let sum = START;
5919
+ const steps = CHANGES.map(({ step, change }) => {
5920
+ const from = sum;
5921
+ sum += change;
5922
+ return { step, from, to: sum };
5923
+ });
5924
+ return [{ step: "Start", from: 0, to: START }, ...steps, { step: "End", from: 0, to: sum }];
5925
+ })();
5926
+
5927
+ export default function Waterfall() {
5928
+ const { Chart, XAxis, YAxis, Bar } = useChart(BRIDGE);
5929
+ return (
5930
+ <Chart height={280} aria-label="Revenue from the opening to the closing total, step by step">
5931
+ <XAxis value="step" label="Step" />
5932
+ <YAxis label="€k" />
5933
+ <Bar value="to" baseline="from" name="Revenue" />
5934
+ <Tooltip mode="x" />
5935
+ <DataTable />
5936
+ </Chart>
5937
+ );
5938
+ }
5939
+ ```
5940
+
5941
+ ##### Error bars
5942
+
5943
+ A mean per batch with its 95 % interval: a thin floating `Bar` runs from the lower bound to the upper, a `Scatter` marks the mean on it. A confidence band along a course is an `Area` with a `baseline`, and range bars, a minimum to a maximum per hour, are the same bar.
5944
+
5945
+ ```tsx
5946
+ import { DataTable, Tooltip, useChart } from "@umriss-ui/charts";
5947
+
5948
+ /* Eight readings of the output in mm per batch, as the line sends them. */
5949
+ const BATCHES = [
5950
+ [50.2, 49.1, 51.0, 50.4, 48.8, 49.9, 50.7, 49.5],
5951
+ [51.3, 52.0, 50.6, 51.8, 50.9, 52.4, 51.1, 51.6],
5952
+ [48.9, 49.6, 49.2, 48.4, 50.1, 49.0, 48.7, 49.8],
5953
+ [50.8, 51.5, 50.3, 51.0, 52.1, 50.5, 51.2, 50.9],
5954
+ [47.6, 48.3, 48.9, 47.9, 49.2, 48.1, 47.4, 48.6],
5955
+ [50.0, 49.4, 50.6, 49.8, 50.3, 49.1, 50.9, 49.7],
5956
+ [52.6, 51.9, 52.8, 53.4, 52.2, 51.7, 53.1, 52.5],
5957
+ [49.3, 50.1, 48.8, 49.6, 50.4, 49.0, 49.9, 50.2],
5958
+ [50.5, 51.2, 49.8, 50.9, 51.6, 50.1, 51.0, 50.3],
5959
+ [48.2, 47.8, 49.1, 48.5, 47.6, 48.9, 48.0, 48.4],
5960
+ [51.9, 51.1, 52.3, 51.4, 50.8, 52.0, 51.6, 51.2],
5961
+ [49.7, 50.5, 49.2, 50.0, 49.5, 50.8, 49.9, 50.2],
5962
+ ];
5963
+
5964
+ /* The two-sided 95 % point of Student's t with seven degrees of freedom: eight readings per batch. */
5965
+ const T95 = 2.365;
5966
+
5967
+ /** The mean of a batch and its interval: the mean plus and minus t times the standard error. */
5968
+ function interval(batch: number, readings: readonly number[]) {
5969
+ const n = readings.length;
5970
+ const mean = readings.reduce((sum, x) => sum + x, 0) / n;
5971
+ const variance = readings.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (n - 1);
5972
+ const half = T95 * Math.sqrt(variance / n);
5973
+ return { batch, mean, lower: mean - half, upper: mean + half };
5974
+ }
5975
+
5976
+ const STATS = BATCHES.map((readings, i) => interval(i + 1, readings));
5977
+
5978
+ export default function ErrorBars() {
5979
+ const { Chart, XAxis, YAxis, Bar, Scatter } = useChart(STATS);
5980
+ return (
5981
+ <Chart height={280} aria-label="Mean output per batch with its 95 % interval">
5982
+ <XAxis value="batch" label="Batch" />
5983
+ <YAxis label="mm" />
5984
+ <Bar value="upper" baseline="lower" barWidth={0.08} name="95 % interval" />
5985
+ <Scatter value="mean" name="Mean" />
5986
+ <Tooltip mode="x" />
5987
+ <DataTable />
5988
+ </Chart>
5989
+ );
5990
+ }
5991
+ ```
5992
+
5993
+ ##### Per month
5994
+
5995
+ Daily readings summed into months by the application's own `perMonth()`, and drawn as one bar per month on `Date` rows. The chart draws the sums it is given: whether a bar is a sum, a count or a mean, and what it counts, is the application's to decide, not the chart's.
5996
+
5997
+ ```tsx
5998
+ import { useMemo } from "react";
5999
+ import { Tooltip, useChart } from "@umriss-ui/charts";
6000
+
6001
+ /* Units shipped per day, first half of 2026, as the warehouse exports them. */
6002
+ const DAYS = Array.from({ length: 181 }, (_, i) => ({
6003
+ day: new Date(2026, 0, 1 + i),
6004
+ units: Math.round(40 + 12 * Math.sin(i / 9) + ((i * 37) % 11)),
6005
+ }));
6006
+
6007
+ /** Each month's sum, placed at the month's first instant. */
6008
+ function perMonth(days: readonly { day: Date; units: number }[]): { month: Date; units: number }[] {
6009
+ const sums = new Map<number, { month: Date; units: number }>();
6010
+ for (const { day, units } of days) {
6011
+ const month = new Date(day.getFullYear(), day.getMonth(), 1);
6012
+ const row = sums.get(month.getTime()) ?? { month, units: 0 };
6013
+ row.units += units;
6014
+ sums.set(month.getTime(), row);
6015
+ }
6016
+ return [...sums.values()];
6017
+ }
6018
+
6019
+ export default function PerMonth() {
6020
+ const months = useMemo(() => perMonth(DAYS), []);
6021
+ const { Chart, XAxis, YAxis, Bar } = useChart(months);
6022
+ return (
6023
+ <Chart height={260} aria-label="Units shipped per month in the first half of 2026">
6024
+ <XAxis value="month" />
6025
+ <YAxis label="Units" />
6026
+ <Bar value="units" name="Units" />
6027
+ <Tooltip mode="x" />
6028
+ </Chart>
6029
+ );
6030
+ }
6031
+ ```
6032
+
5486
6033
  #### When to use something else
5487
6034
 
5488
6035
  - Causes ranked by how much they contribute → Pareto
@@ -5497,17 +6044,18 @@ The keys of [Chart](#chart-1) apply here.
5497
6044
 
5498
6045
  | Prop | Type | Default | Description |
5499
6046
  |---|---|---|---|
5500
- | `value` *required* | `Value``<T>` | — | Height - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap.<br>Shown in: [One bar per category](#bar), [Group bars side by side](#bar), [Show bars above and below zero](#bar) and 15 more |
6047
+ | `value` *required* | `Value``<T>` | — | Height - a number field of the row or a function of it; null/undefined/NaN/±Infinity means a gap.<br>Shown in: [One bar per category](#bar), [Group bars side by side](#bar), [Show bars above and below zero](#bar) and 20 more |
6048
+ | `baseline` | `Value``<T>` | a fixed baseline at 0 | Lower edge - a number field of the row or a function of it: the bar floats from here to `value`, either way up. A gap in either edge leaves the bar out. In a `stack` the stack below is the lower edge, and this is not read.<br>Shown in: [A waterfall](#bar), [Error bars](#bar) |
5501
6049
  | `xAxisId` | `string` | `"x"` | Binding to an x axis.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5502
6050
  | `yAxisId` | `string` | `"y"` | Binding to a y axis.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5503
6051
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5504
- | `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 bar per category](#bar), [Group bars side by side](#bar), [Show bars above and below zero](#bar) and 15 more |
6052
+ | `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 bar per category](#bar), [Group bars side by side](#bar), [Show bars above and below zero](#bar) and 20 more |
5505
6053
  | `format` | `ValueFormat` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it, as a function or `NumberOptions`.<br>Shown in: [Show shares of a whole](#bar), [Compare the budget with the forecast (Scenarios)](#compare-the-budget-with-the-forecast) |
5506
6054
  | `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value.<br>Shown in: [Group bars side by side](#bar), [See where the vans stand idle (Scenarios)](#see-where-the-vans-stand-idle), [Compare the budget with the forecast (Scenarios)](#compare-the-budget-with-the-forecast) |
5507
6055
  | `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it.<br>Shown in: [Compare the budget with the forecast (Scenarios)](#compare-the-budget-with-the-forecast) |
5508
6056
  | `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 bars](#bar), [Show shares of a whole](#bar), [Read a stack's total in the table (Tooltip)](#tooltip) and 1 more |
5509
6057
  | `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 a whole](#bar) |
5510
- | `barWidth` | `number` | `0.8` | Width as a fraction of the step; just under 1, so that neighbours do not touch. Several bar series share this fraction.<br>Shown in: [Group bars side by side](#bar), [Count readings into a histogram](#bar), [Sort causes and collect the tail (Pareto)](#pareto) and 3 more |
6058
+ | `barWidth` | `number` | `0.8` | Width as a fraction of the step; just under 1, so that neighbours do not touch. Several bar series share this fraction.<br>Shown in: [Group bars side by side](#bar), [Count readings into a histogram](#bar), [Error bars](#bar) and 6 more |
5511
6059
 
5512
6060
  ##### Types on this page
5513
6061
 
@@ -5562,13 +6110,13 @@ nothing.
5562
6110
 
5563
6111
  | Prop | Type | Default | Description |
5564
6112
  |---|---|---|---|
5565
- | `decimals` | `number` | — | |
6113
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
5566
6114
 
5567
6115
  #### Known limits
5568
6116
 
5569
6117
  - Horizontal bars are the next kind to come ([ADR-0059](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0059-the-set-of-chart-kinds-is-open.md)); until then a `Matrix` or a `Table` ranks by name.
5570
6118
 
5571
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
6119
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
5572
6120
 
5573
6121
  ### Scatter
5574
6122
 
@@ -5818,6 +6366,138 @@ export default function AgainstATarget() {
5818
6366
  }
5819
6367
  ```
5820
6368
 
6369
+ ##### A fit line with its band
6370
+
6371
+ A regression is a model the analyst chooses - which fit, which band - so it is the application's code, not the chart's: here an ordinary least-squares line, and its 95 % band of the fitted mean from the standard error at each x, by the normal approximation (1.96 standard errors). The chart draws the rows it is given.
6372
+
6373
+ ```tsx
6374
+ import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
6375
+
6376
+ /* Data from the data scientist's world, written out here so the example runs on its own. */
6377
+ /** A small LCG - the same numbers on every computer. */
6378
+ function random(seed: number): () => number {
6379
+ let s = seed >>> 0;
6380
+ return () => {
6381
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
6382
+ return s / 4294967296;
6383
+ };
6384
+ }
6385
+
6386
+ interface Reading {
6387
+ x: number;
6388
+ y: number;
6389
+ }
6390
+
6391
+ /** Sixty measurements along a rising course, with a bell-shaped scatter about it (Box-Muller). */
6392
+ const READINGS: readonly Reading[] = (() => {
6393
+ const r = random(7);
6394
+ return Array.from({ length: 60 }, () => {
6395
+ const x = Math.round(r() * 100) / 10;
6396
+ const noise = Math.sqrt(-2 * Math.log(Math.max(r(), 1e-12))) * Math.cos(2 * Math.PI * r());
6397
+ return { x, y: Math.round((2 + 0.8 * x + 1.2 * noise) * 100) / 100 };
6398
+ });
6399
+ })();
6400
+
6401
+ /* Ordinary least squares, and the standard error of the fitted mean at each x. */
6402
+ const n = READINGS.length;
6403
+ const mx = READINGS.reduce((sum, d) => sum + d.x, 0) / n;
6404
+ const my = READINGS.reduce((sum, d) => sum + d.y, 0) / n;
6405
+ const sxx = READINGS.reduce((sum, d) => sum + (d.x - mx) ** 2, 0);
6406
+ const slope = READINGS.reduce((sum, d) => sum + (d.x - mx) * (d.y - my), 0) / sxx;
6407
+ const intercept = my - slope * mx;
6408
+ const sigma = Math.sqrt(READINGS.reduce((sum, d) => sum + (d.y - intercept - slope * d.x) ** 2, 0) / (n - 2));
6409
+ /** 95 % under the normal approximation. */
6410
+ const Z = 1.96;
6411
+
6412
+ interface Fit {
6413
+ x: number;
6414
+ fit: number;
6415
+ lower: number;
6416
+ upper: number;
6417
+ }
6418
+
6419
+ const GRID: readonly Fit[] = Array.from({ length: 41 }, (_, i) => {
6420
+ const x = (i / 40) * 10;
6421
+ const fit = intercept + slope * x;
6422
+ const se = sigma * Math.sqrt(1 / n + (x - mx) ** 2 / sxx);
6423
+ return { x, fit, lower: fit - Z * se, upper: fit + Z * se };
6424
+ });
6425
+
6426
+ export default function FitLineWithBand() {
6427
+ const { Chart, XAxis, YAxis, Scatter, Line, Area } = useChart(READINGS);
6428
+ return (
6429
+ <Chart height={300} aria-label="Measurements with their least-squares fit and 95 per cent band">
6430
+ <XAxis value="x" label="x" />
6431
+ <YAxis label="y" />
6432
+ <Area data={GRID} value="upper" baseline="lower" name="95 % band" />
6433
+ <Scatter value="y" name="Measurement" radius={2.5} />
6434
+ <Line data={GRID} value="fit" name="Fit" strokeWidth={1.75} markers="never" />
6435
+ <Legend placement="top" />
6436
+ <Tooltip mode="nearest" />
6437
+ </Chart>
6438
+ );
6439
+ }
6440
+ ```
6441
+
6442
+ ##### Where the points crowd
6443
+
6444
+ Count the points into cells and draw the counts as a `Matrix`, a 2D histogram as [`bin`](#api-index-1) is for `Bar` ([ADR-0054](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0054-a-histogram-is-a-helper-for-bar-not-a-chart-kind.md)); a cell without points has no row and is not drawn, so the ground is zero. Point opacity is not offered: a scatter is one path and one fill, so translucency does not add up on it, and a fill per point breaks the million-point budget.
6445
+
6446
+ ```tsx
6447
+ import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
6448
+
6449
+ /* Data from the data scientist's world, written out here so the example runs on its own. */
6450
+ /** A small LCG - the same numbers on every computer. */
6451
+ function random(seed: number): () => number {
6452
+ let s = seed >>> 0;
6453
+ return () => {
6454
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
6455
+ return s / 4294967296;
6456
+ };
6457
+ }
6458
+
6459
+ /** A bell-shaped draw from two uniforms (Box-Muller). */
6460
+ const draw = random(9);
6461
+ const gauss = () => Math.sqrt(-2 * Math.log(Math.max(draw(), 1e-12))) * Math.cos(2 * Math.PI * draw());
6462
+
6463
+ /** Two hundred thousand points in two clusters, counted into a grid of 50 by 50 cells as they come. */
6464
+ const CELLS = 50;
6465
+ const SIZE = 10 / CELLS;
6466
+ const COUNTS = new Uint32Array(CELLS * CELLS);
6467
+ for (let k = 0; k < 200_000; k++) {
6468
+ const [cx, cy, sd] = k % 5 < 3 ? [3, 4, 1.2] : [7, 6.5, 1.6];
6469
+ const i = Math.floor((cx + sd * gauss()) / SIZE);
6470
+ const j = Math.floor((cy + sd * gauss()) / SIZE);
6471
+ if (i >= 0 && i < CELLS && j >= 0 && j < CELLS) COUNTS[j * CELLS + i]! += 1;
6472
+ }
6473
+
6474
+ interface Cell {
6475
+ /** The cell's centre: its column and its row. */
6476
+ x: number;
6477
+ y: number;
6478
+ /** The points in the cell. */
6479
+ count: number;
6480
+ }
6481
+
6482
+ /** One row per cell with points; a cell without any has no row and is not drawn. */
6483
+ const CELL_ROWS: readonly Cell[] = Array.from(COUNTS).flatMap((count, k) =>
6484
+ count === 0 ? [] : [{ x: ((k % CELLS) + 0.5) * SIZE, y: (Math.floor(k / CELLS) + 0.5) * SIZE, count }],
6485
+ );
6486
+
6487
+ export default function WherePointsCrowd() {
6488
+ const { Chart, XAxis, YAxis, Matrix } = useChart(CELL_ROWS);
6489
+ return (
6490
+ <Chart height={320} aria-label="Two hundred thousand points counted into cells, the counts as a gradient">
6491
+ <XAxis value="x" label="x" />
6492
+ <YAxis />
6493
+ <Matrix value="y" level="count" name="Points per cell" format={{ decimals: 0 }} />
6494
+ <Legend />
6495
+ <Tooltip mode="nearest" />
6496
+ </Chart>
6497
+ );
6498
+ }
6499
+ ```
6500
+
5821
6501
  #### Keyboard
5822
6502
 
5823
6503
  The keys of [Chart](#chart-1) apply here.
@@ -5828,15 +6508,15 @@ The keys of [Chart](#chart-1) apply here.
5828
6508
 
5829
6509
  | Prop | Type | Default | Description |
5830
6510
  |---|---|---|---|
5831
- | `value` *required* | `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: [Plot single measurements](#scatter), [Mark samples that miss a target](#scatter), [Mix series kinds (Chart)](#chart-1) and 3 more |
6511
+ | `value` *required* | `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: [Plot single measurements](#scatter), [Mark samples that miss a target](#scatter), [A fit line with its band](#scatter) and 5 more |
5832
6512
  | `xAxisId` | `string` | `"x"` | Binding to an x axis.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5833
6513
  | `yAxisId` | `string` | `"y"` | Binding to a y axis.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5834
6514
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Bind every kind of series to a second axis (Axis)](#axis) |
5835
- | `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: [Plot single measurements](#scatter), [Mark samples that miss a target](#scatter), [Mix series kinds (Chart)](#chart-1) and 3 more |
6515
+ | `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: [Plot single measurements](#scatter), [Mark samples that miss a target](#scatter), [A fit line with its band](#scatter) and 5 more |
5836
6516
  | `format` | `ValueFormat` | the y axis' `tickFormat`, else the built-in number format | The value as the tooltip writes it, as a function or `NumberOptions`. |
5837
6517
  | `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value. |
5838
6518
  | `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it.<br>Shown in: [Mark samples that miss a target](#scatter) |
5839
- | `radius` | `number` | `3` | Point radius in CSS pixels.<br>Shown in: [Plot single measurements](#scatter), [Mark samples that miss a target](#scatter) |
6519
+ | `radius` | `number` | `3` | Point radius in CSS pixels.<br>Shown in: [Plot single measurements](#scatter), [Mark samples that miss a target](#scatter), [A fit line with its band](#scatter) |
5840
6520
 
5841
6521
  ##### Types on this page
5842
6522
 
@@ -5891,7 +6571,7 @@ nothing.
5891
6571
 
5892
6572
  | Prop | Type | Default | Description |
5893
6573
  |---|---|---|---|
5894
- | `decimals` | `number` | — | |
6574
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
5895
6575
 
5896
6576
  ### BoxPlot
5897
6577
 
@@ -6473,13 +7153,13 @@ nothing.
6473
7153
 
6474
7154
  | Prop | Type | Default | Description |
6475
7155
  |---|---|---|---|
6476
- | `decimals` | `number` | — | |
7156
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
6477
7157
 
6478
7158
  #### Known limits
6479
7159
 
6480
7160
  - No horizontal boxes, no violin or jittered points, no box width by count: a density is smoothing, a jitter a picture that does not repeat.
6481
7161
 
6482
- What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.45.0/docs/adr/0032-what-umriss-is-not.md).
7162
+ What umriss deliberately does not build, and why: [ADR-0032](https://github.com/romanhaendler/umriss-ui/blob/charts-v0.47.0/docs/adr/0032-what-umriss-is-not.md).
6483
7163
 
6484
7164
  ### StateBand
6485
7165
 
@@ -7227,10 +7907,10 @@ export default function ByLimits() {
7227
7907
 
7228
7908
  ##### Colour cells along a gradient
7229
7909
 
7230
- Without `coloring` the cells run along `DEFAULT_GRADIENT` from the lowest value to the highest; a cell without a value stays a hole.
7910
+ Without `coloring` the cells run along `DEFAULT_GRADIENT` from the lowest value to the highest; a cell without a value stays a hole. The legend shows the steps as a colour bar, the range's ends written in `format`.
7231
7911
 
7232
7912
  ```tsx
7233
- import { Tooltip, useChart } from "@umriss-ui/charts";
7913
+ import { Legend, Tooltip, useChart } from "@umriss-ui/charts";
7234
7914
 
7235
7915
  /* Data from the operations world, written out here so the example runs on its own. */
7236
7916
  /** A small LCG - the same numbers on every computer. */
@@ -7297,13 +7977,110 @@ export default function Gradient() {
7297
7977
  <Chart height={280} aria-label="Successful requests per service and hour yesterday, as a gradient">
7298
7978
  <XAxis value="hour" ticks={[0, 6, 12, 18]} tickFormat={(v) => `${v}:00`} label="Hour" />
7299
7979
  <YAxis />
7300
- <Matrix value="service" level="success" name="Success rate" />
7980
+ <Matrix value="service" level="success" name="Success rate" format={{ decimals: 1 }} />
7981
+ <Legend />
7301
7982
  <Tooltip mode="nearest" />
7302
7983
  </Chart>
7303
7984
  );
7304
7985
  }
7305
7986
  ```
7306
7987
 
7988
+ ##### A correlation matrix, read
7989
+
7990
+ With `labels` each cell writes its level in the matrix' `format`, in the text colour over a halo of the ground colour, so it reads on the lightest and the darkest stop alike. A cell too small for its text is left without one: the second matrix is dense, and where the chart is narrow its cells stay bare - the tooltip still names every one.
7991
+
7992
+ ```tsx
7993
+ import { Tooltip, useChart } from "@umriss-ui/charts";
7994
+
7995
+ /* Data from the data scientist's world, written out here so the example runs on its own. */
7996
+ /** A small LCG - the same numbers on every computer. */
7997
+ function random(seed: number): () => number {
7998
+ let s = seed >>> 0;
7999
+ return () => {
8000
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
8001
+ return s / 4294967296;
8002
+ };
8003
+ }
8004
+
8005
+ const VARIABLES = ["Age", "Income", "Tenure", "Visits", "Basket", "Returns", "Support", "Churn", "Score"] as const;
8006
+
8007
+ interface Pair {
8008
+ /** The column: the variable's place in `VARIABLES`. */
8009
+ column: number;
8010
+ /** The row: the variable's name. */
8011
+ variable: string;
8012
+ /** Their correlation, from -1 to 1. */
8013
+ r: number;
8014
+ }
8015
+
8016
+ /** The correlations of nine variables: the cosine between direction vectors in
8017
+ a small space, which is a valid correlation matrix by construction. */
8018
+ const CORRELATIONS: readonly Pair[] = (() => {
8019
+ const draw = random(11);
8020
+ const vectors = VARIABLES.map(() => [draw() - 0.45, draw() - 0.5, draw() - 0.5]);
8021
+ const unit = vectors.map((v) => {
8022
+ const length = Math.hypot(...v);
8023
+ return v.map((c) => c / length);
8024
+ });
8025
+ const pairs: Pair[] = [];
8026
+ VARIABLES.forEach((variable, row) => {
8027
+ VARIABLES.forEach((_, column) => {
8028
+ const dot = (unit[row] as number[]).reduce((sum, c, k) => sum + c * (unit[column] as number[])[k]!, 0);
8029
+ pairs.push({ column, variable, r: Math.round(Math.max(-1, Math.min(1, dot)) * 100) / 100 });
8030
+ });
8031
+ });
8032
+ return pairs;
8033
+ })();
8034
+
8035
+ /** Nine stops from blue through white to red, one per ninth of [-1, 1]. */
8036
+ const DIVERGING = ["#2166ac", "#4393c3", "#92c5de", "#d1e5f0", "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b"];
8037
+
8038
+ interface Load {
8039
+ hour: number;
8040
+ service: string;
8041
+ /** The share of capacity in use, 0 to 1. */
8042
+ load: number;
8043
+ }
8044
+
8045
+ /** A load per service and hour: dense on purpose, 24 columns of nine rows. */
8046
+ const LOAD: readonly Load[] = (() => {
8047
+ const draw = random(5);
8048
+ const cells: Load[] = [];
8049
+ VARIABLES.forEach((service) => {
8050
+ for (let hour = 0; hour < 24; hour++) cells.push({ hour, service, load: Math.round(draw() * 100) / 100 });
8051
+ });
8052
+ return cells;
8053
+ })();
8054
+
8055
+ export default function CorrelationRead() {
8056
+ const first = useChart(CORRELATIONS);
8057
+ const second = useChart(LOAD);
8058
+ return (
8059
+ <>
8060
+ <first.Chart height={380} aria-label="Correlation of nine variables, each cell written out">
8061
+ <first.XAxis value="column" ticks={VARIABLES.map((_, k) => k)} tickFormat={(v) => VARIABLES[v] ?? ""} />
8062
+ <first.YAxis />
8063
+ <first.Matrix
8064
+ value="variable"
8065
+ level="r"
8066
+ coloring={{ kind: "gradient", stops: DIVERGING, range: [-1, 1] }}
8067
+ name="Correlation"
8068
+ format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
8069
+ labels
8070
+ />
8071
+ <Tooltip mode="nearest" />
8072
+ </first.Chart>
8073
+ <second.Chart height={300} aria-label="Load per service and hour, labels where the cells have room">
8074
+ <second.XAxis value="hour" ticks={[0, 6, 12, 18]} tickFormat={(v) => `${v}:00`} label="Hour" />
8075
+ <second.YAxis />
8076
+ <second.Matrix value="service" level="load" name="Load" labels />
8077
+ <Tooltip mode="nearest" />
8078
+ </second.Chart>
8079
+ </>
8080
+ );
8081
+ }
8082
+ ```
8083
+
7307
8084
  #### Keyboard
7308
8085
 
7309
8086
  The keys of [Chart](#chart-1) apply here.
@@ -7314,14 +8091,15 @@ The keys of [Chart](#chart-1) apply here.
7314
8091
 
7315
8092
  | Prop | Type | Default | Description |
7316
8093
  |---|---|---|---|
7317
- | `value` *required* | `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.<br>Shown in: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [Mark bands, limits and cells (Chart)](#chart-1) and 1 more |
7318
- | `level` *required* | `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.<br>Shown in: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [Mark bands, limits and cells (Chart)](#chart-1) and 1 more |
7319
- | `coloring` | `MatrixColoring` | `{ kind: "gradient", stops: DEFAULT_GRADIENT }` | How the level colours a cell: by assessment against limits, or across a gradient.<br>Shown in: [Colour cells by their limits](#matrix), [Mark bands, limits and cells (Chart)](#chart-1) |
8094
+ | `value` *required* | `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.<br>Shown in: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [A correlation matrix, read](#matrix) and 3 more |
8095
+ | `level` *required* | `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.<br>Shown in: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [A correlation matrix, read](#matrix) and 3 more |
8096
+ | `coloring` | `MatrixColoring` | `{ kind: "gradient", stops: DEFAULT_GRADIENT }` | How the level colours a cell: by assessment against limits, or across a gradient. A gradient's legend entry shows a colour bar: its steps, with the range's two ends written under them in `format` and the inner edges where they fit. An assessment keeps its three colours.<br>Shown in: [Colour cells by their limits](#matrix), [A correlation matrix, read](#matrix), [Mark bands, limits and cells (Chart)](#chart-1) |
7320
8097
  | `xAxisId` | `string` | `"x"` | Binding to an x axis. |
7321
8098
  | `yAxisId` | `string` | `"y"` | Binding to a y axis. |
7322
8099
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data. |
7323
- | `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: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [Mark bands, limits and cells (Chart)](#chart-1) and 1 more |
7324
- | `format` | `ValueFormat` | the built-in number format | The level as the tooltip writes it, as a function or `NumberOptions`. The y axis' `tickFormat` writes the row, not the level.<br>Shown in: [Colour cells by their limits](#matrix) |
8100
+ | `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: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [A correlation matrix, read](#matrix) and 3 more |
8101
+ | `format` | `ValueFormat` | the built-in number format | The level as the tooltip writes it, as a function or `NumberOptions`. The y axis' `tickFormat` writes the row, not the level.<br>Shown in: [Colour cells by their limits](#matrix), [Colour cells along a gradient](#matrix), [A correlation matrix, read](#matrix) and 1 more |
8102
+ | `labels` | `boolean` | `false` | Each cell writes its level in `format`, in the tick size, in the text colour over a halo of the ground colour - legible on every stop, in both themes and in forced colours. A cell too small for its text is left without one, never truncated; a hole has none. Off by default: a dense heat map would be all clipped text.<br>Shown in: [A correlation matrix, read](#matrix) |
7325
8103
 
7326
8104
  ##### Types on this page
7327
8105
 
@@ -7439,7 +8217,7 @@ nothing.
7439
8217
 
7440
8218
  | Prop | Type | Default | Description |
7441
8219
  |---|---|---|---|
7442
- | `decimals` | `number` | — | |
8220
+ | `decimals` | `number` | — | <br>Shown in: [Colour cells along a gradient](#matrix), [Where the points crowd (Scatter)](#scatter) |
7443
8221
 
7444
8222
  ###### `Limit`
7445
8223
 
@@ -9106,7 +9884,7 @@ nothing.
9106
9884
 
9107
9885
  | Prop | Type | Default | Description |
9108
9886
  |---|---|---|---|
9109
- | `decimals` | `number` | — | |
9887
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
9110
9888
 
9111
9889
  ###### `RuleName`
9112
9890
 
@@ -9174,6 +9952,42 @@ export default function ParetoExample() {
9174
9952
  }
9175
9953
  ```
9176
9954
 
9955
+ ##### Shares, not counts
9956
+
9957
+ A share reads better as sorted bars than as a pie ([ADR-0059](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0059-the-set-of-chart-kinds-is-open.md)): `pareto()` gives each entry its `share` from 0 to 1, and a percent axis says it - no cumulative line and no second axis.
9958
+
9959
+ ```tsx
9960
+ import { useMemo } from "react";
9961
+ import { Tooltip, pareto, useChart } from "@umriss-ui/charts";
9962
+
9963
+ /* Why deliveries failed last month, in no particular order. */
9964
+ const FAILED_DELIVERIES = [
9965
+ { name: "Damaged in transit", value: 21 },
9966
+ { name: "Nobody home", value: 312 },
9967
+ { name: "Address incomplete", value: 96 },
9968
+ { name: "Refused", value: 34 },
9969
+ { name: "Business closed", value: 58 },
9970
+ { name: "No access code", value: 147 },
9971
+ { name: "Wrong address", value: 12 },
9972
+ { name: "Parcel not found", value: 8 },
9973
+ { name: "Weather", value: 6 },
9974
+ { name: "Vehicle breakdown", value: 4 },
9975
+ ];
9976
+
9977
+ export default function SharesNotCounts() {
9978
+ const { entries } = useMemo(() => pareto(FAILED_DELIVERIES, { collectRank: 6, remainderName: "Other" }), []);
9979
+ const { Chart, XAxis, YAxis, Bar } = useChart(entries);
9980
+ return (
9981
+ <Chart height={300} aria-label="Share of failed deliveries by cause, sorted">
9982
+ <XAxis value="name" />
9983
+ <YAxis label="Share of failed deliveries" tickFormat={{ style: "percent", maximumFractionDigits: 0 }} />
9984
+ <Bar value="share" name="Share" barWidth={0.72} />
9985
+ <Tooltip mode="x" />
9986
+ </Chart>
9987
+ );
9988
+ }
9989
+ ```
9990
+
9177
9991
  #### Keyboard
9178
9992
 
9179
9993
  The keys of [Chart](#chart-1) apply here.
@@ -9253,7 +10067,7 @@ From `LegendProps` — the full table stands on [Legend](#legend).
9253
10067
 
9254
10068
  | Prop | Type | Default | Description |
9255
10069
  |---|---|---|---|
9256
- | [`placement`](#legend) | `"top" \| "bottom"` | `"top"` | Which side of the plot area the legend stands on. |
10070
+ | [`placement`](#legend) | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Which side of the plot area the legend stands on. `"left"` and `"right"` set the entries in a column beside the plot, as wide as the longest name up to a quarter of the chart, scrolling where they do not fit; in a chart narrower than 480 px they stand above the plot, as `"top"`. |
9257
10071
 
9258
10072
  ## Around the chart
9259
10073
 
@@ -10065,7 +10879,7 @@ The keys of [Chart](#chart-1) apply here.
10065
10879
 
10066
10880
  | Prop | Type | Default | Description |
10067
10881
  |---|---|---|---|
10068
- | `mode` | `"x" \| "nearest"` | `"x"` | "x": every series at the x position; "nearest": only the nearest point.<br>Shown in: [Report every series or the nearest](#tooltip), [Write the tooltip yourself](#tooltip), [Write each value in its unit](#tooltip) and 65 more |
10882
+ | `mode` | `"x" \| "nearest"` | `"x"` | "x": every series at the x position; "nearest": only the nearest point.<br>Shown in: [Report every series or the nearest](#tooltip), [Write the tooltip yourself](#tooltip), [Write each value in its unit](#tooltip) and 77 more |
10069
10883
  | `limits` | `boolean` | `false` | Adds a row for every labelled `LimitLine` and `LimitBand` on the hit's y axis - its label and value in that axis' format - to the built-in tooltip, and the same to what a key reads out. Limits are not series and stay out of the tooltip unless asked for.<br>Shown in: [Read the limits at the hit](#tooltip) |
10070
10884
  | `render` | `(hit: ``TooltipHit``<T>) => ReactNode` | the built-in tooltip | Render prop for content of one's own. Compared by its source text, like `tickFormat` - with the same closure limit.<br>Shown in: [Write the tooltip yourself](#tooltip), [Fix the domain and name the ticks (Axis)](#axis) |
10071
10885
 
@@ -10079,7 +10893,7 @@ The keys of [Chart](#chart-1) apply here.
10079
10893
 
10080
10894
  | Prop | Type | Default | Description |
10081
10895
  |---|---|---|---|
10082
- | `onViewChange` | `(view: `[`ChartView`](#view)`) => void` | — | Every change the reader makes, once, always the whole view - the one to keep, or to hand another chart's `setView`. A pan or zoom is reported at most once per frame. Not called for the view the chart starts with, nor for one `setView` applies.<br>Shown in: [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Read it by keyboard and screen reader (Chart)](#chart-1) |
10896
+ | `onViewChange` | `(view: `[`ChartView`](#view)`) => void` | — | Every change the reader makes, once, always the whole view - the one to keep, or to hand another chart's `setView`. A pan or zoom is reported at most once per frame. Not called for the view the chart starts with, nor for one `setView` applies.<br>Shown in: [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Follow the live edge, with pause (Zoom and pan)](#zoom-and-pan), [Read it by keyboard and screen reader (Chart)](#chart-1) |
10083
10897
  | `onSelect` | `(hit: ``TooltipHit``<Z>) => void` | — | The reader chose a point: a click on a hit (a tap too), or Enter on the Active point - the same hit the tooltip's `render` receives. Once per click; the end of a drag does not report, and the second click of a double click does not either. The chart holds no selection and draws none: what is chosen is the application's state. Needs a `<Tooltip>`, which makes the hits; the hit does not travel over `syncId`.<br>Shown in: [Choose a hit](#tooltip), [Drill down from a year to its days](#tooltip) |
10084
10898
  | `onRange` | `(range: ``ChartRange``<V>) => void` | — | The reader marked a stretch of an x axis: Alt with a drag where the chart zooms, a plain drag where it does not. Called once, on release - once per x axis -, with `from` not after `to`, in the axis' own values: numbers, milliseconds on a time axis, wall-clock instants on a calendar axis, and on a category axis the first and last category covered, as strings - say so in the handler's parameter, `ChartRange<string>`. A band covers the stretch while the pointer is down; the chart keeps no range and draws none afterwards, and it does not travel over `syncId`. A press that travels 4 px or less is a click and reaches `onSelect`; Escape and a cancelled pointer report nothing.<br>Shown in: [Mark a range (Zoom and pan)](#zoom-and-pan), [Mark a range of bars (Bar)](#bar) |
10085
10899
 
@@ -10122,6 +10936,7 @@ One hit per series in the tooltip.
10122
10936
  | `yValue` *required* | `number` | — | The point's y value; in a stack its own value, not its top - in a normalised stack its share in percent.<br>Shown in: [Choose a hit](#tooltip), [Fix the domain and name the ticks (Axis)](#axis) |
10123
10937
  | `datum` *required* | `T` | — | <br>Shown in: [Write the tooltip yourself](#tooltip), [Drill down from a year to its days](#tooltip) |
10124
10938
  | `index` *required* | `number` | — | |
10939
+ | `baseline` | `number` | — | The lower edge, where the series has a baseline of its own - a `Bar` or an `Area` outside a stack; `yValue` is the upper one. A stack's foot is no reading and never stands here. A field of its own, as `level` and `box` have ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
10125
10940
  | `level` | `number` | — | Only for the matrix: its level, out of the level channel. It stands here and not in yValue, because yValue is the position on the y axis - for a matrix therefore the row. Putting both into one field would be exactly the overloading [ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md) is written against. |
10126
10941
  | `segment` | `{ from: number; to: number; label: string }` | — | Only for state series: the section being pointed at. A duration does not stand here - it is the difference of two numbers the entry already carries, and formatting it would presuppose knowing what the x axis means. The caller knows that, not this library. |
10127
10942
  | `box` | `BoxNumbers`` & Partial<``BoxExtras``` > & { /** As the caller gave them; only where the series has `outliers`. */ outliers?: readonly number[]; } `` | — | Only for a box: its further numbers; yValue is its median. Fields of their own for the reason `level` has one ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
@@ -10393,6 +11208,72 @@ export default function LegendPlacement() {
10393
11208
  }
10394
11209
  ```
10395
11210
 
11211
+ ##### A legend beside the plot
11212
+
11213
+ `placement="right"` or `"left"` sets the names in a column beside the plot - as wide as the longest one up to a quarter of the chart, a longer name wrapping under its own text. Where the chart is narrower than 480 px, as the last one is, the legend stands above the plot as `"top"` does.
11214
+
11215
+ ```tsx
11216
+ import { Legend, useChart } from "@umriss-ui/charts";
11217
+
11218
+ /* Data from the operations world, written out here so the example runs on its own. */
11219
+ interface Reading {
11220
+ hour: number;
11221
+ units: number;
11222
+ }
11223
+
11224
+ const LINES = [
11225
+ "Press 1",
11226
+ "Press 2",
11227
+ "Welding cell, body shop north",
11228
+ "Paint booth",
11229
+ "Assembly A",
11230
+ "Assembly B",
11231
+ "Final inspection and packing",
11232
+ "Dispatch",
11233
+ ];
11234
+
11235
+ /* Once, outside the component: a series' `data` is compared by reference. Units per hour of every line over a
11236
+ shift, a fixed wave of its own for each. */
11237
+ const BY_LINE = LINES.map((line, l) => ({
11238
+ line,
11239
+ rows: Array.from({ length: 8 }, (_, hour): Reading => ({ hour, units: Math.round(60 + l * 9 + 14 * Math.sin((hour + l) * 0.9)) })),
11240
+ }));
11241
+ const READINGS: readonly Reading[] = BY_LINE.flatMap(({ rows }) => rows);
11242
+
11243
+ function Shift({ placement, width }: { placement: "left" | "right"; width?: number }) {
11244
+ const { Chart, XAxis, YAxis, Line } = useChart(READINGS);
11245
+ return (
11246
+ <Chart height={280} width={width} aria-label={`Units per hour of eight lines, legend ${placement}`}>
11247
+ <XAxis value="hour" />
11248
+ <YAxis label="units" />
11249
+ {BY_LINE.map(({ line, rows }) => (
11250
+ <Line key={line} data={rows} value="units" name={line} />
11251
+ ))}
11252
+ <Legend placement={placement} />
11253
+ </Chart>
11254
+ );
11255
+ }
11256
+
11257
+ export default function BesideThePlot() {
11258
+ return (
11259
+ <div className="pair" style={{ gridTemplateColumns: "minmax(0, 1fr)" }}>
11260
+ <div>
11261
+ <p className="pair-caption">placement="right"</p>
11262
+ <Shift placement="right" />
11263
+ </div>
11264
+ <div>
11265
+ <p className="pair-caption">placement="left"</p>
11266
+ <Shift placement="left" />
11267
+ </div>
11268
+ <div>
11269
+ <p className="pair-caption">placement="right", 400 px wide - above the plot</p>
11270
+ <Shift placement="right" width={400} />
11271
+ </div>
11272
+ </div>
11273
+ );
11274
+ }
11275
+ ```
11276
+
10396
11277
  #### Keyboard
10397
11278
 
10398
11279
  | Key | Action |
@@ -10412,7 +11293,7 @@ Every entry of a named series is a button whose `aria-pressed` says whether its
10412
11293
 
10413
11294
  | Prop | Type | Default | Description |
10414
11295
  |---|---|---|---|
10415
- | `placement` | `"top" \| "bottom"` | `"top"` | Which side of the plot area the legend stands on.<br>Shown in: [Legend above or below](#legend), [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1) and 28 more |
11296
+ | `placement` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Which side of the plot area the legend stands on. `"left"` and `"right"` set the entries in a column beside the plot, as wide as the longest name up to a quarter of the chart, scrolling where they do not fit; in a chart narrower than 480 px they stand above the plot, as `"top"`.<br>Shown in: [Legend above or below](#legend), [A legend beside the plot](#legend), [Mix series kinds (Chart)](#chart-1) and 30 more |
10416
11297
 
10417
11298
  ### Benchmark
10418
11299
 
@@ -10908,7 +11789,7 @@ From `LegendProps` — the full table stands on [Legend](#legend).
10908
11789
 
10909
11790
  | Prop | Type | Default | Description |
10910
11791
  |---|---|---|---|
10911
- | [`placement`](#legend) | `"top" \| "bottom"` | `"top"` | Which side of the plot area the legend stands on. |
11792
+ | [`placement`](#legend) | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Which side of the plot area the legend stands on. `"left"` and `"right"` set the entries in a column beside the plot, as wide as the longest name up to a quarter of the chart, scrolling where they do not fit; in a chart narrower than 480 px they stand above the plot, as `"top"`. |
10912
11793
 
10913
11794
  ## API index
10914
11795
 
@@ -10937,13 +11818,13 @@ at once. Place it inside a `Chart`.
10937
11818
  function DataTable(): ReactNode;
10938
11819
  ```
10939
11820
 
10940
- Used on [Chart](#chart-1), [BoxPlot](#boxplot) and [Tooltip](#tooltip).
11821
+ Used on [Chart](#chart-1), [Bar](#bar), [BoxPlot](#boxplot) and [Tooltip](#tooltip).
10941
11822
 
10942
11823
  ##### `Legend`
10943
11824
 
10944
11825
  [Legend](#legend) – What the colours mean (a key), and a switch for every series: a click hides it and shows it again, a double click shows only it (a solo).
10945
11826
 
10946
- Used on [Scenarios](#scenarios), [Chart](#chart-1), [Axis](#axis), [View](#view), [Line](#line), [Area](#area), [Bar](#bar), [Scatter](#scatter), [BoxPlot](#boxplot), [StateBand](#stateband), [LimitLine](#limitline), [Pareto](#pareto), [Tooltip](#tooltip), [Legend](#legend) and [Benchmark](#benchmark).
11827
+ Used on [Scenarios](#scenarios), [Chart](#chart-1), [Axis](#axis), [View](#view), [Line](#line), [Area](#area), [Bar](#bar), [Scatter](#scatter), [BoxPlot](#boxplot), [StateBand](#stateband), [Matrix](#matrix), [LimitLine](#limitline), [Pareto](#pareto), [Tooltip](#tooltip), [Legend](#legend) and [Benchmark](#benchmark).
10947
11828
 
10948
11829
  ##### `LimitBand`
10949
11830
 
@@ -11016,15 +11897,16 @@ Counts values into bins of equal width.
11016
11897
  A value on an edge belongs to the bin that edge starts - `[from, to)` - but
11017
11898
  the last bin is closed, so the greatest value of a domain is counted and not
11018
11899
  lost. Values that are not finite (`NaN`, ±Infinity, `null`, `undefined`)
11019
- are skipped, as a gap is on a chart. Without values there are no bins; with
11900
+ are skipped, as a gap is on a chart. The values may be a typed array
11901
+ (`Float64Array`, `Int32Array`): they are read by index. Without values there are no bins; with
11020
11902
  values there is at least one, even when they are all equal.
11021
11903
 
11022
11904
  ```ts
11023
- function bin(values: readonly (number | null | undefined)[], options?: BinOptions): Bin[];
11905
+ function bin(values: ArrayLike<number | null | undefined>, options?: BinOptions): Bin[];
11024
11906
  function bin<T>(rows: readonly T[], value: (row: T) => number | null | undefined, options?: BinOptions): Bin[];
11025
11907
  ```
11026
11908
 
11027
- Used on [Bar](#bar).
11909
+ Used on [Chart](#chart-1), [Bar](#bar) and [Scatter](#scatter).
11028
11910
 
11029
11911
  ##### `breaks`
11030
11912
 
@@ -11346,6 +12228,8 @@ One second in milliseconds, the unit of a time axis.
11346
12228
  const SECOND = 1000;
11347
12229
  ```
11348
12230
 
12231
+ Used on [Zoom and pan](#zoom-and-pan).
12232
+
11349
12233
  #### Types
11350
12234
 
11351
12235
  ##### `AreaProps`
@@ -11363,6 +12247,7 @@ An `Area` as the chart holds it once registered.
11363
12247
  | `fillOpacity` *required* | `number` | — | Opacity of the fill; the outline stays fully opaque. |
11364
12248
  | `strokeWidth` *required* | `number` | — | |
11365
12249
  | `dash` | `readonly number[]` | — | Dash pattern of the outline. |
12250
+ | `maxGap` | `number` | — | A step between readings longer than this is a gap; not read in a stack. |
11366
12251
  | `accessor` *required* | `ValueFunction``<T>` | — | Y value; null/undefined/NaN/±Infinity means a gap. From `SeriesBase`. |
11367
12252
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data. From `SeriesBase`. |
11368
12253
  | `xAxisId` *required* | `string` | — | From `SeriesBase`. |
@@ -11472,6 +12357,7 @@ A `Bar` as the chart holds it once registered.
11472
12357
  | Prop | Type | Default | Description |
11473
12358
  |---|---|---|---|
11474
12359
  | `kind` *required* | `"bar"` | — | |
12360
+ | `baseline` | `ValueFunction``<T>` | — | Lower edge; without a value the fixed baseline 0. |
11475
12361
  | `barWidth` *required* | `number` | — | Width as a fraction of the step; the group shares this fraction. |
11476
12362
  | `accessor` *required* | `ValueFunction``<T>` | — | Y value; null/undefined/NaN/±Infinity means a gap. From `SeriesBase`. |
11477
12363
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data. From `SeriesBase`. |
@@ -11502,7 +12388,7 @@ The settings of `bin`.
11502
12388
  | Prop | Type | Default | Description |
11503
12389
  |---|---|---|---|
11504
12390
  | `width` | `number` | — | The width of a bin. Wins over `count`. Without a `domain` the edges are multiples of it.<br>Shown in: [Count readings into a histogram (Bar)](#bar) |
11505
- | `count` | `number` | `10` | About how many bins: the width becomes the next 1-2-5 step, so the number comes out near it and not exactly - with a `domain`, exactly. |
12391
+ | `count` | `number` | `10` | About how many bins: the width becomes the next 1-2-5 step, so the number comes out near it and not exactly - with a `domain`, exactly.<br>Shown in: [Columns instead of rows (Chart)](#chart-1) |
11506
12392
  | `domain` | `readonly [number, number]` | — | The span the bins cover, the first edge on its start and the last on its end. Values outside it are left out, not clamped into the end bins. |
11507
12393
 
11508
12394
  ##### `BoxChannels`
@@ -11598,22 +12484,22 @@ is typed by that `data` instead.
11598
12484
 
11599
12485
  | Prop | Type | Default | Description |
11600
12486
  |---|---|---|---|
11601
- | `Chart` *required* | `(props: `[`ChartProps`](#chart-1)`) => ReactNode` | — | The chart over the hook's rows. One per `useChart`: it holds the view, and a second chart is a second call.<br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 79 more |
11602
- | `XAxis` *required* | `<T = Z>(props: `[`XAxisProps`](#axis)`<T>) => null` | — | An x axis reads the rows of every series bound to it: the hook's, or with `<XAxis<Row>>` those the series bring as their own `data`.<br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 79 more |
11603
- | `YAxis` *required* | `(props: `[`YAxisProps`](#axis)`) => null` | — | <br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 79 more |
11604
- | `Line` *required* | `<T = Z>(props: `[`LineProps`](#line)`<T>) => null` | — | <br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 50 more |
11605
- | `Area` *required* | `<T = Z>(props: `[`AreaProps`](#area)`<T>) => null` | — | <br>Shown in: [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1), [Bind every kind of series to a second axis (Axis)](#axis) and 5 more |
11606
- | `Bar` *required* | `<T = Z>(props: `[`BarProps`](#bar)`<T>) => null` | — | <br>Shown in: [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1), [Bind every kind of series to a second axis (Axis)](#axis) and 15 more |
11607
- | `Scatter` *required* | `<T = Z>(props: `[`ScatterProps`](#scatter)`<T>) => null` | — | <br>Shown in: [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1), [Bind every kind of series to a second axis (Axis)](#axis) and 3 more |
12487
+ | `Chart` *required* | `(props: `[`ChartProps`](#chart-1)`) => ReactNode` | — | The chart over the hook's rows. One per `useChart`: it holds the view, and a second chart is a second call.<br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 93 more |
12488
+ | `XAxis` *required* | `<T = Z>(props: `[`XAxisProps`](#axis)`<T>) => null` | — | An x axis reads the rows of every series bound to it: the hook's, or with `<XAxis<Row>>` those the series bring as their own `data`.<br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 93 more |
12489
+ | `YAxis` *required* | `(props: `[`YAxisProps`](#axis)`) => null` | — | <br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 93 more |
12490
+ | `Line` *required* | `<T = Z>(props: `[`LineProps`](#line)`<T>) => null` | — | <br>Shown in: [Draw a first chart (Installation)](#installation), [Size a chart by its container (Installation)](#installation), [Mix series kinds (Chart)](#chart-1) and 58 more |
12491
+ | `Area` *required* | `<T = Z>(props: `[`AreaProps`](#area)`<T>) => null` | — | <br>Shown in: [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1), [Bind every kind of series to a second axis (Axis)](#axis) and 7 more |
12492
+ | `Bar` *required* | `<T = Z>(props: `[`BarProps`](#bar)`<T>) => null` | — | <br>Shown in: [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1), [Columns instead of rows (Chart)](#chart-1) and 20 more |
12493
+ | `Scatter` *required* | `<T = Z>(props: `[`ScatterProps`](#scatter)`<T>) => null` | — | <br>Shown in: [Mix series kinds (Chart)](#chart-1), [Mark every kind (Chart)](#chart-1), [Bind every kind of series to a second axis (Axis)](#axis) and 5 more |
11608
12494
  | `StateBand` *required* | `<T = Z>(props: `[`StateBandProps`](#stateband)`<T> & ``OneLane``) => null` | — | <br>Shown in: [Mark bands, limits and cells (Chart)](#chart-1), [One vehicle through the day (StateBand)](#stateband), [Lay a state under a course (StateBand)](#stateband) and 4 more |
11609
- | `Matrix` *required* | `<T = Z>(props: `[`MatrixProps`](#matrix)`<T>) => null` | — | <br>Shown in: [Mark bands, limits and cells (Chart)](#chart-1), [Colour cells by their limits (Matrix)](#matrix), [Colour cells along a gradient (Matrix)](#matrix) and 1 more |
12495
+ | `Matrix` *required* | `<T = Z>(props: `[`MatrixProps`](#matrix)`<T>) => null` | — | <br>Shown in: [Mark bands, limits and cells (Chart)](#chart-1), [Where the points crowd (Scatter)](#scatter), [Colour cells by their limits (Matrix)](#matrix) and 3 more |
11610
12496
  | `BoxPlot` *required* | `<T = Z>(props: `[`BoxPlotProps`](#boxplot)`<T>) => null` | — | <br>Shown in: [One box per service (BoxPlot)](#boxplot), [A box per hour on a time axis (BoxPlot)](#boxplot), [With outliers (BoxPlot)](#boxplot) and 4 more |
11611
12497
  | `view` *required* | [`ChartView`](#view) | — | How the reader is looking at the chart.<br>Shown in: [Keep and restore a view (View)](#view) |
11612
12498
  | `domains` *required* | `AxisSpans` | — | The span each zoomable x axis shows, by axis `id`; an axis absent shows its own `domain`. |
11613
- | `setView` *required* | `(view: `[`ChartView`](#view)`) => void` | — | Goes to a whole view; what it leaves out is shown whole again. Not reported through `onViewChange`: two charts kept in step by `onViewChange: (v) => other.setView(v)` in both directions do not answer each other. A view with the content already shown changes nothing.<br>Shown in: [Read it by keyboard and screen reader (Chart)](#chart-1), [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Keep and restore a view (View)](#view) |
12499
+ | `setView` *required* | `(view: `[`ChartView`](#view)`) => void` | — | Goes to a whole view; what it leaves out is shown whole again. Not reported through `onViewChange`: two charts kept in step by `onViewChange: (v) => other.setView(v)` in both directions do not answer each other. A view with the content already shown changes nothing.<br>Shown in: [Read it by keyboard and screen reader (Chart)](#chart-1), [Keep charts in step (Zoom and pan)](#zoom-and-pan), [Follow the live edge, with pause (Zoom and pan)](#zoom-and-pan) and 1 more |
11614
12500
  | `setDomain` *required* | `(axisId: string, span: readonly [number, number] \| null) => void` | — | Puts a span in view on a zoomable x axis - a lone one is `"x"`; `null` shows the axis' own `domain` again.<br>Shown in: [Measure a million points (Benchmark)](#benchmark) |
11615
- | `hidden` *required* | `readonly string[]` | — | The hidden series, by `name`. |
11616
- | `toggleSeries` *required* | `(name: string) => void` | — | Hides the series `name`, or shows it where it is hidden - as a click on its legend entry does. Hiding the last series visible shows all. |
12501
+ | `hidden` *required* | `readonly string[]` | — | The hidden series, by `name`.<br>Shown in: [A trend with pens (Axis)](#axis) |
12502
+ | `toggleSeries` *required* | `(name: string) => void` | — | Hides the series `name`, or shows it where it is hidden - as a click on its legend entry does. Hiding the last series visible shows all.<br>Shown in: [A trend with pens (Axis)](#axis) |
11617
12503
  | `showOnly` *required* | `(name: string) => void` | — | Hides every series but `name`; all are shown where none carries it.<br>Shown in: [Hide a series from the legend (Legend)](#legend) |
11618
12504
  | `showAllSeries` *required* | `() => void` | — | Shows every series again.<br>Shown in: [Hide a series from the legend (Legend)](#legend) |
11619
12505
  | `toPng` *required* | `() => Promise<Blob>` | — | A PNG of the chart as it stands: the plot with its grid, series and limits, the axes with ticks, labels and titles, and the legend, on the chart's own ground in the colour scheme in use, at the chart's size on screen and at least twice its CSS pixels. Not in it: the tooltip, the crosshair and hover marks, the "Show all" button, the data key and its table, the screen-reader texts, and any title. Where the picture goes - a download, the clipboard, an upload - is the application's. Rejects with an `Error` when the chart is not on screen or has no size.<br>Shown in: [Hand over a picture (Chart)](#chart-1) |
@@ -11692,6 +12578,7 @@ Everything the charts put out as text.
11692
12578
  | `count` *required* | `string` | — | |
11693
12579
  | `outlierList` *required* | `(values: readonly string[], more: number) => string` | — | |
11694
12580
  | `downsampled` *required* | `(readings: number) => string` | — | Added to the caption where the table shows the downsampled course: how many readings its rows stand for. |
12581
+ | `levelRange` *required* | `(from: string, to: string) => string` | — | Added to the name of a matrix' legend entry that has a colour bar: the range its colours span, both ends written in the matrix' format. The bar's own values are hidden from assistive technology. |
11695
12582
 
11696
12583
  ##### `ChartView`
11697
12584
 
@@ -11745,7 +12632,7 @@ A `Legend` as the chart holds it once registered.
11745
12632
 
11746
12633
  | Prop | Type | Default | Description |
11747
12634
  |---|---|---|---|
11748
- | `placement` *required* | `"top" \| "bottom"` | — | |
12635
+ | `placement` *required* | `"top" \| "bottom" \| "left" \| "right"` | — | |
11749
12636
 
11750
12637
  ##### `LegendProps`
11751
12638
 
@@ -11868,6 +12755,7 @@ A `Line` as the chart holds it once registered.
11868
12755
  | `dash` | `readonly number[]` | — | |
11869
12756
  | `markers` *required* | `"auto" \| "always" \| "never"` | — | |
11870
12757
  | `step` | `boolean` | — | Sample-and-hold: each value holds until the next sample. |
12758
+ | `maxGap` | `number` | — | A step between readings longer than this is a gap. |
11871
12759
  | `accessor` *required* | `ValueFunction``<T>` | — | Y value; null/undefined/NaN/±Infinity means a gap. From `SeriesBase`. |
11872
12760
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data. From `SeriesBase`. |
11873
12761
  | `xAxisId` *required* | `string` | — | From `SeriesBase`. |
@@ -11959,6 +12847,7 @@ series kind whose base accessor reads more than a number.
11959
12847
  | `kind` *required* | `"matrix"` | — | |
11960
12848
  | `level` *required* | `ValueFunction``<T>` | — | The level that decides the colour - the third channel ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). The base accessor yields the row position, not this level. |
11961
12849
  | `coloring` *required* | `MatrixColoring` | — | |
12850
+ | `labels` | `boolean` | — | Each cell writes its level. |
11962
12851
  | `accessor` *required* | `ValueFunction``<T>` | — | Y value; null/undefined/NaN/±Infinity means a gap. From `SeriesBase`. |
11963
12852
  | `data` | `readonly T[]` | — | Series-own data; overrides the container data. From `SeriesBase`. |
11964
12853
  | `xAxisId` *required* | `string` | — | From `SeriesBase`. |
@@ -11989,7 +12878,7 @@ nothing.
11989
12878
 
11990
12879
  | Prop | Type | Default | Description |
11991
12880
  |---|---|---|---|
11992
- | `decimals` | `number` | — | |
12881
+ | `decimals` | `number` | — | <br>Shown in: [Where the points crowd (Scatter)](#scatter), [Colour cells along a gradient (Matrix)](#matrix) |
11993
12882
 
11994
12883
  ##### `OneLane`
11995
12884
 
@@ -12051,7 +12940,7 @@ where the cutoff is crossed.
12051
12940
 
12052
12941
  | Prop | Type | Default | Description |
12053
12942
  |---|---|---|---|
12054
- | `entries` *required* | `readonly ``ParetoEntry``[]` | — | <br>Shown in: [Sort causes and collect the tail (Pareto)](#pareto) |
12943
+ | `entries` *required* | `readonly ``ParetoEntry``[]` | — | <br>Shown in: [Sort causes and collect the tail (Pareto)](#pareto), [Shares, not counts (Pareto)](#pareto) |
12055
12944
  | `total` *required* | `number` | — | Total of the drawn entries - after collecting, the same as before. |
12056
12945
  | `cutoffIndex` *required* | `number` | — | Index of the first entry whose cumulative share reaches the cutoff; -1 when it is never reached. |
12057
12946
 
@@ -12104,6 +12993,7 @@ chart stands.
12104
12993
  | `colorOk` *required* | `string` | — | The third outcome of an assessment. "unknown" deliberately has no colour: a missing value becomes a hole like every gap in this library, and a hole carries no colour, because a colour would be a claim. |
12105
12994
  | `font` *required* | `string` | — | |
12106
12995
  | `fontMono` *required* | `string` | — | |
12996
+ | `sizeTick` *required* | `number` | — | The tick label's size in pixels (`--uc-size-tick`). |
12107
12997
  | `series` *required* | `readonly string[]` | — | |
12108
12998
  | `forced` *required* | `boolean` | — | The page is in forced colours: every colour above is a system colour, and the chart encodes by marks whatever its `encoding`. |
12109
12999
 
@@ -12304,6 +13194,7 @@ One hit per series in the tooltip.
12304
13194
  | `yValue` *required* | `number` | — | The point's y value; in a stack its own value, not its top - in a normalised stack its share in percent.<br>Shown in: [Fix the domain and name the ticks (Axis)](#axis), [Choose a hit (Tooltip)](#tooltip) |
12305
13195
  | `datum` *required* | `T` | — | <br>Shown in: [Write the tooltip yourself (Tooltip)](#tooltip), [Drill down from a year to its days (Tooltip)](#tooltip) |
12306
13196
  | `index` *required* | `number` | — | |
13197
+ | `baseline` | `number` | — | The lower edge, where the series has a baseline of its own - a `Bar` or an `Area` outside a stack; `yValue` is the upper one. A stack's foot is no reading and never stands here. A field of its own, as `level` and `box` have ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |
12307
13198
  | `level` | `number` | — | Only for the matrix: its level, out of the level channel. It stands here and not in yValue, because yValue is the position on the y axis - for a matrix therefore the row. Putting both into one field would be exactly the overloading [ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md) is written against. |
12308
13199
  | `segment` | `{ from: number; to: number; label: string }` | — | Only for state series: the section being pointed at. A duration does not stand here - it is the difference of two numbers the entry already carries, and formatting it would presuppose knowing what the x axis means. The caller knows that, not this library. |
12309
13200
  | `box` | `BoxNumbers`` & Partial<``BoxExtras``` > & { /** As the caller gave them; only where the series has `outliers`. */ outliers?: readonly number[]; } `` | — | Only for a box: its further numbers; yValue is its median. Fields of their own for the reason `level` has one ([ADR-0011](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0011-named-channels-never-overloaded.md)). |