ml-time-graph 1.0.0 → 1.0.5
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/API_DESIGN.md +1211 -0
- package/README.de.md +12 -0
- package/README.md +12 -0
- package/dist/analyze/index.d.ts +272 -46
- package/dist/analyze/index.js +11 -570
- package/dist/index.d.ts +346 -9
- package/dist/index.js +57 -3420
- package/dist/interaction/index.d.ts +1 -1
- package/dist/interaction/index.js +2 -435
- package/dist/internals.d.ts +20 -5
- package/dist/internals.js +40 -2455
- package/dist/{layout-Sc5UkC0r.d.ts → layout-BOYtrsZa.d.ts} +1 -1
- package/dist/{scale-Cbr0KpPz.d.ts → scale-BRE_QhbZ.d.ts} +38 -15
- package/package.json +65 -80
- package/dist/aggregated_subtypes-DZNZyFTX.d.ts +0 -43
- /package/{MKT_AGGREGATE.md → docs/MKT_AGGREGATE.md} +0 -0
package/dist/analyze/index.js
CHANGED
|
@@ -1,574 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
static interpolateDataPoint(p1, p2, t) {
|
|
7
|
-
return {
|
|
8
|
-
time: p1.time + t * (p2.time - p1.time),
|
|
9
|
-
value: (p1.value ?? 0) + t * ((p2.value ?? 0) - (p1.value ?? 0))
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Standard interpolation for AggregatedPoints (interpolates min, max, avg and count).
|
|
14
|
-
*/
|
|
15
|
-
static interpolateAggregatedPoint(p1, p2, t) {
|
|
16
|
-
const lerp = (v1, v2) => v1 !== null && v2 !== null ? v1 + t * (v2 - v1) : null;
|
|
17
|
-
return {
|
|
18
|
-
time: p1.time + t * (p2.time - p1.time),
|
|
19
|
-
min: lerp(p1.min, p2.min),
|
|
20
|
-
max: lerp(p1.max, p2.max),
|
|
21
|
-
avg: lerp(p1.avg, p2.avg),
|
|
22
|
-
count: Math.round(p1.count + t * (p2.count - p1.count))
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Splits a data array into contiguous runs based on null values or time jumps.
|
|
27
|
-
*
|
|
28
|
-
* @param data The raw data points.
|
|
29
|
-
* @param isNull A predicate to identify "gap" points (e.g. value === null).
|
|
30
|
-
* @param gapThreshold Max time distance between points before a new run starts.
|
|
31
|
-
*/
|
|
32
|
-
static getRuns(data, isNull, gapThreshold = 0) {
|
|
33
|
-
const sorted = [...data].sort((a, b) => a.time - b.time);
|
|
34
|
-
const runs = [];
|
|
35
|
-
let current = [];
|
|
36
|
-
let prev = null;
|
|
37
|
-
for (const p of sorted) {
|
|
38
|
-
const isPointNull = isNull(p);
|
|
39
|
-
const isJump = gapThreshold > 0 && prev && p.time - prev.time > gapThreshold;
|
|
40
|
-
if (isPointNull || isJump) {
|
|
41
|
-
if (current.length) {
|
|
42
|
-
runs.push(current);
|
|
43
|
-
current = [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (!isPointNull) {
|
|
47
|
-
current.push(p);
|
|
48
|
-
}
|
|
49
|
-
prev = p;
|
|
50
|
-
}
|
|
51
|
-
if (current.length) {
|
|
52
|
-
runs.push(current);
|
|
53
|
-
}
|
|
54
|
-
return runs;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Splits a contiguous run into sub-segments at the given boundary values.
|
|
58
|
-
* Inserts interpolated points at every boundary crossing so segments
|
|
59
|
-
* meet exactly at the boundary.
|
|
60
|
-
*
|
|
61
|
-
* @param run A gap-free array of points.
|
|
62
|
-
* @param boundaries Values at which to split the run.
|
|
63
|
-
* @param getValue Function to extract the numeric value used for splitting.
|
|
64
|
-
* @param interpolate Function to create an interpolated point between p1 and p2 at factor t [0..1].
|
|
65
|
-
*/
|
|
66
|
-
static splitByBoundaries(run, boundaries, getValue, interpolate) {
|
|
67
|
-
if (run.length === 0) return [];
|
|
68
|
-
if (boundaries.length === 0) {
|
|
69
|
-
return [{ data: run, zoneIndex: 0 }];
|
|
70
|
-
}
|
|
71
|
-
const bs = [...boundaries].sort((a, b) => a - b);
|
|
72
|
-
const out = [];
|
|
73
|
-
const getZone = (v) => {
|
|
74
|
-
let idx = 0;
|
|
75
|
-
for (let i = 0; i < bs.length; i++) {
|
|
76
|
-
if (v >= bs[i]) idx = i + 1;
|
|
77
|
-
else break;
|
|
78
|
-
}
|
|
79
|
-
return idx;
|
|
80
|
-
};
|
|
81
|
-
let currentSeg = [run[0]];
|
|
82
|
-
for (let i = 1; i < run.length; i++) {
|
|
83
|
-
const p1 = run[i - 1];
|
|
84
|
-
const p2 = run[i];
|
|
85
|
-
const v1 = getValue(p1);
|
|
86
|
-
const v2 = getValue(p2);
|
|
87
|
-
let crossed;
|
|
88
|
-
if (v2 > v1) {
|
|
89
|
-
crossed = bs.filter((b) => b > v1 && b <= v2);
|
|
90
|
-
} else if (v2 < v1) {
|
|
91
|
-
crossed = bs.filter((b) => b >= v2 && b < v1).reverse();
|
|
92
|
-
} else {
|
|
93
|
-
crossed = [];
|
|
94
|
-
}
|
|
95
|
-
for (const b of crossed) {
|
|
96
|
-
const t = (b - v1) / (v2 - v1);
|
|
97
|
-
const pInt = interpolate(p1, p2, t);
|
|
98
|
-
currentSeg.push(pInt);
|
|
99
|
-
out.push({ data: currentSeg, zoneIndex: getZone((v1 + b) / 2) });
|
|
100
|
-
currentSeg = [pInt];
|
|
101
|
-
}
|
|
102
|
-
currentSeg.push(p2);
|
|
103
|
-
}
|
|
104
|
-
if (currentSeg.length > 0) {
|
|
105
|
-
const vStart = getValue(currentSeg[0]);
|
|
106
|
-
const vEnd = getValue(currentSeg[currentSeg.length - 1]);
|
|
107
|
-
out.push({ data: currentSeg, zoneIndex: getZone((vStart + vEnd) / 2) });
|
|
108
|
-
}
|
|
109
|
-
return out;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Splits a contiguous run into two groups: those below and those at/above a threshold.
|
|
113
|
-
* Internally uses splitByBoundaries to ensure exact intersection points.
|
|
114
|
-
*/
|
|
115
|
-
static splitByThreshold(run, threshold, getValue, interpolate) {
|
|
116
|
-
const segments = this.splitByBoundaries(run, [threshold], getValue, interpolate);
|
|
117
|
-
const result = { above: [], below: [] };
|
|
118
|
-
for (const seg of segments) {
|
|
119
|
-
if (seg.zoneIndex === 0) result.below.push(seg.data);
|
|
120
|
-
else result.above.push(seg.data);
|
|
121
|
-
}
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// src/analyze/aggregator.ts
|
|
127
|
-
var MS_PER_HOUR = 36e5;
|
|
128
|
-
var MS_PER_DAY = 864e5;
|
|
129
|
-
var PRODUCT_PROFILES = {
|
|
130
|
-
pharma_cold: { limitLow: 2, limitHigh: 8, activationEnergy: 83144 },
|
|
131
|
-
pharma_ambient: {
|
|
132
|
-
limitLow: 15,
|
|
133
|
-
limitHigh: 25,
|
|
134
|
-
activationEnergy: 83144
|
|
135
|
-
},
|
|
136
|
-
blood: { limitLow: 2, limitHigh: 6, activationEnergy: 83144 },
|
|
137
|
-
food_chilled: { limitLow: 0, limitHigh: 4 },
|
|
138
|
-
// Lebensmittel nutzen MKT seltener, dH entfällt meist
|
|
139
|
-
frozen: { limitLow: -40, limitHigh: -18 }
|
|
140
|
-
};
|
|
141
|
-
var R = 8.314462618;
|
|
142
|
-
var CELSIUS_TO_KELVIN = 273.15;
|
|
143
|
-
function createAggrWithMkt(time, rawPointsInSlot, thresholds, minCountForMkt = 1) {
|
|
144
|
-
const validPoints = rawPointsInSlot.filter(
|
|
145
|
-
(pt) => pt.value !== null && pt.value !== void 0 && !isNaN(pt.value)
|
|
146
|
-
);
|
|
147
|
-
const values = validPoints.map((pt) => pt.value);
|
|
148
|
-
if (!values.length) {
|
|
149
|
-
return {
|
|
150
|
-
time,
|
|
151
|
-
min: null,
|
|
152
|
-
max: null,
|
|
153
|
-
avg: null,
|
|
154
|
-
mkt: null,
|
|
155
|
-
deltaMkt: null,
|
|
156
|
-
stdDev: null,
|
|
157
|
-
minutesAboveHigh: thresholds ? 0 : null,
|
|
158
|
-
minutesBelowLow: thresholds ? 0 : null,
|
|
159
|
-
count: 0
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const mn = Math.min(...values);
|
|
163
|
-
const mx = Math.max(...values);
|
|
164
|
-
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
165
|
-
let minutesAboveHigh = thresholds ? 0 : null;
|
|
166
|
-
let minutesBelowLow = thresholds ? 0 : null;
|
|
167
|
-
if (thresholds) {
|
|
168
|
-
for (let i = 0; i < rawPointsInSlot.length; i++) {
|
|
169
|
-
const current = rawPointsInSlot[i];
|
|
170
|
-
if (current.value !== null && current.value !== void 0) {
|
|
171
|
-
const next = rawPointsInSlot[i + 1];
|
|
172
|
-
const durationMs = next ? next.time - current.time : 0;
|
|
173
|
-
const durationMinutes = durationMs / (1e3 * 60);
|
|
174
|
-
if (current.value > thresholds.limitHigh)
|
|
175
|
-
minutesAboveHigh += durationMinutes;
|
|
176
|
-
if (current.value < thresholds.limitLow)
|
|
177
|
-
minutesBelowLow += durationMinutes;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
let sumSquareDiff = 0;
|
|
182
|
-
let sumExp = 0;
|
|
183
|
-
const deltaH = thresholds?.activationEnergy ?? 83144;
|
|
184
|
-
const dh_r = deltaH / R;
|
|
185
|
-
for (const cels of values) {
|
|
186
|
-
sumSquareDiff += Math.pow(cels - avg, 2);
|
|
187
|
-
const kelvin = cels + CELSIUS_TO_KELVIN;
|
|
188
|
-
sumExp += Math.exp(-dh_r / kelvin);
|
|
189
|
-
}
|
|
190
|
-
const variance = sumSquareDiff / values.length;
|
|
191
|
-
const stdDev2 = Math.sqrt(variance);
|
|
192
|
-
let mktCelsius = null;
|
|
193
|
-
let deltaMkt = null;
|
|
194
|
-
if (values.length >= minCountForMkt) {
|
|
195
|
-
const meanExp = sumExp / values.length;
|
|
196
|
-
const mktKelvin = dh_r / -Math.log(meanExp);
|
|
197
|
-
mktCelsius = Number((mktKelvin - CELSIUS_TO_KELVIN).toFixed(2));
|
|
198
|
-
deltaMkt = Number((mktCelsius - avg).toFixed(2));
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
time,
|
|
202
|
-
min: Number(mn.toFixed(2)),
|
|
203
|
-
max: Number(mx.toFixed(2)),
|
|
204
|
-
avg: Number(avg.toFixed(2)),
|
|
205
|
-
mkt: mktCelsius,
|
|
206
|
-
deltaMkt,
|
|
207
|
-
stdDev: Number(stdDev2.toFixed(2)),
|
|
208
|
-
minutesAboveHigh: minutesAboveHigh !== null ? Number(minutesAboveHigh.toFixed(1)) : null,
|
|
209
|
-
minutesBelowLow: minutesBelowLow !== null ? Number(minutesBelowLow.toFixed(1)) : null,
|
|
210
|
-
count: values.length
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function aggregateBySlot(data, mode = "hourly", customInterval, thresholds, minCountForMkt = 1) {
|
|
214
|
-
if (!data.length) return [];
|
|
215
|
-
const interval = mode === "hourly" ? MS_PER_HOUR : mode === "daily" ? MS_PER_DAY : customInterval ?? 60 * 60 * 1e3;
|
|
216
|
-
const sorted = [...data].sort((a, b) => a.time - b.time);
|
|
217
|
-
const slots = [];
|
|
218
|
-
let slotStart = sorted[0].time;
|
|
219
|
-
let pointsInSlot = [];
|
|
220
|
-
for (const pt of sorted) {
|
|
221
|
-
while (pt.time - slotStart >= interval) {
|
|
222
|
-
slots.push(
|
|
223
|
-
createAggrWithMkt(slotStart, pointsInSlot, thresholds, minCountForMkt)
|
|
224
|
-
);
|
|
225
|
-
slotStart += interval;
|
|
226
|
-
pointsInSlot = [];
|
|
227
|
-
}
|
|
228
|
-
pointsInSlot.push(pt);
|
|
229
|
-
}
|
|
230
|
-
slots.push(
|
|
231
|
-
createAggrWithMkt(slotStart, pointsInSlot, thresholds, minCountForMkt)
|
|
232
|
-
);
|
|
233
|
-
return slots;
|
|
234
|
-
}
|
|
235
|
-
function createAggr(time, values) {
|
|
236
|
-
const mn = Math.min(...values);
|
|
237
|
-
const mx = Math.max(...values);
|
|
238
|
-
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
239
|
-
return { time, min: mn, max: mx, avg, count: values.length };
|
|
240
|
-
}
|
|
241
|
-
function detectGaps(data, minGapMs = 6e4) {
|
|
242
|
-
if (data.length < 2) return [];
|
|
243
|
-
const sorted = [...data].sort((a, b) => a.time - b.time);
|
|
244
|
-
const gaps = [];
|
|
245
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
246
|
-
const gap = sorted[i].time - sorted[i - 1].time;
|
|
247
|
-
if (gap > minGapMs) {
|
|
248
|
-
gaps.push({
|
|
249
|
-
startTime: sorted[i - 1].time,
|
|
250
|
-
endTime: sorted[i].time
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return gaps;
|
|
255
|
-
}
|
|
256
|
-
function analyzeLongTermTrends(aggregatedData) {
|
|
257
|
-
const insights = [];
|
|
258
|
-
let totalMinutesTooHot = 0;
|
|
259
|
-
let maxStdDev = 0;
|
|
260
|
-
let slotsWithData = 0;
|
|
261
|
-
let highDeltaMktCount = 0;
|
|
262
|
-
for (const slot of aggregatedData) {
|
|
263
|
-
if (slot.count > 0) {
|
|
264
|
-
slotsWithData++;
|
|
265
|
-
if (slot.minutesAboveHigh) totalMinutesTooHot += slot.minutesAboveHigh;
|
|
266
|
-
if (slot.stdDev && slot.stdDev > maxStdDev) maxStdDev = slot.stdDev;
|
|
267
|
-
if (slot.deltaMkt && slot.deltaMkt > 2) {
|
|
268
|
-
highDeltaMktCount++;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (slotsWithData === 0) return [];
|
|
273
|
-
if (totalMinutesTooHot > 120) {
|
|
274
|
-
insights.push({
|
|
275
|
-
type: "critical",
|
|
276
|
-
message: `Kritische Gesamtbelastung: Das Produkt war im gesamten Zeitraum insgesamt ${Math.round(totalMinutesTooHot)} Minuten zu warm.`,
|
|
277
|
-
metric: "Total Excursion Time"
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
if (highDeltaMktCount > slotsWithData * 0.15) {
|
|
281
|
-
insights.push({
|
|
282
|
-
type: "warning",
|
|
283
|
-
message: "H\xE4ufige thermische Schocks erkannt. Die kinetische Temperatur weicht oft stark vom Schnitt ab (m\xF6gliche regelm\xE4\xDFige T\xFCr\xF6ffnungen).",
|
|
284
|
-
metric: "Delta MKT Instability"
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
if (maxStdDev > 4) {
|
|
288
|
-
insights.push({
|
|
289
|
-
type: "warning",
|
|
290
|
-
message: `Hohe Instabilit\xE4t gemessen. Die maximale Standardabweichung lag bei ${maxStdDev}\xB0C. Das K\xFChlsystem regelt unsauber.`,
|
|
291
|
-
metric: "Standard Deviation Peak"
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
return insights;
|
|
295
|
-
}
|
|
296
|
-
function downsample(data, target) {
|
|
297
|
-
if (data.length <= target) return data;
|
|
298
|
-
const result = [];
|
|
299
|
-
const step = Math.ceil(data.length / target);
|
|
300
|
-
result.push(data[0]);
|
|
301
|
-
for (let i = 1; i < data.length; i += step) {
|
|
302
|
-
result.push(data[Math.min(i, data.length - 1)]);
|
|
303
|
-
}
|
|
304
|
-
return result;
|
|
305
|
-
}
|
|
306
|
-
function aggregateWithStats(data, mode = "hourly", opts) {
|
|
307
|
-
const thresholds = {
|
|
308
|
-
limitLow: opts.low,
|
|
309
|
-
limitHigh: opts.high,
|
|
310
|
-
activationEnergy: opts.activationEnergy
|
|
311
|
-
};
|
|
312
|
-
return aggregateBySlot(
|
|
313
|
-
data,
|
|
314
|
-
mode,
|
|
315
|
-
opts.interval,
|
|
316
|
-
thresholds,
|
|
317
|
-
opts.minCountForMkt ?? 1
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// src/analyze/stats.ts
|
|
322
|
-
var StatsAggregator = class {
|
|
323
|
-
/** Compute statistics from data points */
|
|
324
|
-
static compute(data) {
|
|
325
|
-
const values = data.map((d) => d.value).filter((v) => v !== null).sort((a, b) => a - b);
|
|
326
|
-
if (values.length === 0) {
|
|
327
|
-
return { min: NaN, max: NaN, avg: NaN, mean: NaN, median: NaN, stdDev: NaN, count: 0 };
|
|
328
|
-
}
|
|
329
|
-
const count = values.length;
|
|
330
|
-
const sum = values.reduce((a, b) => a + b, 0);
|
|
331
|
-
const mean = sum / count;
|
|
332
|
-
const median = count % 2 === 1 ? values[Math.floor(count / 2)] : (values[count / 2 - 1] + values[count / 2]) / 2;
|
|
333
|
-
const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / count;
|
|
334
|
-
const stdDev2 = Math.sqrt(variance);
|
|
335
|
-
return {
|
|
336
|
-
min: values[0],
|
|
337
|
-
max: values[count - 1],
|
|
338
|
-
avg: mean,
|
|
339
|
-
mean,
|
|
340
|
-
median,
|
|
341
|
-
stdDev: stdDev2,
|
|
342
|
-
count
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
/** Compute stats for a specific time range (viewport-scoped) */
|
|
346
|
-
static computeInRange(data, startTime, endTime) {
|
|
347
|
-
const filtered = data.filter((d) => d.time >= startTime && d.time <= endTime);
|
|
348
|
-
return this.compute(filtered);
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
// src/analyze/moving_avg.ts
|
|
353
|
-
var MovingAvg = class _MovingAvg {
|
|
354
|
-
#data;
|
|
355
|
-
#windowSize;
|
|
356
|
-
#type;
|
|
357
|
-
#timeScale;
|
|
358
|
-
#valueScale;
|
|
359
|
-
#stroke;
|
|
360
|
-
#strokeWidth;
|
|
361
|
-
constructor(config) {
|
|
362
|
-
this.#data = [...config.data].sort((a, b) => a.time - b.time);
|
|
363
|
-
this.#windowSize = config.windowSize;
|
|
364
|
-
this.#type = config.type ?? "simple";
|
|
365
|
-
this.#timeScale = config.timeScale;
|
|
366
|
-
this.#valueScale = config.valueScale;
|
|
367
|
-
this.#stroke = config.stroke ?? "#e53e3e";
|
|
368
|
-
this.#strokeWidth = config.strokeWidth ?? 2;
|
|
369
|
-
}
|
|
370
|
-
/** Compute simple moving average points */
|
|
371
|
-
static simple(data, windowSize) {
|
|
372
|
-
const sorted = [...data].filter((d) => d.value !== null).sort((a, b) => a.time - b.time);
|
|
373
|
-
const result = [];
|
|
374
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
375
|
-
const start = Math.max(0, i - windowSize + 1);
|
|
376
|
-
const window = sorted.slice(start, i + 1);
|
|
377
|
-
const avg = window.reduce((s, d) => s + d.value, 0) / window.length;
|
|
378
|
-
result.push({ time: sorted[i].time, value: avg });
|
|
379
|
-
}
|
|
380
|
-
return result;
|
|
381
|
-
}
|
|
382
|
-
/** Compute exponential moving average (EMA) */
|
|
383
|
-
static exponential(data, windowSize) {
|
|
384
|
-
const sorted = [...data].filter((d) => d.value !== null).sort((a, b) => a.time - b.time);
|
|
385
|
-
if (sorted.length === 0) return [];
|
|
386
|
-
const multiplier = 2 / (windowSize + 1);
|
|
387
|
-
const result = [];
|
|
388
|
-
let ema = sorted[0].value;
|
|
389
|
-
result.push({ time: sorted[0].time, value: ema });
|
|
390
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
391
|
-
ema = sorted[i].value * multiplier + ema * (1 - multiplier);
|
|
392
|
-
result.push({ time: sorted[i].time, value: ema });
|
|
393
|
-
}
|
|
394
|
-
return result;
|
|
395
|
-
}
|
|
396
|
-
/** Get computed moving average as pixel points */
|
|
397
|
-
points() {
|
|
398
|
-
const raw = this.#type === "exponential" ? _MovingAvg.exponential(this.#data, this.#windowSize) : _MovingAvg.simple(this.#data, this.#windowSize);
|
|
399
|
-
return raw.map((p) => ({
|
|
400
|
-
x: this.#timeScale.map(p.time),
|
|
401
|
-
y: this.#valueScale.map(p.value)
|
|
402
|
-
}));
|
|
403
|
-
}
|
|
404
|
-
/** Render MA as a path command */
|
|
405
|
-
render() {
|
|
406
|
-
const pts = this.points();
|
|
407
|
-
if (pts.length < 2)
|
|
408
|
-
return pts.map((p) => ({
|
|
409
|
-
type: "circle",
|
|
410
|
-
cx: p.x,
|
|
411
|
-
cy: p.y,
|
|
412
|
-
r: this.#strokeWidth,
|
|
413
|
-
fill: this.#stroke
|
|
414
|
-
}));
|
|
415
|
-
return [
|
|
416
|
-
{
|
|
417
|
-
type: "path",
|
|
418
|
-
points: pts,
|
|
419
|
-
smoothing: true,
|
|
420
|
-
stroke: this.#stroke,
|
|
421
|
-
strokeWidth: this.#strokeWidth
|
|
422
|
-
}
|
|
423
|
-
];
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
// src/analyze/mkt.ts
|
|
428
|
-
var R2 = 8.314;
|
|
429
|
-
var C_TO_K = 273.15;
|
|
430
|
-
var DEFAULT_ACTIVATION_ENERGY = 83144;
|
|
431
|
-
function mkt(samples, activationEnergy = DEFAULT_ACTIVATION_ENERGY) {
|
|
432
|
-
const dhR = activationEnergy / R2;
|
|
433
|
-
let sumExp = 0;
|
|
434
|
-
let count = 0;
|
|
435
|
-
for (const v of samples) {
|
|
436
|
-
if (v === null) continue;
|
|
437
|
-
sumExp += Math.exp(-dhR / (v + C_TO_K));
|
|
438
|
-
count++;
|
|
439
|
-
}
|
|
440
|
-
if (count === 0) return null;
|
|
441
|
-
const meanExp = sumExp / count;
|
|
442
|
-
const mktKelvin = dhR / -Math.log(meanExp);
|
|
443
|
-
return mktKelvin - C_TO_K;
|
|
444
|
-
}
|
|
445
|
-
function rollingMkt(data, windowMs, activationEnergy = DEFAULT_ACTIVATION_ENERGY) {
|
|
446
|
-
const out = new Array(data.length);
|
|
447
|
-
let left = 0;
|
|
448
|
-
for (let i = 0; i < data.length; i++) {
|
|
449
|
-
const t = data[i].time;
|
|
450
|
-
const wStart = t - windowMs;
|
|
451
|
-
while (left < i && data[left].time < wStart) left++;
|
|
452
|
-
const slice = data.slice(left, i + 1).map((p) => p.value);
|
|
453
|
-
const value = mkt(slice, activationEnergy);
|
|
454
|
-
out[i] = { time: t, value, synthetic: true };
|
|
455
|
-
}
|
|
456
|
-
return out;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// src/analyze/std_dev.ts
|
|
460
|
-
function stdDev(samples) {
|
|
461
|
-
let sum = 0;
|
|
462
|
-
let count = 0;
|
|
463
|
-
for (const v of samples) {
|
|
464
|
-
if (v === null) continue;
|
|
465
|
-
sum += v;
|
|
466
|
-
count++;
|
|
467
|
-
}
|
|
468
|
-
if (count === 0) return null;
|
|
469
|
-
const mean = sum / count;
|
|
470
|
-
let sqSum = 0;
|
|
471
|
-
for (const v of samples) {
|
|
472
|
-
if (v === null) continue;
|
|
473
|
-
const d = v - mean;
|
|
474
|
-
sqSum += d * d;
|
|
475
|
-
}
|
|
476
|
-
return Math.sqrt(sqSum / count);
|
|
477
|
-
}
|
|
478
|
-
function sampleStdDev(samples) {
|
|
479
|
-
let sum = 0;
|
|
480
|
-
let count = 0;
|
|
481
|
-
for (const v of samples) {
|
|
482
|
-
if (v === null) continue;
|
|
483
|
-
sum += v;
|
|
484
|
-
count++;
|
|
485
|
-
}
|
|
486
|
-
if (count < 2) return null;
|
|
487
|
-
const mean = sum / count;
|
|
488
|
-
let sqSum = 0;
|
|
489
|
-
for (const v of samples) {
|
|
490
|
-
if (v === null) continue;
|
|
491
|
-
const d = v - mean;
|
|
492
|
-
sqSum += d * d;
|
|
493
|
-
}
|
|
494
|
-
return Math.sqrt(sqSum / (count - 1));
|
|
495
|
-
}
|
|
496
|
-
function rollingStdDev(data, windowMs, sample = false) {
|
|
497
|
-
const out = new Array(data.length);
|
|
498
|
-
let left = 0;
|
|
499
|
-
const compute = sample ? sampleStdDev : stdDev;
|
|
500
|
-
for (let i = 0; i < data.length; i++) {
|
|
501
|
-
const t = data[i].time;
|
|
502
|
-
const wStart = t - windowMs;
|
|
503
|
-
while (left < i && data[left].time < wStart) left++;
|
|
504
|
-
const slice = data.slice(left, i + 1).map((p) => p.value);
|
|
505
|
-
const value = compute(slice);
|
|
506
|
-
out[i] = { time: t, value, synthetic: true };
|
|
507
|
-
}
|
|
508
|
-
return out;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// src/analyze/limits.ts
|
|
512
|
-
function computeLimitExcursions(data, opts) {
|
|
513
|
-
const { high, low } = opts;
|
|
514
|
-
const result = {
|
|
515
|
-
msAboveHigh: 0,
|
|
516
|
-
msBelowLow: 0,
|
|
517
|
-
globalMax: null,
|
|
518
|
-
globalMin: null,
|
|
519
|
-
excursions: []
|
|
520
|
-
};
|
|
521
|
-
let openEpisode = null;
|
|
522
|
-
for (let i = 0; i < data.length; i++) {
|
|
523
|
-
const p = data[i];
|
|
524
|
-
if (p.value === null) {
|
|
525
|
-
if (openEpisode) {
|
|
526
|
-
result.excursions.push(openEpisode);
|
|
527
|
-
openEpisode = null;
|
|
528
|
-
}
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
if (result.globalMax === null || p.value > result.globalMax) result.globalMax = p.value;
|
|
532
|
-
if (result.globalMin === null || p.value < result.globalMin) result.globalMin = p.value;
|
|
533
|
-
const aboveHigh = high !== void 0 && p.value > high;
|
|
534
|
-
const belowLow = low !== void 0 && p.value < low;
|
|
535
|
-
const side = aboveHigh ? "above" : belowLow ? "below" : null;
|
|
536
|
-
const prev = data[i - 1];
|
|
537
|
-
if (prev && prev.value !== null && side !== null) {
|
|
538
|
-
const dt = p.time - prev.time;
|
|
539
|
-
if (side === "above") result.msAboveHigh += dt;
|
|
540
|
-
else result.msBelowLow += dt;
|
|
541
|
-
}
|
|
542
|
-
if (side === null) {
|
|
543
|
-
if (openEpisode) {
|
|
544
|
-
result.excursions.push(openEpisode);
|
|
545
|
-
openEpisode = null;
|
|
546
|
-
}
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
if (openEpisode && openEpisode.side === side) {
|
|
550
|
-
openEpisode.endTime = p.time;
|
|
551
|
-
openEpisode.durationMs = openEpisode.endTime - openEpisode.startTime;
|
|
552
|
-
if (side === "above" && p.value > openEpisode.extremum) openEpisode.extremum = p.value;
|
|
553
|
-
else if (side === "below" && p.value < openEpisode.extremum) openEpisode.extremum = p.value;
|
|
554
|
-
} else {
|
|
555
|
-
if (openEpisode) result.excursions.push(openEpisode);
|
|
556
|
-
openEpisode = {
|
|
557
|
-
startTime: p.time,
|
|
558
|
-
endTime: p.time,
|
|
559
|
-
side,
|
|
560
|
-
extremum: p.value,
|
|
561
|
-
durationMs: 0
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (openEpisode) result.excursions.push(openEpisode);
|
|
566
|
-
return result;
|
|
567
|
-
}
|
|
1
|
+
var H=class{static interpolateDataPoint(n,i,t){return {time:n.time+t*(i.time-n.time),value:(n.value??0)+t*((i.value??0)-(n.value??0))}}static interpolateAggregatedPoint(n,i,t){let u=(e,l)=>e!==null&&l!==null?e+t*(l-e):null;return {time:n.time+t*(i.time-n.time),min:u(n.min,i.min),max:u(n.max,i.max),avg:u(n.avg,i.avg),count:Math.round(n.count+t*(i.count-n.count))}}static getRuns(n,i,t=0){let u=[...n].sort((r,o)=>r.time-o.time),e=[],l=[],a=null;for(let r of u){let o=i(r),m=t>0&&a&&r.time-a.time>t;(o||m)&&l.length&&(e.push(l),l=[]),o||l.push(r),a=r;}return l.length&&e.push(l),e}static splitByBoundaries(n,i,t,u){if(n.length===0)return [];if(i.length===0)return [{data:n,zoneIndex:0}];let e=[...i].sort((o,m)=>o-m),l=[],a=o=>{let m=0;for(let s=0;s<e.length&&o>=e[s];s++)m=s+1;return m},r=[n[0]];for(let o=1;o<n.length;o++){let m=n[o-1],s=n[o],h=t(m),g=t(s),d;g>h?d=e.filter(c=>c>h&&c<=g):g<h?d=e.filter(c=>c>=g&&c<h).reverse():d=[];for(let c of d){let p=(c-h)/(g-h),x=u(m,s,p);r.push(x),l.push({data:r,zoneIndex:a((h+c)/2)}),r=[x];}r.push(s);}if(r.length>0){let o=t(r[0]),m=t(r[r.length-1]);l.push({data:r,zoneIndex:a((o+m)/2)});}return l}static splitByThreshold(n,i,t,u){let e=this.splitByBoundaries(n,[i],t,u),l={above:[],below:[]};for(let a of e)a.zoneIndex===0?l.below.push(a.data):l.above.push(a.data);return l}},L={pharma_cold:{limitLow:2,limitHigh:8,activationEnergy:83144},pharma_ambient:{limitLow:15,limitHigh:25,activationEnergy:83144},blood:{limitLow:2,limitHigh:6,activationEnergy:83144},food_chilled:{limitLow:0,limitHigh:4},frozen:{limitLow:-40,limitHigh:-18}},T=8.314462618,w=273.15;function N(n,i,t,u=1){let e=i.filter(f=>f.value!==null&&f.value!==void 0&&!isNaN(f.value)).map(f=>f.value);if(!e.length)return {time:n,min:null,max:null,avg:null,mkt:null,deltaMkt:null,stdDev:null,minutesAboveHigh:t?0:null,minutesBelowLow:t?0:null,count:0};let l=Math.min(...e),a=Math.max(...e),r=e.reduce((f,v)=>f+v,0)/e.length,o=t?0:null,m=t?0:null;if(t)for(let f=0;f<i.length;f++){let v=i[f];if(v.value!==null&&v.value!==void 0){let b=i[f+1],M=(b?b.time-v.time:0)/(1e3*60);v.value>t.limitHigh&&(o+=M),v.value<t.limitLow&&(m+=M);}}let s=0,h=0,g=(t?.activationEnergy??83144)/T;for(let f of e){s+=Math.pow(f-r,2);let v=f+w;h+=Math.exp(-g/v);}let d=s/e.length,c=Math.sqrt(d),p=null,x=null;if(e.length>=u){let f=h/e.length,v=g/-Math.log(f);p=Number((v-w).toFixed(2)),x=Number((p-r).toFixed(2));}return {time:n,min:Number(l.toFixed(2)),max:Number(a.toFixed(2)),avg:Number(r.toFixed(2)),mkt:p,deltaMkt:x,stdDev:Number(c.toFixed(2)),minutesAboveHigh:o!==null?Number(o.toFixed(1)):null,minutesBelowLow:m!==null?Number(m.toFixed(1)):null,count:e.length}}function D(n,i="hourly",t,u,e=1){if(!n.length)return [];let l=i==="hourly"?36e5:i==="daily"?864e5:t??3600*1e3,a=[...n].sort((s,h)=>s.time-h.time),r=[],o=a[0].time,m=[];for(let s of a){for(;s.time-o>=l;)r.push(N(o,m,u,e)),o+=l,m=[];m.push(s);}return r.push(N(o,m,u,e)),r}function A(n,i){let t=Math.min(...i),u=Math.max(...i),e=i.reduce((l,a)=>l+a,0)/i.length;return {time:n,min:t,max:u,avg:e,count:i.length}}function B(n,i=6e4){if(n.length<2)return [];let t=[...n].sort((e,l)=>e.time-l.time),u=[];for(let e=1;e<t.length;e++)t[e].time-t[e-1].time>i&&u.push({startTime:t[e-1].time,endTime:t[e].time});return u}function I(n){let i=[],t=0,u=0,e=0,l=0;for(let a of n)a.count>0&&(e++,a.minutesAboveHigh&&(t+=a.minutesAboveHigh),a.stdDev&&a.stdDev>u&&(u=a.stdDev),a.deltaMkt&&a.deltaMkt>2&&l++);return e===0?[]:(t>120&&i.push({type:"critical",message:`Kritische Gesamtbelastung: Das Produkt war im gesamten Zeitraum insgesamt ${Math.round(t)} Minuten zu warm.`,metric:"Total Excursion Time"}),l>e*.15&&i.push({type:"warning",message:"H\xE4ufige thermische Schocks erkannt. Die kinetische Temperatur weicht oft stark vom Schnitt ab (m\xF6gliche regelm\xE4\xDFige T\xFCr\xF6ffnungen).",metric:"Delta MKT Instability"}),u>4&&i.push({type:"warning",message:`Hohe Instabilit\xE4t gemessen. Die maximale Standardabweichung lag bei ${u}\xB0C. Das K\xFChlsystem regelt unsauber.`,metric:"Standard Deviation Peak"}),i)}function z(n,i){if(n.length<=i)return n;let t=[],u=Math.ceil(n.length/i);t.push(n[0]);for(let e=1;e<n.length;e+=u)t.push(n[Math.min(e,n.length-1)]);return t}function C(n,i="hourly",t){let u={limitLow:t.low,limitHigh:t.high,activationEnergy:t.activationEnergy};return D(n,i,t.interval,u,t.minCountForMkt??1)}var P=class{static compute(n){let i=n.map(r=>r.value).filter(r=>r!==null).sort((r,o)=>r-o);if(i.length===0)return {min:NaN,max:NaN,avg:NaN,mean:NaN,median:NaN,stdDev:NaN,count:0};let t=i.length,u=i.reduce((r,o)=>r+o,0)/t,e=t%2===1?i[Math.floor(t/2)]:(i[t/2-1]+i[t/2])/2,l=i.reduce((r,o)=>r+Math.pow(o-u,2),0)/t,a=Math.sqrt(l);return {min:i[0],max:i[t-1],avg:u,mean:u,median:e,stdDev:a,count:t}}static computeInRange(n,i,t){let u=n.filter(e=>e.time>=i&&e.time<=t);return this.compute(u)}};function _(n,i){let t=typeof n=="number"?n:n.mean,u=typeof n=="number"?i??NaN:n.stdDev;return !Number.isFinite(t)||!Number.isFinite(u)||t===0?null:Math.abs(u/t)*100}var R=83144;function F(n,i=83144){let t=i/8.314,u=0,e=0;for(let a of n)a!==null&&(u+=Math.exp(-t/(a+273.15)),e++);if(e===0)return null;let l=u/e;return t/-Math.log(l)-273.15}function q(n,i,t=83144){let u=new Array(n.length),e=0;for(let l=0;l<n.length;l++){let a=n[l].time,r=a-i;for(;e<l&&n[e].time<r;)e++;let o=n.slice(e,l+1).map(s=>s.value),m=F(o,t);u[l]={time:a,value:m,synthetic:true};}return u}function k(n){let i=0,t=0;for(let l of n)l!==null&&(i+=l,t++);if(t===0)return null;let u=i/t,e=0;for(let l of n){if(l===null)continue;let a=l-u;e+=a*a;}return Math.sqrt(e/t)}function S(n){let i=0,t=0;for(let l of n)l!==null&&(i+=l,t++);if(t<2)return null;let u=i/t,e=0;for(let l of n){if(l===null)continue;let a=l-u;e+=a*a;}return Math.sqrt(e/(t-1))}function K(n,i,t=false){let u=new Array(n.length),e=0,l=t?S:k;for(let a=0;a<n.length;a++){let r=n[a].time,o=r-i;for(;e<a&&n[e].time<o;)e++;let m=n.slice(e,a+1).map(h=>h.value),s=l(m);u[a]={time:r,value:s,synthetic:true};}return u}function G(n,i){let{high:t,low:u}=i,e={msAboveHigh:0,msBelowLow:0,globalMax:null,globalMin:null,excursions:[]},l=null;for(let a=0;a<n.length;a++){let r=n[a];if(r.value===null){l&&(e.excursions.push(l),l=null);continue}(e.globalMax===null||r.value>e.globalMax)&&(e.globalMax=r.value),(e.globalMin===null||r.value<e.globalMin)&&(e.globalMin=r.value);let o=t!==void 0&&r.value>t,m=u!==void 0&&r.value<u,s=o?"above":m?"below":null,h=n[a-1];if(h&&h.value!==null&&s!==null){let g=r.time-h.time;s==="above"?e.msAboveHigh+=g:e.msBelowLow+=g;}if(s===null){l&&(e.excursions.push(l),l=null);continue}l&&l.side===s?(l.endTime=r.time,l.durationMs=l.endTime-l.startTime,(s==="above"&&r.value>l.extremum||s==="below"&&r.value<l.extremum)&&(l.extremum=r.value)):(l&&e.excursions.push(l),l={startTime:r.time,endTime:r.time,side:s,extremum:r.value,durationMs:0});}return l&&e.excursions.push(l),e}function O(n){if(n.length===0)return [];let i=new Map;for(let u of n)for(let e of u.data){if(e.value===null)continue;let l=i.get(e.time);l||(l=[],i.set(e.time,l)),l.push({name:u.name,value:e.value});}let t=[];for(let u of [...i.keys()].sort((e,l)=>e-l)){let e=i.get(u);if(e.length===0){t.push({time:u,min:null,max:null,delta:null,minSensor:null,maxSensor:null});continue}let l=e[0],a=e[0];for(let r=1;r<e.length;r++)e[r].value<l.value&&(l=e[r]),e[r].value>a.value&&(a=e[r]);t.push({time:u,min:l.value,max:a.value,delta:e.length<2?null:a.value-l.value,minSensor:l.name,maxSensor:a.name});}return t}function U(n){let i=[];for(let e of n){let l=0,a=0,r=1/0,o=-1/0;for(let m of e.data)m.value!==null&&(l+=m.value,a++,m.value<r&&(r=m.value),m.value>o&&(o=m.value));a!==0&&i.push({name:e.name,mean:l/a,min:r,max:o,count:a});}if(i.length===0)return {sensors:[],hottest:null,coldest:null,meanDelta:null};let t=i[0],u=i[0];for(let e=1;e<i.length;e++)i[e].mean>t.mean&&(t=i[e]),i[e].mean<u.mean&&(u=i[e]);return {sensors:i,hottest:t,coldest:u,meanDelta:t.mean-u.mean}}function y(n){let i=0,t=0,u=0,e=0,l=0;for(let m of n)m.value!==null&&(i++,t+=m.time,u+=m.value,e+=m.time*m.value,l+=m.time*m.time);if(i<2)return null;let a=i*l-t*t;if(a===0)return null;let r=(i*e-t*u)/a,o=(u-r*t)/i;return {slope:r,intercept:o,n:i}}function E(n,i){if(n.length===0)return null;let t=n[n.length-1].time,u=t-i,e=n.filter(l=>l.time>=u&&l.time<=t);return y(e)}function V(n,i){let t=[],u=0;for(let e=0;e<n.length;e++){let l=n[e].time,a=l-i;for(;u<n.length&&n[u].time<a;)u++;let r=y(n.slice(u,e+1));t.push({time:l,fit:r});}return t}function W(n,i,t={}){let u=t.lookbackMs??9e5,e=t.side??"either";if(n.length===0)return null;let l=n[n.length-1];if(l.value===null)return null;let a=E(n,u);if(!a||a.slope===0)return null;let r=a.slope>0&&l.value<i,o=a.slope<0&&l.value>i;if(e==="above"&&!r||e==="below"&&!o||e==="either"&&!r&&!o)return null;let m=(i-a.intercept)/a.slope,s=m-l.time;return s<=0?null:{msUntil:s,eta:m,fit:a,currentValue:l.value}}function Y(n,i={}){let t=i.refTempCelsius??121.11,u=i.zValueKelvin??10,e=0;for(let l=0;l<n.length-1;l++){let a=n[l],r=n[l+1];if(a.value===null||r.value===null)continue;let o=(r.time-a.time)/6e4;if(o<=0)continue;let m=Math.pow(10,(a.value-t)/u),s=Math.pow(10,(r.value-t)/u);e+=(m+s)/2*o;}return e}/*!
|
|
2
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
3
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
4
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
5
|
+
*/
|
|
568
6
|
/*!
|
|
569
|
-
*
|
|
7
|
+
* ml-time-analyze — Copyright (c) 2026 Michael Lechner
|
|
570
8
|
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
571
9
|
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
572
10
|
*/
|
|
573
|
-
|
|
574
|
-
|
|
11
|
+
/*!
|
|
12
|
+
* MLTimeGraph — Copyright (c) 2026 Michael Lechner
|
|
13
|
+
* MIT with Attribution: free use incl. commercial requires visible credit to
|
|
14
|
+
* "Michael Lechner". Commercial license (no attribution) on request. See LICENSE.
|
|
15
|
+
*/export{R as DEFAULT_ACTIVATION_ENERGY,L as PRODUCT_PROFILES,H as SeriesProcessor,P as StatsAggregator,D as aggregateBySlot,C as aggregateWithStats,I as analyzeLongTermTrends,G as computeLimitExcursions,A as createAggr,E as currentTrend,B as detectGaps,z as downsample,Y as f0Sterilization,U as hotColdSpots,y as linearFit,F as mkt,W as predictTimeToThreshold,q as rollingMkt,K as rollingStdDev,V as rollingTrend,S as sampleStdDev,O as spatialDelta,k as stdDev,_ as varianceCoefficient};
|