pond-ts 0.1.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 +21 -0
- package/README.md +348 -0
- package/dist/BoundedSequence.d.ts +28 -0
- package/dist/BoundedSequence.d.ts.map +1 -0
- package/dist/BoundedSequence.js +70 -0
- package/dist/BoundedSequence.js.map +1 -0
- package/dist/Event.d.ts +84 -0
- package/dist/Event.d.ts.map +1 -0
- package/dist/Event.js +162 -0
- package/dist/Event.js.map +1 -0
- package/dist/Interval.d.ts +51 -0
- package/dist/Interval.d.ts.map +1 -0
- package/dist/Interval.js +130 -0
- package/dist/Interval.js.map +1 -0
- package/dist/Sequence.d.ts +80 -0
- package/dist/Sequence.d.ts.map +1 -0
- package/dist/Sequence.js +197 -0
- package/dist/Sequence.js.map +1 -0
- package/dist/Time.d.ts +43 -0
- package/dist/Time.d.ts.map +1 -0
- package/dist/Time.js +78 -0
- package/dist/Time.js.map +1 -0
- package/dist/TimeRange.d.ts +45 -0
- package/dist/TimeRange.d.ts.map +1 -0
- package/dist/TimeRange.js +144 -0
- package/dist/TimeRange.js.map +1 -0
- package/dist/TimeSeries.d.ts +337 -0
- package/dist/TimeSeries.d.ts.map +1 -0
- package/dist/TimeSeries.js +1217 -0
- package/dist/TimeSeries.js.map +1 -0
- package/dist/calendar.d.ts +24 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +96 -0
- package/dist/calendar.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/temporal.d.ts +37 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +29 -0
- package/dist/temporal.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +3 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +145 -0
- package/dist/validate.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
import { BoundedSequence } from './BoundedSequence.js';
|
|
2
|
+
import { parseTimestampString } from './calendar.js';
|
|
3
|
+
import { Interval } from './Interval.js';
|
|
4
|
+
import { Time } from './Time.js';
|
|
5
|
+
import { TimeRange } from './TimeRange.js';
|
|
6
|
+
import { Sequence } from './Sequence.js';
|
|
7
|
+
import { validateAndNormalize } from './validate.js';
|
|
8
|
+
function isObjectRow(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function parseJsonTimestamp(value, options = {}) {
|
|
12
|
+
if (typeof value === 'number') {
|
|
13
|
+
if (!Number.isFinite(value)) {
|
|
14
|
+
throw new TypeError('expected finite timestamp');
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return parseTimestampString(value, options);
|
|
20
|
+
}
|
|
21
|
+
if (value instanceof Date) {
|
|
22
|
+
return value.getTime();
|
|
23
|
+
}
|
|
24
|
+
throw new TypeError('expected timestamp as number or string');
|
|
25
|
+
}
|
|
26
|
+
function parseJsonKey(kind, value, options = {}) {
|
|
27
|
+
if (value instanceof Time ||
|
|
28
|
+
value instanceof TimeRange ||
|
|
29
|
+
value instanceof Interval) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
switch (kind) {
|
|
33
|
+
case 'time':
|
|
34
|
+
return new Time(parseJsonTimestamp(value, options));
|
|
35
|
+
case 'timeRange':
|
|
36
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
37
|
+
return new TimeRange({
|
|
38
|
+
start: parseJsonTimestamp(value[0], options),
|
|
39
|
+
end: parseJsonTimestamp(value[1], options),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'object' &&
|
|
43
|
+
value !== null &&
|
|
44
|
+
'start' in value &&
|
|
45
|
+
'end' in value &&
|
|
46
|
+
!('value' in value)) {
|
|
47
|
+
return new TimeRange({
|
|
48
|
+
start: parseJsonTimestamp(value.start, options),
|
|
49
|
+
end: parseJsonTimestamp(value.end, options),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
throw new TypeError('expected timeRange as [start, end] or { start, end }');
|
|
53
|
+
case 'interval':
|
|
54
|
+
if (Array.isArray(value) && value.length === 3) {
|
|
55
|
+
return new Interval({
|
|
56
|
+
value: value[0],
|
|
57
|
+
start: parseJsonTimestamp(value[1], options),
|
|
58
|
+
end: parseJsonTimestamp(value[2], options),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === 'object' &&
|
|
62
|
+
value !== null &&
|
|
63
|
+
'value' in value &&
|
|
64
|
+
'start' in value &&
|
|
65
|
+
'end' in value) {
|
|
66
|
+
return new Interval({
|
|
67
|
+
value: value.value,
|
|
68
|
+
start: parseJsonTimestamp(value.start, options),
|
|
69
|
+
end: parseJsonTimestamp(value.end, options),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
throw new TypeError('expected interval as [value, start, end] or { value, start, end }');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function parseJsonRows(schema, rows, options = {}) {
|
|
76
|
+
return rows.map((row) => {
|
|
77
|
+
const values = isObjectRow(row)
|
|
78
|
+
? schema.map((column) => row[column.name])
|
|
79
|
+
: row;
|
|
80
|
+
return Object.freeze(values.map((value, index) => {
|
|
81
|
+
if (value === null) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const column = schema[index];
|
|
85
|
+
if (index === 0) {
|
|
86
|
+
return parseJsonKey(column.kind, value, options);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function toRows(schema, events) {
|
|
93
|
+
return events.map((event) => {
|
|
94
|
+
const data = event.data();
|
|
95
|
+
return Object.freeze([
|
|
96
|
+
event.key(),
|
|
97
|
+
...schema
|
|
98
|
+
.slice(1)
|
|
99
|
+
.map((column) => data[column.name]),
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function isEventKey(value) {
|
|
104
|
+
return (typeof value === 'object' &&
|
|
105
|
+
value !== null &&
|
|
106
|
+
'begin' in value &&
|
|
107
|
+
'end' in value);
|
|
108
|
+
}
|
|
109
|
+
function toBoundaryTimestamp(value) {
|
|
110
|
+
if (isEventKey(value)) {
|
|
111
|
+
return value.begin();
|
|
112
|
+
}
|
|
113
|
+
return value instanceof Date ? value.getTime() : value;
|
|
114
|
+
}
|
|
115
|
+
function toKey(value) {
|
|
116
|
+
if (isEventKey(value)) {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
if (value.length === 2) {
|
|
121
|
+
return new TimeRange(value);
|
|
122
|
+
}
|
|
123
|
+
return new Interval(value);
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === 'object' && value !== null) {
|
|
126
|
+
if ('value' in value) {
|
|
127
|
+
return new Interval(value);
|
|
128
|
+
}
|
|
129
|
+
if ('start' in value && 'end' in value) {
|
|
130
|
+
return new TimeRange(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return new Time(value);
|
|
134
|
+
}
|
|
135
|
+
function toSelectionRange(value) {
|
|
136
|
+
if (value instanceof TimeRange) {
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
if (value instanceof Interval) {
|
|
140
|
+
return value.timeRange();
|
|
141
|
+
}
|
|
142
|
+
if (isEventKey(value)) {
|
|
143
|
+
return new TimeRange({ start: value.begin(), end: value.end() });
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
if (value.length === 2) {
|
|
147
|
+
return new TimeRange(value);
|
|
148
|
+
}
|
|
149
|
+
return new Interval(value).timeRange();
|
|
150
|
+
}
|
|
151
|
+
if ('value' in value) {
|
|
152
|
+
return new Interval(value).timeRange();
|
|
153
|
+
}
|
|
154
|
+
return new TimeRange(value);
|
|
155
|
+
}
|
|
156
|
+
function toOptionalSeriesRange(value) {
|
|
157
|
+
if (typeof value === 'object' &&
|
|
158
|
+
value !== null &&
|
|
159
|
+
'timeRange' in value &&
|
|
160
|
+
typeof value.timeRange === 'function') {
|
|
161
|
+
return value.timeRange() ?? undefined;
|
|
162
|
+
}
|
|
163
|
+
return toSelectionRange(value);
|
|
164
|
+
}
|
|
165
|
+
function makeAlignedSchema(schema) {
|
|
166
|
+
return Object.freeze([
|
|
167
|
+
{ name: 'interval', kind: 'interval' },
|
|
168
|
+
...schema.slice(1).map((column) => ({
|
|
169
|
+
...column,
|
|
170
|
+
required: false,
|
|
171
|
+
})),
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
function sampleTime(interval, sample) {
|
|
175
|
+
return sample === 'center'
|
|
176
|
+
? interval.begin() + interval.duration() / 2
|
|
177
|
+
: interval.begin();
|
|
178
|
+
}
|
|
179
|
+
function eventAnchorTime(key) {
|
|
180
|
+
return key instanceof Time ? key.begin() : key.timeRange().midpoint();
|
|
181
|
+
}
|
|
182
|
+
function loessAt(x, anchors, values, span) {
|
|
183
|
+
const points = anchors.flatMap((anchor, index) => {
|
|
184
|
+
const value = values[index];
|
|
185
|
+
return value === undefined ? [] : [{ x: anchor, y: value }];
|
|
186
|
+
});
|
|
187
|
+
if (points.length === 0) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (points.length === 1) {
|
|
191
|
+
return points[0].y;
|
|
192
|
+
}
|
|
193
|
+
const neighborCount = Math.max(2, Math.min(points.length, Math.ceil(span * points.length)));
|
|
194
|
+
const sortedDistances = points
|
|
195
|
+
.map((point) => ({ point, distance: Math.abs(point.x - x) }))
|
|
196
|
+
.sort((left, right) => left.distance - right.distance);
|
|
197
|
+
const bandwidth = sortedDistances[neighborCount - 1].distance;
|
|
198
|
+
if (bandwidth === 0) {
|
|
199
|
+
const coincident = sortedDistances
|
|
200
|
+
.filter((entry) => entry.distance === 0)
|
|
201
|
+
.map((entry) => entry.point.y);
|
|
202
|
+
return (coincident.reduce((sum, value) => sum + value, 0) / coincident.length);
|
|
203
|
+
}
|
|
204
|
+
let weightedCount = 0;
|
|
205
|
+
let sumW = 0;
|
|
206
|
+
let sumWX = 0;
|
|
207
|
+
let sumWY = 0;
|
|
208
|
+
let sumWXX = 0;
|
|
209
|
+
let sumWXY = 0;
|
|
210
|
+
for (const { point, distance } of sortedDistances.slice(0, neighborCount)) {
|
|
211
|
+
const ratio = distance / bandwidth;
|
|
212
|
+
const weight = ratio >= 1 ? 0 : (1 - ratio ** 3) ** 3;
|
|
213
|
+
if (weight === 0) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
weightedCount += 1;
|
|
217
|
+
sumW += weight;
|
|
218
|
+
sumWX += weight * point.x;
|
|
219
|
+
sumWY += weight * point.y;
|
|
220
|
+
sumWXX += weight * point.x * point.x;
|
|
221
|
+
sumWXY += weight * point.x * point.y;
|
|
222
|
+
}
|
|
223
|
+
if (weightedCount === 0 || sumW === 0) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const denominator = sumW * sumWXX - sumWX * sumWX;
|
|
227
|
+
if (Math.abs(denominator) < Number.EPSILON) {
|
|
228
|
+
return sumWY / sumW;
|
|
229
|
+
}
|
|
230
|
+
const intercept = (sumWY * sumWXX - sumWX * sumWXY) / denominator;
|
|
231
|
+
const slope = (sumW * sumWXY - sumWX * sumWY) / denominator;
|
|
232
|
+
return intercept + slope * x;
|
|
233
|
+
}
|
|
234
|
+
function makeSmoothSchema(schema, target, output) {
|
|
235
|
+
if (output === undefined || output === target) {
|
|
236
|
+
return Object.freeze([
|
|
237
|
+
schema[0],
|
|
238
|
+
...schema.slice(1).map((column) => column.name === target
|
|
239
|
+
? {
|
|
240
|
+
name: column.name,
|
|
241
|
+
kind: 'number',
|
|
242
|
+
required: false,
|
|
243
|
+
}
|
|
244
|
+
: column),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
if (schema.slice(1).some((column) => column.name === output)) {
|
|
248
|
+
throw new TypeError(`smooth output column '${output}' already exists`);
|
|
249
|
+
}
|
|
250
|
+
return Object.freeze([
|
|
251
|
+
schema[0],
|
|
252
|
+
...schema.slice(1),
|
|
253
|
+
{ name: output, kind: 'number', required: false },
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
function toBoundedSequence(sequence, range, sample) {
|
|
257
|
+
return sequence instanceof BoundedSequence
|
|
258
|
+
? sequence
|
|
259
|
+
: sequence.bounded(range, { sample });
|
|
260
|
+
}
|
|
261
|
+
function isTimeKeyed(series) {
|
|
262
|
+
return series.firstColumnKind === 'time';
|
|
263
|
+
}
|
|
264
|
+
function bucketContainsHalfOpen(bucket, timestamp) {
|
|
265
|
+
return timestamp >= bucket.begin() && timestamp < bucket.end();
|
|
266
|
+
}
|
|
267
|
+
function bucketOverlapsHalfOpen(bucket, event) {
|
|
268
|
+
if (event.begin() === event.end()) {
|
|
269
|
+
return bucketContainsHalfOpen(bucket, event.begin());
|
|
270
|
+
}
|
|
271
|
+
return event.begin() < bucket.end() && bucket.begin() < event.end();
|
|
272
|
+
}
|
|
273
|
+
function aggregateValues(operation, values) {
|
|
274
|
+
const defined = values.filter((value) => value !== undefined);
|
|
275
|
+
const numeric = defined.filter((value) => typeof value === 'number');
|
|
276
|
+
switch (operation) {
|
|
277
|
+
case 'count':
|
|
278
|
+
return defined.length;
|
|
279
|
+
case 'sum':
|
|
280
|
+
return numeric.reduce((sum, value) => sum + value, 0);
|
|
281
|
+
case 'avg':
|
|
282
|
+
return numeric.length === 0
|
|
283
|
+
? undefined
|
|
284
|
+
: numeric.reduce((sum, value) => sum + value, 0) / numeric.length;
|
|
285
|
+
case 'min':
|
|
286
|
+
return numeric.length === 0
|
|
287
|
+
? undefined
|
|
288
|
+
: numeric.reduce((left, right) => (left <= right ? left : right));
|
|
289
|
+
case 'max':
|
|
290
|
+
return numeric.length === 0
|
|
291
|
+
? undefined
|
|
292
|
+
: numeric.reduce((left, right) => (left >= right ? left : right));
|
|
293
|
+
case 'first':
|
|
294
|
+
return defined[0];
|
|
295
|
+
case 'last':
|
|
296
|
+
return defined[defined.length - 1];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function parseDurationInput(value) {
|
|
300
|
+
if (typeof value === 'number') {
|
|
301
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
302
|
+
throw new TypeError('rolling window must be a positive finite number of milliseconds');
|
|
303
|
+
}
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
const match = /^(\d+)(ms|s|m|h|d)$/.exec(value);
|
|
307
|
+
if (!match) {
|
|
308
|
+
throw new TypeError(`unsupported duration '${value}'`);
|
|
309
|
+
}
|
|
310
|
+
const amount = Number(match[1]);
|
|
311
|
+
const unit = match[2];
|
|
312
|
+
const multiplier = unit === 'ms'
|
|
313
|
+
? 1
|
|
314
|
+
: unit === 's'
|
|
315
|
+
? 1_000
|
|
316
|
+
: unit === 'm'
|
|
317
|
+
? 60_000
|
|
318
|
+
: unit === 'h'
|
|
319
|
+
? 3_600_000
|
|
320
|
+
: 86_400_000;
|
|
321
|
+
return amount * multiplier;
|
|
322
|
+
}
|
|
323
|
+
function duplicateValueColumnNames(schemas) {
|
|
324
|
+
const counts = new Map();
|
|
325
|
+
for (const schema of schemas) {
|
|
326
|
+
for (const column of schema.slice(1)) {
|
|
327
|
+
counts.set(column.name, (counts.get(column.name) ?? 0) + 1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return [...counts.entries()]
|
|
331
|
+
.filter(([, count]) => count > 1)
|
|
332
|
+
.map(([name]) => name)
|
|
333
|
+
.sort();
|
|
334
|
+
}
|
|
335
|
+
function assertDistinctValueColumns(schemas, message) {
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
const duplicates = new Set();
|
|
338
|
+
for (const schema of schemas) {
|
|
339
|
+
for (const column of schema.slice(1)) {
|
|
340
|
+
if (seen.has(column.name)) {
|
|
341
|
+
duplicates.add(column.name);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
seen.add(column.name);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (duplicates.size > 0) {
|
|
349
|
+
throw new TypeError(`${message}: ${[...duplicates].sort().join(', ')}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function buildConflictRenameMap(schema, duplicates, prefix) {
|
|
353
|
+
const renameMap = {};
|
|
354
|
+
for (const column of schema.slice(1)) {
|
|
355
|
+
if (duplicates.has(column.name)) {
|
|
356
|
+
renameMap[column.name] = `${prefix}_${column.name}`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return renameMap;
|
|
360
|
+
}
|
|
361
|
+
function prepareSeriesForJoin(series, options) {
|
|
362
|
+
const conflictMode = options.onConflict ?? 'error';
|
|
363
|
+
const duplicates = duplicateValueColumnNames(series.map((item) => item.schema));
|
|
364
|
+
if (duplicates.length === 0) {
|
|
365
|
+
return series;
|
|
366
|
+
}
|
|
367
|
+
if (conflictMode === 'error') {
|
|
368
|
+
throw new TypeError(`cannot join series with duplicate column names: ${duplicates.join(', ')}`);
|
|
369
|
+
}
|
|
370
|
+
const prefixOptions = options;
|
|
371
|
+
if (prefixOptions.prefixes.length !== series.length) {
|
|
372
|
+
throw new TypeError(`prefix conflict handling requires exactly ${series.length} prefixes`);
|
|
373
|
+
}
|
|
374
|
+
const duplicateSet = new Set(duplicates);
|
|
375
|
+
const renamedSeries = series.map((item, index) => {
|
|
376
|
+
const renameMap = buildConflictRenameMap(item.schema, duplicateSet, prefixOptions.prefixes[index]);
|
|
377
|
+
return item.rename(renameMap);
|
|
378
|
+
});
|
|
379
|
+
assertDistinctValueColumns(renamedSeries.map((item) => item.schema), 'prefix conflict handling still produced duplicate column names');
|
|
380
|
+
return renamedSeries;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* An immutable ordered collection of typed events sharing a common schema.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* const schema = [
|
|
388
|
+
* { name: "time", kind: "time" },
|
|
389
|
+
* { name: "cpu", kind: "number" },
|
|
390
|
+
* { name: "host", kind: "string" },
|
|
391
|
+
* ] as const;
|
|
392
|
+
*
|
|
393
|
+
* const series = new TimeSeries({
|
|
394
|
+
* name: "cpu-usage",
|
|
395
|
+
* schema,
|
|
396
|
+
* rows: [[new Date("2025-01-01T00:00:00.000Z"), 0.42, "api-1"]],
|
|
397
|
+
* });
|
|
398
|
+
*
|
|
399
|
+
* series.first()?.get("cpu"); // 0.42
|
|
400
|
+
* series.timeRange(); // overall extent of the series
|
|
401
|
+
* series.within(new TimeRange({ start: 0, end: Date.now() })); // fully contained events
|
|
402
|
+
* series.align(Sequence.every("1m")); // uses the series range over an epoch-anchored minute grid
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
export class TimeSeries {
|
|
406
|
+
name;
|
|
407
|
+
schema;
|
|
408
|
+
events;
|
|
409
|
+
static joinMany(series, options = {}) {
|
|
410
|
+
const prepared = prepareSeriesForJoin(series, options);
|
|
411
|
+
const [first, ...rest] = prepared;
|
|
412
|
+
let joined = first;
|
|
413
|
+
for (const next of rest) {
|
|
414
|
+
joined =
|
|
415
|
+
options.type === undefined
|
|
416
|
+
? joined.join(next)
|
|
417
|
+
: joined.join(next, {
|
|
418
|
+
type: options.type,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return joined;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Example: `TimeSeries.fromJSON({ name, schema, rows, parse: { timeZone: "Europe/Madrid" } })`.
|
|
425
|
+
* Creates a typed series from JSON-style row arrays or object rows keyed by schema column names.
|
|
426
|
+
*
|
|
427
|
+
* `null` values are treated as missing values. Ambiguous local timestamp strings are parsed using
|
|
428
|
+
* the supplied `parse.timeZone`, which defaults to `UTC`.
|
|
429
|
+
*/
|
|
430
|
+
static fromJSON(input) {
|
|
431
|
+
return new TimeSeries({
|
|
432
|
+
name: input.name,
|
|
433
|
+
schema: input.schema,
|
|
434
|
+
rows: parseJsonRows(input.schema, input.rows, input.parse),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** Example: `new TimeSeries({ name, schema, rows })`. Creates an immutable time series from a schema and row-oriented input data. */
|
|
438
|
+
constructor(input) {
|
|
439
|
+
this.name = input.name;
|
|
440
|
+
this.schema = Object.freeze(input.schema.slice());
|
|
441
|
+
this.events = validateAndNormalize(input);
|
|
442
|
+
Object.freeze(this);
|
|
443
|
+
}
|
|
444
|
+
/** Example: `series.firstColumnKind`. Returns the first-column kind from the series schema. */
|
|
445
|
+
get firstColumnKind() {
|
|
446
|
+
return this.schema[0].kind;
|
|
447
|
+
}
|
|
448
|
+
/** Example: `series.rows`. Returns the normalized row view of the series. */
|
|
449
|
+
get rows() {
|
|
450
|
+
return toRows(this.schema, this.events);
|
|
451
|
+
}
|
|
452
|
+
/** Example: `series.at(0)`. Returns the event at the supplied zero-based position, if present. */
|
|
453
|
+
at(index) {
|
|
454
|
+
return this.events[index];
|
|
455
|
+
}
|
|
456
|
+
/** Example: `series.first()`. Returns the first event in the series, if present. */
|
|
457
|
+
first() {
|
|
458
|
+
return this.at(0);
|
|
459
|
+
}
|
|
460
|
+
/** Example: `series.last()`. Returns the last event in the series, if present. */
|
|
461
|
+
last() {
|
|
462
|
+
return this.events.length === 0
|
|
463
|
+
? undefined
|
|
464
|
+
: this.events[this.events.length - 1];
|
|
465
|
+
}
|
|
466
|
+
/** Example: `series.map(nextSchema, event => event)`. Maps each event into a new typed schema and returns a new series. */
|
|
467
|
+
map(schema, mapper) {
|
|
468
|
+
const mappedEvents = this.events.map((event, index) => mapper(event, index));
|
|
469
|
+
return new TimeSeries({
|
|
470
|
+
name: this.name,
|
|
471
|
+
schema,
|
|
472
|
+
rows: toRows(schema, mappedEvents),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/** Example: `series.asTime({ at: "center" })`. Converts the series key type to `"time"` using the supplied anchor within each event extent. */
|
|
476
|
+
asTime(options = {}) {
|
|
477
|
+
const schema = Object.freeze([
|
|
478
|
+
{ name: 'time', kind: 'time' },
|
|
479
|
+
...this.schema.slice(1),
|
|
480
|
+
]);
|
|
481
|
+
return new TimeSeries({
|
|
482
|
+
name: this.name,
|
|
483
|
+
schema,
|
|
484
|
+
rows: toRows(schema, this.events.map((event) => event.asTime(options))),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/** Example: `series.asTimeRange()`. Converts the series key type to `"timeRange"` while preserving each event extent. */
|
|
488
|
+
asTimeRange() {
|
|
489
|
+
const schema = Object.freeze([
|
|
490
|
+
{ name: 'timeRange', kind: 'timeRange' },
|
|
491
|
+
...this.schema.slice(1),
|
|
492
|
+
]);
|
|
493
|
+
return new TimeSeries({
|
|
494
|
+
name: this.name,
|
|
495
|
+
schema,
|
|
496
|
+
rows: toRows(schema, this.events.map((event) => event.asTimeRange())),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
asInterval(value) {
|
|
500
|
+
const schema = Object.freeze([
|
|
501
|
+
{ name: 'interval', kind: 'interval' },
|
|
502
|
+
...this.schema.slice(1),
|
|
503
|
+
]);
|
|
504
|
+
const nextEvents = this.events.map((event, index) => {
|
|
505
|
+
return typeof value === 'function'
|
|
506
|
+
? event.asInterval(() => value(event, index))
|
|
507
|
+
: event.asInterval(value);
|
|
508
|
+
});
|
|
509
|
+
return new TimeSeries({
|
|
510
|
+
name: this.name,
|
|
511
|
+
schema,
|
|
512
|
+
rows: toRows(schema, nextEvents),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
join(other, options = {}) {
|
|
516
|
+
const [left, right] = prepareSeriesForJoin([this, other], options);
|
|
517
|
+
const joinType = options.type ?? 'outer';
|
|
518
|
+
if (left.firstColumnKind !== right.firstColumnKind) {
|
|
519
|
+
throw new TypeError('cannot join series with different key kinds');
|
|
520
|
+
}
|
|
521
|
+
const resultSchema = Object.freeze([
|
|
522
|
+
left.schema[0],
|
|
523
|
+
...left.schema
|
|
524
|
+
.slice(1)
|
|
525
|
+
.map((column) => ({ ...column, required: false })),
|
|
526
|
+
...right.schema
|
|
527
|
+
.slice(1)
|
|
528
|
+
.map((column) => ({ ...column, required: false })),
|
|
529
|
+
]);
|
|
530
|
+
const joinedEvents = [];
|
|
531
|
+
let leftIndex = 0;
|
|
532
|
+
let rightIndex = 0;
|
|
533
|
+
while (leftIndex < left.events.length || rightIndex < right.events.length) {
|
|
534
|
+
const leftEvent = left.events[leftIndex];
|
|
535
|
+
const rightEvent = right.events[rightIndex];
|
|
536
|
+
if (leftEvent && !rightEvent) {
|
|
537
|
+
if (joinType === 'left' || joinType === 'outer') {
|
|
538
|
+
joinedEvents.push(leftEvent.merge({}));
|
|
539
|
+
}
|
|
540
|
+
leftIndex += 1;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (rightEvent && !leftEvent) {
|
|
544
|
+
if (joinType === 'right' || joinType === 'outer') {
|
|
545
|
+
joinedEvents.push(rightEvent.merge({}));
|
|
546
|
+
}
|
|
547
|
+
rightIndex += 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const comparison = leftEvent.key().compare(rightEvent.key());
|
|
551
|
+
if (comparison === 0) {
|
|
552
|
+
joinedEvents.push(leftEvent.merge(rightEvent.data()));
|
|
553
|
+
leftIndex += 1;
|
|
554
|
+
rightIndex += 1;
|
|
555
|
+
}
|
|
556
|
+
else if (comparison < 0) {
|
|
557
|
+
if (joinType === 'left' || joinType === 'outer') {
|
|
558
|
+
joinedEvents.push(leftEvent.merge({}));
|
|
559
|
+
}
|
|
560
|
+
leftIndex += 1;
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
if (joinType === 'right' || joinType === 'outer') {
|
|
564
|
+
joinedEvents.push(rightEvent.merge({}));
|
|
565
|
+
}
|
|
566
|
+
rightIndex += 1;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return new TimeSeries({
|
|
570
|
+
name: left.name,
|
|
571
|
+
schema: resultSchema,
|
|
572
|
+
rows: toRows(resultSchema, joinedEvents),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Example: `series.align(Sequence.every("1m"))`.
|
|
577
|
+
* Aligns the series onto a `Sequence` grid or `BoundedSequence` and returns an interval-keyed series.
|
|
578
|
+
*
|
|
579
|
+
* `hold` carries forward the latest known value to each sample position. `linear` interpolates
|
|
580
|
+
* numeric columns between neighboring time-keyed events and falls back to hold behavior for
|
|
581
|
+
* non-numeric columns. Aligned columns are optional because edge buckets may have no value.
|
|
582
|
+
*
|
|
583
|
+
* Defaults:
|
|
584
|
+
* - `method`: `"hold"`
|
|
585
|
+
* - `sample`: `"begin"`
|
|
586
|
+
* - `range`: `series.timeRange()`
|
|
587
|
+
*
|
|
588
|
+
* For `Sequence` inputs, the sequence anchor still comes from the grid definition itself. For
|
|
589
|
+
* procedural sequences created with `Sequence.every(...)`, that anchor defaults to Unix epoch
|
|
590
|
+
* `0`. The `range` only decides which finite slice of that grid is bounded for this alignment.
|
|
591
|
+
*
|
|
592
|
+
* When a `BoundedSequence` is supplied, its intervals are used directly.
|
|
593
|
+
*
|
|
594
|
+
* Example:
|
|
595
|
+
* - `Sequence.every("1m")` defines an epoch-anchored minute grid
|
|
596
|
+
* - `series.align(Sequence.every("1m"))` aligns onto the slice of that minute grid spanning the
|
|
597
|
+
* current series extent
|
|
598
|
+
*/
|
|
599
|
+
align(sequence, options = {}) {
|
|
600
|
+
const method = options.method ?? 'hold';
|
|
601
|
+
const sample = options.sample ?? 'begin';
|
|
602
|
+
const range = options.range ?? this.timeRange();
|
|
603
|
+
const resultSchema = makeAlignedSchema(this.schema);
|
|
604
|
+
if (!range) {
|
|
605
|
+
return new TimeSeries({
|
|
606
|
+
name: this.name,
|
|
607
|
+
schema: resultSchema,
|
|
608
|
+
rows: [],
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (method === 'linear' && !isTimeKeyed(this)) {
|
|
612
|
+
throw new TypeError('linear alignment currently requires a time-keyed series');
|
|
613
|
+
}
|
|
614
|
+
const intervals = toBoundedSequence(sequence, range, sample).intervals();
|
|
615
|
+
const valueColumns = this.schema.slice(1);
|
|
616
|
+
const alignedRows = intervals.map((interval) => {
|
|
617
|
+
const t = sampleTime(interval, sample);
|
|
618
|
+
const data = method === 'linear'
|
|
619
|
+
? this.#alignLinearAt(t, valueColumns)
|
|
620
|
+
: this.#alignHoldAt(t);
|
|
621
|
+
return Object.freeze([
|
|
622
|
+
interval,
|
|
623
|
+
...resultSchema
|
|
624
|
+
.slice(1)
|
|
625
|
+
.map((column) => data[column.name]),
|
|
626
|
+
]);
|
|
627
|
+
});
|
|
628
|
+
return new TimeSeries({
|
|
629
|
+
name: this.name,
|
|
630
|
+
schema: resultSchema,
|
|
631
|
+
rows: alignedRows,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Example: `series.aggregate(Sequence.every("1m"), { value: "avg" })`.
|
|
636
|
+
* Aggregates events into sequence buckets using built-in reducer names.
|
|
637
|
+
*
|
|
638
|
+
* Buckets use half-open membership semantics: `[begin, end)`. Point events contribute to the
|
|
639
|
+
* bucket containing their timestamp. Interval-like events contribute to every bucket they
|
|
640
|
+
* overlap under half-open overlap rules.
|
|
641
|
+
*
|
|
642
|
+
* Defaults:
|
|
643
|
+
* - `range`: `series.timeRange()`
|
|
644
|
+
*
|
|
645
|
+
* As with `align(...)`, `Sequence` defines the underlying grid and `range` selects which portion
|
|
646
|
+
* of that grid is bounded. With `Sequence.every(...)`, the default grid anchor is Unix epoch `0`,
|
|
647
|
+
* but the default aggregation range is always the source series extent. When a
|
|
648
|
+
* `BoundedSequence` is supplied, its intervals are used directly.
|
|
649
|
+
*
|
|
650
|
+
* Override `range` when you need multiple series aggregated over the same reporting window,
|
|
651
|
+
* including leading or trailing empty buckets outside an individual series extent.
|
|
652
|
+
*
|
|
653
|
+
* To align buckets to the beginning of the current series instead of epoch boundaries, override
|
|
654
|
+
* the sequence anchor rather than the aggregation range:
|
|
655
|
+
*
|
|
656
|
+
* ```ts
|
|
657
|
+
* const range = series.timeRange();
|
|
658
|
+
* if (!range) {
|
|
659
|
+
* throw new Error("empty series");
|
|
660
|
+
* }
|
|
661
|
+
*
|
|
662
|
+
* const aggregated = series.aggregate(
|
|
663
|
+
* Sequence.every("1m", { anchor: range.begin() }),
|
|
664
|
+
* { value: "avg" },
|
|
665
|
+
* );
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
668
|
+
aggregate(sequence, mapping, options = {}) {
|
|
669
|
+
const range = options.range ?? this.timeRange();
|
|
670
|
+
const resultSchema = Object.freeze([
|
|
671
|
+
{ name: 'interval', kind: 'interval' },
|
|
672
|
+
...this.schema
|
|
673
|
+
.slice(1)
|
|
674
|
+
.filter((column) => column.name in mapping)
|
|
675
|
+
.map((column) => {
|
|
676
|
+
const operation = mapping[column.name];
|
|
677
|
+
return {
|
|
678
|
+
name: column.name,
|
|
679
|
+
kind: operation === 'sum' ||
|
|
680
|
+
operation === 'avg' ||
|
|
681
|
+
operation === 'count'
|
|
682
|
+
? 'number'
|
|
683
|
+
: column.kind,
|
|
684
|
+
required: false,
|
|
685
|
+
};
|
|
686
|
+
}),
|
|
687
|
+
]);
|
|
688
|
+
if (!range) {
|
|
689
|
+
return new TimeSeries({
|
|
690
|
+
name: this.name,
|
|
691
|
+
schema: resultSchema,
|
|
692
|
+
rows: [],
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const buckets = toBoundedSequence(sequence, range, 'begin').intervals();
|
|
696
|
+
const resultRows = buckets.map((bucket) => {
|
|
697
|
+
const contributors = this.events.filter((event) => bucketOverlapsHalfOpen(bucket, event.key()));
|
|
698
|
+
const aggregated = resultSchema.slice(1).map((column) => {
|
|
699
|
+
const operation = mapping[column.name];
|
|
700
|
+
const values = contributors.map((event) => {
|
|
701
|
+
const data = event.data();
|
|
702
|
+
return data[column.name];
|
|
703
|
+
});
|
|
704
|
+
return aggregateValues(operation, values);
|
|
705
|
+
});
|
|
706
|
+
return Object.freeze([bucket, ...aggregated]);
|
|
707
|
+
});
|
|
708
|
+
return new TimeSeries({
|
|
709
|
+
name: this.name,
|
|
710
|
+
schema: resultSchema,
|
|
711
|
+
rows: resultRows,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
rolling(sequenceOrWindow, windowOrMapping, mappingOrOptions, maybeOptions = {}) {
|
|
715
|
+
const buildResultColumns = () => this.schema
|
|
716
|
+
.slice(1)
|
|
717
|
+
.filter((column) => column.name in mapping)
|
|
718
|
+
.map((column) => {
|
|
719
|
+
const operation = mapping[column.name];
|
|
720
|
+
return {
|
|
721
|
+
name: column.name,
|
|
722
|
+
kind: operation === 'sum' ||
|
|
723
|
+
operation === 'avg' ||
|
|
724
|
+
operation === 'count'
|
|
725
|
+
? 'number'
|
|
726
|
+
: column.kind,
|
|
727
|
+
required: false,
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
let mapping;
|
|
731
|
+
let options;
|
|
732
|
+
let sequence;
|
|
733
|
+
let window;
|
|
734
|
+
if (sequenceOrWindow instanceof Sequence ||
|
|
735
|
+
sequenceOrWindow instanceof BoundedSequence) {
|
|
736
|
+
sequence = sequenceOrWindow;
|
|
737
|
+
window = windowOrMapping;
|
|
738
|
+
mapping = mappingOrOptions;
|
|
739
|
+
options = maybeOptions;
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
window = sequenceOrWindow;
|
|
743
|
+
mapping = windowOrMapping;
|
|
744
|
+
options =
|
|
745
|
+
mappingOrOptions ??
|
|
746
|
+
{};
|
|
747
|
+
}
|
|
748
|
+
const windowMs = parseDurationInput(window);
|
|
749
|
+
const alignment = options.alignment ?? 'trailing';
|
|
750
|
+
const anchorInWindow = (candidate, anchor) => {
|
|
751
|
+
if (alignment === 'trailing') {
|
|
752
|
+
return candidate > anchor - windowMs && candidate <= anchor;
|
|
753
|
+
}
|
|
754
|
+
if (alignment === 'leading') {
|
|
755
|
+
return candidate >= anchor && candidate < anchor + windowMs;
|
|
756
|
+
}
|
|
757
|
+
const halfWindow = windowMs / 2;
|
|
758
|
+
return (candidate >= anchor - halfWindow && candidate < anchor + halfWindow);
|
|
759
|
+
};
|
|
760
|
+
if (sequence) {
|
|
761
|
+
const sample = options.sample ?? 'begin';
|
|
762
|
+
const range = options.range ?? this.timeRange();
|
|
763
|
+
const resultSchema = Object.freeze([
|
|
764
|
+
{ name: 'interval', kind: 'interval' },
|
|
765
|
+
...buildResultColumns(),
|
|
766
|
+
]);
|
|
767
|
+
if (!range) {
|
|
768
|
+
return new TimeSeries({
|
|
769
|
+
name: this.name,
|
|
770
|
+
schema: resultSchema,
|
|
771
|
+
rows: [],
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const buckets = toBoundedSequence(sequence, range, sample).intervals();
|
|
775
|
+
const resultRows = buckets.map((bucket) => {
|
|
776
|
+
const anchor = sampleTime(bucket, sample);
|
|
777
|
+
const contributors = this.events.filter((candidate) => anchorInWindow(candidate.begin(), anchor));
|
|
778
|
+
const aggregated = resultSchema.slice(1).map((column) => {
|
|
779
|
+
const operation = mapping[column.name];
|
|
780
|
+
const values = contributors.map((candidate) => {
|
|
781
|
+
const data = candidate.data();
|
|
782
|
+
return data[column.name];
|
|
783
|
+
});
|
|
784
|
+
return aggregateValues(operation, values);
|
|
785
|
+
});
|
|
786
|
+
return Object.freeze([bucket, ...aggregated]);
|
|
787
|
+
});
|
|
788
|
+
return new TimeSeries({
|
|
789
|
+
name: this.name,
|
|
790
|
+
schema: resultSchema,
|
|
791
|
+
rows: resultRows,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
const resultSchema = Object.freeze([
|
|
795
|
+
this.schema[0],
|
|
796
|
+
...buildResultColumns(),
|
|
797
|
+
]);
|
|
798
|
+
const resultRows = this.events.map((event) => {
|
|
799
|
+
const anchor = event.begin();
|
|
800
|
+
const contributors = this.events.filter((candidate) => anchorInWindow(candidate.begin(), anchor));
|
|
801
|
+
const aggregated = resultSchema.slice(1).map((column) => {
|
|
802
|
+
const operation = mapping[column.name];
|
|
803
|
+
const values = contributors.map((candidate) => {
|
|
804
|
+
const data = candidate.data();
|
|
805
|
+
return data[column.name];
|
|
806
|
+
});
|
|
807
|
+
return aggregateValues(operation, values);
|
|
808
|
+
});
|
|
809
|
+
return Object.freeze([event.key(), ...aggregated]);
|
|
810
|
+
});
|
|
811
|
+
return new TimeSeries({
|
|
812
|
+
name: this.name,
|
|
813
|
+
schema: resultSchema,
|
|
814
|
+
rows: resultRows,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Example: `series.smooth("value", "ema", { alpha: 0.2 })`.
|
|
819
|
+
* Applies a smoothing transform to one numeric payload column while preserving the original key
|
|
820
|
+
* type, key values, and all non-target payload fields.
|
|
821
|
+
*
|
|
822
|
+
* Example: `series.smooth("value", "movingAverage", { window: "5m", alignment: "centered", output: "valueAvg" })`.
|
|
823
|
+
* Computes a moving average over the selected numeric column using anchor points derived from
|
|
824
|
+
* event keys. `Time` keys use their timestamp. `TimeRange` and `Interval` keys use the midpoint
|
|
825
|
+
* of their extent.
|
|
826
|
+
*
|
|
827
|
+
* Example: `series.smooth("value", "loess", { span: 0.75 })`.
|
|
828
|
+
* Computes a LOESS-smoothed value for the selected numeric column using local weighted linear
|
|
829
|
+
* regression over those same anchor points.
|
|
830
|
+
*
|
|
831
|
+
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
832
|
+
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
833
|
+
*/
|
|
834
|
+
smooth(column, method, options) {
|
|
835
|
+
const output = options.output;
|
|
836
|
+
const resultSchema = output === undefined
|
|
837
|
+
? makeSmoothSchema(this.schema, column)
|
|
838
|
+
: makeSmoothSchema(this.schema, column, output);
|
|
839
|
+
const anchors = this.events.map((event) => eventAnchorTime(event.key()));
|
|
840
|
+
const sourceValues = this.events.map((event) => {
|
|
841
|
+
const raw = event.get(column);
|
|
842
|
+
return typeof raw === 'number' ? raw : undefined;
|
|
843
|
+
});
|
|
844
|
+
if (method === 'ema') {
|
|
845
|
+
if (!('alpha' in options)) {
|
|
846
|
+
throw new TypeError('ema smoothing requires an alpha option');
|
|
847
|
+
}
|
|
848
|
+
const alpha = options.alpha;
|
|
849
|
+
if (typeof alpha !== 'number' ||
|
|
850
|
+
!Number.isFinite(alpha) ||
|
|
851
|
+
alpha <= 0 ||
|
|
852
|
+
alpha > 1) {
|
|
853
|
+
throw new TypeError('ema smoothing requires alpha to be a finite number in the range (0, 1]');
|
|
854
|
+
}
|
|
855
|
+
let previous;
|
|
856
|
+
const resultRows = this.events.map((event) => {
|
|
857
|
+
const raw = event.get(column);
|
|
858
|
+
const smoothed = typeof raw !== 'number'
|
|
859
|
+
? undefined
|
|
860
|
+
: previous === undefined
|
|
861
|
+
? raw
|
|
862
|
+
: alpha * raw + (1 - alpha) * previous;
|
|
863
|
+
if (smoothed !== undefined) {
|
|
864
|
+
previous = smoothed;
|
|
865
|
+
}
|
|
866
|
+
const nextEvent = output === undefined
|
|
867
|
+
? event.set(column, smoothed)
|
|
868
|
+
: event.merge({ [output]: smoothed });
|
|
869
|
+
return Object.freeze([
|
|
870
|
+
nextEvent.key(),
|
|
871
|
+
...resultSchema
|
|
872
|
+
.slice(1)
|
|
873
|
+
.map((nextColumn) => nextEvent.data()[nextColumn.name]),
|
|
874
|
+
]);
|
|
875
|
+
});
|
|
876
|
+
return new TimeSeries({
|
|
877
|
+
name: this.name,
|
|
878
|
+
schema: resultSchema,
|
|
879
|
+
rows: resultRows,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (method === 'loess') {
|
|
883
|
+
if (!('span' in options)) {
|
|
884
|
+
throw new TypeError('loess smoothing requires a span option');
|
|
885
|
+
}
|
|
886
|
+
const span = options.span;
|
|
887
|
+
if (typeof span !== 'number' ||
|
|
888
|
+
!Number.isFinite(span) ||
|
|
889
|
+
span <= 0 ||
|
|
890
|
+
span > 1) {
|
|
891
|
+
throw new TypeError('loess smoothing requires span to be a finite number in the range (0, 1]');
|
|
892
|
+
}
|
|
893
|
+
const resultRows = this.events.map((event, index) => {
|
|
894
|
+
const smoothed = loessAt(anchors[index], anchors, sourceValues, span);
|
|
895
|
+
const nextEvent = output === undefined
|
|
896
|
+
? event.set(column, smoothed)
|
|
897
|
+
: event.merge({ [output]: smoothed });
|
|
898
|
+
return Object.freeze([
|
|
899
|
+
nextEvent.key(),
|
|
900
|
+
...resultSchema
|
|
901
|
+
.slice(1)
|
|
902
|
+
.map((nextColumn) => nextEvent.data()[nextColumn.name]),
|
|
903
|
+
]);
|
|
904
|
+
});
|
|
905
|
+
return new TimeSeries({
|
|
906
|
+
name: this.name,
|
|
907
|
+
schema: resultSchema,
|
|
908
|
+
rows: resultRows,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
if (!('window' in options)) {
|
|
912
|
+
throw new TypeError('movingAverage smoothing requires a window option');
|
|
913
|
+
}
|
|
914
|
+
const window = options.window;
|
|
915
|
+
const windowMs = parseDurationInput(window);
|
|
916
|
+
const alignment = options.alignment ?? 'trailing';
|
|
917
|
+
const anchorInWindow = (candidate, anchor) => {
|
|
918
|
+
if (alignment === 'trailing') {
|
|
919
|
+
return candidate > anchor - windowMs && candidate <= anchor;
|
|
920
|
+
}
|
|
921
|
+
if (alignment === 'leading') {
|
|
922
|
+
return candidate >= anchor && candidate < anchor + windowMs;
|
|
923
|
+
}
|
|
924
|
+
const halfWindow = windowMs / 2;
|
|
925
|
+
return (candidate >= anchor - halfWindow && candidate < anchor + halfWindow);
|
|
926
|
+
};
|
|
927
|
+
const resultRows = this.events.map((event, index) => {
|
|
928
|
+
const anchor = anchors[index];
|
|
929
|
+
const values = sourceValues
|
|
930
|
+
.filter((_, candidateIndex) => anchorInWindow(anchors[candidateIndex], anchor))
|
|
931
|
+
.flatMap((value) => (value === undefined ? [] : [value]));
|
|
932
|
+
const smoothed = values.length === 0
|
|
933
|
+
? undefined
|
|
934
|
+
: values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
935
|
+
const nextEvent = output === undefined
|
|
936
|
+
? event.set(column, smoothed)
|
|
937
|
+
: event.merge({ [output]: smoothed });
|
|
938
|
+
return Object.freeze([
|
|
939
|
+
nextEvent.key(),
|
|
940
|
+
...resultSchema
|
|
941
|
+
.slice(1)
|
|
942
|
+
.map((nextColumn) => nextEvent.data()[nextColumn.name]),
|
|
943
|
+
]);
|
|
944
|
+
});
|
|
945
|
+
return new TimeSeries({
|
|
946
|
+
name: this.name,
|
|
947
|
+
schema: resultSchema,
|
|
948
|
+
rows: resultRows,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
/** Example: `series.slice(0, 10)`. Returns a positional half-open slice of the series. */
|
|
952
|
+
slice(beginIndex, endIndex) {
|
|
953
|
+
return new TimeSeries({
|
|
954
|
+
name: this.name,
|
|
955
|
+
schema: this.schema,
|
|
956
|
+
rows: toRows(this.schema, this.events.slice(beginIndex, endIndex)),
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
/** Example: `series.filter(event => event.get("active"))`. Returns a new series containing only events that match the predicate. */
|
|
960
|
+
filter(predicate) {
|
|
961
|
+
return new TimeSeries({
|
|
962
|
+
name: this.name,
|
|
963
|
+
schema: this.schema,
|
|
964
|
+
rows: toRows(this.schema, this.events.filter((event, index) => predicate(event, index))),
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/** Example: `series.find(event => event.get("value") > 0)`. Returns the first event that matches the predicate, if any. */
|
|
968
|
+
find(predicate) {
|
|
969
|
+
return this.events.find((event, index) => predicate(event, index));
|
|
970
|
+
}
|
|
971
|
+
/** Example: `series.some(event => event.get("healthy"))`. Returns `true` when at least one event matches the predicate. */
|
|
972
|
+
some(predicate) {
|
|
973
|
+
return this.events.some((event, index) => predicate(event, index));
|
|
974
|
+
}
|
|
975
|
+
/** Example: `series.every(event => event.get("healthy"))`. Returns `true` when every event matches the predicate. */
|
|
976
|
+
every(predicate) {
|
|
977
|
+
return this.events.every((event, index) => predicate(event, index));
|
|
978
|
+
}
|
|
979
|
+
/** Example: `series.includesKey(new Time(Date.now()))`. Returns `true` when the series contains an event with an exactly matching key. */
|
|
980
|
+
includesKey(key) {
|
|
981
|
+
const normalizedKey = toKey(key);
|
|
982
|
+
return this.events.some((event) => event.key().equals(normalizedKey));
|
|
983
|
+
}
|
|
984
|
+
/** Example: `series.bisect(new Time(Date.now()))`. Returns the insertion index for the supplied key in the ordered event sequence. */
|
|
985
|
+
bisect(key) {
|
|
986
|
+
const normalizedKey = toKey(key);
|
|
987
|
+
let low = 0;
|
|
988
|
+
let high = this.events.length;
|
|
989
|
+
while (low < high) {
|
|
990
|
+
const mid = Math.floor((low + high) / 2);
|
|
991
|
+
if (this.events[mid].key().compare(normalizedKey) < 0) {
|
|
992
|
+
low = mid + 1;
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
high = mid;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return low;
|
|
999
|
+
}
|
|
1000
|
+
/** Example: `series.atOrBefore(new Time(Date.now()))`. Returns the event with the exact key or the nearest earlier event, if any. */
|
|
1001
|
+
atOrBefore(key) {
|
|
1002
|
+
const normalizedKey = toKey(key);
|
|
1003
|
+
const index = this.bisect(normalizedKey);
|
|
1004
|
+
if (index < this.events.length &&
|
|
1005
|
+
this.events[index].key().equals(normalizedKey)) {
|
|
1006
|
+
return this.events[index];
|
|
1007
|
+
}
|
|
1008
|
+
return index === 0 ? undefined : this.events[index - 1];
|
|
1009
|
+
}
|
|
1010
|
+
/** Example: `series.atOrAfter(new Time(Date.now()))`. Returns the event with the exact key or the nearest later event, if any. */
|
|
1011
|
+
atOrAfter(key) {
|
|
1012
|
+
return this.events[this.bisect(key)];
|
|
1013
|
+
}
|
|
1014
|
+
/** Example: `series.timeRange()`. Returns the overall temporal extent of the series, if the series is not empty. */
|
|
1015
|
+
timeRange() {
|
|
1016
|
+
const first = this.first();
|
|
1017
|
+
if (!first) {
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
const start = first.begin();
|
|
1021
|
+
const end = this.events.reduce((maxEnd, event) => Math.max(maxEnd, event.end()), first.end());
|
|
1022
|
+
return new TimeRange({ start, end });
|
|
1023
|
+
}
|
|
1024
|
+
/** Example: `series.overlaps(range)`. Returns `true` when the overall series extent overlaps the supplied temporal value. */
|
|
1025
|
+
overlaps(other) {
|
|
1026
|
+
const range = this.timeRange();
|
|
1027
|
+
const otherRange = toOptionalSeriesRange(other);
|
|
1028
|
+
if (!range || !otherRange) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
return range.overlaps(otherRange);
|
|
1032
|
+
}
|
|
1033
|
+
/** Example: `series.contains(range)`. Returns `true` when the overall series extent fully contains the supplied temporal value. */
|
|
1034
|
+
contains(other) {
|
|
1035
|
+
const range = this.timeRange();
|
|
1036
|
+
const otherRange = toOptionalSeriesRange(other);
|
|
1037
|
+
if (!range || !otherRange) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
return range.contains(otherRange);
|
|
1041
|
+
}
|
|
1042
|
+
/** Example: `series.intersection(range)`. Returns the overlap between the overall series extent and the supplied temporal value, if any. */
|
|
1043
|
+
intersection(other) {
|
|
1044
|
+
const range = this.timeRange();
|
|
1045
|
+
const otherRange = toOptionalSeriesRange(other);
|
|
1046
|
+
if (!range || !otherRange) {
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
return range.intersection(otherRange);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Example: `series.overlapping(range)`.
|
|
1053
|
+
* Returns the portion of the series whose event extents overlap the supplied range.
|
|
1054
|
+
*
|
|
1055
|
+
* Unlike `within(...)`, this keeps partially overlapping events without modifying their keys.
|
|
1056
|
+
* Use `trim(...)` when you want those overlapping keys clipped to the supplied range.
|
|
1057
|
+
*/
|
|
1058
|
+
overlapping(range) {
|
|
1059
|
+
return this.filter((event) => event.overlaps(range));
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Example: `series.containedBy(range)`.
|
|
1063
|
+
* Returns the portion of the series whose event extents are fully contained by the supplied range.
|
|
1064
|
+
*
|
|
1065
|
+
* This is the strict containment selector:
|
|
1066
|
+
* events must start at or after the range start and end at or before the range end.
|
|
1067
|
+
* Unlike `overlapping(...)`, partially overlapping events are excluded.
|
|
1068
|
+
*/
|
|
1069
|
+
containedBy(range) {
|
|
1070
|
+
const selectionRange = toSelectionRange(range);
|
|
1071
|
+
return this.filter((event) => selectionRange.contains(event));
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Example: `series.trim(range)`.
|
|
1075
|
+
* Returns the series trimmed to the supplied range by clipping overlapping event keys.
|
|
1076
|
+
*
|
|
1077
|
+
* Non-overlapping events are dropped. Overlapping `TimeRange` and `Interval` keys are clipped
|
|
1078
|
+
* to the supplied range. Overlapping `Time` keys are preserved unchanged.
|
|
1079
|
+
*/
|
|
1080
|
+
trim(range) {
|
|
1081
|
+
const trimmedEvents = this.events
|
|
1082
|
+
.map((event) => event.trim(range))
|
|
1083
|
+
.filter((event) => event !== undefined);
|
|
1084
|
+
return new TimeSeries({
|
|
1085
|
+
name: this.name,
|
|
1086
|
+
schema: this.schema,
|
|
1087
|
+
rows: toRows(this.schema, trimmedEvents),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/** Example: `series.before(Date.now())`. Returns the events ending strictly before the supplied temporal boundary. */
|
|
1091
|
+
before(boundary) {
|
|
1092
|
+
const limit = toBoundaryTimestamp(boundary);
|
|
1093
|
+
return this.filter((event) => event.end() < limit);
|
|
1094
|
+
}
|
|
1095
|
+
/** Example: `series.after(Date.now())`. Returns the events beginning strictly after the supplied temporal boundary. */
|
|
1096
|
+
after(boundary) {
|
|
1097
|
+
const limit = toBoundaryTimestamp(boundary);
|
|
1098
|
+
return this.filter((event) => event.begin() > limit);
|
|
1099
|
+
}
|
|
1100
|
+
within(beginOrRange, end) {
|
|
1101
|
+
const range = end === undefined
|
|
1102
|
+
? toSelectionRange(beginOrRange)
|
|
1103
|
+
: new TimeRange({ start: beginOrRange, end });
|
|
1104
|
+
return this.filter((event) => event.begin() >= range.begin() && event.end() <= range.end());
|
|
1105
|
+
}
|
|
1106
|
+
/** Example: `series.select("cpu", "healthy")`. Returns a new series containing only the selected payload fields. */
|
|
1107
|
+
select(...keys) {
|
|
1108
|
+
const firstColumn = this.schema[0];
|
|
1109
|
+
const selectedColumns = this.schema
|
|
1110
|
+
.slice(1)
|
|
1111
|
+
.filter((column) => keys.includes(column.name));
|
|
1112
|
+
const resultSchema = Object.freeze([
|
|
1113
|
+
firstColumn,
|
|
1114
|
+
...selectedColumns,
|
|
1115
|
+
]);
|
|
1116
|
+
const resultEvents = this.events.map((event) => {
|
|
1117
|
+
const selectedEvent = event.select(...keys);
|
|
1118
|
+
return selectedEvent;
|
|
1119
|
+
});
|
|
1120
|
+
return new TimeSeries({
|
|
1121
|
+
name: this.name,
|
|
1122
|
+
schema: resultSchema,
|
|
1123
|
+
rows: toRows(resultSchema, resultEvents),
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
/** Example: `series.rename({ cpu: "usage" })`. Returns a new series with payload field names renamed according to the supplied mapping. */
|
|
1127
|
+
rename(mapping) {
|
|
1128
|
+
const firstColumn = this.schema[0];
|
|
1129
|
+
const renamedColumns = this.schema.slice(1).map((column) => ({
|
|
1130
|
+
...column,
|
|
1131
|
+
name: mapping[column.name] ??
|
|
1132
|
+
column.name,
|
|
1133
|
+
}));
|
|
1134
|
+
const resultSchema = Object.freeze([
|
|
1135
|
+
firstColumn,
|
|
1136
|
+
...renamedColumns,
|
|
1137
|
+
]);
|
|
1138
|
+
const resultEvents = this.events.map((event) => {
|
|
1139
|
+
const renamedEvent = event.rename(mapping);
|
|
1140
|
+
return renamedEvent;
|
|
1141
|
+
});
|
|
1142
|
+
return new TimeSeries({
|
|
1143
|
+
name: this.name,
|
|
1144
|
+
schema: resultSchema,
|
|
1145
|
+
rows: toRows(resultSchema, resultEvents),
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
collapse(keys, output, reducer, options) {
|
|
1149
|
+
const nextEvents = this.events.map((event) => {
|
|
1150
|
+
if (options?.append === true) {
|
|
1151
|
+
return event.collapse(keys, output, reducer, { append: true });
|
|
1152
|
+
}
|
|
1153
|
+
return event.collapse(keys, output, reducer);
|
|
1154
|
+
});
|
|
1155
|
+
const firstColumn = this.schema[0];
|
|
1156
|
+
const append = options?.append === true;
|
|
1157
|
+
const keptColumns = append
|
|
1158
|
+
? this.schema.slice(1)
|
|
1159
|
+
: this.schema
|
|
1160
|
+
.slice(1)
|
|
1161
|
+
.filter((column) => !keys.includes(column.name));
|
|
1162
|
+
const resultSchema = Object.freeze([
|
|
1163
|
+
firstColumn,
|
|
1164
|
+
...keptColumns,
|
|
1165
|
+
{
|
|
1166
|
+
name: output,
|
|
1167
|
+
kind: typeof nextEvents[0]?.get(output) === 'number'
|
|
1168
|
+
? 'number'
|
|
1169
|
+
: typeof nextEvents[0]?.get(output) === 'boolean'
|
|
1170
|
+
? 'boolean'
|
|
1171
|
+
: 'string',
|
|
1172
|
+
},
|
|
1173
|
+
]);
|
|
1174
|
+
return new TimeSeries({
|
|
1175
|
+
name: this.name,
|
|
1176
|
+
schema: resultSchema,
|
|
1177
|
+
rows: toRows(resultSchema, nextEvents),
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
/** Example: `series.length`. Returns the number of events in the series. */
|
|
1181
|
+
get length() {
|
|
1182
|
+
return this.events.length;
|
|
1183
|
+
}
|
|
1184
|
+
#alignHoldAt(t) {
|
|
1185
|
+
const event = this.atOrBefore(new Time(t));
|
|
1186
|
+
return (event?.data() ?? {});
|
|
1187
|
+
}
|
|
1188
|
+
#alignLinearAt(t, valueColumns) {
|
|
1189
|
+
const exact = this.find((event) => event.begin() === t);
|
|
1190
|
+
if (exact) {
|
|
1191
|
+
return exact.data();
|
|
1192
|
+
}
|
|
1193
|
+
const previous = this.atOrBefore(new Time(t));
|
|
1194
|
+
const next = this.atOrAfter(new Time(t));
|
|
1195
|
+
if (!previous || !next || previous.begin() === next.begin()) {
|
|
1196
|
+
return (previous?.data() ?? {});
|
|
1197
|
+
}
|
|
1198
|
+
const ratio = (t - previous.begin()) / (next.begin() - previous.begin());
|
|
1199
|
+
const result = {};
|
|
1200
|
+
const previousData = previous.data();
|
|
1201
|
+
const nextData = next.data();
|
|
1202
|
+
for (const column of valueColumns) {
|
|
1203
|
+
const previousValue = previousData[column.name];
|
|
1204
|
+
const nextValue = nextData[column.name];
|
|
1205
|
+
if (column.kind === 'number' &&
|
|
1206
|
+
typeof previousValue === 'number' &&
|
|
1207
|
+
typeof nextValue === 'number') {
|
|
1208
|
+
result[column.name] =
|
|
1209
|
+
previousValue + (nextValue - previousValue) * ratio;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
result[column.name] = previousValue;
|
|
1213
|
+
}
|
|
1214
|
+
return result;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
//# sourceMappingURL=TimeSeries.js.map
|