@umriss-ui/charts 0.5.0 → 0.7.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/CHANGELOG.md +114 -0
- package/README.md +27 -7
- package/dist/charts.css +1 -1
- package/dist/charts.js +2686 -1689
- package/dist/de.d.ts +47 -0
- package/dist/index.d.ts +108 -5
- package/dist/wording/de.d.ts +2 -0
- package/dist/wording/de.js +21 -0
- package/docs/llms-full.md +4031 -0
- package/package.json +7 -2
|
@@ -0,0 +1,4031 @@
|
|
|
1
|
+
# @umriss-ui/charts 0.7.0
|
|
2
|
+
|
|
3
|
+
> Canvas-based chart library - few chart kinds, uncompromisingly clean, high performance.
|
|
4
|
+
|
|
5
|
+
Install with `pnpm add @umriss-ui/charts`. This text is generated from the package's demo (https://romanhaendler.github.io/umriss-ui/charts/): every page with its import line, its examples - the source exactly as it runs, with the package name where the demo imports its own source - its props tables generated from the code, and why it is built the way it is. The pages index stands in https://romanhaendler.github.io/umriss-ui/charts/llms.txt.
|
|
6
|
+
|
|
7
|
+
## Chart
|
|
8
|
+
|
|
9
|
+
The container and its axes: what is drawn on, and what the drawing is measured against.
|
|
10
|
+
|
|
11
|
+
### Chart
|
|
12
|
+
|
|
13
|
+
The container: it holds the data, measures its host and draws its series on canvas - series and axes are its children, and their order in the JSX is the drawing order.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Chart } from "@umriss-ui/charts";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/chart
|
|
20
|
+
|
|
21
|
+
#### Examples
|
|
22
|
+
|
|
23
|
+
##### Mixed series kinds
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
/* One chart, four series kinds, shared axes: inflow and outflow as grouped bars,
|
|
27
|
+
a corridor as a band area with a gap, the stock as a line and samples as a
|
|
28
|
+
scatter.
|
|
29
|
+
|
|
30
|
+
The order in the JSX decides what lies over what - and at the same time the
|
|
31
|
+
palette colour. That is the whole composition rule: a series is an element,
|
|
32
|
+
not an entry in a configuration object. */
|
|
33
|
+
|
|
34
|
+
import { Area, Bar, Chart, Legend, Line, Scatter, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
35
|
+
import { mixedData, type MixedPoint } from "../../data";
|
|
36
|
+
|
|
37
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
38
|
+
example can be copied whole. */
|
|
39
|
+
|
|
40
|
+
export default function Mixed() {
|
|
41
|
+
return (
|
|
42
|
+
<Chart data={mixedData} height={320} ariaLabel="Bars, area, line and scatter in one chart">
|
|
43
|
+
<XAxis accessor={(d: MixedPoint) => d.t} label="Period" tickCount={7} />
|
|
44
|
+
<YAxis accessor={(d: MixedPoint) => d.stock} label="Quantity" />
|
|
45
|
+
<Bar accessor={(d: MixedPoint) => d.inflow} name="Inflow" />
|
|
46
|
+
<Bar accessor={(d: MixedPoint) => d.outflow} name="Outflow" />
|
|
47
|
+
<Area
|
|
48
|
+
accessor={(d: MixedPoint) => d.corridorUpper}
|
|
49
|
+
baseline={(d: MixedPoint) => d.corridorLower}
|
|
50
|
+
name="Corridor (with a gap)"
|
|
51
|
+
/>
|
|
52
|
+
<Line accessor={(d: MixedPoint) => d.stock} name="Stock" strokeWidth={2} />
|
|
53
|
+
<Scatter accessor={(d: MixedPoint) => d.sample} name="Samples" />
|
|
54
|
+
<Legend placement="top" />
|
|
55
|
+
<Tooltip mode="x" />
|
|
56
|
+
</Chart>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
62
|
+
|
|
63
|
+
##### Size and resize
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
/* 100 % width in a draggable container, a fixed size beside it, and a chart in a
|
|
67
|
+
collapsible surrounding.
|
|
68
|
+
|
|
69
|
+
The three cases a chart meets in an application, and the one thing they have
|
|
70
|
+
in common: the chart measures its host rather than being told a pixel. A
|
|
71
|
+
container that collapses to nothing and comes back has to redraw at the width
|
|
72
|
+
it finds, not at the one it had. */
|
|
73
|
+
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
import { Chart, Line, XAxis, YAxis } from "@umriss-ui/charts";
|
|
76
|
+
import { basicData, type Point } from "../../data";
|
|
77
|
+
|
|
78
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
79
|
+
example can be copied whole. */
|
|
80
|
+
|
|
81
|
+
export default function Sizes() {
|
|
82
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<div className="resizable">
|
|
87
|
+
<Chart data={basicData} height={200} ariaLabel="Full width, draggable">
|
|
88
|
+
<XAxis accessor={(d: Point) => d.t} />
|
|
89
|
+
<YAxis accessor={(d: Point) => d.a} />
|
|
90
|
+
<Line accessor={(d: Point) => d.a} name="Series A" />
|
|
91
|
+
</Chart>
|
|
92
|
+
</div>
|
|
93
|
+
<button className="demo-button" onClick={() => setCollapsed((v) => !v)}>{collapsed ? "Expand" : "Collapse"}</button>
|
|
94
|
+
{!collapsed && (
|
|
95
|
+
<div className="side-by-side">
|
|
96
|
+
<Chart data={basicData} width={420} height={180} ariaLabel="Fixed size">
|
|
97
|
+
<XAxis accessor={(d: Point) => d.t} />
|
|
98
|
+
<YAxis accessor={(d: Point) => d.a} />
|
|
99
|
+
<Line accessor={(d: Point) => d.a} name="Series A" />
|
|
100
|
+
</Chart>
|
|
101
|
+
<div className="narrow">
|
|
102
|
+
<Chart data={basicData} height={180} ariaLabel="Narrow container">
|
|
103
|
+
<XAxis accessor={(d: Point) => d.t} />
|
|
104
|
+
<YAxis accessor={(d: Point) => d.a} />
|
|
105
|
+
<Line accessor={(d: Point) => d.a} name="Series A" />
|
|
106
|
+
</Chart>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
116
|
+
|
|
117
|
+
##### Nothing to show
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
/* A chart with nothing to show says so. No data, only gaps, or every series
|
|
121
|
+
hidden: the axes and the frame stay where they are, and the plot area says
|
|
122
|
+
"No data" - or whatever the caller puts into `empty`, in any language. An
|
|
123
|
+
empty frame that says nothing looks like a chart of a line that stood
|
|
124
|
+
still. */
|
|
125
|
+
|
|
126
|
+
import { Chart, Line, XAxis, YAxis } from "@umriss-ui/charts";
|
|
127
|
+
|
|
128
|
+
interface Reading {
|
|
129
|
+
t: number;
|
|
130
|
+
celsius: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nothing: Reading[] = [];
|
|
134
|
+
|
|
135
|
+
function Furnace({ empty }: { empty?: string }) {
|
|
136
|
+
return (
|
|
137
|
+
<Chart data={nothing} height={220} ariaLabel="A furnace without readings" empty={empty}>
|
|
138
|
+
<XAxis accessor={(d: Reading) => d.t} time />
|
|
139
|
+
<YAxis accessor={(d: Reading) => d.celsius} label="°C" />
|
|
140
|
+
<Line accessor={(d: Reading) => d.celsius} name="Furnace 1" />
|
|
141
|
+
</Chart>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default function Empty() {
|
|
146
|
+
return (
|
|
147
|
+
<div className="pair">
|
|
148
|
+
<div>
|
|
149
|
+
<p className="pair-caption">no empty - the default</p>
|
|
150
|
+
<Furnace />
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<p className="pair-caption">empty="Furnace 1 is not reporting"</p>
|
|
154
|
+
<Furnace empty="Furnace 1 is not reporting" />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
##### Cursor sync
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
/* Three charts of one kiln, stacked: its temperature, the flue gas and the gas
|
|
165
|
+
it burns. `syncId` gives them one pointer - hover one, and the other two draw
|
|
166
|
+
their crosshair at the same instant; the tooltip stays where the pointer is.
|
|
167
|
+
Zoom is not shared by the library: the three pass one controlled domain
|
|
168
|
+
between them, so a zoom in any of them zooms all three. Each plot begins
|
|
169
|
+
where its own y axis ends; labels of one width - three figures here - keep
|
|
170
|
+
the three crosshairs in one column. */
|
|
171
|
+
|
|
172
|
+
import { useState } from "react";
|
|
173
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
174
|
+
import { kilnData, type KilnPoint } from "../../data";
|
|
175
|
+
|
|
176
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
177
|
+
example can be copied whole. */
|
|
178
|
+
|
|
179
|
+
const CHANNELS = [
|
|
180
|
+
{ name: "Kiln", unit: "°C", value: (d: KilnPoint) => d.temperature },
|
|
181
|
+
{ name: "Flue gas", unit: "°C", value: (d: KilnPoint) => d.flue },
|
|
182
|
+
{ name: "Gas", unit: "m³/h", value: (d: KilnPoint) => d.gas },
|
|
183
|
+
] as const;
|
|
184
|
+
|
|
185
|
+
export default function CursorSync() {
|
|
186
|
+
const [domain, setDomain] = useState<"data" | readonly [number, number]>("data");
|
|
187
|
+
return (
|
|
188
|
+
<div>
|
|
189
|
+
{CHANNELS.map((c) => (
|
|
190
|
+
<Chart key={c.name} data={kilnData} height={140} syncId="kiln" ariaLabel={`${c.name} across a week`}>
|
|
191
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} />
|
|
192
|
+
<YAxis accessor={c.value} label={c.unit} domain="visible" tickCount={4} />
|
|
193
|
+
<Line accessor={c.value} name={c.name} />
|
|
194
|
+
<Tooltip mode="x" />
|
|
195
|
+
</Chart>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
203
|
+
|
|
204
|
+
##### By keyboard and screen reader
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
/* A chart with a tooltip is one tab stop (ADR-0030). Tab into it: the Active
|
|
208
|
+
point stands at the newest value, ← and → walk the readings, ↑ and ↓ choose
|
|
209
|
+
the series read first, Home and End go to the ends, Escape clears. Where the
|
|
210
|
+
axis is zoomable, + and − zoom, Shift with ← or → pans, 0 shows the week
|
|
211
|
+
again. A screen reader hears the values once the keys rest, and the plot
|
|
212
|
+
is described by a summary of what it shows.
|
|
213
|
+
|
|
214
|
+
The words are the chart's own register (ADR-0031): English without a value,
|
|
215
|
+
German from `@umriss-ui/charts/wording/de`. */
|
|
216
|
+
|
|
217
|
+
import { useState } from "react";
|
|
218
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
219
|
+
import { GERMAN_CHARTS_WORDING } from "@umriss-ui/charts/wording/de";
|
|
220
|
+
import { kilnData, type KilnPoint } from "../../data";
|
|
221
|
+
|
|
222
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
223
|
+
example can be copied whole. */
|
|
224
|
+
|
|
225
|
+
export default function KeyboardAndScreenReader() {
|
|
226
|
+
const [domain, setDomain] = useState<"data" | readonly [number, number]>("data");
|
|
227
|
+
return (
|
|
228
|
+
<div>
|
|
229
|
+
<Chart data={kilnData} height={200} ariaLabel="Kiln and flue gas across a week">
|
|
230
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} />
|
|
231
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" />
|
|
232
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Kiln" />
|
|
233
|
+
<Line accessor={(d: KilnPoint) => d.flue} name="Flue gas" />
|
|
234
|
+
<Tooltip mode="x" />
|
|
235
|
+
</Chart>
|
|
236
|
+
<Chart
|
|
237
|
+
data={kilnData}
|
|
238
|
+
height={200}
|
|
239
|
+
ariaLabel="Ofen und Rauchgas über eine Woche"
|
|
240
|
+
wording={GERMAN_CHARTS_WORDING}
|
|
241
|
+
>
|
|
242
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} />
|
|
243
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" />
|
|
244
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Ofen" />
|
|
245
|
+
<Line accessor={(d: KilnPoint) => d.flue} name="Rauchgas" />
|
|
246
|
+
<Tooltip mode="x" />
|
|
247
|
+
</Chart>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
254
|
+
|
|
255
|
+
##### Told apart without colour
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
/* Four courses a reader has to tell apart - and colour is not enough for
|
|
259
|
+
every reader, nor for a grey printout. With `encoding="marks"` each series
|
|
260
|
+
carries a dash pattern and a marker shape as well as its colour, chosen by
|
|
261
|
+
the same palette place: the first stays solid with circles, the second is
|
|
262
|
+
dashed with squares, the third dotted with triangles, and so on. The legend's
|
|
263
|
+
chips show the same.
|
|
264
|
+
|
|
265
|
+
Left without it, as every chart is by default; right with it. */
|
|
266
|
+
|
|
267
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
268
|
+
import { configData, type Point } from "../../data";
|
|
269
|
+
|
|
270
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
271
|
+
example can be copied whole. */
|
|
272
|
+
|
|
273
|
+
function Courses({ encoding }: { encoding?: "marks" }) {
|
|
274
|
+
return (
|
|
275
|
+
<Chart data={configData} height={240} ariaLabel="Four courses" encoding={encoding}>
|
|
276
|
+
<XAxis accessor={(d: Point) => d.t} label="Hour" />
|
|
277
|
+
<YAxis accessor={(d: Point) => d.a} />
|
|
278
|
+
<Line accessor={(d: Point) => d.a} name="Line A" />
|
|
279
|
+
<Line accessor={(d: Point) => d.b} name="Line B" />
|
|
280
|
+
<Line accessor={(d: Point) => d.c} name="Line C" />
|
|
281
|
+
<Line accessor={(d: Point) => d.d} name="Line D" />
|
|
282
|
+
<Legend />
|
|
283
|
+
<Tooltip mode="x" />
|
|
284
|
+
</Chart>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export default function ToldApartWithoutColour() {
|
|
289
|
+
return (
|
|
290
|
+
<div className="pair">
|
|
291
|
+
<div>
|
|
292
|
+
<p className="pair-caption">by colour - the default</p>
|
|
293
|
+
<Courses />
|
|
294
|
+
</div>
|
|
295
|
+
<div>
|
|
296
|
+
<p className="pair-caption">encoding="marks"</p>
|
|
297
|
+
<Courses encoding="marks" />
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
305
|
+
|
|
306
|
+
##### Marks on every kind
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
/* Encoding by marks on every kind in one chart: the bars take a hatch across
|
|
310
|
+
their fill, the corridor a dash on its outline and a hatch in its own
|
|
311
|
+
colour, the line its dash and markers, the samples their shape - each by
|
|
312
|
+
its palette place, so the pairing of colour and mark is the same in every
|
|
313
|
+
chart of an application. */
|
|
314
|
+
|
|
315
|
+
import { Area, Bar, Chart, Legend, Line, Scatter, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
316
|
+
import { mixedData, type MixedPoint } from "../../data";
|
|
317
|
+
|
|
318
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
319
|
+
example can be copied whole. */
|
|
320
|
+
|
|
321
|
+
export default function MarksOnEveryKind() {
|
|
322
|
+
return (
|
|
323
|
+
<Chart data={mixedData} height={320} ariaLabel="Bars, area, line and scatter, told apart by marks" encoding="marks">
|
|
324
|
+
<XAxis accessor={(d: MixedPoint) => d.t} label="Period" tickCount={7} />
|
|
325
|
+
<YAxis accessor={(d: MixedPoint) => d.stock} label="Quantity" />
|
|
326
|
+
<Bar accessor={(d: MixedPoint) => d.inflow} name="Inflow" />
|
|
327
|
+
<Bar accessor={(d: MixedPoint) => d.outflow} name="Outflow" />
|
|
328
|
+
<Area
|
|
329
|
+
accessor={(d: MixedPoint) => d.corridorUpper}
|
|
330
|
+
baseline={(d: MixedPoint) => d.corridorLower}
|
|
331
|
+
name="Corridor"
|
|
332
|
+
/>
|
|
333
|
+
<Line accessor={(d: MixedPoint) => d.stock} name="Stock" strokeWidth={2} />
|
|
334
|
+
<Scatter accessor={(d: MixedPoint) => d.sample} name="Samples" />
|
|
335
|
+
<Legend placement="top" />
|
|
336
|
+
<Tooltip mode="x" />
|
|
337
|
+
</Chart>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
343
|
+
|
|
344
|
+
##### Bands, limits and cells
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
/* What a plant screen says in colour alone - a state, a tolerance, a verdict
|
|
348
|
+
per cell - told by marks as well. Each state is hatched by its name, in the
|
|
349
|
+
order the names first come (the first plain), the tolerance band is
|
|
350
|
+
hatched in its own colour, and a matrix hatches each step of its
|
|
351
|
+
colouring. A reader who
|
|
352
|
+
cannot tell the fault's red from the setup's amber still tells the hatches
|
|
353
|
+
apart, and the legend shows which is which. */
|
|
354
|
+
|
|
355
|
+
import { Chart, Legend, LimitBand, LimitLine, Line, Matrix, StateBand, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
356
|
+
import { MACHINES, PLANT_STATES, matrixData, shiftData, type CellPoint, type StatePoint } from "../../data";
|
|
357
|
+
|
|
358
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
359
|
+
example can be copied whole. */
|
|
360
|
+
|
|
361
|
+
const timeOfDay = (v: number) =>
|
|
362
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
363
|
+
|
|
364
|
+
export default function BandsLimitsAndCells() {
|
|
365
|
+
return (
|
|
366
|
+
<div className="side-by-side">
|
|
367
|
+
<Chart data={shiftData} height={280} ariaLabel="Furnace temperature against its tolerance above its states" encoding="marks">
|
|
368
|
+
<XAxis accessor={(d: StatePoint) => d.t} tickFormat={timeOfDay} label="Time" tickCount={4} />
|
|
369
|
+
<YAxis accessor={(d: StatePoint) => d.temperature} domain={[740, 900]} label="°C" />
|
|
370
|
+
<YAxis id="lane" position="right" accessor={() => 0} domain={[0, 5]} ticks={[0.45]} tickFormat={() => "Furnace 1"} />
|
|
371
|
+
<LimitBand from={780} to={820} severity="warning" label="Tolerance" />
|
|
372
|
+
<LimitLine value={860} severity="alarm" label="Alarm" />
|
|
373
|
+
<StateBand
|
|
374
|
+
accessor={(d: StatePoint) => d.m1}
|
|
375
|
+
states={PLANT_STATES}
|
|
376
|
+
yAxisId="lane"
|
|
377
|
+
laneFrom={0}
|
|
378
|
+
laneTo={0.9}
|
|
379
|
+
name="Furnace 1"
|
|
380
|
+
/>
|
|
381
|
+
<Line accessor={(d: StatePoint) => d.temperature} name="Temperature" strokeWidth={1.75} />
|
|
382
|
+
<Legend placement="top" />
|
|
383
|
+
<Tooltip mode="x" />
|
|
384
|
+
</Chart>
|
|
385
|
+
<Chart data={matrixData} height={280} ariaLabel="OEE per machine and hour, by assessment and by marks" encoding="marks">
|
|
386
|
+
<XAxis accessor={(d: CellPoint) => d.hour} ticks={[0, 6, 12, 18]} tickFormat={(v) => `${v}:00`} label="Hour" />
|
|
387
|
+
<YAxis
|
|
388
|
+
accessor={(d: CellPoint) => d.machine}
|
|
389
|
+
ticks={MACHINES.map((_, i) => i)}
|
|
390
|
+
tickFormat={(v) => MACHINES[v] ?? ""}
|
|
391
|
+
/>
|
|
392
|
+
<Matrix
|
|
393
|
+
accessor={(d: CellPoint) => d.machine}
|
|
394
|
+
value={(d: CellPoint) => d.oee}
|
|
395
|
+
coloring={{
|
|
396
|
+
kind: "assessment",
|
|
397
|
+
limits: {
|
|
398
|
+
limits: [
|
|
399
|
+
{ value: 70, side: "lower", severity: "warning" },
|
|
400
|
+
{ value: 40, side: "lower", severity: "alarm" },
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
}}
|
|
404
|
+
name="OEE"
|
|
405
|
+
/>
|
|
406
|
+
<Legend placement="top" />
|
|
407
|
+
<Tooltip mode="nearest" />
|
|
408
|
+
</Chart>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
415
|
+
|
|
416
|
+
#### API
|
|
417
|
+
|
|
418
|
+
##### `ChartProps<T>`
|
|
419
|
+
|
|
420
|
+
| Prop | Type | Default | Description |
|
|
421
|
+
|---|---|---|---|
|
|
422
|
+
| `data` (required) | `readonly T[]` | — | Shared data basis (required); access only through accessors (R-2.4). |
|
|
423
|
+
| `width` | `number \| "100%"` | — | Width in CSS pixels, or `"100%"` for the host's. Without a value the chart measures its host and follows it through a ResizeObserver. |
|
|
424
|
+
| `height` | `number` | — | Height in CSS pixels; 300 without a value. Unlike the width, the chart does not take its host's. |
|
|
425
|
+
| `padding` | `number \| Partial<Padding>` | — | Outer spacing of the plot area in CSS pixels. |
|
|
426
|
+
| `ariaLabel` | `string` | — | Required in DEV through a warning (R-7.6). |
|
|
427
|
+
| `className` | `string` | — | Goes to the root element, as it does everywhere in this workspace. |
|
|
428
|
+
| `style` | `CSSProperties` | — | Goes to the root element. The plot area is measured, never styled from here: a height set past the component is a height the scene does not know about. |
|
|
429
|
+
| `empty` | `ReactNode` | — | 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. Without a value the wording's `empty`; this prop wins over it. |
|
|
430
|
+
| `wording` | `Partial<ChartsWording>` | — | The chart's words (ADR-0031): English without a value, entries left out fall back to English. German: `GERMAN_CHARTS_WORDING` from `@umriss-ui/charts/wording/de`. |
|
|
431
|
+
| `encoding` | `"color" \| "marks"` | — | How series are told apart. `"color"` (the default): 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. |
|
|
432
|
+
| `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 - give every chart the same controlled `domain`. |
|
|
433
|
+
| `onPerf` | `(perf: ChartPerf) => void` | — | Instrumentation for the benchmark page (R-5.1); not needed otherwise. |
|
|
434
|
+
| `children` | `ReactNode` | — | The axes, series and companions of this chart. They draw nothing themselves: each registers its configuration and the scene draws it (R-2.1), and the order in the JSX is the drawing order. |
|
|
435
|
+
|
|
436
|
+
#### Why it is like this
|
|
437
|
+
|
|
438
|
+
##### Every scale is affine, and the draw loop relies on it
|
|
439
|
+
|
|
440
|
+
A scale here is a slope and an offset, never a callback the loop asks per point. At a million points the difference is not a micro-optimisation but the difference between a chart and a freeze: the loop reads two numbers and multiplies. Everything that is not affine - a log axis, a category scale - is therefore not a scale in this library but a mapping that happens earlier, in materialisation. See `docs/adr/0001-affine-scale-contract.md`.
|
|
441
|
+
|
|
442
|
+
##### Two layers, and a hover touches only one
|
|
443
|
+
|
|
444
|
+
The series lie on one canvas, crosshair and markers on a second above it. A hover redraws the overlay and leaves the series where they are - which is why the frame rate stays at sixty while the pointer travels over a million points. The benchmark page measures exactly that.
|
|
445
|
+
|
|
446
|
+
##### Canvas for the marks, DOM for everything that is read
|
|
447
|
+
|
|
448
|
+
Axes, ticks, labels, legend and tooltip are elements. They can be selected, read by a screen reader, styled with tokens and found by a test. Only what would be thousands of nodes - the marks themselves - is drawn.
|
|
449
|
+
|
|
450
|
+
### Axis
|
|
451
|
+
|
|
452
|
+
What a value is measured against: extent, ticks and labelling, several per orientation - and an operating time axis that leaves the empty hours out and says so.
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import { XAxis, YAxis, operatingCalendar } from "@umriss-ui/charts";
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/axis
|
|
459
|
+
|
|
460
|
+
#### Examples
|
|
461
|
+
|
|
462
|
+
##### Domain, ticks and tooltip
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
/* A fixed domain [0, 100], a tickFormat of its own, markers `always`, a custom
|
|
466
|
+
tooltip through the render prop, mode `nearest`.
|
|
467
|
+
|
|
468
|
+
The explicit type argument on `Tooltip<Point>` is what makes the hit fully
|
|
469
|
+
typed in the render prop - without it the callback would take the widest
|
|
470
|
+
shape the container can offer. */
|
|
471
|
+
|
|
472
|
+
import { useMemo } from "react";
|
|
473
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
474
|
+
import type { TooltipHit } from "@umriss-ui/charts";
|
|
475
|
+
import { configData, type Point } from "../../data";
|
|
476
|
+
|
|
477
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
478
|
+
example can be copied whole. */
|
|
479
|
+
|
|
480
|
+
function CustomTooltip({ hit }: { hit: TooltipHit<Point> }) {
|
|
481
|
+
const point = hit.points[0];
|
|
482
|
+
return (
|
|
483
|
+
<div className="custom-tooltip">
|
|
484
|
+
<strong>Index {hit.xValue}</strong>
|
|
485
|
+
{point === undefined ? null : (
|
|
486
|
+
<span>
|
|
487
|
+
{point.seriesName}: {point.yValue.toFixed(2)}
|
|
488
|
+
</span>
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export default function Configuration() {
|
|
495
|
+
const configFormat = useMemo(() => (v: number) => `${v.toFixed(0)} pt`, []);
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<Chart
|
|
499
|
+
data={configData}
|
|
500
|
+
height={280}
|
|
501
|
+
padding={{ top: 12, right: 16, bottom: 8, left: 8 }}
|
|
502
|
+
ariaLabel="Configured course with a fixed domain"
|
|
503
|
+
>
|
|
504
|
+
<XAxis accessor={(d: Point) => d.t} label="Index" tickCount={6} />
|
|
505
|
+
<YAxis accessor={(d: Point) => d.a} domain={[0, 100]} tickFormat={configFormat} label="Points" />
|
|
506
|
+
<Line accessor={(d: Point) => d.a} name="Series A" markers="always" strokeWidth={2} />
|
|
507
|
+
<Line accessor={(d: Point) => d.b} name="Series B" markers="always" color="#7c3aed" />
|
|
508
|
+
<Legend placement="bottom" />
|
|
509
|
+
<Tooltip<Point> mode="nearest" render={(hit) => <CustomTooltip hit={hit} />} />
|
|
510
|
+
</Chart>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
516
|
+
|
|
517
|
+
##### Multiple axes
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
/* Three y axes with clearly different extents: one on the left, two stacked on
|
|
521
|
+
the right (the second lies outside, separated by the gap). The series "large"
|
|
522
|
+
is bound to a second x axis on top and counts in relative steps - the tooltip
|
|
523
|
+
still compares the series correctly in pixel space.
|
|
524
|
+
|
|
525
|
+
Only the first registered axis per orientation draws a grid. Two grids over
|
|
526
|
+
one plot area are two rulers over one drawing. */
|
|
527
|
+
|
|
528
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
529
|
+
import { axesData, type DualPoint } from "../../data";
|
|
530
|
+
|
|
531
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
532
|
+
example can be copied whole. */
|
|
533
|
+
|
|
534
|
+
export default function Axes() {
|
|
535
|
+
return (
|
|
536
|
+
<Chart data={axesData} height={340} ariaLabel="Three extents on three y axes">
|
|
537
|
+
<XAxis accessor={(d: DualPoint) => d.t} label="Step" />
|
|
538
|
+
<XAxis id="top" position="top" accessor={(d: DualPoint) => d.t - 2000} label="Step (relative)" />
|
|
539
|
+
<YAxis id="small" position="left" label="small" accessor={(d: DualPoint) => d.small} />
|
|
540
|
+
<YAxis id="large" position="right" label="large" accessor={(d: DualPoint) => d.large} />
|
|
541
|
+
<YAxis id="medium" position="right" label="medium" accessor={(d: DualPoint) => d.medium} />
|
|
542
|
+
<Line accessor={(d: DualPoint) => d.small} yAxisId="small" name="small" />
|
|
543
|
+
<Line
|
|
544
|
+
accessor={(d: DualPoint) => d.large}
|
|
545
|
+
xAxisId="top"
|
|
546
|
+
yAxisId="large"
|
|
547
|
+
name="large"
|
|
548
|
+
dash={[4, 4]}
|
|
549
|
+
/>
|
|
550
|
+
<Line accessor={(d: DualPoint) => d.medium} yAxisId="medium" name="medium" />
|
|
551
|
+
<Legend placement="top" />
|
|
552
|
+
<Tooltip mode="x" />
|
|
553
|
+
</Chart>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
559
|
+
|
|
560
|
+
##### Operating time axis
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
/* The same week, only without the hours in which nobody was there: Monday to
|
|
564
|
+
Friday, 6 to 22 o'clock.
|
|
565
|
+
|
|
566
|
+
On a wall clock axis the chart would consist, for a good forty per cent, of
|
|
567
|
+
flat lines over an empty hall. Every removed span carries a break mark - a
|
|
568
|
+
chart that takes a weekend out and does not say so claims a continuity it does
|
|
569
|
+
not have. The scale stays affine; the mapping happens in materialisation
|
|
570
|
+
(ADR-0001). */
|
|
571
|
+
|
|
572
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
573
|
+
import { WEEK_CALENDAR, weekData, type WeekPoint } from "../../data";
|
|
574
|
+
|
|
575
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
576
|
+
example can be copied whole. */
|
|
577
|
+
|
|
578
|
+
const weekdayAndTime = (v: number) =>
|
|
579
|
+
new Date(v).toLocaleString("en-GB", { weekday: "short", hour: "2-digit", minute: "2-digit" });
|
|
580
|
+
|
|
581
|
+
export default function OperatingTime() {
|
|
582
|
+
return (
|
|
583
|
+
<Chart data={weekData} height={280} ariaLabel="Output across the operating time of one week">
|
|
584
|
+
<XAxis
|
|
585
|
+
accessor={(d: WeekPoint) => d.t}
|
|
586
|
+
calendar={WEEK_CALENDAR}
|
|
587
|
+
tickFormat={weekdayAndTime}
|
|
588
|
+
tickCount={8}
|
|
589
|
+
label="Operating time"
|
|
590
|
+
/>
|
|
591
|
+
<YAxis accessor={(d: WeekPoint) => d.output} label="Pieces/h" />
|
|
592
|
+
<Line accessor={(d: WeekPoint) => d.output} name="Output" />
|
|
593
|
+
<Tooltip mode="x" />
|
|
594
|
+
</Chart>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
600
|
+
|
|
601
|
+
##### Time axis
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
/* A day of hall temperature on a time axis: `time` says the values are
|
|
605
|
+
instants, and the axis stands its ticks on the local clock and labels them by
|
|
606
|
+
level - the hours, and the date where the day changes. No `tickFormat`; one
|
|
607
|
+
would take over, in any language. */
|
|
608
|
+
|
|
609
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
610
|
+
import { hallData, type HallPoint } from "../../data";
|
|
611
|
+
|
|
612
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
613
|
+
example can be copied whole. */
|
|
614
|
+
|
|
615
|
+
export default function TimeAxis() {
|
|
616
|
+
return (
|
|
617
|
+
<Chart data={hallData} height={260} ariaLabel="Hall temperature across a day and a night">
|
|
618
|
+
<XAxis accessor={(d: HallPoint) => d.t} time />
|
|
619
|
+
<YAxis accessor={(d: HallPoint) => d.temperature} label="°C" />
|
|
620
|
+
<Line accessor={(d: HallPoint) => d.temperature} name="Hall" />
|
|
621
|
+
<Tooltip mode="x" />
|
|
622
|
+
</Chart>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
628
|
+
|
|
629
|
+
##### Zoom and pan
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
/* A week of kiln temperature, one reading a minute - too long to read a shift
|
|
633
|
+
in. `onDomainChange` makes the x axis zoomable: Ctrl or ⌘ with the wheel, or
|
|
634
|
+
a pinch, zooms around the pointer, a drag pans, a double click shows the
|
|
635
|
+
whole week again. The axis only proposes; the domain stays the caller's,
|
|
636
|
+
who passes it back - and could clamp it on the way. */
|
|
637
|
+
|
|
638
|
+
import { useState } from "react";
|
|
639
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
640
|
+
import { kilnData, type KilnPoint } from "../../data";
|
|
641
|
+
|
|
642
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
643
|
+
example can be copied whole. */
|
|
644
|
+
|
|
645
|
+
export default function ZoomAndPan() {
|
|
646
|
+
const [domain, setDomain] = useState<"data" | readonly [number, number]>("data");
|
|
647
|
+
return (
|
|
648
|
+
<Chart data={kilnData} height={260} ariaLabel="Kiln temperature across a week, zoomable">
|
|
649
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} />
|
|
650
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" />
|
|
651
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Kiln" />
|
|
652
|
+
<Tooltip mode="x" />
|
|
653
|
+
</Chart>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
659
|
+
|
|
660
|
+
##### The visible domain
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
/* The same week, opened on Wednesday afternoon, when the burner tripped. With
|
|
664
|
+
`domain="visible"` the y axis fits what the x domain shows: the twenty
|
|
665
|
+
minutes the kiln fell and the hour it took to climb back, not the week's
|
|
666
|
+
600 to 850 °C that would press them flat. Zoom out - Ctrl or ⌘ with the
|
|
667
|
+
wheel, or a double click - and the axis follows. */
|
|
668
|
+
|
|
669
|
+
import { useState } from "react";
|
|
670
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
671
|
+
import { DAY_MS, HOUR_MS, WEEK_START, kilnData, type KilnPoint } from "../../data";
|
|
672
|
+
|
|
673
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
674
|
+
example can be copied whole. */
|
|
675
|
+
|
|
676
|
+
const WEDNESDAY = WEEK_START + 2 * DAY_MS;
|
|
677
|
+
|
|
678
|
+
export default function VisibleDomain() {
|
|
679
|
+
const [domain, setDomain] = useState<readonly [number, number]>([
|
|
680
|
+
WEDNESDAY + 12 * HOUR_MS,
|
|
681
|
+
WEDNESDAY + 17 * HOUR_MS,
|
|
682
|
+
]);
|
|
683
|
+
return (
|
|
684
|
+
<Chart data={kilnData} height={260} ariaLabel="Kiln temperature on Wednesday afternoon, the y axis fitted to it">
|
|
685
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} />
|
|
686
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" domain="visible" />
|
|
687
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Kiln" />
|
|
688
|
+
<Tooltip mode="x" />
|
|
689
|
+
</Chart>
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
695
|
+
|
|
696
|
+
##### Aligned ticks
|
|
697
|
+
|
|
698
|
+
```tsx
|
|
699
|
+
/* Two quantities, two y axes, one grid: the kiln's temperature on the left and
|
|
700
|
+
the gas it burns on the right. Only the first axis draws grid lines; with
|
|
701
|
+
`alignTicks` the gas axis takes its tick count and widens its own domain
|
|
702
|
+
until each of its ticks stands on one of those lines - in steps of 1, 2 or
|
|
703
|
+
5, never 17.3. Without it the right labels would float between the lines,
|
|
704
|
+
and a reader would take a line for a value it is not. */
|
|
705
|
+
|
|
706
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
707
|
+
import { kilnData, type KilnPoint } from "../../data";
|
|
708
|
+
|
|
709
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
710
|
+
example can be copied whole. */
|
|
711
|
+
|
|
712
|
+
export default function AlignedTicks() {
|
|
713
|
+
return (
|
|
714
|
+
<Chart data={kilnData} height={260} ariaLabel="Kiln temperature and gas flow across a week on one grid">
|
|
715
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time />
|
|
716
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" />
|
|
717
|
+
<YAxis id="gas" position="right" accessor={(d: KilnPoint) => d.gas} label="m³/h" alignTicks />
|
|
718
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Temperature" />
|
|
719
|
+
<Line accessor={(d: KilnPoint) => d.gas} yAxisId="gas" name="Gas" />
|
|
720
|
+
<Legend placement="top" />
|
|
721
|
+
<Tooltip mode="x" />
|
|
722
|
+
</Chart>
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
728
|
+
|
|
729
|
+
#### API
|
|
730
|
+
|
|
731
|
+
##### `XAxisProps<T>`
|
|
732
|
+
|
|
733
|
+
| Prop | Type | Default | Description |
|
|
734
|
+
|---|---|---|---|
|
|
735
|
+
| `domain` | `"nice" \| "data" \| readonly [number, number]` | — | `"nice"` widens the data's extent to ticks, `"data"` keeps it, a pair is fixed - the one a zoom passes back. |
|
|
736
|
+
| `position` | `"bottom" \| "top"` | — | 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. |
|
|
737
|
+
| `time` | `boolean` | — | The values are instants (milliseconds since the epoch): 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. `calendar` implies it. |
|
|
738
|
+
| `calendar` | `readonly OperatingInterval[]` | — | Operating calendar: the intervals in which time counts. With it the axis stands in operating time - weekends and night shifts are out, and every removed span gets a break mark. The scale stays affine; the mapping happens in materialisation (ADR-0001). |
|
|
739
|
+
| `onDomainChange` | `(domain: [number, number]) => void` | — | Zoom and pan, controlled: 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 proposes the whole data range. Each proposes a domain in the axis' units and changes nothing; the caller passes it back as `domain`, clamped as it likes. Without a handler the axis does not zoom, and the plain wheel always scrolls the page. |
|
|
740
|
+
| `axisId` | `string` | — | Axis on which the value lies. Without one, the first axis of the orientation. |
|
|
741
|
+
| `orientation` | `"x" \| "y"` | — | Whether the value lies on the x or on the y axis. Without one, y: a limit is nearly always a value and not a moment. |
|
|
742
|
+
| `severity` | `Severity` | — | Two severities, not five: five urgencies are five colours, and five colours on a plant screen are noise. |
|
|
743
|
+
| `label` | `string` | — | What the value means. A line at 90 does not say what 90 is. |
|
|
744
|
+
| `color` | `string` | — | Any CSS colour value, in place of the one the severity yields. A colour handed in instead of a severity puts the rule in the application and the presentation in the library, and the two drift apart as soon as a bound moves - so this is for the case that has no severity, not for taste. |
|
|
745
|
+
| `inExtent` | `boolean` | — | Whether the value widens the axis extent, so that the limit is certainly visible. Default true; the head of this file says why. |
|
|
746
|
+
| `role` | `LimitRole` | — | A chosen specification limit (the default) or a calculated control limit. They look different because they mean different things (ADR-0008). |
|
|
747
|
+
|
|
748
|
+
##### `YAxisProps<T>`
|
|
749
|
+
|
|
750
|
+
| Prop | Type | Default | Description |
|
|
751
|
+
|---|---|---|---|
|
|
752
|
+
| `domain` | `"nice" \| "data" \| "visible" \| readonly [number, number]` | — | 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. |
|
|
753
|
+
| `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. |
|
|
754
|
+
| `position` | `"left" \| "right"` | — | Which edge the axis stands at. Several y axes per side are stacked outwards, so that extents of clearly different magnitude stay readable. |
|
|
755
|
+
| `axisId` | `string` | — | Axis on which the value lies. Without one, the first axis of the orientation. |
|
|
756
|
+
| `orientation` | `"x" \| "y"` | — | Whether the value lies on the x or on the y axis. Without one, y: a limit is nearly always a value and not a moment. |
|
|
757
|
+
| `severity` | `Severity` | — | Two severities, not five: five urgencies are five colours, and five colours on a plant screen are noise. |
|
|
758
|
+
| `label` | `string` | — | What the value means. A line at 90 does not say what 90 is. |
|
|
759
|
+
| `color` | `string` | — | Any CSS colour value, in place of the one the severity yields. A colour handed in instead of a severity puts the rule in the application and the presentation in the library, and the two drift apart as soon as a bound moves - so this is for the case that has no severity, not for taste. |
|
|
760
|
+
| `inExtent` | `boolean` | — | Whether the value widens the axis extent, so that the limit is certainly visible. Default true; the head of this file says why. |
|
|
761
|
+
| `role` | `LimitRole` | — | A chosen specification limit (the default) or a calculated control limit. They look different because they mean different things (ADR-0008). |
|
|
762
|
+
|
|
763
|
+
#### Why it is like this
|
|
764
|
+
|
|
765
|
+
##### Operating time is a mapping, not a scale
|
|
766
|
+
|
|
767
|
+
An operating calendar takes out the hours in which nobody was there - nights, weekends. The obvious implementation is a scale that skips them. This one does not: every scale in the library stays affine, a slope and an offset the draw loop multiplies with. Instead the x values are mapped into operating time once, during materialisation, and the scale never learns that a weekend existed. The drawing loop pays nothing for the calendar. See `docs/adr/0001-affine-scale-contract.md`.
|
|
768
|
+
|
|
769
|
+
##### Removed time is a gap, and the axis says so
|
|
770
|
+
|
|
771
|
+
A measurement from an hour the calendar removed has no place on the axis. It is not laid on the edge of the interval, where it would pile up on what really happened there; it becomes a gap, like every missing value in this library - never drawn, never hit. The seam itself is where Friday evening and Monday morning now stand side by side, and every removed span gets a break mark on the axis at that seam. A chart that took a weekend out without saying so would claim a continuity it does not have.
|
|
772
|
+
|
|
773
|
+
## Series
|
|
774
|
+
|
|
775
|
+
The kinds a chart can draw. They share the axes and mix freely; the order in the JSX decides what lies over what.
|
|
776
|
+
|
|
777
|
+
### Line
|
|
778
|
+
|
|
779
|
+
A course over an axis - with markers, a dash pattern and a gap that stays a gap instead of being interpolated.
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
import { Line } from "@umriss-ui/charts";
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/line
|
|
786
|
+
|
|
787
|
+
#### Examples
|
|
788
|
+
|
|
789
|
+
##### One series
|
|
790
|
+
|
|
791
|
+
```tsx
|
|
792
|
+
/* One series, default axes, grid only on the first y axis. */
|
|
793
|
+
|
|
794
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
795
|
+
import { basicData, type Point } from "../../data";
|
|
796
|
+
|
|
797
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
798
|
+
example can be copied whole. */
|
|
799
|
+
|
|
800
|
+
export default function Basic() {
|
|
801
|
+
return (
|
|
802
|
+
<Chart data={basicData} height={280} ariaLabel="Basic course of one series">
|
|
803
|
+
<XAxis accessor={(d: Point) => d.t} label="Index" />
|
|
804
|
+
<YAxis accessor={(d: Point) => d.a} />
|
|
805
|
+
<Line accessor={(d: Point) => d.a} name="Series A" />
|
|
806
|
+
<Tooltip />
|
|
807
|
+
</Chart>
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
813
|
+
|
|
814
|
+
##### Several series and gaps
|
|
815
|
+
|
|
816
|
+
```tsx
|
|
817
|
+
/* Four series, the legend on top, one dashed series and one series with a gap -
|
|
818
|
+
the line is interrupted, not interpolated. A gap is a hole and never a value
|
|
819
|
+
the chart invents (CONTEXT.md, **Gap**). The one reading inside the gap has
|
|
820
|
+
no neighbour to be joined to; it keeps its marker, whatever the number of
|
|
821
|
+
points. */
|
|
822
|
+
|
|
823
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
824
|
+
import { multiData, type Point } from "../../data";
|
|
825
|
+
|
|
826
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
827
|
+
example can be copied whole. */
|
|
828
|
+
|
|
829
|
+
export default function MultiSeries() {
|
|
830
|
+
return (
|
|
831
|
+
<Chart data={multiData} height={300} ariaLabel="Four series with a legend">
|
|
832
|
+
<XAxis accessor={(d: Point) => d.t} label="Index" />
|
|
833
|
+
<YAxis accessor={(d: Point) => d.a} label="Value" />
|
|
834
|
+
<Line accessor={(d: Point) => d.a} name="Series A" />
|
|
835
|
+
<Line accessor={(d: Point) => d.b} name="Series B" />
|
|
836
|
+
<Line accessor={(d: Point) => d.c} name="Series C" dash={[4, 4]} />
|
|
837
|
+
<Line accessor={(d: Point) => d.d} name="Series D (with a gap)" />
|
|
838
|
+
<Legend placement="top" />
|
|
839
|
+
<Tooltip mode="x" />
|
|
840
|
+
</Chart>
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
846
|
+
|
|
847
|
+
##### Step line
|
|
848
|
+
|
|
849
|
+
```tsx
|
|
850
|
+
/* A set point is not a ramp: it holds until the next entry and jumps there.
|
|
851
|
+
`step` draws a line that way - sample-and-hold - beside the temperature that
|
|
852
|
+
follows it. The set point is logged only when it changes; while no recipe is
|
|
853
|
+
loaded it is a gap, and the hold ends where the gap begins. The tooltip
|
|
854
|
+
reports the entry the hold began with, not the nearer one after it. */
|
|
855
|
+
|
|
856
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
857
|
+
import { corridorData, setPoints, type CorridorPoint, type SetPoint } from "../../data";
|
|
858
|
+
|
|
859
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
860
|
+
example can be copied whole. */
|
|
861
|
+
|
|
862
|
+
export default function StepLine() {
|
|
863
|
+
return (
|
|
864
|
+
<Chart data={corridorData} height={260} ariaLabel="Furnace temperature following its set point">
|
|
865
|
+
<XAxis accessor={(d: { t: number }) => d.t} time />
|
|
866
|
+
<YAxis accessor={(d: CorridorPoint) => d.temperature} label="°C" />
|
|
867
|
+
<Line data={setPoints} accessor={(d: SetPoint) => d.setPoint} name="Set point" step strokeWidth={2} />
|
|
868
|
+
<Line accessor={(d: CorridorPoint) => d.temperature} name="Temperature" />
|
|
869
|
+
<Legend />
|
|
870
|
+
<Tooltip mode="x" />
|
|
871
|
+
</Chart>
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
877
|
+
|
|
878
|
+
#### API
|
|
879
|
+
|
|
880
|
+
##### `LineProps<T>`
|
|
881
|
+
|
|
882
|
+
| Prop | Type | Default | Description |
|
|
883
|
+
|---|---|---|---|
|
|
884
|
+
| `accessor` (required) | `Accessor<T>` | — | Y value; null/undefined/NaN/±Infinity means a gap (R-2.5). |
|
|
885
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
886
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
887
|
+
| `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
|
|
888
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
889
|
+
| `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent - a fixed `domain` keeps the axis still. Its legend entry stays, drawn back. Controlled: the caller sets it, typically from `Legend onToggle`. |
|
|
890
|
+
| `format` | `(value: number) => string` | — | The value as the tooltip writes it; without one the y axis' `tickFormat`, then the default. |
|
|
891
|
+
| `color` | `string` | — | Any CSS colour value; without one the palette --uc-series-N. |
|
|
892
|
+
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. |
|
|
893
|
+
| `strokeWidth` | `number` | — | Line width in CSS pixels. |
|
|
894
|
+
| `dash` | `readonly number[]` | — | Dash pattern as a run of lengths in CSS pixels; without one a solid line. |
|
|
895
|
+
| `markers` | `"auto" \| "always" \| "never"` | — | 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. |
|
|
896
|
+
| `step` | `boolean` | — | Sample-and-hold: each value holds as a horizontal until the next sample and jumps there - a set point, a digital signal. A gap ends the hold at its x. The tooltip reports the sample the hold began with. |
|
|
897
|
+
|
|
898
|
+
### Area
|
|
899
|
+
|
|
900
|
+
A filled course, or a corridor between two channels - the baseline is a channel of its own and never a second series.
|
|
901
|
+
|
|
902
|
+
```ts
|
|
903
|
+
import { Area } from "@umriss-ui/charts";
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/area
|
|
907
|
+
|
|
908
|
+
#### Examples
|
|
909
|
+
|
|
910
|
+
##### Filled down to 0
|
|
911
|
+
|
|
912
|
+
```tsx
|
|
913
|
+
/* One area, filled down to 0: the power draw of a line over the early shift.
|
|
914
|
+
|
|
915
|
+
Without a baseline the foot is 0, and that 0 enters the extent of the y axis -
|
|
916
|
+
a draw has a natural zero, and an axis that started at 30 kW would make the
|
|
917
|
+
break look like a shutdown. Fill and outline are set here explicitly: the fill
|
|
918
|
+
carries the quantity, the outline the course. */
|
|
919
|
+
|
|
920
|
+
import { Area, Chart, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
921
|
+
import { powerData, type PowerPoint } from "../../data";
|
|
922
|
+
|
|
923
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
924
|
+
example can be copied whole. */
|
|
925
|
+
|
|
926
|
+
const timeOfDay = (v: number) =>
|
|
927
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
928
|
+
|
|
929
|
+
export default function Filled() {
|
|
930
|
+
return (
|
|
931
|
+
<Chart data={powerData} height={260} ariaLabel="Power draw of one line over the early shift">
|
|
932
|
+
<XAxis accessor={(d: PowerPoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
933
|
+
<YAxis accessor={(d: PowerPoint) => d.kw} label="kW" />
|
|
934
|
+
<Area accessor={(d: PowerPoint) => d.kw} name="Power draw" fillOpacity={0.35} strokeWidth={2} />
|
|
935
|
+
<Tooltip mode="x" />
|
|
936
|
+
</Chart>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
942
|
+
|
|
943
|
+
##### A corridor with a gap
|
|
944
|
+
|
|
945
|
+
```tsx
|
|
946
|
+
/* A corridor between two channels, and the measured temperature inside it.
|
|
947
|
+
|
|
948
|
+
The lower edge is the `baseline` accessor - a channel of the same series, not a
|
|
949
|
+
second series the area is filled towards. During the recipe change there is
|
|
950
|
+
no corridor: the fill gets a hole there, while the line runs on, because the
|
|
951
|
+
furnace was measured the whole time. */
|
|
952
|
+
|
|
953
|
+
import { Area, Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
954
|
+
import { corridorData, type CorridorPoint } from "../../data";
|
|
955
|
+
|
|
956
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
957
|
+
example can be copied whole. */
|
|
958
|
+
|
|
959
|
+
const timeOfDay = (v: number) =>
|
|
960
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
961
|
+
|
|
962
|
+
export default function Corridor() {
|
|
963
|
+
return (
|
|
964
|
+
<Chart data={corridorData} height={280} ariaLabel="Furnace temperature inside the corridor of its recipe">
|
|
965
|
+
<XAxis accessor={(d: CorridorPoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
966
|
+
<YAxis accessor={(d: CorridorPoint) => d.temperature} label="°C" />
|
|
967
|
+
{/* An outline runs along the upper edge only; a corridor has two edges, so
|
|
968
|
+
it gets neither. */}
|
|
969
|
+
<Area
|
|
970
|
+
accessor={(d: CorridorPoint) => d.upper}
|
|
971
|
+
baseline={(d: CorridorPoint) => d.lower}
|
|
972
|
+
name="Recipe corridor"
|
|
973
|
+
strokeWidth={0}
|
|
974
|
+
/>
|
|
975
|
+
<Line accessor={(d: CorridorPoint) => d.temperature} name="Furnace temperature" strokeWidth={1.75} />
|
|
976
|
+
<Legend placement="top" />
|
|
977
|
+
<Tooltip mode="x" />
|
|
978
|
+
</Chart>
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
984
|
+
|
|
985
|
+
##### Stacked areas
|
|
986
|
+
|
|
987
|
+
```tsx
|
|
988
|
+
/* The output of three lines over a day, stacked: the upper edge is the plant's
|
|
989
|
+
output, each band a line's part of it. Every Area with the same `stack`
|
|
990
|
+
stands on the ones registered before it.
|
|
991
|
+
|
|
992
|
+
Line 3 stands at night and reports 0 - its band closes to a line, and the
|
|
993
|
+
plant's edge drops with it. A stacked band carries a quantity, so the fill
|
|
994
|
+
is stronger than a lone area's and the outline thin: the edges are the
|
|
995
|
+
boundaries between parts, not courses of their own. */
|
|
996
|
+
|
|
997
|
+
import { Area, Chart, Legend, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
998
|
+
import { lineOutputData, type LineOutput } from "../../data";
|
|
999
|
+
|
|
1000
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1001
|
+
example can be copied whole. */
|
|
1002
|
+
|
|
1003
|
+
export default function StackedAreas() {
|
|
1004
|
+
return (
|
|
1005
|
+
<Chart data={lineOutputData} height={280} ariaLabel="Output of three lines over a day, stacked">
|
|
1006
|
+
<XAxis accessor={(d: LineOutput) => d.t} time domain="data" />
|
|
1007
|
+
<YAxis accessor={(d: LineOutput) => d.line1} label="Pieces per hour" />
|
|
1008
|
+
<Area accessor={(d: LineOutput) => d.line1} name="Line 1" stack="plant" fillOpacity={0.5} strokeWidth={1} />
|
|
1009
|
+
<Area accessor={(d: LineOutput) => d.line2} name="Line 2" stack="plant" fillOpacity={0.5} strokeWidth={1} />
|
|
1010
|
+
<Area accessor={(d: LineOutput) => d.line3} name="Line 3" stack="plant" fillOpacity={0.5} strokeWidth={1} />
|
|
1011
|
+
<Legend placement="top" />
|
|
1012
|
+
<Tooltip mode="x" />
|
|
1013
|
+
</Chart>
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1019
|
+
|
|
1020
|
+
#### API
|
|
1021
|
+
|
|
1022
|
+
##### `AreaProps<T>`
|
|
1023
|
+
|
|
1024
|
+
| Prop | Type | Default | Description |
|
|
1025
|
+
|---|---|---|---|
|
|
1026
|
+
| `accessor` (required) | `Accessor<T>` | — | Upper edge; null/undefined/NaN/±Infinity means a gap (R-2.5). |
|
|
1027
|
+
| `baseline` | `Accessor<T>` | — | Lower edge; without a value the fixed baseline 0. In a `stack` the stack below is the lower edge, and this is not read. |
|
|
1028
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
1029
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
1030
|
+
| `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
|
|
1031
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
1032
|
+
| `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent - a fixed `domain` keeps the axis still. Its legend entry stays, drawn back. Controlled: the caller sets it, typically from `Legend onToggle`. |
|
|
1033
|
+
| `format` | `(value: number) => string` | — | The value as the tooltip writes it; without one the y axis' `tickFormat`, then the default. |
|
|
1034
|
+
| `color` | `string` | — | Any CSS colour value; without one the palette --uc-series-N. |
|
|
1035
|
+
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it. |
|
|
1036
|
+
| `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. |
|
|
1037
|
+
| `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`. |
|
|
1038
|
+
| `fillOpacity` | `number` | — | Opacity of the fill, from 0 to 1; the outline stays fully opaque. |
|
|
1039
|
+
| `strokeWidth` | `number` | — | Width of the outline along the upper edge in CSS pixels; 0 leaves it out. |
|
|
1040
|
+
| `dash` | `readonly number[]` | — | Dash pattern of the outline as a run of lengths in CSS pixels; without one a solid outline. The fill stays whole. |
|
|
1041
|
+
|
|
1042
|
+
#### Why it is like this
|
|
1043
|
+
|
|
1044
|
+
##### The baseline is a channel, not a second series
|
|
1045
|
+
|
|
1046
|
+
A corridor has two edges, and the obvious way to draw it is two series and a fill between them. This library does not do that: the lower edge is the `baseline` accessor of the same `Area`, materialised into a second channel beside the first. One series has one name, one colour, one legend entry and one gap - where either edge is missing, the fill has a hole, and there is no way for the two edges to disagree about where it is. A fill „towards another series“ would make the order of the JSX a statement about geometry, and hiding one of the two would leave the other filled towards nothing. The kinds share one materialisation shape, and a second channel is how an area fits into it; see `docs/adr/0002-bars-on-a-numeric-x-axis.md`.
|
|
1047
|
+
|
|
1048
|
+
##### Without a baseline the foot is 0, and 0 is in the extent
|
|
1049
|
+
|
|
1050
|
+
A filled area says „this much“, and a quantity is read from its foot. So the fixed baseline 0 enters the extent of the y axis like the values do - an axis that began at the smallest value would cut the foot off, and a dip would read as a shutdown. A course that has no natural zero is a **Line**, not an area.
|
|
1051
|
+
|
|
1052
|
+
### Bar
|
|
1053
|
+
|
|
1054
|
+
Bars on the numeric x axis, grouped where several stand side by side (ADR-0002).
|
|
1055
|
+
|
|
1056
|
+
```ts
|
|
1057
|
+
import { Bar } from "@umriss-ui/charts";
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/bar
|
|
1061
|
+
|
|
1062
|
+
#### Examples
|
|
1063
|
+
|
|
1064
|
+
##### One bar per shift
|
|
1065
|
+
|
|
1066
|
+
```tsx
|
|
1067
|
+
/* Scrap per shift: one bar series, the shifts as positions 0 to 8 on the
|
|
1068
|
+
ordinary numeric x axis, named by the tickFormat (ADR-0002).
|
|
1069
|
+
|
|
1070
|
+
A category is a number the axis places and a function names - no band scale,
|
|
1071
|
+
no category axis. `ticks` puts one tick under every bar; without it the 1-2-5
|
|
1072
|
+
algorithm would pick its own positions and name every second shift. */
|
|
1073
|
+
|
|
1074
|
+
import { Bar, Chart, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1075
|
+
import { scrapData, type ShiftScrap } from "../../data";
|
|
1076
|
+
|
|
1077
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1078
|
+
example can be copied whole. */
|
|
1079
|
+
|
|
1080
|
+
export default function PerShift() {
|
|
1081
|
+
return (
|
|
1082
|
+
<Chart data={scrapData} height={260} ariaLabel="Scrap per shift over three days">
|
|
1083
|
+
<XAxis
|
|
1084
|
+
accessor={(d: ShiftScrap) => d.shift}
|
|
1085
|
+
ticks={scrapData.map((d) => d.shift)}
|
|
1086
|
+
tickFormat={(v) => scrapData[v]?.name ?? ""}
|
|
1087
|
+
/>
|
|
1088
|
+
<YAxis accessor={(d: ShiftScrap) => d.scrap} label="Pieces" />
|
|
1089
|
+
<Bar accessor={(d: ShiftScrap) => d.scrap} name="Scrap" />
|
|
1090
|
+
<Tooltip mode="x" />
|
|
1091
|
+
</Chart>
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1097
|
+
|
|
1098
|
+
##### Grouped bars
|
|
1099
|
+
|
|
1100
|
+
```tsx
|
|
1101
|
+
/* Planned and made, side by side: two bar series on the same x axis share the
|
|
1102
|
+
step and stand next to each other, centred on the day as a group.
|
|
1103
|
+
|
|
1104
|
+
`barWidth` is the fraction of the step the whole group takes - so both series
|
|
1105
|
+
carry the same value, and a different one would be a DEV warning. The plan
|
|
1106
|
+
gets a quiet `color` of its own: it is the reference, and the palette colour
|
|
1107
|
+
goes to what was made. A token, so that it follows the colour scheme - the
|
|
1108
|
+
canvas resolves it like the palette. */
|
|
1109
|
+
|
|
1110
|
+
import { Bar, Chart, Legend, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1111
|
+
import { outputData, type DayOutput } from "../../data";
|
|
1112
|
+
|
|
1113
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1114
|
+
example can be copied whole. */
|
|
1115
|
+
|
|
1116
|
+
export default function Grouped() {
|
|
1117
|
+
return (
|
|
1118
|
+
<Chart data={outputData} height={280} ariaLabel="Planned and made output per working day">
|
|
1119
|
+
<XAxis
|
|
1120
|
+
accessor={(d: DayOutput) => d.day}
|
|
1121
|
+
ticks={outputData.map((d) => d.day)}
|
|
1122
|
+
tickFormat={(v) => outputData[v]?.name ?? ""}
|
|
1123
|
+
/>
|
|
1124
|
+
<YAxis accessor={(d: DayOutput) => d.actual} label="Pieces" />
|
|
1125
|
+
<Bar accessor={(d: DayOutput) => d.planned} name="Planned" color="var(--uc-color-text)" barWidth={0.7} />
|
|
1126
|
+
<Bar accessor={(d: DayOutput) => d.actual} name="Made" barWidth={0.7} />
|
|
1127
|
+
<Legend placement="top" />
|
|
1128
|
+
<Tooltip mode="x" />
|
|
1129
|
+
</Chart>
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1135
|
+
|
|
1136
|
+
##### Above and below plan
|
|
1137
|
+
|
|
1138
|
+
```tsx
|
|
1139
|
+
/* The deviation from plan, per working day: above plan upwards, below plan
|
|
1140
|
+
downwards, the foot at 0 in the middle of the axis.
|
|
1141
|
+
|
|
1142
|
+
The foot of a bar is 0 and not the bottom of the axis - which is what lets one
|
|
1143
|
+
series carry both signs. The deviation is computed in the accessor, not
|
|
1144
|
+
stored: it is the difference of two numbers the data already has. */
|
|
1145
|
+
|
|
1146
|
+
import { Bar, Chart, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1147
|
+
import { outputData, type DayOutput } from "../../data";
|
|
1148
|
+
|
|
1149
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1150
|
+
example can be copied whole. */
|
|
1151
|
+
|
|
1152
|
+
const signed = (v: number) => (v > 0 ? `+${v.toFixed(0)}` : v.toFixed(0));
|
|
1153
|
+
|
|
1154
|
+
export default function Deviation() {
|
|
1155
|
+
return (
|
|
1156
|
+
<Chart data={outputData} height={280} ariaLabel="Deviation from plan per working day">
|
|
1157
|
+
<XAxis
|
|
1158
|
+
accessor={(d: DayOutput) => d.day}
|
|
1159
|
+
ticks={outputData.map((d) => d.day)}
|
|
1160
|
+
tickFormat={(v) => outputData[v]?.name ?? ""}
|
|
1161
|
+
/>
|
|
1162
|
+
<YAxis accessor={(d: DayOutput) => d.actual - d.planned} tickFormat={signed} label="Pieces" />
|
|
1163
|
+
<Bar accessor={(d: DayOutput) => d.actual - d.planned} name="Deviation from plan" />
|
|
1164
|
+
<Tooltip mode="x" />
|
|
1165
|
+
</Chart>
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1171
|
+
|
|
1172
|
+
##### Stacked bars
|
|
1173
|
+
|
|
1174
|
+
```tsx
|
|
1175
|
+
/* Downtime by reason, stacked: each day's bar is its whole downtime, and each
|
|
1176
|
+
reason a part of it. Every Bar with the same `stack` stands on the ones
|
|
1177
|
+
registered before it - the order in the JSX is the order from the foot up.
|
|
1178
|
+
|
|
1179
|
+
The material log was not kept on the first Wednesday. That day's bar says
|
|
1180
|
+
so: its material segment is missing, the breakdown stands directly on the
|
|
1181
|
+
setup - a gap stacks as zero -, and the tooltip names the reasons it has
|
|
1182
|
+
and their total. */
|
|
1183
|
+
|
|
1184
|
+
import { Bar, Chart, Legend, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1185
|
+
import { downtimeData, type DayDowntime } from "../../data";
|
|
1186
|
+
|
|
1187
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1188
|
+
example can be copied whole. */
|
|
1189
|
+
|
|
1190
|
+
export default function Stacked() {
|
|
1191
|
+
return (
|
|
1192
|
+
<Chart data={downtimeData} height={280} ariaLabel="Downtime per working day by reason">
|
|
1193
|
+
<XAxis
|
|
1194
|
+
accessor={(d: DayDowntime) => d.day}
|
|
1195
|
+
ticks={downtimeData.map((d) => d.day)}
|
|
1196
|
+
tickFormat={(v) => downtimeData[v]?.name ?? ""}
|
|
1197
|
+
/>
|
|
1198
|
+
<YAxis accessor={(d: DayDowntime) => d.setup} label="Minutes" />
|
|
1199
|
+
<Bar accessor={(d: DayDowntime) => d.setup} name="Setup" stack="downtime" />
|
|
1200
|
+
<Bar accessor={(d: DayDowntime) => d.material} name="Material" stack="downtime" />
|
|
1201
|
+
<Bar accessor={(d: DayDowntime) => d.breakdown} name="Breakdown" stack="downtime" />
|
|
1202
|
+
<Legend placement="top" />
|
|
1203
|
+
<Tooltip mode="x" />
|
|
1204
|
+
</Chart>
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1210
|
+
|
|
1211
|
+
##### Shares of a whole
|
|
1212
|
+
|
|
1213
|
+
```tsx
|
|
1214
|
+
/* The same downtime as shares: `normalize` on a stack makes every day sum to
|
|
1215
|
+
100 %, so the question moves from "how long" to "of what". The breakdown on
|
|
1216
|
+
the second Thursday is no longer the tallest bar - it is the widest part.
|
|
1217
|
+
|
|
1218
|
+
The y axis reads in percent by itself, and so does the tooltip's row per
|
|
1219
|
+
reason. The total stays in minutes: it is the one number the shares hide,
|
|
1220
|
+
and `format` writes it, as it would write any reading. */
|
|
1221
|
+
|
|
1222
|
+
import { Bar, Chart, Legend, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1223
|
+
import { downtimeData, type DayDowntime } from "../../data";
|
|
1224
|
+
|
|
1225
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1226
|
+
example can be copied whole. */
|
|
1227
|
+
|
|
1228
|
+
const minutes = (v: number) => `${v} min`;
|
|
1229
|
+
|
|
1230
|
+
export default function Percent() {
|
|
1231
|
+
return (
|
|
1232
|
+
<Chart data={downtimeData} height={280} ariaLabel="Share of each reason in the downtime per working day">
|
|
1233
|
+
<XAxis
|
|
1234
|
+
accessor={(d: DayDowntime) => d.day}
|
|
1235
|
+
ticks={downtimeData.map((d) => d.day)}
|
|
1236
|
+
tickFormat={(v) => downtimeData[v]?.name ?? ""}
|
|
1237
|
+
/>
|
|
1238
|
+
<YAxis accessor={(d: DayDowntime) => d.setup} />
|
|
1239
|
+
<Bar accessor={(d: DayDowntime) => d.setup} name="Setup" stack="downtime" normalize format={minutes} />
|
|
1240
|
+
<Bar accessor={(d: DayDowntime) => d.material} name="Material" stack="downtime" normalize format={minutes} />
|
|
1241
|
+
<Bar accessor={(d: DayDowntime) => d.breakdown} name="Breakdown" stack="downtime" normalize format={minutes} />
|
|
1242
|
+
<Legend placement="top" />
|
|
1243
|
+
<Tooltip mode="x" />
|
|
1244
|
+
</Chart>
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1250
|
+
|
|
1251
|
+
#### API
|
|
1252
|
+
|
|
1253
|
+
##### `BarProps<T>`
|
|
1254
|
+
|
|
1255
|
+
| Prop | Type | Default | Description |
|
|
1256
|
+
|---|---|---|---|
|
|
1257
|
+
| `accessor` (required) | `Accessor<T>` | — | Height; null/undefined/NaN/±Infinity means a gap (R-2.5). |
|
|
1258
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
1259
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
1260
|
+
| `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
|
|
1261
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
1262
|
+
| `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent - a fixed `domain` keeps the axis still. Its legend entry stays, drawn back. Controlled: the caller sets it, typically from `Legend onToggle`. |
|
|
1263
|
+
| `format` | `(value: number) => string` | — | The value as the tooltip writes it; without one the y axis' `tickFormat`, then the default. |
|
|
1264
|
+
| `color` | `string` | — | Any CSS colour value; without one the palette --uc-series-N. |
|
|
1265
|
+
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it. |
|
|
1266
|
+
| `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. |
|
|
1267
|
+
| `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`. |
|
|
1268
|
+
| `barWidth` | `number` | — | Width as a fraction of the step; just under 1, so that neighbours do not touch. Several bar series share this fraction. |
|
|
1269
|
+
|
|
1270
|
+
#### Why it is like this
|
|
1271
|
+
|
|
1272
|
+
##### Bars sit on a numeric x axis
|
|
1273
|
+
|
|
1274
|
+
Every other chart library draws bars against a band scale over categories. This one does not: a bar has a position and a width in domain units, on the same axis a line uses. That is what lets a bar, a line and a scatter share one chart and one tooltip without translating between two kinds of axis - and it is what makes a Pareto possible, where the bars are categories and the cumulative line is not. Categories are passed as numeric positions with a naming `tickFormat`. See `docs/adr/0002-bars-on-a-numeric-x-axis.md`.
|
|
1275
|
+
|
|
1276
|
+
##### What it costs, said plainly
|
|
1277
|
+
|
|
1278
|
+
A band scale would place bars automatically and keep their gaps even. Here the width is a number the caller sets, and grouped bars divide it among themselves. The trade was made for the mixed chart, and the mixed chart is the case this library exists for.
|
|
1279
|
+
|
|
1280
|
+
### Scatter
|
|
1281
|
+
|
|
1282
|
+
Individual points without a connecting line - samples, measurements, anything that is not a course.
|
|
1283
|
+
|
|
1284
|
+
```ts
|
|
1285
|
+
import { Scatter } from "@umriss-ui/charts";
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/scatter
|
|
1289
|
+
|
|
1290
|
+
#### Examples
|
|
1291
|
+
|
|
1292
|
+
##### Individual measurements
|
|
1293
|
+
|
|
1294
|
+
```tsx
|
|
1295
|
+
/* Wall thickness, sampled by hand whenever the inspector came by: individual
|
|
1296
|
+
measurements, drawn as points and joined by nothing.
|
|
1297
|
+
|
|
1298
|
+
A line between two samples would claim a thickness at every moment in
|
|
1299
|
+
between, and nobody measured one. `radius` is in CSS pixels; a little larger
|
|
1300
|
+
than the default, because each point here is a person's walk to the line. */
|
|
1301
|
+
|
|
1302
|
+
import { Chart, Scatter, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1303
|
+
import { thicknessData, type ThicknessSample } from "../../data";
|
|
1304
|
+
|
|
1305
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1306
|
+
example can be copied whole. */
|
|
1307
|
+
|
|
1308
|
+
const timeOfDay = (v: number) =>
|
|
1309
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1310
|
+
|
|
1311
|
+
export default function Measurements() {
|
|
1312
|
+
return (
|
|
1313
|
+
<Chart data={thicknessData} height={260} ariaLabel="Wall thickness samples over one shift">
|
|
1314
|
+
<XAxis accessor={(d: ThicknessSample) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
1315
|
+
<YAxis accessor={(d: ThicknessSample) => d.mm} label="mm" />
|
|
1316
|
+
<Scatter accessor={(d: ThicknessSample) => d.mm} name="Wall thickness" radius={4} />
|
|
1317
|
+
<Tooltip mode="nearest" />
|
|
1318
|
+
</Chart>
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1324
|
+
|
|
1325
|
+
##### Samples against a set point
|
|
1326
|
+
|
|
1327
|
+
```tsx
|
|
1328
|
+
/* Fill weights read against their set point: the set point as a line, the
|
|
1329
|
+
samples as points over it, and those out of tolerance with `tone="alarm"`.
|
|
1330
|
+
|
|
1331
|
+
The outliers are a series of their own and not a colour per point - a
|
|
1332
|
+
scatter has one colour, and the tone is a role the theme resolves, so the
|
|
1333
|
+
alarm here is the same red as an alarm limit. The two channels never both
|
|
1334
|
+
carry a value, so no sample is drawn twice. */
|
|
1335
|
+
|
|
1336
|
+
import { Chart, Legend, Line, Scatter, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1337
|
+
import { weightData, type WeightSample } from "../../data";
|
|
1338
|
+
|
|
1339
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1340
|
+
example can be copied whole. */
|
|
1341
|
+
|
|
1342
|
+
const timeOfDay = (v: number) =>
|
|
1343
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1344
|
+
|
|
1345
|
+
export default function OverACourse() {
|
|
1346
|
+
return (
|
|
1347
|
+
<Chart data={weightData} height={300} ariaLabel="Fill weight samples against their set point">
|
|
1348
|
+
<XAxis accessor={(d: WeightSample) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
1349
|
+
<YAxis accessor={(d: WeightSample) => d.setPoint} label="g" />
|
|
1350
|
+
<Line accessor={(d: WeightSample) => d.setPoint} name="Set point" dash={[4, 4]} />
|
|
1351
|
+
<Scatter accessor={(d: WeightSample) => d.sample} name="Sample" radius={2.5} />
|
|
1352
|
+
<Scatter accessor={(d: WeightSample) => d.outlier} name="Out of tolerance" tone="alarm" radius={4} />
|
|
1353
|
+
<Legend placement="top" />
|
|
1354
|
+
<Tooltip mode="nearest" />
|
|
1355
|
+
</Chart>
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1361
|
+
|
|
1362
|
+
#### API
|
|
1363
|
+
|
|
1364
|
+
##### `ScatterProps<T>`
|
|
1365
|
+
|
|
1366
|
+
| Prop | Type | Default | Description |
|
|
1367
|
+
|---|---|---|---|
|
|
1368
|
+
| `accessor` (required) | `Accessor<T>` | — | Y value; null/undefined/NaN/±Infinity means a gap (R-2.5). |
|
|
1369
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
1370
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
1371
|
+
| `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
|
|
1372
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
1373
|
+
| `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent - a fixed `domain` keeps the axis still. Its legend entry stays, drawn back. Controlled: the caller sets it, typically from `Legend onToggle`. |
|
|
1374
|
+
| `format` | `(value: number) => string` | — | The value as the tooltip writes it; without one the y axis' `tickFormat`, then the default. |
|
|
1375
|
+
| `color` | `string` | — | Any CSS colour value; without one the palette --uc-series-N. |
|
|
1376
|
+
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. |
|
|
1377
|
+
| `radius` | `number` | — | Point radius in CSS pixels. |
|
|
1378
|
+
|
|
1379
|
+
### StateBand
|
|
1380
|
+
|
|
1381
|
+
What a machine was doing, as a partition of the time axis: every segment ends where the next begins, and a state is a number the state list names (ADR-0007).
|
|
1382
|
+
|
|
1383
|
+
```ts
|
|
1384
|
+
import { StateBand } from "@umriss-ui/charts";
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/stateband
|
|
1388
|
+
|
|
1389
|
+
#### Examples
|
|
1390
|
+
|
|
1391
|
+
##### One machine over a shift
|
|
1392
|
+
|
|
1393
|
+
```tsx
|
|
1394
|
+
/* What one machine did over a shift, as a band: production, setup, fault,
|
|
1395
|
+
maintenance - every segment ends where the next begins.
|
|
1396
|
+
|
|
1397
|
+
No `laneFrom`, no `laneTo`: without them a band fills the whole domain of its
|
|
1398
|
+
y axis, and a chart of one machine needs nothing else. The y axis carries a
|
|
1399
|
+
single tick in the middle of the band, and the tickFormat names the machine
|
|
1400
|
+
there. The legend names the states, not the series - a band is read by its
|
|
1401
|
+
colours. */
|
|
1402
|
+
|
|
1403
|
+
import { Chart, Legend, StateBand, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1404
|
+
import { PLANT_STATES, shiftData, type StatePoint } from "../../data";
|
|
1405
|
+
|
|
1406
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1407
|
+
example can be copied whole. */
|
|
1408
|
+
|
|
1409
|
+
const timeOfDay = (v: number) =>
|
|
1410
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1411
|
+
|
|
1412
|
+
export default function Shift() {
|
|
1413
|
+
return (
|
|
1414
|
+
<Chart data={shiftData} height={150} ariaLabel="States of Furnace 1 over one shift">
|
|
1415
|
+
<XAxis accessor={(d: StatePoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
1416
|
+
<YAxis accessor={() => 0} domain={[0, 1]} ticks={[0.5]} tickFormat={() => "Furnace 1"} grid={false} />
|
|
1417
|
+
<StateBand accessor={(d: StatePoint) => d.m1} states={PLANT_STATES} name="Furnace 1" />
|
|
1418
|
+
<Legend placement="top" />
|
|
1419
|
+
<Tooltip mode="x" />
|
|
1420
|
+
</Chart>
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1426
|
+
|
|
1427
|
+
##### A state under a course
|
|
1428
|
+
|
|
1429
|
+
```tsx
|
|
1430
|
+
/* The furnace temperature, and under it the furnace's state in a lane of its
|
|
1431
|
+
own: where the band turns to fault, the curve climbs.
|
|
1432
|
+
|
|
1433
|
+
The lane stands on a second y axis, in its domain units - the bottom unit of
|
|
1434
|
+
five, the rest is air for the curve. Two charts one above the other would
|
|
1435
|
+
need their x axes kept in step by hand; one chart has one x axis, and the
|
|
1436
|
+
cause lies exactly under its effect. */
|
|
1437
|
+
|
|
1438
|
+
import { Chart, Legend, Line, StateBand, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1439
|
+
import { PLANT_STATES, shiftData, type StatePoint } from "../../data";
|
|
1440
|
+
|
|
1441
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1442
|
+
example can be copied whole. */
|
|
1443
|
+
|
|
1444
|
+
const timeOfDay = (v: number) =>
|
|
1445
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1446
|
+
|
|
1447
|
+
export default function UnderACourse() {
|
|
1448
|
+
return (
|
|
1449
|
+
<Chart data={shiftData} height={320} ariaLabel="Furnace temperature above the states of the furnace">
|
|
1450
|
+
<XAxis accessor={(d: StatePoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
1451
|
+
<YAxis accessor={(d: StatePoint) => d.temperature} domain={[720, 900]} label="°C" />
|
|
1452
|
+
<YAxis
|
|
1453
|
+
id="lane"
|
|
1454
|
+
position="right"
|
|
1455
|
+
accessor={() => 0}
|
|
1456
|
+
domain={[0, 5]}
|
|
1457
|
+
ticks={[0.4]}
|
|
1458
|
+
tickFormat={() => "State"}
|
|
1459
|
+
/>
|
|
1460
|
+
<StateBand
|
|
1461
|
+
accessor={(d: StatePoint) => d.m1}
|
|
1462
|
+
states={PLANT_STATES}
|
|
1463
|
+
yAxisId="lane"
|
|
1464
|
+
laneFrom={0}
|
|
1465
|
+
laneTo={0.8}
|
|
1466
|
+
name="Furnace 1"
|
|
1467
|
+
/>
|
|
1468
|
+
<Line accessor={(d: StatePoint) => d.temperature} name="Furnace temperature" strokeWidth={1.75} />
|
|
1469
|
+
<Legend placement="top" />
|
|
1470
|
+
<Tooltip mode="x" />
|
|
1471
|
+
</Chart>
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1477
|
+
|
|
1478
|
+
#### API
|
|
1479
|
+
|
|
1480
|
+
##### `StateBandProps<T>`
|
|
1481
|
+
|
|
1482
|
+
| Prop | Type | Default | Description |
|
|
1483
|
+
|---|---|---|---|
|
|
1484
|
+
| `accessor` (required) | `Accessor<T>` | — | Index of the state in `states`; null/undefined/NaN/±Infinity is a gap - and a gap stays a hole, it gets no colour for "unknown". A colour would be a claim about the interval. |
|
|
1485
|
+
| `states` (required) | `readonly StateEntry[]` | — | The closed set of states, in the order of their codes. |
|
|
1486
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
1487
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
1488
|
+
| `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
|
|
1489
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
1490
|
+
| `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent - a fixed `domain` keeps the axis still. Its legend entry stays, drawn back. Controlled: the caller sets it, typically from `Legend onToggle`. |
|
|
1491
|
+
| `laneFrom` | `number` | — | Lower edge of the lane in domain units of the y axis. |
|
|
1492
|
+
| `laneTo` | `number` | — | Upper edge of the lane in domain units of the y axis. |
|
|
1493
|
+
|
|
1494
|
+
#### Why it is like this
|
|
1495
|
+
|
|
1496
|
+
##### A state is a number
|
|
1497
|
+
|
|
1498
|
+
The accessor of a state band returns the **index** of a state in the state list, not a string and not a colour. The list is the code: it fixes the order, the name and the colour in one place, so that two charts of the same plant cannot disagree about what yellow means. See `docs/adr/0007-a-state-is-a-number.md`.
|
|
1499
|
+
|
|
1500
|
+
##### A band is a partition, and a hole is not a state
|
|
1501
|
+
|
|
1502
|
+
Every segment ends where the next begins - that is what makes it a band. Where a machine reports nothing, a hole stays: there is no colour for „unknown“, because a fifth colour in the legend would claim a state that was never measured. Work with an explicit end - idle time between jobs, two jobs on one machine - is occupancy, and it belongs to `@umriss-ui/schedule`.
|
|
1503
|
+
|
|
1504
|
+
### Matrix
|
|
1505
|
+
|
|
1506
|
+
One value per cell across two discrete axes - coloured by assessment or across a gradient, with a hole where nothing was measured.
|
|
1507
|
+
|
|
1508
|
+
```ts
|
|
1509
|
+
import { Matrix, DEFAULT_GRADIENT } from "@umriss-ui/charts";
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/matrix
|
|
1513
|
+
|
|
1514
|
+
#### Examples
|
|
1515
|
+
|
|
1516
|
+
##### Matrix
|
|
1517
|
+
|
|
1518
|
+
```tsx
|
|
1519
|
+
/* Eight machines across twenty-four hours at a glance. Above by assessment - the
|
|
1520
|
+
same limits that colour a tile - below across a gradient.
|
|
1521
|
+
|
|
1522
|
+
A bad HOUR everywhere looks different from a bad MACHINE, and exactly that
|
|
1523
|
+
difference is the reason for the form. Where nothing was measured, a hole
|
|
1524
|
+
stays. */
|
|
1525
|
+
|
|
1526
|
+
import { Chart, Matrix, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1527
|
+
import { MACHINES, matrixData, type CellPoint } from "../../data";
|
|
1528
|
+
|
|
1529
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1530
|
+
example can be copied whole. */
|
|
1531
|
+
|
|
1532
|
+
export default function MatrixExample() {
|
|
1533
|
+
return (
|
|
1534
|
+
<div className="side-by-side">
|
|
1535
|
+
<Chart data={matrixData} height={260} ariaLabel="OEE per machine and hour, by assessment">
|
|
1536
|
+
<XAxis
|
|
1537
|
+
accessor={(d: CellPoint) => d.hour}
|
|
1538
|
+
ticks={[0, 6, 12, 18]}
|
|
1539
|
+
tickFormat={(v) => `${v}:00`}
|
|
1540
|
+
label="Hour"
|
|
1541
|
+
/>
|
|
1542
|
+
<YAxis
|
|
1543
|
+
accessor={(d: CellPoint) => d.machine}
|
|
1544
|
+
ticks={MACHINES.map((_, i) => i)}
|
|
1545
|
+
tickFormat={(v) => MACHINES[v] ?? ""}
|
|
1546
|
+
/>
|
|
1547
|
+
<Matrix
|
|
1548
|
+
accessor={(d: CellPoint) => d.machine}
|
|
1549
|
+
value={(d: CellPoint) => d.oee}
|
|
1550
|
+
coloring={{
|
|
1551
|
+
kind: "assessment",
|
|
1552
|
+
limits: {
|
|
1553
|
+
limits: [
|
|
1554
|
+
{ value: 70, side: "lower", severity: "warning" },
|
|
1555
|
+
{ value: 40, side: "lower", severity: "alarm" },
|
|
1556
|
+
],
|
|
1557
|
+
},
|
|
1558
|
+
}}
|
|
1559
|
+
name="OEE"
|
|
1560
|
+
/>
|
|
1561
|
+
<Tooltip mode="nearest" />
|
|
1562
|
+
</Chart>
|
|
1563
|
+
<Chart data={matrixData} height={260} ariaLabel="OEE per machine and hour, as a gradient">
|
|
1564
|
+
<XAxis
|
|
1565
|
+
accessor={(d: CellPoint) => d.hour}
|
|
1566
|
+
ticks={[0, 6, 12, 18]}
|
|
1567
|
+
tickFormat={(v) => `${v}:00`}
|
|
1568
|
+
label="Hour"
|
|
1569
|
+
/>
|
|
1570
|
+
<YAxis
|
|
1571
|
+
accessor={(d: CellPoint) => d.machine}
|
|
1572
|
+
ticks={MACHINES.map((_, i) => i)}
|
|
1573
|
+
tickFormat={(v) => MACHINES[v] ?? ""}
|
|
1574
|
+
/>
|
|
1575
|
+
<Matrix accessor={(d: CellPoint) => d.machine} value={(d: CellPoint) => d.oee} name="OEE" />
|
|
1576
|
+
<Tooltip mode="nearest" />
|
|
1577
|
+
</Chart>
|
|
1578
|
+
</div>
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1584
|
+
|
|
1585
|
+
#### API
|
|
1586
|
+
|
|
1587
|
+
##### `MatrixProps<T>`
|
|
1588
|
+
|
|
1589
|
+
| Prop | Type | Default | Description |
|
|
1590
|
+
|---|---|---|---|
|
|
1591
|
+
| `accessor` (required) | `Accessor<T>` | — | Row position on the y axis. |
|
|
1592
|
+
| `value` (required) | `Accessor<T>` | — | The value that decides the colour. |
|
|
1593
|
+
| `coloring` | `MatrixColoring` | — | Without a value the default gradient across the data range. |
|
|
1594
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
1595
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
1596
|
+
| `data` | `readonly T[]` | — | Series-own data; overrides the container data (R-2.4). |
|
|
1597
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
1598
|
+
| `hidden` | `boolean` | — | Not drawn, not hit and not counted for its axes' extent - a fixed `domain` keeps the axis still. Its legend entry stays, drawn back. Controlled: the caller sets it, typically from `Legend onToggle`. |
|
|
1599
|
+
| `format` | `(value: number) => string` | — | The value as the tooltip writes it; without one the default. The y axis' `tickFormat` writes the row, not the value. |
|
|
1600
|
+
|
|
1601
|
+
## Monitoring
|
|
1602
|
+
|
|
1603
|
+
A value read against its limits - and its shape over time.
|
|
1604
|
+
|
|
1605
|
+
### LimitLine
|
|
1606
|
+
|
|
1607
|
+
The limits a curve is read against: a line as a landmark above the series, a band as ground below them - the same limit model the components of @umriss-ui/core use (ADR-0006).
|
|
1608
|
+
|
|
1609
|
+
```ts
|
|
1610
|
+
import { LimitLine, LimitBand } from "@umriss-ui/charts";
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/limitline
|
|
1614
|
+
|
|
1615
|
+
#### Examples
|
|
1616
|
+
|
|
1617
|
+
##### Limits and state band
|
|
1618
|
+
|
|
1619
|
+
```tsx
|
|
1620
|
+
/* The core of a plant screen: a curve, the limits it is read against, and below
|
|
1621
|
+
it the state of three machines.
|
|
1622
|
+
|
|
1623
|
+
Everything in ONE chart on ONE x axis - which is exactly why the fault at
|
|
1624
|
+
12:15 lies visibly below the temperature jump. A band lies below every series,
|
|
1625
|
+
a limit line above them: the one is ground, the other a landmark. Machine 3
|
|
1626
|
+
reports nothing for a while; a hole stays there, no colour for "unknown". */
|
|
1627
|
+
|
|
1628
|
+
import { Chart, Legend, LimitBand, LimitLine, Line, StateBand, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1629
|
+
import { PLANT_STATES, shiftData, type StatePoint } from "../../data";
|
|
1630
|
+
|
|
1631
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1632
|
+
example can be copied whole. */
|
|
1633
|
+
|
|
1634
|
+
const timeOfDay = (v: number) =>
|
|
1635
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1636
|
+
|
|
1637
|
+
/* The lanes of a stack: one domain unit per machine. The labelling goes through
|
|
1638
|
+
the axis' tickFormat - there is no lane concept of its own. */
|
|
1639
|
+
const LANE_TICKS = [0.45, 1.45, 2.45];
|
|
1640
|
+
/* Lane 0 lies at the bottom - the list therefore runs from the bottom upwards. */
|
|
1641
|
+
const LANE_NAMES = ["Mill 3", "Press 2", "Furnace 1"];
|
|
1642
|
+
|
|
1643
|
+
export default function LimitsAndState() {
|
|
1644
|
+
return (
|
|
1645
|
+
<Chart
|
|
1646
|
+
data={shiftData}
|
|
1647
|
+
height={340}
|
|
1648
|
+
ariaLabel="Furnace temperature with a warning and an alarm limit above the states of three machines"
|
|
1649
|
+
>
|
|
1650
|
+
<XAxis accessor={(d: StatePoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
1651
|
+
<YAxis accessor={(d: StatePoint) => d.temperature} domain={[740, 900]} label="°C" />
|
|
1652
|
+
{/* The lane axis: one unit per machine, the rest is air for the curve
|
|
1653
|
+
above it. Not a pixel in this file. */}
|
|
1654
|
+
<YAxis
|
|
1655
|
+
id="lanes"
|
|
1656
|
+
position="right"
|
|
1657
|
+
accessor={() => 0}
|
|
1658
|
+
domain={[0, 8]}
|
|
1659
|
+
ticks={LANE_TICKS}
|
|
1660
|
+
tickFormat={(v) => LANE_NAMES[Math.floor(v)] ?? ""}
|
|
1661
|
+
/>
|
|
1662
|
+
<LimitBand from={780} to={820} severity="warning" label="Tolerance" />
|
|
1663
|
+
<LimitLine value={820} severity="warning" label="Warning limit" />
|
|
1664
|
+
<LimitLine value={860} severity="alarm" label="Alarm limit" />
|
|
1665
|
+
<StateBand
|
|
1666
|
+
accessor={(d: StatePoint) => d.m1}
|
|
1667
|
+
states={PLANT_STATES}
|
|
1668
|
+
yAxisId="lanes"
|
|
1669
|
+
laneFrom={2}
|
|
1670
|
+
laneTo={2.9}
|
|
1671
|
+
name="Furnace 1"
|
|
1672
|
+
/>
|
|
1673
|
+
<StateBand
|
|
1674
|
+
accessor={(d: StatePoint) => d.m2}
|
|
1675
|
+
states={PLANT_STATES}
|
|
1676
|
+
yAxisId="lanes"
|
|
1677
|
+
laneFrom={1}
|
|
1678
|
+
laneTo={1.9}
|
|
1679
|
+
name="Press 2"
|
|
1680
|
+
/>
|
|
1681
|
+
<StateBand
|
|
1682
|
+
accessor={(d: StatePoint) => d.m3}
|
|
1683
|
+
states={PLANT_STATES}
|
|
1684
|
+
yAxisId="lanes"
|
|
1685
|
+
laneFrom={0}
|
|
1686
|
+
laneTo={0.9}
|
|
1687
|
+
name="Mill 3"
|
|
1688
|
+
/>
|
|
1689
|
+
<Line accessor={(d: StatePoint) => d.temperature} name="Furnace temperature" strokeWidth={1.75} />
|
|
1690
|
+
<Legend placement="top" />
|
|
1691
|
+
<Tooltip mode="x" />
|
|
1692
|
+
</Chart>
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1698
|
+
|
|
1699
|
+
#### API
|
|
1700
|
+
|
|
1701
|
+
##### `LimitLineProps`
|
|
1702
|
+
|
|
1703
|
+
| Prop | Type | Default | Description |
|
|
1704
|
+
|---|---|---|---|
|
|
1705
|
+
| `value` (required) | `number` | — | Where the line lies, in domain units of its axis. |
|
|
1706
|
+
| `axisId` | `string` | — | Axis on which the value lies. Without one, the first axis of the orientation. |
|
|
1707
|
+
| `orientation` | `"x" \| "y"` | — | Whether the value lies on the x or on the y axis. Without one, y: a limit is nearly always a value and not a moment. |
|
|
1708
|
+
| `severity` | `Severity` | — | Two severities, not five: five urgencies are five colours, and five colours on a plant screen are noise. |
|
|
1709
|
+
| `label` | `string` | — | What the value means. A line at 90 does not say what 90 is. |
|
|
1710
|
+
| `color` | `string` | — | Any CSS colour value, in place of the one the severity yields. A colour handed in instead of a severity puts the rule in the application and the presentation in the library, and the two drift apart as soon as a bound moves - so this is for the case that has no severity, not for taste. |
|
|
1711
|
+
| `inExtent` | `boolean` | — | Whether the value widens the axis extent, so that the limit is certainly visible. Default true; the head of this file says why. |
|
|
1712
|
+
| `role` | `LimitRole` | — | A chosen specification limit (the default) or a calculated control limit. They look different because they mean different things (ADR-0008). |
|
|
1713
|
+
|
|
1714
|
+
##### `LimitBandProps`
|
|
1715
|
+
|
|
1716
|
+
| Prop | Type | Default | Description |
|
|
1717
|
+
|---|---|---|---|
|
|
1718
|
+
| `from` (required) | `number` | — | Lower edge of the band, in domain units of its axis. |
|
|
1719
|
+
| `to` (required) | `number` | — | Upper edge, in domain units of its axis. A band drawn backwards (`from` above `to`) is drawn between the two all the same - the pair is a range, not a direction. |
|
|
1720
|
+
| `axisId` | `string` | — | Axis on which the value lies. Without one, the first axis of the orientation. |
|
|
1721
|
+
| `orientation` | `"x" \| "y"` | — | Whether the value lies on the x or on the y axis. Without one, y: a limit is nearly always a value and not a moment. |
|
|
1722
|
+
| `severity` | `Severity` | — | Two severities, not five: five urgencies are five colours, and five colours on a plant screen are noise. |
|
|
1723
|
+
| `label` | `string` | — | What the value means. A line at 90 does not say what 90 is. |
|
|
1724
|
+
| `color` | `string` | — | Any CSS colour value, in place of the one the severity yields. A colour handed in instead of a severity puts the rule in the application and the presentation in the library, and the two drift apart as soon as a bound moves - so this is for the case that has no severity, not for taste. |
|
|
1725
|
+
| `inExtent` | `boolean` | — | Whether the value widens the axis extent, so that the limit is certainly visible. Default true; the head of this file says why. |
|
|
1726
|
+
| `role` | `LimitRole` | — | A chosen specification limit (the default) or a calculated control limit. They look different because they mean different things (ADR-0008). |
|
|
1727
|
+
|
|
1728
|
+
#### Why it is like this
|
|
1729
|
+
|
|
1730
|
+
##### The limit model lives in both packages, on purpose
|
|
1731
|
+
|
|
1732
|
+
A limit is a value, a side and a severity; a verdict is what a number *is* given those limits. The same model stands in `@umriss-ui/core` and here - duplicated deliberately, because the alternative was a dependency between two packages that are meant to be usable apart. What holds the two copies together is not discipline but a conformance test that runs both versions from one case table. See `docs/adr/0006-the-limit-model-lives-in-both-packages.md`.
|
|
1733
|
+
|
|
1734
|
+
##### A line is a landmark, a band is ground
|
|
1735
|
+
|
|
1736
|
+
The band lies below every series, the line above them. That is not a taste in layering: a tolerance band is the area a value should be in, and a curve has to be readable on top of it, while a limit is the thing a reader looks for and must not be covered by a stroke that happens to run along it.
|
|
1737
|
+
|
|
1738
|
+
##### The colour follows the severity, and nothing else
|
|
1739
|
+
|
|
1740
|
+
A limit carries `severity`, and the tone is derived from it. Handing a colour in instead would put the rule in the application and the presentation in the library, and the two would drift apart the moment a bound moves.
|
|
1741
|
+
|
|
1742
|
+
### ControlChart
|
|
1743
|
+
|
|
1744
|
+
Center line, control limits and the rule violations of a process - out of a named reference window, never out of what happens to be visible (ADR-0008).
|
|
1745
|
+
|
|
1746
|
+
```ts
|
|
1747
|
+
import { ControlChart, controlLimits, violations } from "@umriss-ui/charts";
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/controlchart
|
|
1751
|
+
|
|
1752
|
+
#### Examples
|
|
1753
|
+
|
|
1754
|
+
##### Control chart
|
|
1755
|
+
|
|
1756
|
+
```tsx
|
|
1757
|
+
/* Two kinds of limit, and they look different because they mean different
|
|
1758
|
+
things.
|
|
1759
|
+
|
|
1760
|
+
The SPECIFICATION limits (dashed, coloured) are chosen: what the customer
|
|
1761
|
+
assumes. The CONTROL limits (solid, neutral) are calculated: what this process
|
|
1762
|
+
normally does - and out of a named reference window, never out of what happens
|
|
1763
|
+
to be visible (ADR-0008). From value 62 the process drifts; the run rule fires
|
|
1764
|
+
long before a point leaves the specification. */
|
|
1765
|
+
|
|
1766
|
+
import { Chart, ControlChart, LimitLine, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1767
|
+
import { measurementData, type Measurement } from "../../data";
|
|
1768
|
+
|
|
1769
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1770
|
+
example can be copied whole. */
|
|
1771
|
+
|
|
1772
|
+
/* The control limits come out of the first 60 per cent of the series - the
|
|
1773
|
+
period in which the process demonstrably ran in control. */
|
|
1774
|
+
const REFERENCE_WINDOW = { kind: "referenceWindow", from: 0, to: 54 } as const;
|
|
1775
|
+
|
|
1776
|
+
export default function ControlChartExample() {
|
|
1777
|
+
return (
|
|
1778
|
+
<Chart data={measurementData} height={300} ariaLabel="Control chart of a feature inspection">
|
|
1779
|
+
<XAxis accessor={(d: Measurement) => d.n} label="Inspection" />
|
|
1780
|
+
<YAxis accessor={(d: Measurement) => d.value} label="mm" />
|
|
1781
|
+
<LimitLine value={13.2} severity="alarm" label="USL" />
|
|
1782
|
+
<LimitLine value={11.8} severity="alarm" label="LSL" />
|
|
1783
|
+
<ControlChart
|
|
1784
|
+
accessor={(d: Measurement) => d.value}
|
|
1785
|
+
data={measurementData}
|
|
1786
|
+
origin={REFERENCE_WINDOW}
|
|
1787
|
+
name="Feature Ø"
|
|
1788
|
+
labelUpper="UCL"
|
|
1789
|
+
labelLower="LCL"
|
|
1790
|
+
violationName="Feature Ø - rule violation"
|
|
1791
|
+
/>
|
|
1792
|
+
<Tooltip mode="x" />
|
|
1793
|
+
</Chart>
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1799
|
+
|
|
1800
|
+
#### API
|
|
1801
|
+
|
|
1802
|
+
##### `ControlChartProps<T>`
|
|
1803
|
+
|
|
1804
|
+
| Prop | Type | Default | Description |
|
|
1805
|
+
|---|---|---|---|
|
|
1806
|
+
| `accessor` (required) | `Accessor<T>` | — | The values of the chart. |
|
|
1807
|
+
| `data` (required) | `readonly T[]` | — | Required: the limits arise out of these values, not out of what happens to be visible. |
|
|
1808
|
+
| `origin` (required) | `ControlLimitOrigin` | — | Given, or from a named reference window - never tacitly from everything on the screen (ADR-0008). |
|
|
1809
|
+
| `xAxisId` | `string` | — | Binding to an x axis (R-4.12). |
|
|
1810
|
+
| `yAxisId` | `string` | — | Binding to a y axis (R-4.12). |
|
|
1811
|
+
| `name` | `string` | — | The name in legend and tooltip. Without one the series is called "Series n" and a warning stands in DEV - an unnamed series is a colour nobody can look up. |
|
|
1812
|
+
| `format` | `(value: number) => string` | — | The value as the tooltip writes it - on the line and on the violations; without one the y axis' `tickFormat`, then the default. |
|
|
1813
|
+
| `color` | `string` | — | Any CSS colour value; without one the palette --uc-series-N. |
|
|
1814
|
+
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value for the line; the theme resolves it. The violations are always "alarm". |
|
|
1815
|
+
| `rules` | `Partial<RuleOptions>` | — | Switchable one by one; run lengths are parameters. |
|
|
1816
|
+
| `zoneLines` | `boolean` | — | The faint zone lines at one and two sigma. Rule 4 is about the two-sigma zone; without them a reader cannot check it. |
|
|
1817
|
+
| `labelUpper` | `string` | — | Labels of the control limits in the axis band. Without a value, none: the package brings no text along, not even "UCL" and "LCL". |
|
|
1818
|
+
| `labelLower` | `string` | — | The same for the lower control limit. Named separately because a chart may well want one of the two labelled and not the other. |
|
|
1819
|
+
| `violationName` | `string` | — | Name of the scatter of violations in the legend and the tooltip. Without a value, none: a legend then carries it as "Series n" and warns once in DEV. Before this, a German suffix stood on the name of the chart. |
|
|
1820
|
+
| `onViolations` | `(found: readonly Violation[]) => void` | — | The violations as data - they can be listed beside the chart as well as marked on it. |
|
|
1821
|
+
|
|
1822
|
+
#### Why it is like this
|
|
1823
|
+
|
|
1824
|
+
##### A control limit is not a specification limit
|
|
1825
|
+
|
|
1826
|
+
Two numbers on a control chart look alike and mean opposite things. A **specification limit** is chosen: what the customer assumes, what the drawing says. A **control limit** is calculated: what this process actually does when nothing is wrong with it. A process can sit comfortably inside the specification and be out of control, and that is the case the chart exists to show. See `docs/adr/0008-control-limits-are-not-specification-limits.md`.
|
|
1827
|
+
|
|
1828
|
+
##### Never computed from what happens to be visible
|
|
1829
|
+
|
|
1830
|
+
The limits come from a named **reference window** - a period in which the process demonstrably ran in control - and never from the data the chart is currently showing. Limits recomputed over a drifting process drift with it, and the chart then certifies every state as normal, most confidently at the moment it stops being so.
|
|
1831
|
+
|
|
1832
|
+
##### The rules fire before a point leaves the specification
|
|
1833
|
+
|
|
1834
|
+
Outlier, run, trend and two-of-three: four rules over the zones, and the point of them is the warning that comes early. They are exported as pure functions, so an application that draws something else can still ask the same question.
|
|
1835
|
+
|
|
1836
|
+
### Pareto
|
|
1837
|
+
|
|
1838
|
+
Sorted descending with a cumulative line: the pure module that sorts, accumulates and collects the tail - the composition stays the caller's.
|
|
1839
|
+
|
|
1840
|
+
```ts
|
|
1841
|
+
import { pareto } from "@umriss-ui/charts";
|
|
1842
|
+
```
|
|
1843
|
+
|
|
1844
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/pareto
|
|
1845
|
+
|
|
1846
|
+
#### Examples
|
|
1847
|
+
|
|
1848
|
+
##### Pareto
|
|
1849
|
+
|
|
1850
|
+
```tsx
|
|
1851
|
+
/* The first question after a bad week. Bars sorted descending, the cumulative
|
|
1852
|
+
line on a second y axis on the right, categories as indices on the ordinary
|
|
1853
|
+
numeric x axis (ADR-0002).
|
|
1854
|
+
|
|
1855
|
+
The long tail is collected into "Other" and stands LAST - independently of its
|
|
1856
|
+
value, because otherwise the chart would claim there is a downtime reason
|
|
1857
|
+
called "Other" in third place.
|
|
1858
|
+
|
|
1859
|
+
`pareto()` is the pure module: sort, accumulate, collect. The composition
|
|
1860
|
+
below it is four lines and belongs to the caller - which is why there is no
|
|
1861
|
+
<Pareto> that would co-own the second axis. */
|
|
1862
|
+
|
|
1863
|
+
import { useMemo } from "react";
|
|
1864
|
+
import { Bar, Chart, Legend, LimitLine, Line, Tooltip, XAxis, YAxis, pareto } from "@umriss-ui/charts";
|
|
1865
|
+
import type { ParetoEntry } from "@umriss-ui/charts";
|
|
1866
|
+
import { DOWNTIME_REASONS } from "../../data";
|
|
1867
|
+
|
|
1868
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1869
|
+
example can be copied whole. */
|
|
1870
|
+
|
|
1871
|
+
export default function ParetoExample() {
|
|
1872
|
+
const result = useMemo(() => pareto(DOWNTIME_REASONS, { collectRank: 6, remainderName: "Other" }), []);
|
|
1873
|
+
const entries = result.entries;
|
|
1874
|
+
|
|
1875
|
+
return (
|
|
1876
|
+
<Chart data={entries as ParetoEntry[]} height={300} ariaLabel="Downtime reasons by frequency">
|
|
1877
|
+
<XAxis
|
|
1878
|
+
accessor={(d: ParetoEntry) => d.index}
|
|
1879
|
+
ticks={entries.map((e) => e.index)}
|
|
1880
|
+
tickFormat={(v) => entries[v]?.name ?? ""}
|
|
1881
|
+
/>
|
|
1882
|
+
<YAxis accessor={(d: ParetoEntry) => d.value} label="Minutes" />
|
|
1883
|
+
<YAxis
|
|
1884
|
+
id="share"
|
|
1885
|
+
position="right"
|
|
1886
|
+
accessor={(d: ParetoEntry) => d.cumulative * 100}
|
|
1887
|
+
domain={[0, 100]}
|
|
1888
|
+
tickFormat={(v) => `${v.toFixed(0)} %`}
|
|
1889
|
+
label="cumulative"
|
|
1890
|
+
/>
|
|
1891
|
+
<Bar accessor={(d: ParetoEntry) => d.value} name="Downtime" barWidth={0.72} />
|
|
1892
|
+
<Line
|
|
1893
|
+
accessor={(d: ParetoEntry) => d.cumulative * 100}
|
|
1894
|
+
yAxisId="share"
|
|
1895
|
+
name="cumulative"
|
|
1896
|
+
markers="always"
|
|
1897
|
+
strokeWidth={1.75}
|
|
1898
|
+
/>
|
|
1899
|
+
<LimitLine value={80} axisId="share" severity="warning" label="80 %" inExtent={false} />
|
|
1900
|
+
<Legend placement="top" />
|
|
1901
|
+
<Tooltip mode="x" />
|
|
1902
|
+
</Chart>
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
```
|
|
1906
|
+
|
|
1907
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1908
|
+
|
|
1909
|
+
## Around the chart
|
|
1910
|
+
|
|
1911
|
+
What stands beside the series: what a hover says, what the colours mean - and what the whole thing costs.
|
|
1912
|
+
|
|
1913
|
+
### Tooltip & Legend
|
|
1914
|
+
|
|
1915
|
+
What a hover reports and what the colours mean: the hit as a whole x position or as the nearest point, the legend above or below the plot.
|
|
1916
|
+
|
|
1917
|
+
```ts
|
|
1918
|
+
import { Tooltip, Legend, DataTable } from "@umriss-ui/charts";
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/tooltip
|
|
1922
|
+
|
|
1923
|
+
#### Examples
|
|
1924
|
+
|
|
1925
|
+
##### Mode x or nearest
|
|
1926
|
+
|
|
1927
|
+
```tsx
|
|
1928
|
+
/* The same two furnaces twice: on the left the tooltip reports every series at
|
|
1929
|
+
the x position, on the right only the point nearest the pointer.
|
|
1930
|
+
|
|
1931
|
+
`"x"` answers "what was everything at 08:40"; `"nearest"` answers "which
|
|
1932
|
+
curve is this". The hit is searched per series in its own axis space and
|
|
1933
|
+
compared in pixels, so the crosshair snaps to a data point and never stands
|
|
1934
|
+
between two. Hover to see the difference - a picture of the page cannot. */
|
|
1935
|
+
|
|
1936
|
+
import { Chart, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1937
|
+
import { furnaceData, type FurnacePoint } from "../../data";
|
|
1938
|
+
|
|
1939
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1940
|
+
example can be copied whole. */
|
|
1941
|
+
|
|
1942
|
+
const timeOfDay = (v: number) =>
|
|
1943
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1944
|
+
|
|
1945
|
+
function Furnaces({ mode }: { mode: "x" | "nearest" }) {
|
|
1946
|
+
return (
|
|
1947
|
+
<Chart data={furnaceData} height={220} ariaLabel={`Two furnace temperatures, tooltip mode ${mode}`}>
|
|
1948
|
+
<XAxis accessor={(d: FurnacePoint) => d.t} tickFormat={timeOfDay} tickCount={4} />
|
|
1949
|
+
<YAxis accessor={(d: FurnacePoint) => d.f1} label="°C" />
|
|
1950
|
+
<Line accessor={(d: FurnacePoint) => d.f1} name="Furnace 1" />
|
|
1951
|
+
<Line accessor={(d: FurnacePoint) => d.f2} name="Furnace 2" />
|
|
1952
|
+
<Tooltip mode={mode} />
|
|
1953
|
+
</Chart>
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
export default function XOrNearest() {
|
|
1958
|
+
return (
|
|
1959
|
+
<div className="pair">
|
|
1960
|
+
<div>
|
|
1961
|
+
<p className="pair-caption">mode="x" - every series at the x position</p>
|
|
1962
|
+
<Furnaces mode="x" />
|
|
1963
|
+
</div>
|
|
1964
|
+
<div>
|
|
1965
|
+
<p className="pair-caption">mode="nearest" - only the nearest point</p>
|
|
1966
|
+
<Furnaces mode="nearest" />
|
|
1967
|
+
</div>
|
|
1968
|
+
</div>
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
```
|
|
1972
|
+
|
|
1973
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
1974
|
+
|
|
1975
|
+
##### Content of one's own
|
|
1976
|
+
|
|
1977
|
+
```tsx
|
|
1978
|
+
/* A tooltip in the plant's language: "Furnace 1 - 812 °C, 4 °C above set
|
|
1979
|
+
point" instead of a name and a number.
|
|
1980
|
+
|
|
1981
|
+
The render prop receives the hit - the x value and one entry per series, each
|
|
1982
|
+
with its datum - and returns what stands in the box. The set point is not a
|
|
1983
|
+
series here but a field of the datum, which is exactly what the render prop
|
|
1984
|
+
can read and the built-in tooltip cannot know. */
|
|
1985
|
+
|
|
1986
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
1987
|
+
import type { TooltipHit } from "@umriss-ui/charts";
|
|
1988
|
+
import { furnaceData, type FurnacePoint } from "../../data";
|
|
1989
|
+
|
|
1990
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
1991
|
+
example can be copied whole. */
|
|
1992
|
+
|
|
1993
|
+
const timeOfDay = (v: number) =>
|
|
1994
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
1995
|
+
|
|
1996
|
+
function PlantTooltip({ hit }: { hit: TooltipHit<FurnacePoint> }) {
|
|
1997
|
+
return (
|
|
1998
|
+
<div className="custom-tooltip">
|
|
1999
|
+
<strong>{timeOfDay(hit.xValue)}</strong>
|
|
2000
|
+
{hit.points.map((point) => {
|
|
2001
|
+
const off = point.yValue - point.datum.setPoint;
|
|
2002
|
+
return (
|
|
2003
|
+
<span key={point.seriesName}>
|
|
2004
|
+
{point.seriesName} - {point.yValue.toFixed(0)} °C, {Math.abs(off).toFixed(0)} °C{" "}
|
|
2005
|
+
{off >= 0 ? "above" : "below"} set point
|
|
2006
|
+
</span>
|
|
2007
|
+
);
|
|
2008
|
+
})}
|
|
2009
|
+
</div>
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
export default function OwnContent() {
|
|
2014
|
+
return (
|
|
2015
|
+
<Chart data={furnaceData} height={280} ariaLabel="Two furnace temperatures with a tooltip in plant language">
|
|
2016
|
+
<XAxis accessor={(d: FurnacePoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
2017
|
+
<YAxis accessor={(d: FurnacePoint) => d.f1} label="°C" />
|
|
2018
|
+
<Line accessor={(d: FurnacePoint) => d.f1} name="Furnace 1" />
|
|
2019
|
+
<Line accessor={(d: FurnacePoint) => d.f2} name="Furnace 2" />
|
|
2020
|
+
<Legend placement="top" />
|
|
2021
|
+
<Tooltip<FurnacePoint> mode="x" render={(hit) => <PlantTooltip hit={hit} />} />
|
|
2022
|
+
</Chart>
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
```
|
|
2026
|
+
|
|
2027
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2028
|
+
|
|
2029
|
+
##### Legend above or below
|
|
2030
|
+
|
|
2031
|
+
```tsx
|
|
2032
|
+
/* The legend above the plot and below it. Above is the default: a chart without
|
|
2033
|
+
`placement` looks like the left one, and `placement="top"` says the same thing
|
|
2034
|
+
out loud.
|
|
2035
|
+
|
|
2036
|
+
A legend stands beside the plot area, never inside it - it takes its height
|
|
2037
|
+
from the chart, not from the curves. Hovering an entry lifts its series and
|
|
2038
|
+
draws the others faint. */
|
|
2039
|
+
|
|
2040
|
+
import { Chart, Legend, Line, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2041
|
+
import { furnaceData, type FurnacePoint } from "../../data";
|
|
2042
|
+
|
|
2043
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2044
|
+
example can be copied whole. */
|
|
2045
|
+
|
|
2046
|
+
const timeOfDay = (v: number) =>
|
|
2047
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
2048
|
+
|
|
2049
|
+
export default function LegendPlacement() {
|
|
2050
|
+
return (
|
|
2051
|
+
<div className="pair">
|
|
2052
|
+
<div>
|
|
2053
|
+
<p className="pair-caption">no placement - above, the default</p>
|
|
2054
|
+
<Chart data={furnaceData} height={220} ariaLabel="Legend above the plot">
|
|
2055
|
+
<XAxis accessor={(d: FurnacePoint) => d.t} tickFormat={timeOfDay} tickCount={4} />
|
|
2056
|
+
<YAxis accessor={(d: FurnacePoint) => d.f1} label="°C" />
|
|
2057
|
+
<Line accessor={(d: FurnacePoint) => d.f1} name="Furnace 1" />
|
|
2058
|
+
<Line accessor={(d: FurnacePoint) => d.f2} name="Furnace 2" />
|
|
2059
|
+
<Legend />
|
|
2060
|
+
</Chart>
|
|
2061
|
+
</div>
|
|
2062
|
+
<div>
|
|
2063
|
+
<p className="pair-caption">placement="bottom"</p>
|
|
2064
|
+
<Chart data={furnaceData} height={220} ariaLabel="Legend below the plot">
|
|
2065
|
+
<XAxis accessor={(d: FurnacePoint) => d.t} tickFormat={timeOfDay} tickCount={4} />
|
|
2066
|
+
<YAxis accessor={(d: FurnacePoint) => d.f1} label="°C" />
|
|
2067
|
+
<Line accessor={(d: FurnacePoint) => d.f1} name="Furnace 1" />
|
|
2068
|
+
<Line accessor={(d: FurnacePoint) => d.f2} name="Furnace 2" />
|
|
2069
|
+
<Legend placement="bottom" />
|
|
2070
|
+
</Chart>
|
|
2071
|
+
</div>
|
|
2072
|
+
</div>
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
```
|
|
2076
|
+
|
|
2077
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2078
|
+
|
|
2079
|
+
##### Value format per series
|
|
2080
|
+
|
|
2081
|
+
```tsx
|
|
2082
|
+
/* A furnace and the line it heats, on two y axes: each series writes its value
|
|
2083
|
+
in its own unit, through `format`. Without it the tooltip would take the y
|
|
2084
|
+
axis' `tickFormat`, and without that the default - a bare number beside
|
|
2085
|
+
another bare number. No render prop is needed for "°C". Hover to read it. */
|
|
2086
|
+
|
|
2087
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2088
|
+
import { corridorData, powerData, type CorridorPoint, type PowerPoint } from "../../data";
|
|
2089
|
+
|
|
2090
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2091
|
+
example can be copied whole. */
|
|
2092
|
+
|
|
2093
|
+
const celsius = (v: number) => `${v.toFixed(1)} °C`;
|
|
2094
|
+
const kilowatts = (v: number) => `${v.toFixed(0)} kW`;
|
|
2095
|
+
|
|
2096
|
+
export default function ValueFormat() {
|
|
2097
|
+
return (
|
|
2098
|
+
<Chart data={corridorData} height={260} ariaLabel="Furnace temperature and power draw with their units">
|
|
2099
|
+
<XAxis accessor={(d: { t: number }) => d.t} time />
|
|
2100
|
+
<YAxis accessor={(d: CorridorPoint) => d.temperature} label="°C" />
|
|
2101
|
+
<YAxis id="power" position="right" accessor={(d: PowerPoint) => d.kw} label="kW" />
|
|
2102
|
+
<Line accessor={(d: CorridorPoint) => d.temperature} name="Furnace" format={celsius} />
|
|
2103
|
+
<Line data={powerData} accessor={(d: PowerPoint) => d.kw} yAxisId="power" name="Power draw" format={kilowatts} />
|
|
2104
|
+
<Legend />
|
|
2105
|
+
<Tooltip mode="x" />
|
|
2106
|
+
</Chart>
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2112
|
+
|
|
2113
|
+
##### Legend that hides a series
|
|
2114
|
+
|
|
2115
|
+
```tsx
|
|
2116
|
+
/* The legend as a switch. `hidden` is the caller's state and `onToggle` hands
|
|
2117
|
+
it the name of the entry clicked - the chart keeps nothing of its own. A
|
|
2118
|
+
hidden series is not drawn and gives up its say in the extent, so the y axis
|
|
2119
|
+
closes in on what is left; its entry stays, struck through, to be clicked
|
|
2120
|
+
back. Without `onToggle` a legend is not clickable. */
|
|
2121
|
+
|
|
2122
|
+
import { useState } from "react";
|
|
2123
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2124
|
+
import { furnaceData, type FurnacePoint } from "../../data";
|
|
2125
|
+
|
|
2126
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2127
|
+
example can be copied whole. */
|
|
2128
|
+
|
|
2129
|
+
export default function TogglingLegend() {
|
|
2130
|
+
const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set(["Set point"]));
|
|
2131
|
+
const toggle = (name: string) =>
|
|
2132
|
+
setHidden((previous) => {
|
|
2133
|
+
const next = new Set(previous);
|
|
2134
|
+
if (!next.delete(name)) next.add(name);
|
|
2135
|
+
return next;
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
return (
|
|
2139
|
+
<Chart data={furnaceData} height={260} ariaLabel="Two furnaces and their set point, one series hidden">
|
|
2140
|
+
<XAxis accessor={(d: FurnacePoint) => d.t} time />
|
|
2141
|
+
<YAxis accessor={(d: FurnacePoint) => d.f1} label="°C" />
|
|
2142
|
+
<Line accessor={(d: FurnacePoint) => d.f1} name="Furnace 1" hidden={hidden.has("Furnace 1")} />
|
|
2143
|
+
<Line accessor={(d: FurnacePoint) => d.f2} name="Furnace 2" hidden={hidden.has("Furnace 2")} />
|
|
2144
|
+
<Line
|
|
2145
|
+
accessor={(d: FurnacePoint) => d.setPoint}
|
|
2146
|
+
name="Set point"
|
|
2147
|
+
dash={[4, 4]}
|
|
2148
|
+
hidden={hidden.has("Set point")}
|
|
2149
|
+
/>
|
|
2150
|
+
<Legend onToggle={toggle} />
|
|
2151
|
+
<Tooltip mode="x" />
|
|
2152
|
+
</Chart>
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
```
|
|
2156
|
+
|
|
2157
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2158
|
+
|
|
2159
|
+
##### The values as a table
|
|
2160
|
+
|
|
2161
|
+
```tsx
|
|
2162
|
+
/* The chart's values as a table, for a reader who wants them all at once
|
|
2163
|
+
rather than walked. `<DataTable />` puts a key at the end of the legend;
|
|
2164
|
+
"Show data" lays a plain table over the plot - the time in the first column,
|
|
2165
|
+
then one column per visible series, every value in the format the tooltip
|
|
2166
|
+
writes it in - and "Hide data" gives the picture back.
|
|
2167
|
+
|
|
2168
|
+
The table lists what the chart shows: hide a series in the legend, or zoom
|
|
2169
|
+
the axis, and the table follows. */
|
|
2170
|
+
|
|
2171
|
+
import { useState } from "react";
|
|
2172
|
+
import { Chart, DataTable, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2173
|
+
import { furnaceData, type FurnacePoint } from "../../data";
|
|
2174
|
+
|
|
2175
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2176
|
+
example can be copied whole. */
|
|
2177
|
+
|
|
2178
|
+
const timeOfDay = (v: number) =>
|
|
2179
|
+
new Date(v).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
2180
|
+
|
|
2181
|
+
const celsius = (v: number) => `${v.toFixed(1)} °C`;
|
|
2182
|
+
|
|
2183
|
+
export default function ValuesAsTable() {
|
|
2184
|
+
const [hidden, setHidden] = useState<readonly string[]>([]);
|
|
2185
|
+
const toggle = (name: string) =>
|
|
2186
|
+
setHidden((h) => (h.includes(name) ? h.filter((n) => n !== name) : [...h, name]));
|
|
2187
|
+
return (
|
|
2188
|
+
<Chart data={furnaceData} height={260} ariaLabel="Two furnaces over the early shift">
|
|
2189
|
+
<XAxis accessor={(d: FurnacePoint) => d.t} tickFormat={timeOfDay} label="Time" />
|
|
2190
|
+
<YAxis accessor={(d: FurnacePoint) => d.f1} label="°C" />
|
|
2191
|
+
<Line accessor={(d: FurnacePoint) => d.f1} name="Furnace 1" format={celsius} hidden={hidden.includes("Furnace 1")} />
|
|
2192
|
+
<Line accessor={(d: FurnacePoint) => d.f2} name="Furnace 2" format={celsius} hidden={hidden.includes("Furnace 2")} />
|
|
2193
|
+
<Legend onToggle={toggle} />
|
|
2194
|
+
<Tooltip mode="x" />
|
|
2195
|
+
<DataTable />
|
|
2196
|
+
</Chart>
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
```
|
|
2200
|
+
|
|
2201
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2202
|
+
|
|
2203
|
+
##### A week as a table
|
|
2204
|
+
|
|
2205
|
+
```tsx
|
|
2206
|
+
/* A week of a kiln, a reading a minute: 10,080 rows, and no reader wants
|
|
2207
|
+
them all. Above 500 rows the table lists the course the way the line is
|
|
2208
|
+
drawn - the first, lowest, highest and last value of each stretch of the
|
|
2209
|
+
week - and its caption says how many readings the rows stand for. Zoom in
|
|
2210
|
+
(Ctrl or ⌘ with the wheel) and the table lists every reading again.
|
|
2211
|
+
|
|
2212
|
+
Without a legend the key stands on a line of its own. */
|
|
2213
|
+
|
|
2214
|
+
import { useState } from "react";
|
|
2215
|
+
import { Chart, DataTable, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2216
|
+
import { kilnData, type KilnPoint } from "../../data";
|
|
2217
|
+
|
|
2218
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2219
|
+
example can be copied whole. */
|
|
2220
|
+
|
|
2221
|
+
const celsius = (v: number) => `${v.toFixed(0)} °C`;
|
|
2222
|
+
|
|
2223
|
+
export default function WeekAsTable() {
|
|
2224
|
+
const [domain, setDomain] = useState<"data" | readonly [number, number]>("data");
|
|
2225
|
+
return (
|
|
2226
|
+
<Chart data={kilnData} height={240} ariaLabel="Kiln temperature across a week">
|
|
2227
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} label="Time" />
|
|
2228
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" />
|
|
2229
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Kiln" format={celsius} />
|
|
2230
|
+
<Tooltip mode="x" />
|
|
2231
|
+
<DataTable />
|
|
2232
|
+
</Chart>
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
```
|
|
2236
|
+
|
|
2237
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2238
|
+
|
|
2239
|
+
#### API
|
|
2240
|
+
|
|
2241
|
+
##### `TooltipProps<T>`
|
|
2242
|
+
|
|
2243
|
+
| Prop | Type | Default | Description |
|
|
2244
|
+
|---|---|---|---|
|
|
2245
|
+
| `mode` | `"x" \| "nearest"` | — | "x": every series at the x position; "nearest": only the nearest point. |
|
|
2246
|
+
| `render` | `(hit: TooltipHit<T>) => ReactNode` | — | Render prop for content of one's own; without it the built-in tooltip applies. Compared by its source text, like `tickFormat` - with the same closure limit. |
|
|
2247
|
+
|
|
2248
|
+
##### `LegendProps`
|
|
2249
|
+
|
|
2250
|
+
| Prop | Type | Default | Description |
|
|
2251
|
+
|---|---|---|---|
|
|
2252
|
+
| `placement` | `"top" \| "bottom"` | `"top"` | Which side of the plot area the legend stands on. Without a value, above it. |
|
|
2253
|
+
| `onToggle` | `(name: string) => void` | — | Called with an entry's name when it is clicked - the series' name, or a state's label. The caller flips `hidden` on the series it means. Without it the legend is not clickable. |
|
|
2254
|
+
|
|
2255
|
+
#### Why it is like this
|
|
2256
|
+
|
|
2257
|
+
##### Every series is searched in its own axis space
|
|
2258
|
+
|
|
2259
|
+
A chart here can hold several x axes, and a series knows only its own. So the hit is not one search over „the data“ but one binary search per series, over its own materialised x values, in the domain units of its own axis. The candidates are then compared in pixels - the one space all axes share. Comparing domain values across two axes would be comparing hours with pieces.
|
|
2260
|
+
|
|
2261
|
+
##### The crosshair snaps to a point
|
|
2262
|
+
|
|
2263
|
+
The crosshair stands on the data point that was hit, not under the pointer. A line between two samples is drawn, but nothing was measured there, and a crosshair between two points would invite reading off a value nobody has. In mode `"x"` the tooltip lists every series at that position; in mode `"nearest"` only the one point nearest the pointer, which is the question to ask when curves cross.
|
|
2264
|
+
|
|
2265
|
+
### Benchmark
|
|
2266
|
+
|
|
2267
|
+
Millions of points, measured instead of claimed - and the proof that a hover draws the overlay layer and nothing else.
|
|
2268
|
+
|
|
2269
|
+
```ts
|
|
2270
|
+
import { Chart } from "@umriss-ui/charts";
|
|
2271
|
+
```
|
|
2272
|
+
|
|
2273
|
+
Demo page: https://romanhaendler.github.io/umriss-ui/charts/#/benchmark
|
|
2274
|
+
|
|
2275
|
+
#### Examples
|
|
2276
|
+
|
|
2277
|
+
##### Benchmark
|
|
2278
|
+
|
|
2279
|
+
```tsx
|
|
2280
|
+
/* Benchmark example (R-5.1 to R-5.3).
|
|
2281
|
+
|
|
2282
|
+
Three series; lines and the area downsample on their own above two points
|
|
2283
|
+
per pixel column (charts-long-series 03). A hover draws the overlay layer
|
|
2284
|
+
exclusively - the FPS display proves it. Switchable between three lines and a
|
|
2285
|
+
mixed set of a bar, an area and a line.
|
|
2286
|
+
|
|
2287
|
+
Excluded from the screenshot comparisons (R-5.1) - it measures the moment it
|
|
2288
|
+
exists, so a picture of it would compare two different runs.
|
|
2289
|
+
Shows: the time of materialisation, the time of the last series draw and an FPS
|
|
2290
|
+
counter that runs while the mouse moves over the plot area.
|
|
2291
|
+
Live mode appends 10 points/s and shifts the data window along - the axis width
|
|
2292
|
+
has to stay calm through the hysteresis (R-5.3). */
|
|
2293
|
+
|
|
2294
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2295
|
+
import { Area, Bar, Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2296
|
+
import type { ChartPerf } from "@umriss-ui/charts";
|
|
2297
|
+
import { load, random, type LoadPoint } from "../../data";
|
|
2298
|
+
|
|
2299
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2300
|
+
example can be copied whole. */
|
|
2301
|
+
|
|
2302
|
+
const SIZES = [1_000, 100_000, 1_000_000] as const;
|
|
2303
|
+
const WINDOW = 400;
|
|
2304
|
+
|
|
2305
|
+
/** Series kinds of the measuring run. A filled mark and a rectangle per point are
|
|
2306
|
+
different work from a stroked path - the measurements in the capability record
|
|
2307
|
+
must not hold for lines only. */
|
|
2308
|
+
type Mode = "lines" | "mixed";
|
|
2309
|
+
|
|
2310
|
+
export default function Benchmark() {
|
|
2311
|
+
const [data, setData] = useState<readonly LoadPoint[]>(() => load(5, 1_000));
|
|
2312
|
+
const [perf, setPerf] = useState<ChartPerf>({
|
|
2313
|
+
materializeMs: 0,
|
|
2314
|
+
seriesDrawMs: 0,
|
|
2315
|
+
points: 0,
|
|
2316
|
+
});
|
|
2317
|
+
const [fps, setFps] = useState<number | null>(null);
|
|
2318
|
+
const [live, setLive] = useState(false);
|
|
2319
|
+
const [mode, setMode] = useState<Mode>("lines");
|
|
2320
|
+
|
|
2321
|
+
const onPerf = useCallback((p: ChartPerf) => setPerf(p), []);
|
|
2322
|
+
|
|
2323
|
+
/* FPS measurement: runs only while the pointer is over the plot area. What is
|
|
2324
|
+
measured is the actual frame rate of the page - if it drops, the series layer
|
|
2325
|
+
is drawing along (which it must not do on a hover). */
|
|
2326
|
+
const measuring = useRef<number | null>(null);
|
|
2327
|
+
const startMeasuring = useCallback(() => {
|
|
2328
|
+
if (measuring.current !== null) return;
|
|
2329
|
+
let frames = 0;
|
|
2330
|
+
let start = performance.now();
|
|
2331
|
+
const step = () => {
|
|
2332
|
+
frames++;
|
|
2333
|
+
const now = performance.now();
|
|
2334
|
+
if (now - start >= 400) {
|
|
2335
|
+
setFps(Math.round((frames * 1000) / (now - start)));
|
|
2336
|
+
frames = 0;
|
|
2337
|
+
start = now;
|
|
2338
|
+
}
|
|
2339
|
+
measuring.current = requestAnimationFrame(step);
|
|
2340
|
+
};
|
|
2341
|
+
measuring.current = requestAnimationFrame(step);
|
|
2342
|
+
}, []);
|
|
2343
|
+
const stopMeasuring = useCallback(() => {
|
|
2344
|
+
if (measuring.current !== null) cancelAnimationFrame(measuring.current);
|
|
2345
|
+
measuring.current = null;
|
|
2346
|
+
setFps(null);
|
|
2347
|
+
}, []);
|
|
2348
|
+
useEffect(() => stopMeasuring, [stopMeasuring]);
|
|
2349
|
+
|
|
2350
|
+
/* Streaming test (R-5.3): 10 points/s, a travelling window. */
|
|
2351
|
+
useEffect(() => {
|
|
2352
|
+
if (!live) return;
|
|
2353
|
+
const r = random(4711);
|
|
2354
|
+
const timer = window.setInterval(() => {
|
|
2355
|
+
setData((previous) => {
|
|
2356
|
+
const last = previous[previous.length - 1];
|
|
2357
|
+
if (last === undefined) return previous;
|
|
2358
|
+
const next: LoadPoint = {
|
|
2359
|
+
t: last.t + 1,
|
|
2360
|
+
s1: last.s1 + (r() - 0.5) * 3,
|
|
2361
|
+
s2: last.s2 + (r() - 0.5) * 2.4,
|
|
2362
|
+
s3: last.s3 + (r() - 0.5) * 3.6,
|
|
2363
|
+
};
|
|
2364
|
+
const grown = [...previous, next];
|
|
2365
|
+
return grown.length > WINDOW ? grown.slice(grown.length - WINDOW) : grown;
|
|
2366
|
+
});
|
|
2367
|
+
}, 100);
|
|
2368
|
+
return () => window.clearInterval(timer);
|
|
2369
|
+
}, [live]);
|
|
2370
|
+
|
|
2371
|
+
const loadPoints = (n: number) => {
|
|
2372
|
+
setLive(false);
|
|
2373
|
+
setData(load(5, n));
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2376
|
+
return (
|
|
2377
|
+
<>
|
|
2378
|
+
<div className="demo-actions">
|
|
2379
|
+
{SIZES.map((n) => (
|
|
2380
|
+
<button key={n} className="demo-button" onClick={() => loadPoints(n)}>
|
|
2381
|
+
{n.toLocaleString("en-US")} points
|
|
2382
|
+
</button>
|
|
2383
|
+
))}
|
|
2384
|
+
<button className="demo-button" onClick={() => setLive((v) => !v)}>{live ? "Stop live" : "Live"}</button>
|
|
2385
|
+
<button
|
|
2386
|
+
className="demo-button"
|
|
2387
|
+
data-role="mode"
|
|
2388
|
+
onClick={() => setMode((m) => (m === "lines" ? "mixed" : "lines"))}
|
|
2389
|
+
>
|
|
2390
|
+
{mode === "lines" ? "Mixed" : "Lines only"}
|
|
2391
|
+
</button>
|
|
2392
|
+
</div>
|
|
2393
|
+
<dl className="metrics">
|
|
2394
|
+
<div>
|
|
2395
|
+
<dt>Points</dt>
|
|
2396
|
+
<dd>{perf.points.toLocaleString("en-US")}</dd>
|
|
2397
|
+
</div>
|
|
2398
|
+
<div>
|
|
2399
|
+
<dt>Materialisation</dt>
|
|
2400
|
+
<dd>{perf.materializeMs.toFixed(1)} ms</dd>
|
|
2401
|
+
</div>
|
|
2402
|
+
<div>
|
|
2403
|
+
<dt>Series draw</dt>
|
|
2404
|
+
<dd>{perf.seriesDrawMs.toFixed(1)} ms</dd>
|
|
2405
|
+
</div>
|
|
2406
|
+
<div>
|
|
2407
|
+
<dt>FPS (hover)</dt>
|
|
2408
|
+
<dd>{fps === null ? "–" : fps}</dd>
|
|
2409
|
+
</div>
|
|
2410
|
+
</dl>
|
|
2411
|
+
<div onPointerEnter={startMeasuring} onPointerLeave={stopMeasuring}>
|
|
2412
|
+
<Chart
|
|
2413
|
+
data={data}
|
|
2414
|
+
height={320}
|
|
2415
|
+
ariaLabel="Benchmark with three series"
|
|
2416
|
+
onPerf={onPerf}
|
|
2417
|
+
>
|
|
2418
|
+
<XAxis accessor={(d: LoadPoint) => d.t} label="Index" />
|
|
2419
|
+
<YAxis accessor={(d: LoadPoint) => d.s1} />
|
|
2420
|
+
{mode === "lines" ? (
|
|
2421
|
+
<Line accessor={(d: LoadPoint) => d.s1} name="S1" />
|
|
2422
|
+
) : (
|
|
2423
|
+
<Bar accessor={(d: LoadPoint) => d.s1} name="S1" />
|
|
2424
|
+
)}
|
|
2425
|
+
{mode === "lines" ? (
|
|
2426
|
+
<Line accessor={(d: LoadPoint) => d.s2} name="S2" />
|
|
2427
|
+
) : (
|
|
2428
|
+
<Area accessor={(d: LoadPoint) => d.s2} name="S2" />
|
|
2429
|
+
)}
|
|
2430
|
+
<Line accessor={(d: LoadPoint) => d.s3} name="S3" />
|
|
2431
|
+
<Legend placement="top" />
|
|
2432
|
+
<Tooltip mode="x" />
|
|
2433
|
+
</Chart>
|
|
2434
|
+
</div>
|
|
2435
|
+
</>
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
```
|
|
2439
|
+
|
|
2440
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2441
|
+
|
|
2442
|
+
##### A week of seconds
|
|
2443
|
+
|
|
2444
|
+
```tsx
|
|
2445
|
+
/* A week of a kiln at one reading a second: 604,800 points per series, two
|
|
2446
|
+
series. Lines and areas downsample on their own above two points per pixel
|
|
2447
|
+
column: each column draws where the course entered it, its lowest and its
|
|
2448
|
+
highest reading and where it left - a spike stays a spike -, so the week
|
|
2449
|
+
costs a path of a few thousand points instead of 1.2 million. Zoom in with
|
|
2450
|
+
Ctrl or ⌘ and the wheel: from about half an hour on, the chart draws every
|
|
2451
|
+
reading again. The tooltip always reads the raw data.
|
|
2452
|
+
|
|
2453
|
+
It measures, like the benchmark above it - the time of the last series
|
|
2454
|
+
draw -, and is therefore not photographed either (R-5.1). */
|
|
2455
|
+
|
|
2456
|
+
import { useCallback, useState } from "react";
|
|
2457
|
+
import { Chart, Legend, Line, Tooltip, XAxis, YAxis } from "@umriss-ui/charts";
|
|
2458
|
+
import type { ChartPerf } from "@umriss-ui/charts";
|
|
2459
|
+
import { kiln, type KilnPoint } from "../../data";
|
|
2460
|
+
|
|
2461
|
+
/* The plant this is drawn from stands in the second tab, so that the
|
|
2462
|
+
example can be copied whole. */
|
|
2463
|
+
|
|
2464
|
+
export default function WeekOfSeconds() {
|
|
2465
|
+
// Made on first render, not on import: 604,800 readings are no page's
|
|
2466
|
+
// business until this one is open.
|
|
2467
|
+
const [data] = useState(() => kiln(2024, 1000));
|
|
2468
|
+
const [domain, setDomain] = useState<"data" | readonly [number, number]>("data");
|
|
2469
|
+
const [drawMs, setDrawMs] = useState(0);
|
|
2470
|
+
const onPerf = useCallback((p: ChartPerf) => setDrawMs(p.seriesDrawMs), []);
|
|
2471
|
+
return (
|
|
2472
|
+
<>
|
|
2473
|
+
<dl className="metrics">
|
|
2474
|
+
<div>
|
|
2475
|
+
<dt>Points</dt>
|
|
2476
|
+
<dd>{(2 * data.length).toLocaleString("en-US")}</dd>
|
|
2477
|
+
</div>
|
|
2478
|
+
<div>
|
|
2479
|
+
<dt>Series draw</dt>
|
|
2480
|
+
<dd>{drawMs.toFixed(1)} ms</dd>
|
|
2481
|
+
</div>
|
|
2482
|
+
</dl>
|
|
2483
|
+
<Chart data={data} height={300} ariaLabel="Kiln temperature and gas flow across a week, one reading a second" onPerf={onPerf}>
|
|
2484
|
+
<XAxis accessor={(d: KilnPoint) => d.t} time domain={domain} onDomainChange={setDomain} />
|
|
2485
|
+
<YAxis accessor={(d: KilnPoint) => d.temperature} label="°C" domain="visible" />
|
|
2486
|
+
<YAxis id="gas" position="right" accessor={(d: KilnPoint) => d.gas} label="m³/h" domain="visible" />
|
|
2487
|
+
<Line accessor={(d: KilnPoint) => d.temperature} name="Temperature" />
|
|
2488
|
+
<Line accessor={(d: KilnPoint) => d.gas} yAxisId="gas" name="Gas" />
|
|
2489
|
+
<Legend placement="top" />
|
|
2490
|
+
<Tooltip mode="x" />
|
|
2491
|
+
</Chart>
|
|
2492
|
+
</>
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
```
|
|
2496
|
+
|
|
2497
|
+
It imports `data.ts` from beside itself; the file stands once, under "Files the examples show" at the end.
|
|
2498
|
+
|
|
2499
|
+
## The rest of the API
|
|
2500
|
+
|
|
2501
|
+
Exported as well, and named on no page above: the pure modules behind the components, and the types they are made of. Each with its declaration as the package's `.d.ts` carries it.
|
|
2502
|
+
|
|
2503
|
+
### `invalidateTheme`
|
|
2504
|
+
|
|
2505
|
+
```ts
|
|
2506
|
+
/** Explicit invalidation: devalue the cache, every living chart reads anew and
|
|
2507
|
+
redraws. */
|
|
2508
|
+
function invalidateTheme(): void;
|
|
2509
|
+
```
|
|
2510
|
+
|
|
2511
|
+
### `ResolvedTheme`
|
|
2512
|
+
|
|
2513
|
+
```ts
|
|
2514
|
+
interface ResolvedTheme {
|
|
2515
|
+
colorAxis: string;
|
|
2516
|
+
colorGrid: string;
|
|
2517
|
+
colorText: string;
|
|
2518
|
+
colorBg: string;
|
|
2519
|
+
/** Colours of the two limit severities. A limit carries the colour of its
|
|
2520
|
+
severity, not that of its series - it belongs to none. */
|
|
2521
|
+
colorWarning: string;
|
|
2522
|
+
colorAlarm: string;
|
|
2523
|
+
/** The third outcome of an assessment. "unknown" deliberately has no
|
|
2524
|
+
colour: a missing value becomes a hole like every gap in this library, and
|
|
2525
|
+
a hole carries no colour, because a colour would be a claim. */
|
|
2526
|
+
colorOk: string;
|
|
2527
|
+
font: string;
|
|
2528
|
+
fontMono: string;
|
|
2529
|
+
series: readonly string[];
|
|
2530
|
+
/** The page is in forced colours: every colour above is a system colour, and
|
|
2531
|
+
the chart encodes by marks whatever its `encoding`. */
|
|
2532
|
+
forced: boolean;
|
|
2533
|
+
}
|
|
2534
|
+
```
|
|
2535
|
+
|
|
2536
|
+
### `LinearScale`
|
|
2537
|
+
|
|
2538
|
+
```ts
|
|
2539
|
+
class LinearScale implements Scale {
|
|
2540
|
+
readonly domain: readonly [number, number];
|
|
2541
|
+
readonly range: readonly [number, number];
|
|
2542
|
+
/** Pre-computed line equation px = v·m + b - the draw loop uses it directly. */
|
|
2543
|
+
readonly m: number;
|
|
2544
|
+
readonly b: number;
|
|
2545
|
+
constructor(domain: readonly [number, number], range: readonly [number, number]);
|
|
2546
|
+
toPx(v: number): number;
|
|
2547
|
+
fromPx(px: number): number;
|
|
2548
|
+
ticks(n: number): number[];
|
|
2549
|
+
}
|
|
2550
|
+
```
|
|
2551
|
+
|
|
2552
|
+
### `assess`
|
|
2553
|
+
|
|
2554
|
+
```ts
|
|
2555
|
+
/** What the value is, given the limit set. A pure function of the two.
|
|
2556
|
+
|
|
2557
|
+
A missing or non-finite value yields `unknown` - and nothing else: the
|
|
2558
|
+
deviation of a value nobody has is unknown to everybody too.
|
|
2559
|
+
|
|
2560
|
+
A bound is violated when the value lies beyond it, not when it lies on it. A
|
|
2561
|
+
value exactly on the upper bound is in order. That is decided once here
|
|
2562
|
+
(ADR-0006), so that two building blocks on the same screen do not disagree
|
|
2563
|
+
about the edge.
|
|
2564
|
+
|
|
2565
|
+
Where several bounds are violated the most severe wins; among equally severe
|
|
2566
|
+
ones the one the value lies furthest beyond. Under a tie in both, the one
|
|
2567
|
+
named first.
|
|
2568
|
+
|
|
2569
|
+
No hysteresis: that would need the previous state, and a function of one
|
|
2570
|
+
value has none. Exactly that is the property which makes this module safe to
|
|
2571
|
+
duplicate. */
|
|
2572
|
+
function assess(value: number | null | undefined, set?: LimitSet): Assessment;
|
|
2573
|
+
```
|
|
2574
|
+
|
|
2575
|
+
### `verdictWeight`
|
|
2576
|
+
|
|
2577
|
+
```ts
|
|
2578
|
+
/** How heavily a verdict weighs, as a number - for sorting and comparing, as
|
|
2579
|
+
when a matrix is to bring its worst cell to the top. It stands here so that
|
|
2580
|
+
the order is not reinvented at every call site. */
|
|
2581
|
+
function verdictWeight(verdict: Verdict): number;
|
|
2582
|
+
```
|
|
2583
|
+
|
|
2584
|
+
### `Assessment`
|
|
2585
|
+
|
|
2586
|
+
```ts
|
|
2587
|
+
/** The verdict and what led to it. */
|
|
2588
|
+
interface Assessment {
|
|
2589
|
+
verdict: Verdict;
|
|
2590
|
+
/** The violated limit - the same object the caller passed in, so that it can
|
|
2591
|
+
be recognised against the caller's own data. Absent where none is
|
|
2592
|
+
violated. */
|
|
2593
|
+
limit?: Limit;
|
|
2594
|
+
/** The amount beyond the bound, always positive. Absent where none is
|
|
2595
|
+
violated. */
|
|
2596
|
+
excess?: number;
|
|
2597
|
+
/** The signed distance from the target. Absent where none is set - a missing
|
|
2598
|
+
target yields no deviation of zero.
|
|
2599
|
+
|
|
2600
|
+
Two fields and not one: excess and deviation are two numbers, and a single
|
|
2601
|
+
field meaning the one thing when a bound is violated and the other
|
|
2602
|
+
otherwise reads well and is wrong at three in the morning. */
|
|
2603
|
+
deviation?: number;
|
|
2604
|
+
}
|
|
2605
|
+
```
|
|
2606
|
+
|
|
2607
|
+
### `Limit`
|
|
2608
|
+
|
|
2609
|
+
```ts
|
|
2610
|
+
/** A bound: a value, a side and a severity. No more - a tolerance band is
|
|
2611
|
+
derived and never an input (two limits of equal severity on opposite sides
|
|
2612
|
+
enclose one). That keeps the one-sided case, which is the more common one,
|
|
2613
|
+
free of a `null` on the other edge. */
|
|
2614
|
+
interface Limit {
|
|
2615
|
+
value: number;
|
|
2616
|
+
side: Side;
|
|
2617
|
+
severity: Severity;
|
|
2618
|
+
}
|
|
2619
|
+
```
|
|
2620
|
+
|
|
2621
|
+
### `LimitSet`
|
|
2622
|
+
|
|
2623
|
+
```ts
|
|
2624
|
+
/** The set a value is read against: its bounds and, where there is one, its
|
|
2625
|
+
target.
|
|
2626
|
+
|
|
2627
|
+
The target is not a limit. It is missed, never violated, and it is never the
|
|
2628
|
+
edge of a region. Conflating the two is how a tile turns red for being above
|
|
2629
|
+
target. */
|
|
2630
|
+
interface LimitSet {
|
|
2631
|
+
/** In any order; none, one or several. */
|
|
2632
|
+
limits?: readonly Limit[];
|
|
2633
|
+
target?: number;
|
|
2634
|
+
}
|
|
2635
|
+
```
|
|
2636
|
+
|
|
2637
|
+
### `Side`
|
|
2638
|
+
|
|
2639
|
+
```ts
|
|
2640
|
+
/** Whether a limit is an upper or a lower bound. */
|
|
2641
|
+
type Side = "upper" | "lower";
|
|
2642
|
+
```
|
|
2643
|
+
|
|
2644
|
+
### `Verdict`
|
|
2645
|
+
|
|
2646
|
+
```ts
|
|
2647
|
+
/** What a value is. Four outcomes, not three.
|
|
2648
|
+
|
|
2649
|
+
`unknown` is an outcome of its own and not a `null` return: a `null`
|
|
2650
|
+
invites `?? "ok"` at the call site, and that very expression is the defect
|
|
2651
|
+
this module exists to prevent. A value nobody has is a reason to look and
|
|
2652
|
+
not a reason to relax. */
|
|
2653
|
+
type Verdict = "ok" | "unknown" | "warning" | "alarm";
|
|
2654
|
+
```
|
|
2655
|
+
|
|
2656
|
+
### `sigmaFromLimit`
|
|
2657
|
+
|
|
2658
|
+
```ts
|
|
2659
|
+
/** Sigma calculated back from a limit that stands on paper. For callers whose
|
|
2660
|
+
baseline has been handed down as a centre line and a control limit. */
|
|
2661
|
+
function sigmaFromLimit(center: number, upper: number): number;
|
|
2662
|
+
```
|
|
2663
|
+
|
|
2664
|
+
### `zones`
|
|
2665
|
+
|
|
2666
|
+
```ts
|
|
2667
|
+
/** The faintly drawn zone lines. Rule 4 is about the two-sigma zone, and a
|
|
2668
|
+
reader cannot check it against a chart that does not show it. */
|
|
2669
|
+
function zones(limits: ControlLimits, multiples?: readonly number[]): Zone[];
|
|
2670
|
+
```
|
|
2671
|
+
|
|
2672
|
+
### `ruleOutlier`
|
|
2673
|
+
|
|
2674
|
+
```ts
|
|
2675
|
+
/** Rule 1 (Nelson 1, Western Electric 1): one point beyond three sigma.
|
|
2676
|
+
Beyond, not on it - a point exactly on the limit violates nothing. */
|
|
2677
|
+
function ruleOutlier(values: readonly number[], limits: ControlLimits): number[];
|
|
2678
|
+
```
|
|
2679
|
+
|
|
2680
|
+
### `ruleRun`
|
|
2681
|
+
|
|
2682
|
+
```ts
|
|
2683
|
+
/** Rule 2 (Nelson 2): a run of points on one side of the centre line. The
|
|
2684
|
+
default length is nine - Nelson. Western Electric counts eight; that is why
|
|
2685
|
+
the length is a parameter and not a literal, so that a works with a rule of
|
|
2686
|
+
its own need not argue with the library. */
|
|
2687
|
+
function ruleRun(values: readonly number[], limits: ControlLimits, length?: number): number[];
|
|
2688
|
+
```
|
|
2689
|
+
|
|
2690
|
+
### `ruleTrend`
|
|
2691
|
+
|
|
2692
|
+
```ts
|
|
2693
|
+
/** Rule 3 (Nelson 3): a run of nothing but rises or nothing but falls. The
|
|
2694
|
+
default length is six points, so five steps - Nelson. Two equal values are
|
|
2695
|
+
neither a rise nor a fall and break the run. */
|
|
2696
|
+
function ruleTrend(values: readonly number[], length?: number): number[];
|
|
2697
|
+
```
|
|
2698
|
+
|
|
2699
|
+
### `ruleTwoOfThree`
|
|
2700
|
+
|
|
2701
|
+
```ts
|
|
2702
|
+
/** Rule 4 (Nelson 5): two of three consecutive points beyond two sigma on the
|
|
2703
|
+
same side. Reported are the points beyond the zone, not the one in between -
|
|
2704
|
+
that one is unremarkable and, marked, would look like a measurement error. */
|
|
2705
|
+
function ruleTwoOfThree(values: readonly number[], limits: ControlLimits): number[];
|
|
2706
|
+
```
|
|
2707
|
+
|
|
2708
|
+
### `violatedIndices`
|
|
2709
|
+
|
|
2710
|
+
```ts
|
|
2711
|
+
/** All violated indices, ascending and free of duplicates - the scatter drawn
|
|
2712
|
+
beside the line. */
|
|
2713
|
+
function violatedIndices(found: readonly Violation[]): number[];
|
|
2714
|
+
```
|
|
2715
|
+
|
|
2716
|
+
### `defaultRules`
|
|
2717
|
+
|
|
2718
|
+
```ts
|
|
2719
|
+
/** All four rules, with Nelson's run lengths: nine points on one side, six
|
|
2720
|
+
points in succession rising or falling. Western Electric counts eight; both
|
|
2721
|
+
lengths are therefore parameters. */
|
|
2722
|
+
const defaultRules: RuleOptions;
|
|
2723
|
+
```
|
|
2724
|
+
|
|
2725
|
+
### `ControlLimits`
|
|
2726
|
+
|
|
2727
|
+
```ts
|
|
2728
|
+
/** Centre line, estimated spread and the control limits at three sigma. */
|
|
2729
|
+
interface ControlLimits {
|
|
2730
|
+
readonly center: number;
|
|
2731
|
+
/** Estimated spread of a single value, not the distance to the limit. */
|
|
2732
|
+
readonly sigma: number;
|
|
2733
|
+
readonly upper: number;
|
|
2734
|
+
readonly lower: number;
|
|
2735
|
+
}
|
|
2736
|
+
```
|
|
2737
|
+
|
|
2738
|
+
### `RuleName`
|
|
2739
|
+
|
|
2740
|
+
```ts
|
|
2741
|
+
/** Name of a rule, the way it is switched on and off in `violations`. */
|
|
2742
|
+
type RuleName = "outlier" | "run" | "trend" | "twoOfThree";
|
|
2743
|
+
```
|
|
2744
|
+
|
|
2745
|
+
### `Zone`
|
|
2746
|
+
|
|
2747
|
+
```ts
|
|
2748
|
+
/** A zone line: k times sigma above and below the centre line. */
|
|
2749
|
+
interface Zone {
|
|
2750
|
+
readonly k: number;
|
|
2751
|
+
readonly upper: number;
|
|
2752
|
+
readonly lower: number;
|
|
2753
|
+
}
|
|
2754
|
+
```
|
|
2755
|
+
|
|
2756
|
+
### `paretoDefaults`
|
|
2757
|
+
|
|
2758
|
+
```ts
|
|
2759
|
+
const defaultOptions: Omit<ParetoOptions, "remainderName">;
|
|
2760
|
+
// exported as paretoDefaults
|
|
2761
|
+
```
|
|
2762
|
+
|
|
2763
|
+
### `ParetoSettings`
|
|
2764
|
+
|
|
2765
|
+
```ts
|
|
2766
|
+
/** What a call may specify: a collect rank only together with the name of the
|
|
2767
|
+
remainder it creates. */
|
|
2768
|
+
type ParetoSettings = {
|
|
2769
|
+
readonly cutoff?: number;
|
|
2770
|
+
} & ({
|
|
2771
|
+
readonly collectRank?: undefined;
|
|
2772
|
+
readonly remainderName?: undefined;
|
|
2773
|
+
} | {
|
|
2774
|
+
readonly collectRank: number;
|
|
2775
|
+
readonly remainderName: string;
|
|
2776
|
+
});
|
|
2777
|
+
```
|
|
2778
|
+
|
|
2779
|
+
### `ParetoItem`
|
|
2780
|
+
|
|
2781
|
+
```ts
|
|
2782
|
+
/** An item, the way the caller counts it. */
|
|
2783
|
+
interface ParetoItem {
|
|
2784
|
+
readonly name: string;
|
|
2785
|
+
readonly value: number;
|
|
2786
|
+
}
|
|
2787
|
+
```
|
|
2788
|
+
|
|
2789
|
+
### `ParetoOptions`
|
|
2790
|
+
|
|
2791
|
+
```ts
|
|
2792
|
+
interface ParetoOptions {
|
|
2793
|
+
/** The share whose crossing is sought. Whoever lands exactly on it counts as
|
|
2794
|
+
the crossing: the question is "which reasons make up eighty per cent", and
|
|
2795
|
+
whoever reaches eighty exactly belongs to them. */
|
|
2796
|
+
readonly cutoff: number;
|
|
2797
|
+
/** How many items stay on their own. Everything behind that becomes a single
|
|
2798
|
+
entry. Beyond the number of items no remainder arises at all - not an empty
|
|
2799
|
+
one. Default: infinity, so no collecting. */
|
|
2800
|
+
readonly collectRank: number;
|
|
2801
|
+
/** Name of the collected remainder. `@umriss-ui/charts` has no wording module
|
|
2802
|
+
- every string it draws comes from the caller - and no module is introduced
|
|
2803
|
+
for a single label. Hence no default either: until library-audit 03 the
|
|
2804
|
+
default here was "Sonstige", the one German word the package brought along
|
|
2805
|
+
itself. */
|
|
2806
|
+
readonly remainderName?: string;
|
|
2807
|
+
}
|
|
2808
|
+
```
|
|
2809
|
+
|
|
2810
|
+
### `ParetoResult`
|
|
2811
|
+
|
|
2812
|
+
```ts
|
|
2813
|
+
interface ParetoResult {
|
|
2814
|
+
readonly entries: readonly ParetoEntry[];
|
|
2815
|
+
/** Total of the drawn entries - after collecting, the same as before. */
|
|
2816
|
+
readonly total: number;
|
|
2817
|
+
/** Index of the first entry whose cumulative share reaches the cutoff; -1
|
|
2818
|
+
when it is never reached. */
|
|
2819
|
+
readonly cutoffIndex: number;
|
|
2820
|
+
}
|
|
2821
|
+
```
|
|
2822
|
+
|
|
2823
|
+
### `calendarFrom`
|
|
2824
|
+
|
|
2825
|
+
```ts
|
|
2826
|
+
/** Calendar from an input - a built calendar is passed straight through, a raw
|
|
2827
|
+
list is built once and remembered. */
|
|
2828
|
+
function calendarFrom(input: CalendarInput): OperatingCalendar;
|
|
2829
|
+
```
|
|
2830
|
+
|
|
2831
|
+
### `toOperatingTime`
|
|
2832
|
+
|
|
2833
|
+
```ts
|
|
2834
|
+
/** Wall clock time → operating time.
|
|
2835
|
+
|
|
2836
|
+
A point in a removed interval is a gap and becomes `NaN` - the encoding every
|
|
2837
|
+
gap of this library already uses, and the drawing loop breaks the mark at it
|
|
2838
|
+
of its own accord. Not the edge of the interval: a measurement from a time
|
|
2839
|
+
which, according to the calendar, did not exist could not be laid there
|
|
2840
|
+
honestly; it would pile up on what really does lie there. The same holds
|
|
2841
|
+
before the first and after the last interval.
|
|
2842
|
+
|
|
2843
|
+
The end of one interval and the beginning of the next fall on the same value:
|
|
2844
|
+
in operating time they are neighbours, and the line between two points on
|
|
2845
|
+
either side is drawn. That is not a lie but the whole purpose of the axis; the
|
|
2846
|
+
mark in the axis band tells the reader that time was taken out there. */
|
|
2847
|
+
function toOperatingTime(wallClock: number, input: CalendarInput): number;
|
|
2848
|
+
```
|
|
2849
|
+
|
|
2850
|
+
### `toWallClock`
|
|
2851
|
+
|
|
2852
|
+
```ts
|
|
2853
|
+
/** Operating time → wall clock time; outside [0, total] there is none.
|
|
2854
|
+
|
|
2855
|
+
At a seam - the end of an interval which is at the same time the beginning of
|
|
2856
|
+
the next - the beginning of the next comes out: from there time counts again,
|
|
2857
|
+
and a pointer aiming there aims at the coming shift. Only the end of the last
|
|
2858
|
+
interval has no next and comes back as itself. */
|
|
2859
|
+
function toWallClock(operatingTime: number, input: CalendarInput): number;
|
|
2860
|
+
```
|
|
2861
|
+
|
|
2862
|
+
### `mapSeries`
|
|
2863
|
+
|
|
2864
|
+
```ts
|
|
2865
|
+
/** Map a whole series inwards - the route materialisation takes when it wants to
|
|
2866
|
+
go it in one piece. Only the first n values are touched. */
|
|
2867
|
+
function mapSeries(input: CalendarInput, source: Float64Array, n: number, target: Float64Array): void;
|
|
2868
|
+
```
|
|
2869
|
+
|
|
2870
|
+
### `removedIntervals`
|
|
2871
|
+
|
|
2872
|
+
```ts
|
|
2873
|
+
/** The removed spans between the operating intervals, for the mark in the axis
|
|
2874
|
+
band. A chart that takes a weekend out and does not say so claims a
|
|
2875
|
+
continuity it does not have. Before the first and after the last interval
|
|
2876
|
+
there is nothing to mark: that lies outside the axis' extent. */
|
|
2877
|
+
function removedIntervals(input: CalendarInput): RemovedSpan[];
|
|
2878
|
+
```
|
|
2879
|
+
|
|
2880
|
+
### `breaks`
|
|
2881
|
+
|
|
2882
|
+
```ts
|
|
2883
|
+
/** The breaks in operating time that lie within the extent [from, to] - what the
|
|
2884
|
+
axis must mark, in domain units. */
|
|
2885
|
+
function breaks(input: CalendarInput, from: number, to: number): number[];
|
|
2886
|
+
```
|
|
2887
|
+
|
|
2888
|
+
### `operatingTicks`
|
|
2889
|
+
|
|
2890
|
+
```ts
|
|
2891
|
+
/** Tick values for an axis extent that already stands in OPERATING TIME - the
|
|
2892
|
+
route the axis layout takes.
|
|
2893
|
+
|
|
2894
|
+
The extent is trimmed to the operating time that exists (an extent widened to
|
|
2895
|
+
nice boundaries reaches beyond it), calculated back into wall clock time, and
|
|
2896
|
+
the candidates arise there. The step comes from the span in operating time and
|
|
2897
|
+
not from the one in wall clock time: roughly one candidate per step of
|
|
2898
|
+
operating time survives, and calculated from the wall clock span, four out of
|
|
2899
|
+
ten wanted ticks would be left. */
|
|
2900
|
+
function operatingTicks(input: CalendarInput, from: number, to: number, count: number, offset?: number): number[];
|
|
2901
|
+
```
|
|
2902
|
+
|
|
2903
|
+
### `operatingTimeTicks`
|
|
2904
|
+
|
|
2905
|
+
```ts
|
|
2906
|
+
/** Ticks for an operating time axis, from an extent in WALL CLOCK TIME.
|
|
2907
|
+
|
|
2908
|
+
The candidates arise in wall clock time on multiples of the step - hours,
|
|
2909
|
+
shift changes, days - whatever lies in removed time falls away, the rest is
|
|
2910
|
+
mapped. Never the other way round: ticks generated in operating time land in
|
|
2911
|
+
the middle of a shift and yield an axis labelled 14.5 and 29.0 which nobody
|
|
2912
|
+
can read.
|
|
2913
|
+
|
|
2914
|
+
The offset shifts the grid; with it a local time hits its own midnight without
|
|
2915
|
+
this module having to know about time zones.
|
|
2916
|
+
|
|
2917
|
+
If two candidates fall on the same operating time - the end of one shift and
|
|
2918
|
+
the beginning of the next - the earlier one stays: two labels on the same
|
|
2919
|
+
pixel would be one too many.
|
|
2920
|
+
|
|
2921
|
+
Both times come back, because the axis needs both: the operating time to
|
|
2922
|
+
place the tick, the wall clock time to label it. */
|
|
2923
|
+
function operatingTimeTicks(input: CalendarInput, from: number, to: number, step: number, offset?: number): OperatingTimeTick[];
|
|
2924
|
+
```
|
|
2925
|
+
|
|
2926
|
+
### `timeStep`
|
|
2927
|
+
|
|
2928
|
+
```ts
|
|
2929
|
+
/** Step for tick candidates in wall clock time, at an intended count. Rounded up
|
|
2930
|
+
to the next readable step, so that no more ticks arise than wanted. Above the
|
|
2931
|
+
day steps it continues in whole days. */
|
|
2932
|
+
function timeStep(span: number, count: number): number;
|
|
2933
|
+
```
|
|
2934
|
+
|
|
2935
|
+
### `SECOND`
|
|
2936
|
+
|
|
2937
|
+
```ts
|
|
2938
|
+
const SECOND = 1000;
|
|
2939
|
+
```
|
|
2940
|
+
|
|
2941
|
+
### `MINUTE`
|
|
2942
|
+
|
|
2943
|
+
```ts
|
|
2944
|
+
const MINUTE: number;
|
|
2945
|
+
```
|
|
2946
|
+
|
|
2947
|
+
### `DAY`
|
|
2948
|
+
|
|
2949
|
+
```ts
|
|
2950
|
+
const DAY: number;
|
|
2951
|
+
```
|
|
2952
|
+
|
|
2953
|
+
### `CalendarInput`
|
|
2954
|
+
|
|
2955
|
+
```ts
|
|
2956
|
+
/** Every function of this module takes either: the raw list as it stands on the
|
|
2957
|
+
axis, or the calendar built from it. */
|
|
2958
|
+
type CalendarInput = OperatingCalendar | readonly OperatingInterval[];
|
|
2959
|
+
```
|
|
2960
|
+
|
|
2961
|
+
### `OperatingCalendar`
|
|
2962
|
+
|
|
2963
|
+
```ts
|
|
2964
|
+
/** An operating calendar: sorted, merged intervals with the operating time
|
|
2965
|
+
accumulated before each one begins. An empty calendar is no calendar: it maps
|
|
2966
|
+
unchanged.
|
|
2967
|
+
|
|
2968
|
+
Build once, then pass around: the mapping runs once per point, and were it to
|
|
2969
|
+
sort and accumulate every time, the calendar would be the most expensive
|
|
2970
|
+
thing about the frame. */
|
|
2971
|
+
interface OperatingCalendar {
|
|
2972
|
+
readonly intervals: readonly OperatingInterval[];
|
|
2973
|
+
/** Operating time accumulated before the respective interval begins. */
|
|
2974
|
+
readonly offsets: readonly number[];
|
|
2975
|
+
/** The whole operating time - the axis' extent is [0, total]. */
|
|
2976
|
+
readonly total: number;
|
|
2977
|
+
}
|
|
2978
|
+
```
|
|
2979
|
+
|
|
2980
|
+
### `OperatingTimeTick`
|
|
2981
|
+
|
|
2982
|
+
```ts
|
|
2983
|
+
/** A tick: where it stands on the clock and where it lies on the axis. */
|
|
2984
|
+
interface OperatingTimeTick {
|
|
2985
|
+
readonly wallClock: number;
|
|
2986
|
+
readonly operatingTime: number;
|
|
2987
|
+
}
|
|
2988
|
+
```
|
|
2989
|
+
|
|
2990
|
+
### `RemovedSpan`
|
|
2991
|
+
|
|
2992
|
+
```ts
|
|
2993
|
+
/** A removed span and the place it collapses onto. */
|
|
2994
|
+
interface RemovedSpan {
|
|
2995
|
+
readonly from: number;
|
|
2996
|
+
readonly to: number;
|
|
2997
|
+
/** Operating time on which the whole span lies - a point, not a stretch. */
|
|
2998
|
+
readonly operatingTime: number;
|
|
2999
|
+
}
|
|
3000
|
+
```
|
|
3001
|
+
|
|
3002
|
+
### `AreaSeriesConfig`
|
|
3003
|
+
|
|
3004
|
+
```ts
|
|
3005
|
+
interface AreaSeriesConfig<T = unknown> extends SeriesBase<T>, Stackable {
|
|
3006
|
+
kind: "area";
|
|
3007
|
+
/** Lower edge; without a value the fixed baseline 0. */
|
|
3008
|
+
baseline?: Accessor<T>;
|
|
3009
|
+
/** Opacity of the fill; the outline stays fully opaque. */
|
|
3010
|
+
fillOpacity: number;
|
|
3011
|
+
strokeWidth: number;
|
|
3012
|
+
/** Dash pattern of the outline. */
|
|
3013
|
+
dash?: readonly number[];
|
|
3014
|
+
}
|
|
3015
|
+
```
|
|
3016
|
+
|
|
3017
|
+
### `LimitBandConfig`
|
|
3018
|
+
|
|
3019
|
+
```ts
|
|
3020
|
+
interface LimitBandConfig extends LimitBase {
|
|
3021
|
+
kind: "band";
|
|
3022
|
+
from: number;
|
|
3023
|
+
to: number;
|
|
3024
|
+
}
|
|
3025
|
+
```
|
|
3026
|
+
|
|
3027
|
+
### `LimitConfig`
|
|
3028
|
+
|
|
3029
|
+
```ts
|
|
3030
|
+
type LimitConfig = LimitLineConfig | LimitBandConfig;
|
|
3031
|
+
```
|
|
3032
|
+
|
|
3033
|
+
### `LimitLineConfig`
|
|
3034
|
+
|
|
3035
|
+
```ts
|
|
3036
|
+
interface LimitLineConfig extends LimitBase {
|
|
3037
|
+
kind: "line";
|
|
3038
|
+
value: number;
|
|
3039
|
+
}
|
|
3040
|
+
```
|
|
3041
|
+
|
|
3042
|
+
### `MatrixSeriesConfig`
|
|
3043
|
+
|
|
3044
|
+
```ts
|
|
3045
|
+
interface MatrixSeriesConfig<T = unknown> extends SeriesBase<T> {
|
|
3046
|
+
kind: "matrix";
|
|
3047
|
+
/** The value that decides the colour - the third channel (ADR-0011).
|
|
3048
|
+
The base accessor yields the row position, not this value. */
|
|
3049
|
+
value: Accessor<T>;
|
|
3050
|
+
coloring: MatrixColoring;
|
|
3051
|
+
}
|
|
3052
|
+
```
|
|
3053
|
+
|
|
3054
|
+
### `StateSeriesConfig`
|
|
3055
|
+
|
|
3056
|
+
```ts
|
|
3057
|
+
interface StateSeriesConfig<T = unknown> extends SeriesBase<T> {
|
|
3058
|
+
kind: "state";
|
|
3059
|
+
/** The closed set of states. The accessor yields the index into it - a number,
|
|
3060
|
+
not a key (ADR-0007). */
|
|
3061
|
+
states: readonly StateEntry[];
|
|
3062
|
+
/** Lower edge of the lane in domain units of the y axis; without a value the
|
|
3063
|
+
beginning of the axis domain. */
|
|
3064
|
+
laneFrom?: number;
|
|
3065
|
+
/** Upper edge of the lane; without a value the end of the axis domain. */
|
|
3066
|
+
laneTo?: number;
|
|
3067
|
+
}
|
|
3068
|
+
```
|
|
3069
|
+
|
|
3070
|
+
### `AxisConfig`
|
|
3071
|
+
|
|
3072
|
+
```ts
|
|
3073
|
+
interface AxisConfig<T = unknown> {
|
|
3074
|
+
id: string;
|
|
3075
|
+
orientation: AxisOrientation;
|
|
3076
|
+
position: AxisPosition;
|
|
3077
|
+
accessor: (d: T, index: number) => number;
|
|
3078
|
+
label?: string;
|
|
3079
|
+
tickCount?: number;
|
|
3080
|
+
tickFormat?: (v: number) => string;
|
|
3081
|
+
/** "visible" is a y axis' only: the extent of what its series show inside
|
|
3082
|
+
their x axis' domain. */
|
|
3083
|
+
domain: "nice" | "data" | "visible" | readonly [number, number];
|
|
3084
|
+
/** undefined = the default per R-4.15 (only the first registered axis per
|
|
3085
|
+
orientation). */
|
|
3086
|
+
grid?: boolean;
|
|
3087
|
+
/** Fixed tick values instead of the 1-2-5 algorithm. For axes whose values are
|
|
3088
|
+
not numbers but places: the lanes of a state stack, the categories of a
|
|
3089
|
+
Pareto. */
|
|
3090
|
+
ticks?: readonly number[];
|
|
3091
|
+
/** The values are instants in milliseconds: ticks on local boundaries,
|
|
3092
|
+
labels by level. A calendar implies it. */
|
|
3093
|
+
time?: boolean;
|
|
3094
|
+
/** Operating calendar: the intervals in which time counts. With it,
|
|
3095
|
+
materialisation maps wall clock time onto operating time before the scale
|
|
3096
|
+
calculates - the scale stays affine (ADR-0001). */
|
|
3097
|
+
calendar?: readonly OperatingInterval[];
|
|
3098
|
+
/** y axes only: this axis' ticks on the first y axis' grid. */
|
|
3099
|
+
alignTicks?: boolean;
|
|
3100
|
+
/** x axes only: where wheel, drag, pinch and double click propose a
|
|
3101
|
+
domain. Without it the axis does not zoom. */
|
|
3102
|
+
onDomainChange?: (domain: [number, number]) => void;
|
|
3103
|
+
}
|
|
3104
|
+
```
|
|
3105
|
+
|
|
3106
|
+
### `AxisOrientation`
|
|
3107
|
+
|
|
3108
|
+
```ts
|
|
3109
|
+
type AxisOrientation = "x" | "y";
|
|
3110
|
+
```
|
|
3111
|
+
|
|
3112
|
+
### `AxisPosition`
|
|
3113
|
+
|
|
3114
|
+
```ts
|
|
3115
|
+
type AxisPosition = "bottom" | "top" | "left" | "right";
|
|
3116
|
+
```
|
|
3117
|
+
|
|
3118
|
+
### `BarSeriesConfig`
|
|
3119
|
+
|
|
3120
|
+
```ts
|
|
3121
|
+
interface BarSeriesConfig<T = unknown> extends SeriesBase<T>, Stackable {
|
|
3122
|
+
kind: "bar";
|
|
3123
|
+
/** Width as a fraction of the step; the group shares this fraction. */
|
|
3124
|
+
barWidth: number;
|
|
3125
|
+
}
|
|
3126
|
+
```
|
|
3127
|
+
|
|
3128
|
+
### `LegendConfig`
|
|
3129
|
+
|
|
3130
|
+
```ts
|
|
3131
|
+
interface LegendConfig {
|
|
3132
|
+
placement: "top" | "bottom";
|
|
3133
|
+
}
|
|
3134
|
+
```
|
|
3135
|
+
|
|
3136
|
+
### `LineSeriesConfig`
|
|
3137
|
+
|
|
3138
|
+
```ts
|
|
3139
|
+
interface LineSeriesConfig<T = unknown> extends SeriesBase<T> {
|
|
3140
|
+
kind: "line";
|
|
3141
|
+
strokeWidth: number;
|
|
3142
|
+
dash?: readonly number[];
|
|
3143
|
+
markers: "auto" | "always" | "never";
|
|
3144
|
+
/** Sample-and-hold: each value holds until the next sample. */
|
|
3145
|
+
step?: boolean;
|
|
3146
|
+
}
|
|
3147
|
+
```
|
|
3148
|
+
|
|
3149
|
+
### `MaterializedSeries`
|
|
3150
|
+
|
|
3151
|
+
```ts
|
|
3152
|
+
/** Materialised series: the draw loop works only on this (R-2.7).
|
|
3153
|
+
Gaps (null/undefined/NaN/±Infinity out of the accessor) are encoded as NaN
|
|
3154
|
+
(R-2.5). */
|
|
3155
|
+
interface MaterializedSeries {
|
|
3156
|
+
x: Float64Array;
|
|
3157
|
+
y: Float64Array;
|
|
3158
|
+
/** Lower edge, where the baseline is an accessor of its own; otherwise null.
|
|
3159
|
+
A fixed baseline needs no channel - it is a number (R-2.7). */
|
|
3160
|
+
y0: Float64Array | null;
|
|
3161
|
+
/** Value channel: the third value per point, which today only the matrix
|
|
3162
|
+
needs. Named rather than overloading y0 - a property that only some kinds
|
|
3163
|
+
carry does not belong in the shared type with a comment (ADR-0011). null
|
|
3164
|
+
for every kind that does not use it. */
|
|
3165
|
+
w: Float64Array | null;
|
|
3166
|
+
length: number;
|
|
3167
|
+
}
|
|
3168
|
+
```
|
|
3169
|
+
|
|
3170
|
+
### `Rect`
|
|
3171
|
+
|
|
3172
|
+
```ts
|
|
3173
|
+
interface Rect {
|
|
3174
|
+
x: number;
|
|
3175
|
+
y: number;
|
|
3176
|
+
width: number;
|
|
3177
|
+
height: number;
|
|
3178
|
+
}
|
|
3179
|
+
```
|
|
3180
|
+
|
|
3181
|
+
### `Scale`
|
|
3182
|
+
|
|
3183
|
+
```ts
|
|
3184
|
+
interface Scale {
|
|
3185
|
+
domain: readonly [number, number];
|
|
3186
|
+
range: readonly [number, number];
|
|
3187
|
+
/** Affine coefficients of the mapping px = v·m + b.
|
|
3188
|
+
|
|
3189
|
+
They stand in the interface because the drawing code reads them and
|
|
3190
|
+
computes inline in the point loop instead of calling toPx() per point -
|
|
3191
|
+
that is what carries 60 fps at millions of points. Every scale of this
|
|
3192
|
+
library is affine; see docs/adr/0001-affine-scale-contract.md, which also
|
|
3193
|
+
says what a later log scale would cost. */
|
|
3194
|
+
readonly m: number;
|
|
3195
|
+
readonly b: number;
|
|
3196
|
+
toPx(v: number): number;
|
|
3197
|
+
fromPx(px: number): number;
|
|
3198
|
+
ticks(n: number): number[];
|
|
3199
|
+
}
|
|
3200
|
+
```
|
|
3201
|
+
|
|
3202
|
+
### `ScatterSeriesConfig`
|
|
3203
|
+
|
|
3204
|
+
```ts
|
|
3205
|
+
interface ScatterSeriesConfig<T = unknown> extends SeriesBase<T> {
|
|
3206
|
+
kind: "scatter";
|
|
3207
|
+
/** Point radius in CSS pixels. */
|
|
3208
|
+
radius: number;
|
|
3209
|
+
}
|
|
3210
|
+
```
|
|
3211
|
+
|
|
3212
|
+
### `SeriesBase`
|
|
3213
|
+
|
|
3214
|
+
```ts
|
|
3215
|
+
interface SeriesBase<T = unknown> {
|
|
3216
|
+
/** Y value; null/undefined/NaN/±Infinity means a gap (R-2.5). */
|
|
3217
|
+
accessor: Accessor<T>;
|
|
3218
|
+
/** Series-own data; overrides the container data (R-2.4). */
|
|
3219
|
+
data?: readonly T[];
|
|
3220
|
+
xAxisId: string;
|
|
3221
|
+
yAxisId: string;
|
|
3222
|
+
name?: string;
|
|
3223
|
+
/** The value in the tooltip; before the y axis' `tickFormat`. Compared by its
|
|
3224
|
+
source text, as an accessor is. */
|
|
3225
|
+
format?: (value: number) => string;
|
|
3226
|
+
color?: string;
|
|
3227
|
+
/** A role instead of a colour value: the theme resolves it, in both themes.
|
|
3228
|
+
For series that mean something rather than just being the next one - the
|
|
3229
|
+
violating points of a control chart, say. `color` beats it. */
|
|
3230
|
+
tone?: "ok" | "warning" | "alarm";
|
|
3231
|
+
/** Not drawn, not hit, not in its axes' extent; its legend entry stays. */
|
|
3232
|
+
hidden?: boolean;
|
|
3233
|
+
}
|
|
3234
|
+
```
|
|
3235
|
+
|
|
3236
|
+
### `SeriesConfig`
|
|
3237
|
+
|
|
3238
|
+
```ts
|
|
3239
|
+
type SeriesConfig<T = unknown> = LineSeriesConfig<T> | AreaSeriesConfig<T> | BarSeriesConfig<T> | ScatterSeriesConfig<T> | StateSeriesConfig<T> | MatrixSeriesConfig<T>;
|
|
3240
|
+
```
|
|
3241
|
+
|
|
3242
|
+
### `SeriesKind`
|
|
3243
|
+
|
|
3244
|
+
```ts
|
|
3245
|
+
type SeriesKind = SeriesConfig["kind"];
|
|
3246
|
+
```
|
|
3247
|
+
|
|
3248
|
+
### `TooltipConfig`
|
|
3249
|
+
|
|
3250
|
+
```ts
|
|
3251
|
+
interface TooltipConfig<T = unknown> {
|
|
3252
|
+
mode: "x" | "nearest";
|
|
3253
|
+
render?: (hit: TooltipHit<T>) => ReactNode;
|
|
3254
|
+
}
|
|
3255
|
+
```
|
|
3256
|
+
|
|
3257
|
+
### `TooltipPoint`
|
|
3258
|
+
|
|
3259
|
+
```ts
|
|
3260
|
+
/** One hit per series in the tooltip (R-4.4). */
|
|
3261
|
+
interface TooltipPoint<T = unknown> {
|
|
3262
|
+
seriesName: string;
|
|
3263
|
+
color: string;
|
|
3264
|
+
/** The point's x value on its own x axis - with several x axes it can
|
|
3265
|
+
differ from the hit's. */
|
|
3266
|
+
xValue: number;
|
|
3267
|
+
/** The point's y value; in a stack its own value, not its top - in a
|
|
3268
|
+
normalised stack its share in percent. */
|
|
3269
|
+
yValue: number;
|
|
3270
|
+
datum: T;
|
|
3271
|
+
index: number;
|
|
3272
|
+
/** Only for the matrix: the value out of the value channel. It stands here and
|
|
3273
|
+
not in yValue, because yValue is the position on the y axis - for a matrix
|
|
3274
|
+
therefore the row. Putting both into one field would be exactly the
|
|
3275
|
+
overloading ADR-0011 is written against. */
|
|
3276
|
+
value?: number;
|
|
3277
|
+
/** Only for state series: the section being pointed at.
|
|
3278
|
+
A duration does not stand here - it is the difference of two numbers the
|
|
3279
|
+
entry already carries, and formatting it would presuppose knowing what the
|
|
3280
|
+
x axis means. The caller knows that, not this library. */
|
|
3281
|
+
segment?: {
|
|
3282
|
+
from: number;
|
|
3283
|
+
to: number;
|
|
3284
|
+
label: string;
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
```
|
|
3288
|
+
|
|
3289
|
+
### `resolveColours`
|
|
3290
|
+
|
|
3291
|
+
```ts
|
|
3292
|
+
/** Any number of CSS colours - a token, `light-dark(…)`, `var(--…)`, a
|
|
3293
|
+
literal - resolved to values a canvas can draw, in the scheme that applies
|
|
3294
|
+
at `root`. The same probe `resolveTheme` uses, for a canvas of a
|
|
3295
|
+
neighbouring package (ADR-0022): the schedule draws its subtasks with the
|
|
3296
|
+
colours of @umriss-ui/core and of its caller, not with the chart palette.
|
|
3297
|
+
|
|
3298
|
+
Not cached: the caller keeps the result and reads anew when
|
|
3299
|
+
`subscribeTheme` tells it to. Where no resolution comes back (a DOM
|
|
3300
|
+
without a style engine), a colour is given back as it stands. */
|
|
3301
|
+
function resolveColours<K extends string>(root: Element, colours: Readonly<Record<K, string>>): Record<K, string>;
|
|
3302
|
+
```
|
|
3303
|
+
|
|
3304
|
+
### `subscribeTheme`
|
|
3305
|
+
|
|
3306
|
+
```ts
|
|
3307
|
+
/** Subscribe to theme invalidations - a switch of `color-scheme` on the
|
|
3308
|
+
document, the system's preference, or `invalidateTheme()`. The scenes of
|
|
3309
|
+
the charts use it, and so does any canvas that resolved its colours with
|
|
3310
|
+
`resolveColours`. Returns the unsubscribe. */
|
|
3311
|
+
function subscribeTheme(notify: () => void): () => void;
|
|
3312
|
+
```
|
|
3313
|
+
|
|
3314
|
+
### `toOperatingTimeClamped`
|
|
3315
|
+
|
|
3316
|
+
```ts
|
|
3317
|
+
/** Wall clock time → operating time, but without NaN: whatever lies in removed
|
|
3318
|
+
time falls onto the seam that this span collapses onto anyway; whatever lies
|
|
3319
|
+
before the first or after the last interval onto 0 or onto the whole
|
|
3320
|
+
operating time respectively.
|
|
3321
|
+
|
|
3322
|
+
Why this exists: the materialised x values must stay ascending. Binary search
|
|
3323
|
+
- for a hit as for a segment boundary - assumes that, and every comparison
|
|
3324
|
+
with NaN is false, so that a search over a block of NaN silently lands on the
|
|
3325
|
+
wrong point. The clamped value is a POSITION, not a statement: the point
|
|
3326
|
+
itself is carried as a gap and is never drawn and never hit. */
|
|
3327
|
+
function toOperatingTimeClamped(wallClock: number, input: CalendarInput): number;
|
|
3328
|
+
```
|
|
3329
|
+
|
|
3330
|
+
### `localOffset`
|
|
3331
|
+
|
|
3332
|
+
```ts
|
|
3333
|
+
/** The local offset at an instant, as the shift of a grid that is to lie on
|
|
3334
|
+
local time: `wallClock = offset + k · step`. Taken at one instant, it holds
|
|
3335
|
+
for a grid up to the next clock change. */
|
|
3336
|
+
function localOffset(instant: number): number;
|
|
3337
|
+
```
|
|
3338
|
+
|
|
3339
|
+
### `DEFAULT_CHARTS_WORDING`
|
|
3340
|
+
|
|
3341
|
+
```ts
|
|
3342
|
+
const DEFAULT_CHARTS_WORDING: ChartsWording;
|
|
3343
|
+
```
|
|
3344
|
+
|
|
3345
|
+
### `DataTableGroup`
|
|
3346
|
+
|
|
3347
|
+
```ts
|
|
3348
|
+
/** One table of the data table: the series of one x axis, or one matrix. Every
|
|
3349
|
+
cell is text already, in the format the tooltip writes. */
|
|
3350
|
+
interface DataTableGroup {
|
|
3351
|
+
key: string;
|
|
3352
|
+
caption: string;
|
|
3353
|
+
columns: readonly string[];
|
|
3354
|
+
/** The first cell of each row is its x - the row's heading. */
|
|
3355
|
+
rows: readonly (readonly string[])[];
|
|
3356
|
+
}
|
|
3357
|
+
```
|
|
3358
|
+
|
|
3359
|
+
## Files the examples show
|
|
3360
|
+
|
|
3361
|
+
The demo's own data, which some examples import from beside themselves. Copied with the example, it runs.
|
|
3362
|
+
|
|
3363
|
+
### `data.ts`
|
|
3364
|
+
|
|
3365
|
+
```ts
|
|
3366
|
+
/* Deterministic, generic number series (R-6.2).
|
|
3367
|
+
Seed-based, so that screenshots are stable; deliberately without any subject
|
|
3368
|
+
matter. */
|
|
3369
|
+
|
|
3370
|
+
export interface Point {
|
|
3371
|
+
t: number;
|
|
3372
|
+
a: number;
|
|
3373
|
+
b: number;
|
|
3374
|
+
c: number;
|
|
3375
|
+
d: number | null;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
/** A small LCG - reproducible across runs and platforms. */
|
|
3379
|
+
export function random(seed: number): () => number {
|
|
3380
|
+
let s = seed >>> 0;
|
|
3381
|
+
return () => {
|
|
3382
|
+
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
3383
|
+
return s / 4294967296;
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
/** A series with four channels; channel d contains a deliberate gap (R-2.5),
|
|
3388
|
+
and one reading in the middle of it - a point between two gaps. */
|
|
3389
|
+
export function series(seed: number, n: number, gap = false): Point[] {
|
|
3390
|
+
const r = random(seed);
|
|
3391
|
+
const points: Point[] = new Array<Point>(n);
|
|
3392
|
+
let a = 50;
|
|
3393
|
+
let b = 30;
|
|
3394
|
+
let c = 70;
|
|
3395
|
+
let d = 45;
|
|
3396
|
+
const gapFrom = Math.floor(n * 0.42);
|
|
3397
|
+
const gapTo = Math.floor(n * 0.55);
|
|
3398
|
+
const lone = Math.floor((gapFrom + gapTo) / 2);
|
|
3399
|
+
for (let i = 0; i < n; i++) {
|
|
3400
|
+
a += (r() - 0.5) * 6;
|
|
3401
|
+
b += (r() - 0.48) * 4;
|
|
3402
|
+
c += (r() - 0.52) * 5;
|
|
3403
|
+
d += (r() - 0.5) * 7;
|
|
3404
|
+
points[i] = {
|
|
3405
|
+
t: i,
|
|
3406
|
+
a,
|
|
3407
|
+
b,
|
|
3408
|
+
c,
|
|
3409
|
+
d: gap && i >= gapFrom && i <= gapTo && i !== lone ? null : d,
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
return points;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
export interface DualPoint {
|
|
3416
|
+
t: number;
|
|
3417
|
+
small: number;
|
|
3418
|
+
large: number;
|
|
3419
|
+
medium: number;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
/** Two to three series with clearly different extents. */
|
|
3423
|
+
export function dualSeries(seed: number, n: number): DualPoint[] {
|
|
3424
|
+
const r = random(seed);
|
|
3425
|
+
const points: DualPoint[] = new Array<DualPoint>(n);
|
|
3426
|
+
let small = 21;
|
|
3427
|
+
let large = 128000;
|
|
3428
|
+
let medium = 640;
|
|
3429
|
+
for (let i = 0; i < n; i++) {
|
|
3430
|
+
small += (r() - 0.5) * 0.9;
|
|
3431
|
+
large += (r() - 0.5) * 9000;
|
|
3432
|
+
medium += (r() - 0.5) * 40;
|
|
3433
|
+
points[i] = { t: 2000 + i, small, large, medium };
|
|
3434
|
+
}
|
|
3435
|
+
return points;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
export interface LoadPoint {
|
|
3439
|
+
t: number;
|
|
3440
|
+
s1: number;
|
|
3441
|
+
s2: number;
|
|
3442
|
+
s3: number;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
/** Data set for the benchmark example: three series of any length. */
|
|
3446
|
+
export function load(seed: number, n: number): LoadPoint[] {
|
|
3447
|
+
const r = random(seed);
|
|
3448
|
+
const points: LoadPoint[] = new Array<LoadPoint>(n);
|
|
3449
|
+
let s1 = 100;
|
|
3450
|
+
let s2 = 60;
|
|
3451
|
+
let s3 = 140;
|
|
3452
|
+
for (let i = 0; i < n; i++) {
|
|
3453
|
+
s1 += r() - 0.5;
|
|
3454
|
+
s2 += (r() - 0.5) * 0.8;
|
|
3455
|
+
s3 += (r() - 0.5) * 1.2;
|
|
3456
|
+
points[i] = { t: i, s1, s2, s3 };
|
|
3457
|
+
}
|
|
3458
|
+
return points;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
export interface MixedPoint {
|
|
3462
|
+
t: number;
|
|
3463
|
+
inflow: number;
|
|
3464
|
+
outflow: number;
|
|
3465
|
+
stock: number;
|
|
3466
|
+
/** A corridor with a deliberate gap - the fill gets a hole there. */
|
|
3467
|
+
corridorLower: number | null;
|
|
3468
|
+
corridorUpper: number | null;
|
|
3469
|
+
sample: number;
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
/** Data set of the mixed example: two bar series, a corridor as a band area with a
|
|
3473
|
+
gap, a line above it and samples as a scatter. Few periods, so that individual
|
|
3474
|
+
bars stay visible. */
|
|
3475
|
+
export function mixed(seed: number, n: number): MixedPoint[] {
|
|
3476
|
+
const r = random(seed);
|
|
3477
|
+
const points: MixedPoint[] = new Array<MixedPoint>(n);
|
|
3478
|
+
let stock = 62;
|
|
3479
|
+
const gapFrom = Math.floor(n * 0.45);
|
|
3480
|
+
const gapTo = Math.floor(n * 0.62);
|
|
3481
|
+
for (let i = 0; i < n; i++) {
|
|
3482
|
+
const inflow = 30 + r() * 25;
|
|
3483
|
+
const outflow = 22 + r() * 20;
|
|
3484
|
+
stock += (inflow - outflow) * 0.35;
|
|
3485
|
+
const inGap = i >= gapFrom && i <= gapTo;
|
|
3486
|
+
points[i] = {
|
|
3487
|
+
t: i + 1,
|
|
3488
|
+
inflow,
|
|
3489
|
+
outflow,
|
|
3490
|
+
stock,
|
|
3491
|
+
corridorLower: inGap ? null : stock - 9 - r() * 3,
|
|
3492
|
+
corridorUpper: inGap ? null : stock + 9 + r() * 3,
|
|
3493
|
+
sample: stock + (r() - 0.5) * 26,
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
return points;
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
/* ---------------------------------------------------------------------------
|
|
3500
|
+
Data of the operation instruments.
|
|
3501
|
+
|
|
3502
|
+
Up to here every series in this file is deliberately without subject matter
|
|
3503
|
+
(R-6.2): a curve is a curve, and "Series A" says enough. From here on that no
|
|
3504
|
+
longer works. A state band without states, a Pareto without fault reasons and a
|
|
3505
|
+
control chart without a measurement series show nothing - with these instruments
|
|
3506
|
+
the meaning IS the subject. What remains is the other half of the rule, and that
|
|
3507
|
+
is the important one: everything here is seed-based and therefore identical
|
|
3508
|
+
across runs and platforms.
|
|
3509
|
+
--------------------------------------------------------------------------- */
|
|
3510
|
+
|
|
3511
|
+
export const HOUR_MS = 3_600_000;
|
|
3512
|
+
export const DAY_MS = 24 * HOUR_MS;
|
|
3513
|
+
|
|
3514
|
+
/** Monday, 16 March 2026, 00:00 local time - the anchor of every time series
|
|
3515
|
+
below. */
|
|
3516
|
+
export const WEEK_START = new Date("2026-03-16T00:00:00").getTime();
|
|
3517
|
+
|
|
3518
|
+
export interface StatePoint {
|
|
3519
|
+
t: number;
|
|
3520
|
+
/** A code in PLANT_STATES. */
|
|
3521
|
+
m1: number;
|
|
3522
|
+
m2: number;
|
|
3523
|
+
/** A machine without a report: a gap stands here. */
|
|
3524
|
+
m3: number | null;
|
|
3525
|
+
temperature: number;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
/** The closed set of states of the demo. The order is the code. */
|
|
3529
|
+
export const PLANT_STATES = [
|
|
3530
|
+
{ label: "Production", color: "#2f7d51" },
|
|
3531
|
+
{ label: "Setup", color: "#c08a2e" },
|
|
3532
|
+
{ label: "Fault", color: "#b4483f" },
|
|
3533
|
+
{ label: "Maintenance", color: "#5b6b8c" },
|
|
3534
|
+
] as const;
|
|
3535
|
+
|
|
3536
|
+
/** One shift of three machines: states per quarter of an hour, plus a temperature
|
|
3537
|
+
curve on the same x axis - that is what shows why the band sits in the same
|
|
3538
|
+
chart and not beside it. */
|
|
3539
|
+
export function shift(seed: number, n: number): StatePoint[] {
|
|
3540
|
+
const r = random(seed);
|
|
3541
|
+
const points: StatePoint[] = new Array<StatePoint>(n);
|
|
3542
|
+
const start = WEEK_START + 6 * HOUR_MS;
|
|
3543
|
+
let m1 = 0;
|
|
3544
|
+
let m2 = 0;
|
|
3545
|
+
let m3: number | null = 0;
|
|
3546
|
+
let temperature = 806;
|
|
3547
|
+
for (let i = 0; i < n; i++) {
|
|
3548
|
+
if (r() < 0.16) m1 = Math.floor(r() * 4);
|
|
3549
|
+
if (r() < 0.12) m2 = Math.floor(r() * 3);
|
|
3550
|
+
// From a point on, machine 3 reports nothing any more: the band gets a hole,
|
|
3551
|
+
// and a hole is not a colour for "unknown".
|
|
3552
|
+
if (i > n * 0.62 && i < n * 0.78) m3 = null;
|
|
3553
|
+
else if (r() < 0.1) m3 = Math.floor(r() * 3);
|
|
3554
|
+
temperature += (r() - 0.5) * 9 + (m1 === 2 ? 6 : -0.6);
|
|
3555
|
+
points[i] = {
|
|
3556
|
+
t: start + i * 15 * 60_000,
|
|
3557
|
+
m1,
|
|
3558
|
+
m2,
|
|
3559
|
+
m3,
|
|
3560
|
+
temperature,
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
return points;
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
export interface Measurement {
|
|
3567
|
+
n: number;
|
|
3568
|
+
value: number;
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
/** Individual values of a feature inspection. The process runs cleanly and then
|
|
3572
|
+
begins to drift - in such a way that the run rule fires and not only the
|
|
3573
|
+
outlier rule. */
|
|
3574
|
+
export function measurements(seed: number, n: number): Measurement[] {
|
|
3575
|
+
const r = random(seed);
|
|
3576
|
+
const points: Measurement[] = new Array<Measurement>(n);
|
|
3577
|
+
for (let i = 0; i < n; i++) {
|
|
3578
|
+
const drift = i > n * 0.68 ? (i - n * 0.68) * 0.05 : 0;
|
|
3579
|
+
const outlier = i === Math.floor(n * 0.42) ? 0.9 : 0;
|
|
3580
|
+
points[i] = { n: i, value: 12.5 + (r() - 0.5) * 0.34 + drift + outlier };
|
|
3581
|
+
}
|
|
3582
|
+
return points;
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
export interface DowntimeReason {
|
|
3586
|
+
name: string;
|
|
3587
|
+
value: number;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
/** Downtime reasons of one week, in an arbitrary input order - the sorting is the
|
|
3591
|
+
job of the Pareto module, not of the data. */
|
|
3592
|
+
export const DOWNTIME_REASONS: readonly DowntimeReason[] = [
|
|
3593
|
+
{ name: "Tool breakage", value: 34 },
|
|
3594
|
+
{ name: "Material shortage", value: 128 },
|
|
3595
|
+
{ name: "Setup over plan", value: 96 },
|
|
3596
|
+
{ name: "Sensor fault", value: 21 },
|
|
3597
|
+
{ name: "Coolant", value: 12 },
|
|
3598
|
+
{ name: "Operator error", value: 47 },
|
|
3599
|
+
{ name: "Power dip", value: 8 },
|
|
3600
|
+
{ name: "Labeller", value: 6 },
|
|
3601
|
+
{ name: "Conveyor", value: 5 },
|
|
3602
|
+
{ name: "Small part", value: 3 },
|
|
3603
|
+
];
|
|
3604
|
+
|
|
3605
|
+
export interface CellPoint {
|
|
3606
|
+
/** Hour of the day. */
|
|
3607
|
+
hour: number;
|
|
3608
|
+
/** The row: the machine. */
|
|
3609
|
+
machine: number;
|
|
3610
|
+
/** Overall equipment effectiveness in per cent; null = no measurement. */
|
|
3611
|
+
oee: number | null;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
export const MACHINES = [
|
|
3615
|
+
"Press 1",
|
|
3616
|
+
"Press 2",
|
|
3617
|
+
"Mill 3",
|
|
3618
|
+
"Mill 4",
|
|
3619
|
+
"Furnace 1",
|
|
3620
|
+
"Furnace 2",
|
|
3621
|
+
"Assembly A",
|
|
3622
|
+
"Assembly B",
|
|
3623
|
+
] as const;
|
|
3624
|
+
|
|
3625
|
+
/** Machine × hour. One machine has a bad hour, one whole hour is bad everywhere -
|
|
3626
|
+
only the difference makes the matrix worth reading. And a few cells are
|
|
3627
|
+
missing: a hole, not a zero. */
|
|
3628
|
+
export function utilizationMatrix(seed: number): CellPoint[] {
|
|
3629
|
+
const r = random(seed);
|
|
3630
|
+
const cells: CellPoint[] = [];
|
|
3631
|
+
for (let m = 0; m < MACHINES.length; m++) {
|
|
3632
|
+
for (let h = 0; h < 24; h++) {
|
|
3633
|
+
let oee: number | null = 62 + r() * 32;
|
|
3634
|
+
if (m === 3 && h > 9 && h < 15) oee = 18 + r() * 12; // one machine
|
|
3635
|
+
if (h === 21) oee = 24 + r() * 10; // one hour, everywhere
|
|
3636
|
+
if (m === 6 && h > 1 && h < 5) oee = null; // no measurement
|
|
3637
|
+
cells.push({ hour: h, machine: m, oee });
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return cells;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
export interface WeekPoint {
|
|
3644
|
+
t: number;
|
|
3645
|
+
output: number;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
/** A working week: values only during the shifts. On a wall clock axis this chart
|
|
3649
|
+
would consist, for a good forty per cent, of flat lines over an empty hall. */
|
|
3650
|
+
export function week(seed: number): WeekPoint[] {
|
|
3651
|
+
const r = random(seed);
|
|
3652
|
+
const points: WeekPoint[] = [];
|
|
3653
|
+
let value = 420;
|
|
3654
|
+
for (let day = 0; day < 5; day++) {
|
|
3655
|
+
for (let quarter = 0; quarter < 4 * 16; quarter++) {
|
|
3656
|
+
const hour = 6 + quarter / 4;
|
|
3657
|
+
value += (r() - 0.5) * 26;
|
|
3658
|
+
points.push({
|
|
3659
|
+
t: WEEK_START + day * DAY_MS + hour * HOUR_MS,
|
|
3660
|
+
output: value,
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return points;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
/** The operating calendar to go with it: Monday to Friday, 6 to 22 o'clock. */
|
|
3668
|
+
export const WEEK_CALENDAR = Array.from({ length: 5 }, (_, day) => ({
|
|
3669
|
+
from: WEEK_START + day * DAY_MS + 6 * HOUR_MS,
|
|
3670
|
+
to: WEEK_START + day * DAY_MS + 22 * HOUR_MS,
|
|
3671
|
+
}));
|
|
3672
|
+
|
|
3673
|
+
/* ---------------------------------------------------------------------------
|
|
3674
|
+
Data of the kind pages.
|
|
3675
|
+
|
|
3676
|
+
Area, Bar, Scatter and the tooltip each get a page with examples of their
|
|
3677
|
+
own, and each example shows one property. The data is plant data like the
|
|
3678
|
+
instruments' above, and for the same reason: a filled area of "Series A"
|
|
3679
|
+
does not say why it is filled down to 0, a power draw does.
|
|
3680
|
+
--------------------------------------------------------------------------- */
|
|
3681
|
+
|
|
3682
|
+
export interface PowerPoint {
|
|
3683
|
+
t: number;
|
|
3684
|
+
/** Power draw of the line in kW. */
|
|
3685
|
+
kw: number;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
/** The power draw of one line over the early shift, every five minutes: the
|
|
3689
|
+
start-up, production, the break at half past nine and a setup towards the
|
|
3690
|
+
end. A draw has a natural 0, which is why the area is filled down to it. */
|
|
3691
|
+
export function powerDraw(seed: number): PowerPoint[] {
|
|
3692
|
+
const r = random(seed);
|
|
3693
|
+
const start = WEEK_START + 6 * HOUR_MS;
|
|
3694
|
+
const points: PowerPoint[] = [];
|
|
3695
|
+
for (let i = 0; i <= 8 * 12; i++) {
|
|
3696
|
+
const hour = 6 + i / 12;
|
|
3697
|
+
let base = 182;
|
|
3698
|
+
if (hour < 6.5) base = 30 + (hour - 6) * 300; // start-up
|
|
3699
|
+
else if (hour >= 9.5 && hour < 10) base = 42; // break
|
|
3700
|
+
else if (hour >= 12 && hour < 12.75) base = 75; // setup
|
|
3701
|
+
points.push({ t: start + i * 5 * 60_000, kw: base + (r() - 0.5) * 18 });
|
|
3702
|
+
}
|
|
3703
|
+
return points;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
export interface CorridorPoint {
|
|
3707
|
+
t: number;
|
|
3708
|
+
/** The corridor the recipe permits; null during the recipe change. */
|
|
3709
|
+
lower: number | null;
|
|
3710
|
+
upper: number | null;
|
|
3711
|
+
/** The measured temperature - always there, also where no corridor is. */
|
|
3712
|
+
temperature: number;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
/** A hardening furnace over one shift. The corridor comes from the recipe;
|
|
3716
|
+
during the change from one recipe to the next there is none - a gap, not a
|
|
3717
|
+
corridor of zero width - and the temperature climbs to the new one. */
|
|
3718
|
+
export function corridor(seed: number): CorridorPoint[] {
|
|
3719
|
+
const r = random(seed);
|
|
3720
|
+
const start = WEEK_START + 6 * HOUR_MS;
|
|
3721
|
+
const points: CorridorPoint[] = [];
|
|
3722
|
+
let temperature = 842;
|
|
3723
|
+
for (let i = 0; i <= 8 * 12; i++) {
|
|
3724
|
+
const hour = 6 + i / 12;
|
|
3725
|
+
const change = hour >= 10 && hour < 11;
|
|
3726
|
+
const target = hour < 10 ? 840 : 880;
|
|
3727
|
+
temperature += (target - temperature) * 0.12 + (r() - 0.5) * 6;
|
|
3728
|
+
points.push({
|
|
3729
|
+
t: start + i * 5 * 60_000,
|
|
3730
|
+
lower: change ? null : target - 12,
|
|
3731
|
+
upper: change ? null : target + 12,
|
|
3732
|
+
temperature,
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
return points;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
export interface ShiftScrap {
|
|
3739
|
+
/** The position on the x axis: the shift's index, named by the tickFormat. */
|
|
3740
|
+
shift: number;
|
|
3741
|
+
name: string;
|
|
3742
|
+
/** Scrap in pieces. */
|
|
3743
|
+
scrap: number;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
/** Scrap per shift over three days. The night shift runs on fewer hands and
|
|
3747
|
+
scraps more - that is the one thing a reader is to find. */
|
|
3748
|
+
export function scrapPerShift(seed: number): ShiftScrap[] {
|
|
3749
|
+
const r = random(seed);
|
|
3750
|
+
const days = ["Mon", "Tue", "Wed"];
|
|
3751
|
+
const shifts = ["early", "late", "night"];
|
|
3752
|
+
return days.flatMap((day, d) =>
|
|
3753
|
+
shifts.map((shift, s) => ({
|
|
3754
|
+
shift: d * 3 + s,
|
|
3755
|
+
name: `${day} ${shift}`,
|
|
3756
|
+
scrap: Math.round((s === 2 ? 38 : 18) + r() * 14),
|
|
3757
|
+
})),
|
|
3758
|
+
);
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
export interface DayOutput {
|
|
3762
|
+
/** The position on the x axis: the working day's index. */
|
|
3763
|
+
day: number;
|
|
3764
|
+
name: string;
|
|
3765
|
+
/** Pieces planned and made. */
|
|
3766
|
+
planned: number;
|
|
3767
|
+
actual: number;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/** Two working weeks of one line, planned against made. Some days above plan,
|
|
3771
|
+
some below - the deviation has both signs. */
|
|
3772
|
+
export function planAndActual(seed: number): DayOutput[] {
|
|
3773
|
+
const r = random(seed);
|
|
3774
|
+
const names = ["Mon", "Tue", "Wed", "Thu", "Fri"];
|
|
3775
|
+
const points: DayOutput[] = [];
|
|
3776
|
+
for (let day = 0; day < 10; day++) {
|
|
3777
|
+
const planned = day % 5 === 4 ? 1600 : 2000;
|
|
3778
|
+
const date = new Date(WEEK_START + (day + 2 * Math.floor(day / 5)) * DAY_MS).getDate();
|
|
3779
|
+
points.push({
|
|
3780
|
+
day,
|
|
3781
|
+
name: `${names[day % 5]} ${date}`,
|
|
3782
|
+
planned,
|
|
3783
|
+
actual: Math.round(planned + (r() - 0.55) * 520),
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
return points;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
export interface DayDowntime {
|
|
3790
|
+
/** The position on the x axis: the working day's index. */
|
|
3791
|
+
day: number;
|
|
3792
|
+
name: string;
|
|
3793
|
+
/** Minutes stopped, per reason; null where the reason was not logged. */
|
|
3794
|
+
setup: number;
|
|
3795
|
+
material: number | null;
|
|
3796
|
+
breakdown: number;
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
/** Two working weeks of downtime by reason. On one Wednesday the material log
|
|
3800
|
+
was not kept - a gap, not a zero - and on the second Thursday a breakdown
|
|
3801
|
+
takes the day. */
|
|
3802
|
+
export function downtimePerDay(seed: number): DayDowntime[] {
|
|
3803
|
+
const r = random(seed);
|
|
3804
|
+
const names = ["Mon", "Tue", "Wed", "Thu", "Fri"];
|
|
3805
|
+
return Array.from({ length: 10 }, (_, day) => ({
|
|
3806
|
+
day,
|
|
3807
|
+
name: `${names[day % 5]} ${new Date(WEEK_START + (day + 2 * Math.floor(day / 5)) * DAY_MS).getDate()}`,
|
|
3808
|
+
setup: Math.round(40 + r() * 30),
|
|
3809
|
+
material: day === 2 ? null : Math.round(10 + r() * 45),
|
|
3810
|
+
breakdown: Math.round((day === 8 ? 150 : 5) + r() * 25),
|
|
3811
|
+
}));
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
export interface LineOutput {
|
|
3815
|
+
t: number;
|
|
3816
|
+
/** Pieces per hour of each of three lines. */
|
|
3817
|
+
line1: number;
|
|
3818
|
+
line2: number;
|
|
3819
|
+
line3: number;
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
/** A day of three lines, hour by hour from six to six. Line 3 runs the early
|
|
3823
|
+
and late shift only - at night it stands, and says 0, not nothing. */
|
|
3824
|
+
export function outputPerLine(seed: number): LineOutput[] {
|
|
3825
|
+
const r = random(seed);
|
|
3826
|
+
return Array.from({ length: 25 }, (_, i) => {
|
|
3827
|
+
const hour = (6 + i) % 24;
|
|
3828
|
+
const night = hour >= 22 || hour < 6;
|
|
3829
|
+
return {
|
|
3830
|
+
t: WEEK_START + (6 + i) * HOUR_MS,
|
|
3831
|
+
line1: Math.round(120 + r() * 30),
|
|
3832
|
+
line2: Math.round((night ? 60 : 90) + r() * 25),
|
|
3833
|
+
line3: night ? 0 : Math.round(70 + r() * 30),
|
|
3834
|
+
};
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
export interface SetPoint {
|
|
3839
|
+
t: number;
|
|
3840
|
+
/** The set point in °C; null while no recipe is loaded. */
|
|
3841
|
+
setPoint: number | null;
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
/** The set point of the hardening furnace above, logged only when it changes -
|
|
3845
|
+
a raise at 08:30, the recipe change from 10 to 11 o'clock without one, and a
|
|
3846
|
+
last entry at the end of the shift that closes the hold. */
|
|
3847
|
+
export const setPoints: readonly SetPoint[] = [
|
|
3848
|
+
{ t: WEEK_START + 6 * HOUR_MS, setPoint: 840 },
|
|
3849
|
+
{ t: WEEK_START + 8.5 * HOUR_MS, setPoint: 845 },
|
|
3850
|
+
{ t: WEEK_START + 10 * HOUR_MS, setPoint: null },
|
|
3851
|
+
{ t: WEEK_START + 11 * HOUR_MS, setPoint: 880 },
|
|
3852
|
+
{ t: WEEK_START + 14 * HOUR_MS, setPoint: 880 },
|
|
3853
|
+
];
|
|
3854
|
+
|
|
3855
|
+
export interface ThicknessSample {
|
|
3856
|
+
t: number;
|
|
3857
|
+
/** Wall thickness in mm. */
|
|
3858
|
+
mm: number;
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
/** Wall thickness of a pipe, sampled by hand over a shift - whenever the
|
|
3862
|
+
inspector came by, not on a clock. Individual measurements, not a course:
|
|
3863
|
+
nothing was measured between two of them. */
|
|
3864
|
+
export function wallThickness(seed: number, n: number): ThicknessSample[] {
|
|
3865
|
+
const r = random(seed);
|
|
3866
|
+
const points: ThicknessSample[] = [];
|
|
3867
|
+
let t = WEEK_START + 6 * HOUR_MS;
|
|
3868
|
+
for (let i = 0; i < n; i++) {
|
|
3869
|
+
t += (4 + r() * 16) * 60_000;
|
|
3870
|
+
points.push({ t, mm: 3.2 + (r() - 0.5) * 0.24 + (r() < 0.08 ? 0.18 : 0) });
|
|
3871
|
+
}
|
|
3872
|
+
return points;
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
export interface WeightSample {
|
|
3876
|
+
t: number;
|
|
3877
|
+
/** The set point of the fill weight in g. */
|
|
3878
|
+
setPoint: number;
|
|
3879
|
+
/** A sample within tolerance; null where it is an outlier. */
|
|
3880
|
+
sample: number | null;
|
|
3881
|
+
/** A sample outside tolerance; null where it is none. */
|
|
3882
|
+
outlier: number | null;
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
/** Fill weights of a filling line against their set point, which changes with
|
|
3886
|
+
the product at eleven. A sample further than the tolerance from its set
|
|
3887
|
+
point is an outlier - the two channels never both carry a value. */
|
|
3888
|
+
export function fillWeights(seed: number, n: number, tolerance: number): WeightSample[] {
|
|
3889
|
+
const r = random(seed);
|
|
3890
|
+
const points: WeightSample[] = [];
|
|
3891
|
+
const start = WEEK_START + 6 * HOUR_MS;
|
|
3892
|
+
for (let i = 0; i < n; i++) {
|
|
3893
|
+
const t = start + (i * 8 * HOUR_MS) / (n - 1);
|
|
3894
|
+
const setPoint = t < WEEK_START + 11 * HOUR_MS ? 500 : 525;
|
|
3895
|
+
const value = setPoint + (r() - 0.5) * 16 + (r() < 0.06 ? (r() < 0.5 ? -1 : 1) * 14 : 0);
|
|
3896
|
+
const out = Math.abs(value - setPoint) > tolerance;
|
|
3897
|
+
points.push({ t, setPoint, sample: out ? null : value, outlier: out ? value : null });
|
|
3898
|
+
}
|
|
3899
|
+
return points;
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
export interface FurnacePoint {
|
|
3903
|
+
t: number;
|
|
3904
|
+
/** Temperatures of the two furnaces in °C. */
|
|
3905
|
+
f1: number;
|
|
3906
|
+
f2: number;
|
|
3907
|
+
/** The set point both run at. */
|
|
3908
|
+
setPoint: number;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
/** Two furnaces at one set point over the early shift, every ten minutes -
|
|
3912
|
+
close enough that "which one is it" is a question the tooltip answers. */
|
|
3913
|
+
export function furnaces(seed: number): FurnacePoint[] {
|
|
3914
|
+
const r = random(seed);
|
|
3915
|
+
const start = WEEK_START + 6 * HOUR_MS;
|
|
3916
|
+
const points: FurnacePoint[] = [];
|
|
3917
|
+
let f1 = 806;
|
|
3918
|
+
let f2 = 800;
|
|
3919
|
+
for (let i = 0; i <= 8 * 6; i++) {
|
|
3920
|
+
f1 += (808 - f1) * 0.15 + (r() - 0.45) * 5;
|
|
3921
|
+
f2 += (808 - f2) * 0.1 + (r() - 0.55) * 5;
|
|
3922
|
+
points.push({ t: start + i * 10 * 60_000, f1, f2, setPoint: 808 });
|
|
3923
|
+
}
|
|
3924
|
+
return points;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
export interface HallPoint {
|
|
3928
|
+
t: number;
|
|
3929
|
+
/** Hall temperature in °C. */
|
|
3930
|
+
temperature: number;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
/** A day of hall temperature from Monday noon, every quarter hour - warm while
|
|
3934
|
+
the ovens run, cooling through the night. Crosses midnight on purpose. */
|
|
3935
|
+
export function hallTemperature(seed: number): HallPoint[] {
|
|
3936
|
+
const r = random(seed);
|
|
3937
|
+
const start = WEEK_START + 12 * HOUR_MS;
|
|
3938
|
+
const points: HallPoint[] = [];
|
|
3939
|
+
let temperature = 24;
|
|
3940
|
+
for (let i = 0; i <= 24 * 4; i++) {
|
|
3941
|
+
const t = start + i * 15 * 60_000;
|
|
3942
|
+
const hour = new Date(t).getHours();
|
|
3943
|
+
const target = hour >= 6 && hour < 22 ? 25 : 18;
|
|
3944
|
+
temperature += (target - temperature) * 0.08 + (r() - 0.5) * 0.4;
|
|
3945
|
+
points.push({ t, temperature });
|
|
3946
|
+
}
|
|
3947
|
+
return points;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
export interface KilnPoint {
|
|
3951
|
+
t: number;
|
|
3952
|
+
/** Kiln temperature in °C. */
|
|
3953
|
+
temperature: number;
|
|
3954
|
+
/** Gas flow in m³/h. */
|
|
3955
|
+
gas: number;
|
|
3956
|
+
/** Draught in Pa - the kiln runs below the hall's pressure. */
|
|
3957
|
+
draught: number;
|
|
3958
|
+
/** Flue gas temperature in °C: the harder the kiln fires, the hotter it
|
|
3959
|
+
leaves. */
|
|
3960
|
+
flue: number;
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
/** A week of a kiln from Monday midnight, one reading every `step`
|
|
3964
|
+
milliseconds: 850 °C while the shifts run on weekdays, held at 600 °C at
|
|
3965
|
+
night and over the weekend, the gas following the heat it has to make. On
|
|
3966
|
+
Wednesday at 14:00 the burner trips for twenty minutes. The week has no
|
|
3967
|
+
clock change, so the hour is counted rather than asked of a Date - a week
|
|
3968
|
+
of seconds is 604,800 of them. */
|
|
3969
|
+
export function kiln(seed: number, step: number): KilnPoint[] {
|
|
3970
|
+
const r = random(seed);
|
|
3971
|
+
const n = Math.floor((7 * DAY_MS) / step);
|
|
3972
|
+
const points = new Array<KilnPoint>(n);
|
|
3973
|
+
// Ten minutes to answer: the same kiln at any step.
|
|
3974
|
+
const k = Math.min(1, step / 600_000);
|
|
3975
|
+
let temperature = 600;
|
|
3976
|
+
let gas = 22;
|
|
3977
|
+
for (let i = 0; i < n; i++) {
|
|
3978
|
+
const offset = i * step;
|
|
3979
|
+
const day = Math.floor(offset / DAY_MS);
|
|
3980
|
+
const minute = Math.floor((offset % DAY_MS) / 60_000);
|
|
3981
|
+
const running = day < 5 && minute >= 6 * 60 && minute < 22 * 60;
|
|
3982
|
+
const tripped = day === 2 && minute >= 14 * 60 && minute < 14 * 60 + 20;
|
|
3983
|
+
const target = running ? 850 : 600;
|
|
3984
|
+
// What holds the temperature; above it the kiln heats, below it cools.
|
|
3985
|
+
const holding = running ? 38 : 22;
|
|
3986
|
+
const demand = tripped ? 0 : Math.max(0, holding + (target - temperature) * 0.3);
|
|
3987
|
+
gas += (demand - gas) * Math.min(1, k * 4);
|
|
3988
|
+
temperature += (gas - holding) * k;
|
|
3989
|
+
const shown = temperature + (r() - 0.5) * 4;
|
|
3990
|
+
const reading = Math.max(0, gas + (r() - 0.5) * 1.5);
|
|
3991
|
+
points[i] = {
|
|
3992
|
+
t: WEEK_START + offset,
|
|
3993
|
+
temperature: shown,
|
|
3994
|
+
gas: reading,
|
|
3995
|
+
draught: -8 - gas * 0.12 + (r() - 0.5) * 1.2,
|
|
3996
|
+
flue: 250 + reading * 3,
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
return points;
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
/* ---------------------------------------------------------------------------
|
|
4003
|
+
The series the examples show.
|
|
4004
|
+
|
|
4005
|
+
They stand here and not in the example files because several examples share
|
|
4006
|
+
one of them - `basicData` is the course in three examples - and because R-6.2
|
|
4007
|
+
wants one seed per series, not one per place it is drawn. An example imports
|
|
4008
|
+
what it needs; nobody inlines a second generator.
|
|
4009
|
+
--------------------------------------------------------------------------- */
|
|
4010
|
+
|
|
4011
|
+
export const basicData = series(42, 120);
|
|
4012
|
+
export const multiData = series(7, 90, true);
|
|
4013
|
+
export const configData = series(1312, 24);
|
|
4014
|
+
export const axesData = dualSeries(99, 60);
|
|
4015
|
+
export const mixedData = mixed(2026, 14);
|
|
4016
|
+
export const shiftData = shift(4711, 64);
|
|
4017
|
+
export const measurementData = measurements(815, 90);
|
|
4018
|
+
export const matrixData = utilizationMatrix(23);
|
|
4019
|
+
export const weekData = week(1963);
|
|
4020
|
+
export const powerData = powerDraw(612);
|
|
4021
|
+
export const corridorData = corridor(1400);
|
|
4022
|
+
export const scrapData = scrapPerShift(333);
|
|
4023
|
+
export const outputData = planAndActual(1017);
|
|
4024
|
+
export const thicknessData = wallThickness(88, 36);
|
|
4025
|
+
export const weightData = fillWeights(4040, 97, 9);
|
|
4026
|
+
export const furnaceData = furnaces(2718);
|
|
4027
|
+
export const hallData = hallTemperature(1603);
|
|
4028
|
+
export const kilnData = kiln(2024, 60_000);
|
|
4029
|
+
export const downtimeData = downtimePerDay(1204);
|
|
4030
|
+
export const lineOutputData = outputPerLine(906);
|
|
4031
|
+
```
|