ml-time-graph 1.0.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/LICENSE +32 -0
- package/MKT_AGGREGATE.md +154 -0
- package/README.de.md +154 -0
- package/README.md +152 -0
- package/USAGE.md +343 -0
- package/dist/aggregated_subtypes-DZNZyFTX.d.ts +43 -0
- package/dist/analyze/index.d.ts +296 -0
- package/dist/analyze/index.js +574 -0
- package/dist/index.d.ts +481 -0
- package/dist/index.js +3432 -0
- package/dist/interaction/index.d.ts +141 -0
- package/dist/interaction/index.js +438 -0
- package/dist/internals.d.ts +899 -0
- package/dist/internals.js +2465 -0
- package/dist/layout-Sc5UkC0r.d.ts +246 -0
- package/dist/scale-Cbr0KpPz.d.ts +824 -0
- package/package.json +85 -0
package/USAGE.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# MLTimeGraph — Usage & API
|
|
2
|
+
|
|
3
|
+
*[← README](README.md) · [Deutsch](README.de.md)*
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add ml-time-graph # or npm install / yarn add
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { mount } from 'ml-time-graph';
|
|
15
|
+
|
|
16
|
+
mount('#chart', {
|
|
17
|
+
height: 320,
|
|
18
|
+
series: [{
|
|
19
|
+
name: 'Temperature',
|
|
20
|
+
style: { line: { color: '#ef4444' } },
|
|
21
|
+
data: [
|
|
22
|
+
{ time: Date.UTC(2026, 0, 1, 0), value: 18.2 },
|
|
23
|
+
{ time: Date.UTC(2026, 0, 1, 1), value: 19.1 },
|
|
24
|
+
// …
|
|
25
|
+
],
|
|
26
|
+
}],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`mount` accepts an HTMLElement or a CSS selector, picks up the target's
|
|
31
|
+
width if `options.width` is unset, and returns the chart instance for
|
|
32
|
+
later use (`chart.invertTime` / `chart.setData` / `chart.legendItems`).
|
|
33
|
+
|
|
34
|
+
Under the hood it's just the three explicit steps:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { MLTimeGraph, SVGRenderer } from 'ml-time-graph';
|
|
38
|
+
|
|
39
|
+
const chart = new MLTimeGraph({ ...options });
|
|
40
|
+
const { content } = new SVGRenderer().render(chart.renderCommands());
|
|
41
|
+
el.innerHTML = content;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`renderCommands()` returns renderer-agnostic draw commands; `SVGRenderer`
|
|
45
|
+
turns them into an SVG string. It needs **no DOM**, so it works both in
|
|
46
|
+
the browser and server-side. For DOM-specific behaviour, extend
|
|
47
|
+
`SVGRenderer`.
|
|
48
|
+
|
|
49
|
+
## Series
|
|
50
|
+
|
|
51
|
+
A `series` entry is either a **raw** time series (`DataPoint[]` = `{ time, value }`)
|
|
52
|
+
or an **aggregated** series (`AggregatedPoint[]` = `{ time, min, max, avg, count }`).
|
|
53
|
+
|
|
54
|
+
Visual styling lives in a nested `style` block — `line`, `fill`, `markers`, `shadow`,
|
|
55
|
+
`gap`. Any combination is allowed: set `line` for a line, add `fill` for an area
|
|
56
|
+
underneath, add `markers` for points, set `style.line: false` for points only.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// Raw line
|
|
60
|
+
{
|
|
61
|
+
name: 'CPU',
|
|
62
|
+
data,
|
|
63
|
+
style: {
|
|
64
|
+
line: { color: '#4285f4', width: 2, style: 'solid', smoothing: true },
|
|
65
|
+
markers: { type: 'circle', size: 3 },
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Step + smoothed overlay (multi-line array — same data, both rendered)
|
|
70
|
+
{
|
|
71
|
+
name: 'CPU',
|
|
72
|
+
data,
|
|
73
|
+
style: { line: [
|
|
74
|
+
{ color: '#999', width: 1, shape: 'step' },
|
|
75
|
+
{ color: '#3b82f6', width: 2, smoothing: true },
|
|
76
|
+
] },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Aggregated min/max/avg band
|
|
80
|
+
{
|
|
81
|
+
name: 'Temp',
|
|
82
|
+
showAs: 'minmaxavg',
|
|
83
|
+
data: aggregateBySlot(raw, 'hourly'),
|
|
84
|
+
minColor: '#3b82f6', maxColor: '#ef4444', avgColor: '#64748b',
|
|
85
|
+
avgDashed: true,
|
|
86
|
+
fillToMax: '#ef444433', fillToMin: '#3b82f633',
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`LineVariant` covers `'solid' | 'dotted' | 'sparse-dots' | 'dashed' | 'long-dash' |
|
|
91
|
+
'dense-dash' | 'dash-dot' | 'dash-dot-dot' | 'loose-dash'`.
|
|
92
|
+
|
|
93
|
+
A point with `value: null` (or a slot with `min/max/avg: null`) marks a **gap** — the
|
|
94
|
+
line breaks there. Two y-axes are supported via `yAxisIndex: 0 | 1` on the series.
|
|
95
|
+
|
|
96
|
+
## Thresholds (incident analysis)
|
|
97
|
+
|
|
98
|
+
Define named thresholds once and reference them from a series:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
new MLTimeGraph({
|
|
102
|
+
thresholds: [
|
|
103
|
+
{ name: 'warn', value: 22, color: '#f59e0b' },
|
|
104
|
+
{ name: 'crit', value: 28, color: '#ef4444', fill: 'above', label: 'Critical' },
|
|
105
|
+
],
|
|
106
|
+
series: [
|
|
107
|
+
{ name: 'Temp', data, colorByThresholds: ['warn', 'crit'] }, // line coloured per zone
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- A threshold draws a `line: 'solid' | 'dashed' | 'dotted' | 'none'`, an optional
|
|
113
|
+
half-plane `fill: 'above' | 'below'`, and a `label`
|
|
114
|
+
(`false | string | { text?, position? }`; position `left | right | above | below | center`).
|
|
115
|
+
- `colorByThresholds: string[]` colours the line by zone — base colour below the
|
|
116
|
+
lowest threshold, then each threshold's colour for values above it.
|
|
117
|
+
|
|
118
|
+
### Fill regions (the structured way)
|
|
119
|
+
|
|
120
|
+
The new `style.fill` model handles every shape of "fill area":
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { fillBetweenThresholds } from 'ml-time-graph';
|
|
124
|
+
|
|
125
|
+
// A. Above threshold:
|
|
126
|
+
style: { fill: { regions: [
|
|
127
|
+
{ from: { threshold: 'warn' }, to: 'series', side: 'above', fill: '#ef444433' },
|
|
128
|
+
] } }
|
|
129
|
+
|
|
130
|
+
// B. Between two thresholds (the "OK corridor"):
|
|
131
|
+
style: { fill: { regions: [
|
|
132
|
+
{ from: { threshold: 'low' }, to: { threshold: 'high' }, fill: { color: '#22c55e22', hatch: 'dots' } },
|
|
133
|
+
] } }
|
|
134
|
+
|
|
135
|
+
// C. Multi-zone heatmap — one call, N+1 colours / hatches:
|
|
136
|
+
style: { fill: fillBetweenThresholds({
|
|
137
|
+
thresholds: ['cold', 'norm', 'hot'],
|
|
138
|
+
colors: ['#60a5fa33', '#22c55e33', '#fbbf2433', '#ef444433'],
|
|
139
|
+
hatches: [undefined, 'dots', undefined, 'crosshatch'],
|
|
140
|
+
}) }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A `FillBound` is `'series' | 'chartTop' | 'chartBottom' | { threshold: name } | { value: n }`.
|
|
144
|
+
|
|
145
|
+
For *classified* (mutually-exclusive) zone bands, give a region both a `side`
|
|
146
|
+
and an `outer` bound — the region renders only in X-spans where the line's
|
|
147
|
+
value is between the fixed inner bound and `outer`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
style: { fill: { regions: [
|
|
151
|
+
// Orange between `high` and `highwarn` only when the line is in that zone
|
|
152
|
+
{ from: 'series', to: { threshold: 'high' }, side: 'above',
|
|
153
|
+
outer: { threshold: 'highwarn' }, fill: '#f59e0b66' },
|
|
154
|
+
// Red above `highwarn`
|
|
155
|
+
{ from: 'series', to: { threshold: 'highwarn' }, side: 'above', fill: '#ef444499' },
|
|
156
|
+
] } }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Legend
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
legend: {
|
|
163
|
+
show: true,
|
|
164
|
+
position: 'inside-right' | 'inside-left' | 'outside-right' | 'outside-left' | 'separate',
|
|
165
|
+
orientation: 'vertical' | 'horizontal',
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`outside-*` reserves margin space (the plot shrinks). `'separate'` draws **no** legend
|
|
170
|
+
in the SVG — call `chart.legendItems()` (`{ name, color }[]`) and render your own HTML.
|
|
171
|
+
|
|
172
|
+
## Axes
|
|
173
|
+
|
|
174
|
+
Per-axis config — label + tick formatter + tick density + grid + line styling:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
new MLTimeGraph({
|
|
178
|
+
axes: {
|
|
179
|
+
x: {
|
|
180
|
+
label: 'Time',
|
|
181
|
+
domain: 'auto', // or [tMin, tMax]
|
|
182
|
+
format: (d) => d.toISOString().slice(0, 10),
|
|
183
|
+
axis: { color: '#7c3aed' }, // baseline + ticks
|
|
184
|
+
},
|
|
185
|
+
left: {
|
|
186
|
+
label: 'Temperature (°C)',
|
|
187
|
+
format: (v) => `${v.toFixed(1)} °C`,
|
|
188
|
+
ticks: { major: 6 },
|
|
189
|
+
grid: { major: { color: '#e2e8f0' } },
|
|
190
|
+
axis: { color: '#7c3aed', width: 2 },
|
|
191
|
+
labels: { color: '#555', fontSize: 12 },
|
|
192
|
+
},
|
|
193
|
+
right: { label: 'Humidity (%)', format: (v) => `${v}%` },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Gaps
|
|
199
|
+
|
|
200
|
+
Per-series gaps (the line just breaks at `value: null`) plus chart-level
|
|
201
|
+
auto-detection and manual regions:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// chart-level: auto-detect time gaps + a manually flagged region
|
|
205
|
+
new MLTimeGraph({
|
|
206
|
+
gaps: {
|
|
207
|
+
autoDetect: true,
|
|
208
|
+
minGapMs: 60_000,
|
|
209
|
+
regions: [
|
|
210
|
+
{ startTime: t1, endTime: t2, label: 'Maintenance', style: { display: 'dashed_border' } },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// per-series style.gap — overrides default rendering for the value:null
|
|
216
|
+
// breaks INSIDE this series
|
|
217
|
+
{
|
|
218
|
+
name: 'CPU', data,
|
|
219
|
+
style: { gap: { fill: '#ef444422', opacity: 0.3 } },
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 'bridge_line' — dotted bridge across each null gap (missing-data hint)
|
|
223
|
+
{
|
|
224
|
+
name: 'CPU', data,
|
|
225
|
+
style: {
|
|
226
|
+
line: { color: '#3b82f6' },
|
|
227
|
+
gap: { display: 'bridge_line',
|
|
228
|
+
bridge: { color: '#94a3b8', width: 1.5, style: 'dotted' } },
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`gaps:` accepts either the structured `GapsConfig` (preferred) or a plain
|
|
234
|
+
`Gap[]` array for shorthand chart-level regions.
|
|
235
|
+
|
|
236
|
+
## Statistic overlays
|
|
237
|
+
|
|
238
|
+
Computed at render time from the series' raw data; each overlay has its own
|
|
239
|
+
style and appears as a sub-element on top of the parent series.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
{
|
|
243
|
+
name: 'Fridge sensor', data,
|
|
244
|
+
style: { line: { color: '#3b82f6' } },
|
|
245
|
+
overlays: [
|
|
246
|
+
{ kind: 'movingAverage', window: 4 * 60 * 60_000, style: { line: { color: '#7c3aed', style: 'dashed' } } },
|
|
247
|
+
{ kind: 'movingMkt', window: 24 * 60 * 60_000, activationEnergy: 83144,
|
|
248
|
+
style: { line: { color: '#ef4444' } } },
|
|
249
|
+
{ kind: 'stdDevBand', window: 4 * 60 * 60_000, multiplier: 2,
|
|
250
|
+
style: { fill: '#94a3b833' } },
|
|
251
|
+
{ kind: 'limits', low: 2, high: 8,
|
|
252
|
+
style: { line: { color: '#ef4444', style: 'dotted' } } },
|
|
253
|
+
],
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Supported overlay kinds: `'movingAverage'`, `'movingMkt'` (USP <1079.2>),
|
|
258
|
+
`'stdDevBand'` (mean ± multiplier·σ band), `'limits'`.
|
|
259
|
+
|
|
260
|
+
## Aggregation & helpers
|
|
261
|
+
|
|
262
|
+
The full analyse companion is at `ml-time-graph/analyze`:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import {
|
|
266
|
+
aggregateBySlot, downsample, detectGaps,
|
|
267
|
+
mkt, rollingMkt, DEFAULT_ACTIVATION_ENERGY,
|
|
268
|
+
stdDev, sampleStdDev, rollingStdDev,
|
|
269
|
+
computeLimitExcursions,
|
|
270
|
+
MovingAvg, StatsAggregator,
|
|
271
|
+
} from 'ml-time-graph/analyze';
|
|
272
|
+
|
|
273
|
+
aggregateBySlot(raw, 'hourly'); // min/max/avg per slot
|
|
274
|
+
downsample(raw, 2000); // LTTB → ~2000 points
|
|
275
|
+
detectGaps(raw, 60_000); // time-gap detection
|
|
276
|
+
|
|
277
|
+
rollingMkt(raw, 24 * 60 * 60_000); // rolling MKT (USP <1079.2>)
|
|
278
|
+
rollingStdDev(raw, 60 * 60_000); // rolling σ (population)
|
|
279
|
+
computeLimitExcursions(raw, { low: 2, high: 8 }); // → LimitStats with excursion list
|
|
280
|
+
|
|
281
|
+
StatsAggregator.compute(raw); // { min, max, avg, mean, median, stdDev, count }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The main barrel (`'ml-time-graph'`) carries only the rendering surface; the
|
|
285
|
+
analysis helpers live behind the `'ml-time-graph/analyze'` subpath.
|
|
286
|
+
|
|
287
|
+
## Annotations (custom overlays)
|
|
288
|
+
|
|
289
|
+
Draw your own shapes in **data coordinates** (time/value); the chart projects them to
|
|
290
|
+
pixels, so they track the data through zoom/resize:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
new MLTimeGraph({
|
|
294
|
+
series,
|
|
295
|
+
annotations: [
|
|
296
|
+
{ type: 'arrow', from: { time: t1, value: 22 }, to: { time: t2, value: 30 }, color: '#7c3aed' },
|
|
297
|
+
{ type: 'label', at: { time: t2, value: 30 }, text: 'Spike', dy: -8 },
|
|
298
|
+
{ type: 'rect', from: { time: a, value: 20 }, to: { time: b, value: 26 }, fill: '#7c3aed18' },
|
|
299
|
+
{ type: 'point', at: { time: t, value: v }, shape: 'circle' },
|
|
300
|
+
{ type: 'line', from: { time: a, value: v }, to: { time: b, value: v }, dash: 'dotted' },
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A coordinate is a `DataPointRef` = `{ time, value, axis? }` (`axis` selects the y-scale).
|
|
306
|
+
|
|
307
|
+
Annotations can also be managed **dynamically** (e.g. on a click). Like `setData`,
|
|
308
|
+
these update state — re-render afterwards (`renderCommands()` → renderer):
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const id = chart.addAnnotation({ type: 'point', at: { time, value } }); // → id
|
|
312
|
+
chart.disableAnnotation(id); // hide without removing
|
|
313
|
+
chart.enableAnnotation(id); // show again
|
|
314
|
+
chart.removeAnnotation(id);
|
|
315
|
+
chart.setAnnotations([...]); // replace all
|
|
316
|
+
chart.clearAnnotations();
|
|
317
|
+
chart.getAnnotations(); // read-only snapshot
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
For anything beyond these shapes, use the projection directly — the inverse of
|
|
321
|
+
`invertTime` / `invertValue`:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
chart.renderCommands();
|
|
325
|
+
const { x, y } = chart.project(time, value, axis); // data → pixel
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Interaction
|
|
329
|
+
|
|
330
|
+
Zoom/pan and tooltips are intentionally **not** baked into the chart. Map pixels to
|
|
331
|
+
data with `chart.invertTime(px)` / `chart.invertValue(py, axisIndex)` and build the
|
|
332
|
+
interaction in your app — see the demo gallery's "Fridge" (range slider) and
|
|
333
|
+
"Interaction" (tooltip) pages.
|
|
334
|
+
|
|
335
|
+
## Renderer
|
|
336
|
+
|
|
337
|
+
`render()` is the only method a renderer must implement
|
|
338
|
+
(`abstract render(commands: DrawCommand[]): RenderOutput`). The shipped `SVGRenderer`
|
|
339
|
+
emits a DOM-free SVG string. To produce live DOM nodes or add interactivity, extend it.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
Runnable examples of every feature live in the [demo gallery](demos/) — run `pnpm dev`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { A as AggregatedPoint } from './scale-Cbr0KpPz.js';
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
5
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
6
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Slot carrying a rolling Mean Kinetic Temperature value (USP <1079.2>). */
|
|
10
|
+
interface MktPoint extends AggregatedPoint {
|
|
11
|
+
/** Mean kinetic temperature for this slot. `null` = gap. */
|
|
12
|
+
mkt: number | null;
|
|
13
|
+
/** Delta vs the previous slot's MKT (optional). */
|
|
14
|
+
deltaMkt?: number | null;
|
|
15
|
+
}
|
|
16
|
+
/** Slot carrying the standard deviation of the samples in the window. */
|
|
17
|
+
interface StdDevPoint extends AggregatedPoint {
|
|
18
|
+
/** Standard deviation of the samples in this slot. `null` = gap. */
|
|
19
|
+
stdDev: number | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Slot carrying minutes-above-/below-limit statistics. At least one of
|
|
23
|
+
* `minutesAboveHigh` or `minutesBelowLow` must be set.
|
|
24
|
+
*/
|
|
25
|
+
interface LimitStatsPoint extends AggregatedPoint {
|
|
26
|
+
/** Minutes above the upper limit in this slot. */
|
|
27
|
+
minutesAboveHigh?: number | null;
|
|
28
|
+
/** Minutes below the lower limit in this slot. */
|
|
29
|
+
minutesBelowLow?: number | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The "full-stats" aggregated slot — combines all three specialised
|
|
33
|
+
* subtypes via intersection. This is what `aggregateBySlot(data, mode,
|
|
34
|
+
* thresholds)` returns when thresholds are provided: per-slot MKT (USP
|
|
35
|
+
* <1079.2>), standard deviation, and out-of-bounds minute counters.
|
|
36
|
+
*
|
|
37
|
+
* Prefer this typed alias over plain `AggregatedPoint` whenever you
|
|
38
|
+
* downstream-consume `aggregateBySlot`'s output and want guaranteed access
|
|
39
|
+
* to the stat fields.
|
|
40
|
+
*/
|
|
41
|
+
type StatsAggregatedPoint = MktPoint & StdDevPoint & LimitStatsPoint;
|
|
42
|
+
|
|
43
|
+
export type { LimitStatsPoint as L, MktPoint as M, StatsAggregatedPoint as S, StdDevPoint as a };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { l as DataPoint, A as AggregatedPoint, d as AggregationThresholds, J as Gap, ae as TimeScale, W as LinearScale, _ as Point, p as DrawCommand } from '../scale-Cbr0KpPz.js';
|
|
2
|
+
import { S as StatsAggregatedPoint } from '../aggregated_subtypes-DZNZyFTX.js';
|
|
3
|
+
|
|
4
|
+
/*!
|
|
5
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
6
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
7
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Utility for processing time-series data: sorting, gap detection,
|
|
12
|
+
* and splitting by value boundaries (thresholds).
|
|
13
|
+
*/
|
|
14
|
+
declare class SeriesProcessor {
|
|
15
|
+
/**
|
|
16
|
+
* Standard interpolation for scalar DataPoints.
|
|
17
|
+
*/
|
|
18
|
+
static interpolateDataPoint(p1: DataPoint, p2: DataPoint, t: number): DataPoint;
|
|
19
|
+
/**
|
|
20
|
+
* Standard interpolation for AggregatedPoints (interpolates min, max, avg and count).
|
|
21
|
+
*/
|
|
22
|
+
static interpolateAggregatedPoint(p1: AggregatedPoint, p2: AggregatedPoint, t: number): AggregatedPoint;
|
|
23
|
+
/**
|
|
24
|
+
* Splits a data array into contiguous runs based on null values or time jumps.
|
|
25
|
+
*
|
|
26
|
+
* @param data The raw data points.
|
|
27
|
+
* @param isNull A predicate to identify "gap" points (e.g. value === null).
|
|
28
|
+
* @param gapThreshold Max time distance between points before a new run starts.
|
|
29
|
+
*/
|
|
30
|
+
static getRuns<T extends {
|
|
31
|
+
time: number;
|
|
32
|
+
}>(data: T[], isNull: (p: T) => boolean, gapThreshold?: number): T[][];
|
|
33
|
+
/**
|
|
34
|
+
* Splits a contiguous run into sub-segments at the given boundary values.
|
|
35
|
+
* Inserts interpolated points at every boundary crossing so segments
|
|
36
|
+
* meet exactly at the boundary.
|
|
37
|
+
*
|
|
38
|
+
* @param run A gap-free array of points.
|
|
39
|
+
* @param boundaries Values at which to split the run.
|
|
40
|
+
* @param getValue Function to extract the numeric value used for splitting.
|
|
41
|
+
* @param interpolate Function to create an interpolated point between p1 and p2 at factor t [0..1].
|
|
42
|
+
*/
|
|
43
|
+
static splitByBoundaries<T>(run: T[], boundaries: number[], getValue: (p: T) => number, interpolate: (p1: T, p2: T, t: number) => T): {
|
|
44
|
+
data: T[];
|
|
45
|
+
zoneIndex: number;
|
|
46
|
+
}[];
|
|
47
|
+
/**
|
|
48
|
+
* Splits a contiguous run into two groups: those below and those at/above a threshold.
|
|
49
|
+
* Internally uses splitByBoundaries to ensure exact intersection points.
|
|
50
|
+
*/
|
|
51
|
+
static splitByThreshold<T>(run: T[], threshold: number, getValue: (p: T) => number, interpolate: (p1: T, p2: T, t: number) => T): {
|
|
52
|
+
above: T[][];
|
|
53
|
+
below: T[][];
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*!
|
|
58
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
59
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
60
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
type ProductType = "pharma_cold" | "pharma_ambient" | "blood" | "food_chilled" | "frozen" | "custom";
|
|
64
|
+
interface ProductConfig {
|
|
65
|
+
limitLow: number;
|
|
66
|
+
limitHigh: number;
|
|
67
|
+
activationEnergy?: number;
|
|
68
|
+
}
|
|
69
|
+
declare const PRODUCT_PROFILES: Record<Exclude<ProductType, "custom">, ProductConfig>;
|
|
70
|
+
declare function aggregateBySlot(data: DataPoint[], mode?: "hourly" | "daily" | "custom", customInterval?: number, thresholds?: AggregationThresholds, minCountForMkt?: number): StatsAggregatedPoint[];
|
|
71
|
+
declare function createAggr(time: number, values: number[]): AggregatedPoint;
|
|
72
|
+
/**
|
|
73
|
+
* Detect gaps in time-series data (autoDetect).
|
|
74
|
+
* Gibt alle Zeitenrücken zurück wo das Zeitintervall > minGapMs ist.
|
|
75
|
+
*/
|
|
76
|
+
declare function detectGaps(data: DataPoint[], minGapMs?: number): Gap[];
|
|
77
|
+
interface LongTermInsight {
|
|
78
|
+
type: "warning" | "info" | "critical";
|
|
79
|
+
message: string;
|
|
80
|
+
metric?: string;
|
|
81
|
+
}
|
|
82
|
+
declare function analyzeLongTermTrends(aggregatedData: StatsAggregatedPoint[]): LongTermInsight[];
|
|
83
|
+
declare function downsample(data: DataPoint[], target: number): DataPoint[];
|
|
84
|
+
/**
|
|
85
|
+
* Typed aggregation wrapper — returns slots with MKT + StdDev + Limit-Excursion
|
|
86
|
+
* fields all guaranteed present (as `number | null`). Equivalent to calling
|
|
87
|
+
* {@link aggregateBySlot} with thresholds, but with a clean typed return so
|
|
88
|
+
* downstream code can rely on the stats fields without `'mkt' in slot` guards.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* import { aggregateWithStats } from 'ml-time-graph/analyze';
|
|
92
|
+
* const slots = aggregateWithStats(rawTempData, 'hourly', { low: 2, high: 8 });
|
|
93
|
+
* for (const s of slots) {
|
|
94
|
+
* if (s.mkt !== null) console.log(s.time, 'MKT', s.mkt, 'σ', s.stdDev);
|
|
95
|
+
* }
|
|
96
|
+
*/
|
|
97
|
+
declare function aggregateWithStats(data: DataPoint[], mode: "hourly" | "daily" | "custom" | undefined, opts: {
|
|
98
|
+
/** Lower bound for the limit-excursion counter (°C). */
|
|
99
|
+
low: number;
|
|
100
|
+
/** Upper bound for the limit-excursion counter (°C). */
|
|
101
|
+
high: number;
|
|
102
|
+
/** Activation energy in J/mol (default 83144 per USP <1079.2>). */
|
|
103
|
+
activationEnergy?: number;
|
|
104
|
+
/** Slot interval in ms — required when `mode === 'custom'`. */
|
|
105
|
+
interval?: number;
|
|
106
|
+
/** Minimum sample count per slot before MKT is computed; below → `null`. */
|
|
107
|
+
minCountForMkt?: number;
|
|
108
|
+
}): StatsAggregatedPoint[];
|
|
109
|
+
|
|
110
|
+
/*!
|
|
111
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
112
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
113
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Statistics aggregator.
|
|
118
|
+
* Computes min, max, avg, median from a set of data points.
|
|
119
|
+
*/
|
|
120
|
+
interface StatsResult {
|
|
121
|
+
min: number;
|
|
122
|
+
max: number;
|
|
123
|
+
avg: number;
|
|
124
|
+
mean: number;
|
|
125
|
+
median: number;
|
|
126
|
+
stdDev: number;
|
|
127
|
+
count: number;
|
|
128
|
+
}
|
|
129
|
+
declare class StatsAggregator {
|
|
130
|
+
/** Compute statistics from data points */
|
|
131
|
+
static compute(data: DataPoint[]): StatsResult;
|
|
132
|
+
/** Compute stats for a specific time range (viewport-scoped) */
|
|
133
|
+
static computeInRange(data: DataPoint[], startTime: number, endTime: number): StatsResult;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*!
|
|
137
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
138
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
139
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/** Moving average type */
|
|
143
|
+
type MovingAvgType = "simple" | "exponential";
|
|
144
|
+
interface MovingAvgConfig {
|
|
145
|
+
/** Data points */
|
|
146
|
+
data: DataPoint[];
|
|
147
|
+
/** Window size (number of data points) */
|
|
148
|
+
windowSize: number;
|
|
149
|
+
/** MA type */
|
|
150
|
+
type?: MovingAvgType;
|
|
151
|
+
/** Time scale for X-axis */
|
|
152
|
+
timeScale: TimeScale;
|
|
153
|
+
/** Value scale for Y-axis */
|
|
154
|
+
valueScale: LinearScale;
|
|
155
|
+
/** Stroke color */
|
|
156
|
+
stroke?: string;
|
|
157
|
+
/** Stroke width */
|
|
158
|
+
strokeWidth?: number;
|
|
159
|
+
}
|
|
160
|
+
declare class MovingAvg {
|
|
161
|
+
#private;
|
|
162
|
+
constructor(config: MovingAvgConfig);
|
|
163
|
+
/** Compute simple moving average points */
|
|
164
|
+
static simple(data: DataPoint[], windowSize: number): {
|
|
165
|
+
time: number;
|
|
166
|
+
value: number;
|
|
167
|
+
}[];
|
|
168
|
+
/** Compute exponential moving average (EMA) */
|
|
169
|
+
static exponential(data: DataPoint[], windowSize: number): {
|
|
170
|
+
time: number;
|
|
171
|
+
value: number;
|
|
172
|
+
}[];
|
|
173
|
+
/** Get computed moving average as pixel points */
|
|
174
|
+
points(): Point[];
|
|
175
|
+
/** Render MA as a path command */
|
|
176
|
+
render(): DrawCommand[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/*!
|
|
180
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
181
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
182
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/** Default activation energy for pharma stability (USP <1079.2>): 83144 J/mol. */
|
|
186
|
+
declare const DEFAULT_ACTIVATION_ENERGY = 83144;
|
|
187
|
+
/**
|
|
188
|
+
* Compute the Mean Kinetic Temperature (in °C) of a list of temperature
|
|
189
|
+
* samples (also in °C). All samples are weighted equally. Returns `null`
|
|
190
|
+
* if the input is empty or contains no non-null values.
|
|
191
|
+
*
|
|
192
|
+
* Formula (USP <1079.2>):
|
|
193
|
+
*
|
|
194
|
+
* T_mkt = (E_a / R) / (-ln( (1/N) · Σ exp(-E_a / (R · T_i)) ))
|
|
195
|
+
*
|
|
196
|
+
* where `T_i` are the per-sample temperatures in Kelvin and `N` is the
|
|
197
|
+
* count of valid samples.
|
|
198
|
+
*/
|
|
199
|
+
declare function mkt(samples: (number | null)[], activationEnergy?: number): number | null;
|
|
200
|
+
/**
|
|
201
|
+
* Rolling Mean Kinetic Temperature over a sliding time window. For each
|
|
202
|
+
* input point at time `t`, emits an output point whose value is the MKT
|
|
203
|
+
* of all samples in the closed interval `[t - windowMs, t]`. The output
|
|
204
|
+
* has the same length and timestamps as the input. A `value: null` in
|
|
205
|
+
* the output marks a window that contained no valid samples.
|
|
206
|
+
*
|
|
207
|
+
* `data` must be sorted by `time` ascending. `value: null` inputs are
|
|
208
|
+
* skipped (treated as gaps).
|
|
209
|
+
*
|
|
210
|
+
* @param data Input DataPoints in °C, sorted by time ascending.
|
|
211
|
+
* @param windowMs Window length in ms (e.g. `7 * 24 * 3_600_000` for 7d).
|
|
212
|
+
* @param activationEnergy Default `83144` J/mol (USP <1079.2>).
|
|
213
|
+
*/
|
|
214
|
+
declare function rollingMkt(data: DataPoint[], windowMs: number, activationEnergy?: number): DataPoint[];
|
|
215
|
+
|
|
216
|
+
/*!
|
|
217
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
218
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
219
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Population standard deviation (σ, divisor `N`) of the non-null samples.
|
|
224
|
+
* Returns `null` for an empty or all-null input.
|
|
225
|
+
*/
|
|
226
|
+
declare function stdDev(samples: (number | null)[]): number | null;
|
|
227
|
+
/**
|
|
228
|
+
* Sample standard deviation (s, divisor `N - 1`). Returns `null` for fewer
|
|
229
|
+
* than 2 non-null samples (the sample-stddev is undefined for a single point).
|
|
230
|
+
*/
|
|
231
|
+
declare function sampleStdDev(samples: (number | null)[]): number | null;
|
|
232
|
+
/**
|
|
233
|
+
* Rolling standard deviation over a sliding time window. For each input
|
|
234
|
+
* point at time `t`, emits an output point whose value is the population
|
|
235
|
+
* standard deviation of all valid samples in `[t - windowMs, t]`. The
|
|
236
|
+
* output has the same length / timestamps as the input.
|
|
237
|
+
*
|
|
238
|
+
* `data` must be sorted by `time` ascending. `value: null` is skipped.
|
|
239
|
+
*
|
|
240
|
+
* @param data Sorted input DataPoints.
|
|
241
|
+
* @param windowMs Window length in ms.
|
|
242
|
+
* @param sample Use sample-stddev (divisor N-1) instead of
|
|
243
|
+
* population-stddev (divisor N). Default `false`.
|
|
244
|
+
*/
|
|
245
|
+
declare function rollingStdDev(data: DataPoint[], windowMs: number, sample?: boolean): DataPoint[];
|
|
246
|
+
|
|
247
|
+
/*!
|
|
248
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
249
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
250
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
251
|
+
*/
|
|
252
|
+
|
|
253
|
+
/** A single out-of-bounds episode. */
|
|
254
|
+
interface LimitExcursion {
|
|
255
|
+
/** Start time of the excursion (ms epoch). */
|
|
256
|
+
startTime: number;
|
|
257
|
+
/** End time of the excursion (ms epoch). */
|
|
258
|
+
endTime: number;
|
|
259
|
+
/** 'above' = exceeded `high`. 'below' = fell under `low`. */
|
|
260
|
+
side: "above" | "below";
|
|
261
|
+
/** Maximum (for 'above') or minimum (for 'below') value reached in the episode. */
|
|
262
|
+
extremum: number;
|
|
263
|
+
/** Duration in ms. */
|
|
264
|
+
durationMs: number;
|
|
265
|
+
}
|
|
266
|
+
/** Aggregate metrics computed over the full input series. */
|
|
267
|
+
interface LimitStats {
|
|
268
|
+
/** Total time (ms) the series spent above `high`. */
|
|
269
|
+
msAboveHigh: number;
|
|
270
|
+
/** Total time (ms) the series spent below `low`. */
|
|
271
|
+
msBelowLow: number;
|
|
272
|
+
/** Maximum value reached anywhere (ignoring nulls). `null` if no samples. */
|
|
273
|
+
globalMax: number | null;
|
|
274
|
+
/** Minimum value reached anywhere (ignoring nulls). `null` if no samples. */
|
|
275
|
+
globalMin: number | null;
|
|
276
|
+
/** All excursion episodes in chronological order. */
|
|
277
|
+
excursions: LimitExcursion[];
|
|
278
|
+
}
|
|
279
|
+
interface ComputeLimitsOpts {
|
|
280
|
+
/** Upper bound — values strictly greater are 'above'. */
|
|
281
|
+
high?: number;
|
|
282
|
+
/** Lower bound — values strictly less are 'below'. */
|
|
283
|
+
low?: number;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Compute limit-excursion intervals and aggregate metrics for a temperature
|
|
287
|
+
* series. `data` must be sorted by `time` ascending. `value: null` is treated
|
|
288
|
+
* as a gap and breaks any in-progress excursion.
|
|
289
|
+
*
|
|
290
|
+
* The duration attributed to an in-bounds-to-out-of-bounds transition is
|
|
291
|
+
* the elapsed time between two adjacent samples — there is no sub-sample
|
|
292
|
+
* interpolation in this v1; pass denser data for finer accuracy.
|
|
293
|
+
*/
|
|
294
|
+
declare function computeLimitExcursions(data: DataPoint[], opts: ComputeLimitsOpts): LimitStats;
|
|
295
|
+
|
|
296
|
+
export { type ComputeLimitsOpts, DEFAULT_ACTIVATION_ENERGY, type LimitExcursion, type LimitStats, type LongTermInsight, MovingAvg, type MovingAvgConfig, type MovingAvgType, PRODUCT_PROFILES, type ProductConfig, type ProductType, SeriesProcessor, StatsAggregator, type StatsResult, aggregateBySlot, aggregateWithStats, analyzeLongTermTrends, computeLimitExcursions, createAggr, detectGaps, downsample, mkt, rollingMkt, rollingStdDev, sampleStdDev, stdDev };
|