vaporous 0.0.2 → 0.0.4
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/Vaporous.js +344 -129
- package/package.json +2 -1
- package/readme.md +22 -0
- package/styles.css +82 -78
- package/types/Aggregation.js +56 -52
- package/examples/sensors/exampleData/temp_0_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_1_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_2_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_3_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_4_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_5_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_6_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_7_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_8_10.jsonStream +0 -100
- package/examples/sensors/exampleData/temp_9_10.jsonStream +0 -100
- package/examples/sensors/query.js +0 -98
- package/examples/sensors/run.sh +0 -2
- package/examples/sensors/sensor_data.js +0 -55
package/Vaporous.js
CHANGED
|
@@ -11,6 +11,8 @@ const path = require('path')
|
|
|
11
11
|
|
|
12
12
|
const styles = fs.readFileSync(__dirname + '/styles.css')
|
|
13
13
|
|
|
14
|
+
const Papa = require('papaparse')
|
|
15
|
+
|
|
14
16
|
// These globals allow us to write functions from the HTML page directly without needing to stringify
|
|
15
17
|
class google { }
|
|
16
18
|
const document = {}
|
|
@@ -20,15 +22,10 @@ const keyFromEvent = (event, bys) => bys.map(i => event[i.bySplit]).join('|')
|
|
|
20
22
|
const _sort = (order, data, ...keys) => {
|
|
21
23
|
return data.sort((a, b) => {
|
|
22
24
|
let directive = 0;
|
|
23
|
-
keys.some(key => {
|
|
24
|
-
const type = typeof a[key];
|
|
25
|
-
|
|
26
|
-
if (type === 'number') {
|
|
27
|
-
directive = order === 'asc' ? a[key] - b[key] : b[key] - a[key];
|
|
28
|
-
} else if (type === 'string') {
|
|
29
|
-
directive = order === 'asc' ? a[key].localeCompare(b[key]) : b[key].localeCompare(a[key]);
|
|
30
|
-
}
|
|
31
25
|
|
|
26
|
+
keys.some(key => {
|
|
27
|
+
directive = typeof a[key] === 'number' ? a[key] - b[key] : a[key].localeCompare(b[key])
|
|
28
|
+
if (order === 'dsc') directive = directive * -1
|
|
32
29
|
if (directive !== 0) return true;
|
|
33
30
|
})
|
|
34
31
|
|
|
@@ -49,13 +46,13 @@ class Vaporous {
|
|
|
49
46
|
this.checkpoints = {}
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
method(operation, name,
|
|
49
|
+
method(operation, name, options) {
|
|
53
50
|
const operations = {
|
|
54
51
|
create: () => {
|
|
55
|
-
this.savedMethods[name] =
|
|
52
|
+
this.savedMethods[name] = options
|
|
56
53
|
},
|
|
57
54
|
retrieve: () => {
|
|
58
|
-
this.savedMethods[name](this)
|
|
55
|
+
this.savedMethods[name](this, options)
|
|
59
56
|
},
|
|
60
57
|
delete: () => {
|
|
61
58
|
delete this.savedMethods[name]
|
|
@@ -127,16 +124,51 @@ class Vaporous {
|
|
|
127
124
|
return this;
|
|
128
125
|
}
|
|
129
126
|
|
|
127
|
+
async csvLoad(parser) {
|
|
128
|
+
const tasks = this.events.map(obj => {
|
|
129
|
+
const content = []
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const thisStream = fs.createReadStream(obj._fileInput)
|
|
133
|
+
|
|
134
|
+
Papa.parse(thisStream, {
|
|
135
|
+
header: true,
|
|
136
|
+
skipEmptyLines: true,
|
|
137
|
+
step: (row) => {
|
|
138
|
+
try {
|
|
139
|
+
const event = parser(row)
|
|
140
|
+
if (event !== null) content.push(event)
|
|
141
|
+
} catch (err) {
|
|
142
|
+
reject(err)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
complete: () => {
|
|
146
|
+
obj._raw = content
|
|
147
|
+
resolve(this)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await Promise.all(tasks)
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
130
157
|
async fileLoad(delim, parser) {
|
|
131
158
|
const tasks = this.events.map(obj => {
|
|
132
159
|
const content = []
|
|
133
160
|
|
|
134
|
-
return new Promise(resolve => {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
135
162
|
fs.createReadStream(obj._fileInput)
|
|
136
163
|
.pipe(split2(delim))
|
|
137
164
|
.on('data', line => {
|
|
138
|
-
|
|
139
|
-
|
|
165
|
+
try {
|
|
166
|
+
const event = parser(line)
|
|
167
|
+
if (event !== null) content.push(event)
|
|
168
|
+
} catch (err) {
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
|
|
140
172
|
})
|
|
141
173
|
.on('end', () => {
|
|
142
174
|
obj._raw = content;
|
|
@@ -149,8 +181,15 @@ class Vaporous {
|
|
|
149
181
|
return this;
|
|
150
182
|
}
|
|
151
183
|
|
|
152
|
-
output() {
|
|
153
|
-
|
|
184
|
+
output(...args) {
|
|
185
|
+
if (args.length) {
|
|
186
|
+
console.log(this.events.map(event => {
|
|
187
|
+
return args.map(item => event[item])
|
|
188
|
+
}))
|
|
189
|
+
} else {
|
|
190
|
+
console.log(this.events)
|
|
191
|
+
}
|
|
192
|
+
|
|
154
193
|
return this;
|
|
155
194
|
}
|
|
156
195
|
|
|
@@ -206,11 +245,19 @@ class Vaporous {
|
|
|
206
245
|
const arr = Object.keys(map).map(key => {
|
|
207
246
|
const result = map[key]
|
|
208
247
|
|
|
248
|
+
let sortedCache = {}
|
|
209
249
|
aggregations.forEach(aggregation => {
|
|
210
|
-
|
|
250
|
+
const outputField = aggregation.outputField
|
|
251
|
+
const reference = map[key]._statsRaw[aggregation.field]
|
|
252
|
+
|
|
253
|
+
if (aggregation.sortable) {
|
|
254
|
+
sortedCache[aggregation.field] = reference.slice().sort((a, b) => a - b)
|
|
255
|
+
result[outputField] = aggregation.calculate(sortedCache[aggregation.field])
|
|
256
|
+
} else {
|
|
257
|
+
result[outputField] = aggregation.calculate(reference)
|
|
258
|
+
}
|
|
259
|
+
|
|
211
260
|
|
|
212
|
-
const aggregationField = aggregation.outputField
|
|
213
|
-
result[aggregationField] = aggregation.calculate(map[key])
|
|
214
261
|
})
|
|
215
262
|
|
|
216
263
|
delete map[key]._statsRaw
|
|
@@ -238,51 +285,63 @@ class Vaporous {
|
|
|
238
285
|
}
|
|
239
286
|
|
|
240
287
|
streamstats(...args) {
|
|
288
|
+
const backwardIterate = (event, i, by, maxBoundary = 0) => {
|
|
289
|
+
let backwardIndex = 0
|
|
290
|
+
const thisKey = keyFromEvent(event, by)
|
|
291
|
+
const byKey = thisKey
|
|
292
|
+
|
|
293
|
+
while (true) {
|
|
294
|
+
const target = i - backwardIndex
|
|
295
|
+
|
|
296
|
+
if (target < 0 || target < maxBoundary) break
|
|
297
|
+
|
|
298
|
+
const newKey = keyFromEvent(this.events[target], by)
|
|
299
|
+
if (thisKey !== newKey) break
|
|
300
|
+
backwardIndex++
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { byKey, start: i - backwardIndex + 1 }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
241
307
|
const window = args.filter(i => i instanceof Window)
|
|
242
308
|
const by = args.filter(i => i instanceof By)
|
|
243
309
|
|
|
244
310
|
// Perform some validation
|
|
245
311
|
if (window.length > 1) throw new Error('Only one window allowed in streamstats')
|
|
246
|
-
if (window.length > 0 && by.length > 0) throw new Error('Window and By not supported together in streamstats')
|
|
247
312
|
|
|
248
313
|
this.events.forEach((event, i) => {
|
|
249
|
-
let start, byKey;
|
|
250
|
-
if (window.length > 0) {
|
|
251
|
-
start = Math.max(i - window[0].size + 1, 0)
|
|
252
|
-
byKey = ""
|
|
253
|
-
} else if (by.length > 0) {
|
|
254
|
-
let backwardIndex = 0
|
|
255
|
-
const thisKey = keyFromEvent(event, by)
|
|
256
|
-
byKey = thisKey
|
|
257
|
-
let keyChange = false
|
|
258
|
-
while (!keyChange) {
|
|
259
|
-
const target = i - backwardIndex
|
|
260
|
-
|
|
261
|
-
if (target < 0) {
|
|
262
|
-
keyChange = true
|
|
263
|
-
break
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const newKey = keyFromEvent(this.events[target], by)
|
|
267
|
-
if (thisKey !== newKey) {
|
|
268
|
-
keyChange = true
|
|
269
|
-
break
|
|
270
|
-
}
|
|
314
|
+
let start, byKey = "";
|
|
271
315
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
316
|
+
// Refine to window size
|
|
317
|
+
if (window.length > 0) start = Math.max(i - window[0].size + 1, 0)
|
|
318
|
+
if (by.length !== 0) ({ start, byKey } = backwardIterate(event, i, by, start))
|
|
276
319
|
|
|
277
320
|
const eventRange = this.events.slice(start, i + 1)
|
|
278
|
-
|
|
321
|
+
const embed = this._stats(args, eventRange).map[byKey]
|
|
322
|
+
Object.assign(event, {
|
|
323
|
+
_streamstats: embed
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// We need to assign to a separate streamstats object to avoid collusions
|
|
328
|
+
// As streamstats iteratively updates the data but rlies on previous samples
|
|
329
|
+
// Modifying data in place corrupts the results of the query
|
|
330
|
+
this.events.forEach(event => {
|
|
331
|
+
Object.assign(event, event._streamstats)
|
|
332
|
+
delete event._streamstats
|
|
279
333
|
})
|
|
280
334
|
|
|
281
335
|
return this;
|
|
282
336
|
}
|
|
283
337
|
|
|
338
|
+
delta(field, remapField, ...bys) {
|
|
339
|
+
this.streamstats(new Aggregation(field, 'range', remapField), new Window(2), ...bys)
|
|
340
|
+
return this;
|
|
341
|
+
}
|
|
342
|
+
|
|
284
343
|
sort(order, ...keys) {
|
|
285
|
-
this.
|
|
344
|
+
this.events = _sort(order, this.events, ...keys)
|
|
286
345
|
return this;
|
|
287
346
|
}
|
|
288
347
|
|
|
@@ -294,14 +353,163 @@ class Vaporous {
|
|
|
294
353
|
return this;
|
|
295
354
|
}
|
|
296
355
|
|
|
297
|
-
build(name, type,
|
|
298
|
-
|
|
356
|
+
build(name, type, { tab = 'Default', columns = 2, y2, y1Type, y2Type, y1Stacked, y2Stacked, sortX = 'asc', xTicks = false, trellisAxis = "shared" } = {}) {
|
|
357
|
+
|
|
358
|
+
const visualisationOptions = { tab, columns }
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
let bounds = {}
|
|
362
|
+
|
|
363
|
+
const isY2 = (data) => {
|
|
364
|
+
let y2Mapped = false;
|
|
365
|
+
|
|
366
|
+
if (y2 instanceof Array) {
|
|
367
|
+
y2Mapped = y2.includes(data)
|
|
368
|
+
}
|
|
369
|
+
else if (y2 instanceof RegExp) {
|
|
370
|
+
y2Mapped = y2.test(data)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return y2Mapped
|
|
374
|
+
}
|
|
375
|
+
const graphData = this.events.map((trellis, i) => {
|
|
376
|
+
if (type === 'Table') {
|
|
377
|
+
return trellis;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const dataOptions = {}
|
|
381
|
+
|
|
382
|
+
// For every event in this trellis restructure to chart.js
|
|
383
|
+
if (sortX) trellis = _sort(sortX, trellis, '_time')
|
|
384
|
+
|
|
385
|
+
const trellisName = this.graphFlags.at(-1).trellisName?.[i] || ""
|
|
386
|
+
const columnDefinitions = this.graphFlags.at(-1).columnDefinitions[i]
|
|
387
|
+
|
|
388
|
+
trellis.forEach(event => {
|
|
389
|
+
columnDefinitions.forEach(prop => {
|
|
390
|
+
if (!dataOptions[prop]) dataOptions[prop] = []
|
|
391
|
+
const val = event[prop]
|
|
392
|
+
dataOptions[prop].push(val)
|
|
393
|
+
|
|
394
|
+
if (!bounds[prop]) bounds[prop] = {
|
|
395
|
+
min: val,
|
|
396
|
+
max: val
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (val < bounds[prop].min) bounds[prop].min = val;
|
|
400
|
+
if (val > bounds[prop].max) bounds[prop].max = val
|
|
401
|
+
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
const _time = dataOptions._time
|
|
407
|
+
delete dataOptions._time
|
|
408
|
+
|
|
409
|
+
let y2WasMapped = false
|
|
410
|
+
const data = {
|
|
411
|
+
labels: _time,
|
|
412
|
+
datasets: Object.keys(dataOptions).map(data => {
|
|
413
|
+
const y2Mapped = isY2(data)
|
|
414
|
+
if (y2Mapped) y2WasMapped = y2Mapped
|
|
415
|
+
|
|
416
|
+
const base = {
|
|
417
|
+
label: data,
|
|
418
|
+
yAxisID: y2Mapped ? 'y2' : undefined,
|
|
419
|
+
data: dataOptions[data],
|
|
420
|
+
type: y2Mapped ? y2Type : y1Type,
|
|
421
|
+
// borderColor: 'red',
|
|
422
|
+
// backgroundColor: 'red',
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (type === 'Scatter') {
|
|
426
|
+
base.showLine = false
|
|
427
|
+
base.pointRadius = 8
|
|
428
|
+
base.pointStyle = 'rect'
|
|
429
|
+
} else if (type === 'Area') {
|
|
430
|
+
base.fill = 'origin'
|
|
431
|
+
} else if (type === 'Line') {
|
|
432
|
+
base.pointRadius = 0;
|
|
433
|
+
}
|
|
434
|
+
return base
|
|
435
|
+
})
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const scales = {
|
|
439
|
+
y: {
|
|
440
|
+
type: 'linear',
|
|
441
|
+
display: true,
|
|
442
|
+
position: 'left',
|
|
443
|
+
stacked: y1Stacked
|
|
444
|
+
},
|
|
445
|
+
x: {
|
|
446
|
+
type: 'linear',
|
|
447
|
+
ticks: {
|
|
448
|
+
display: xTicks
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (y2WasMapped) scales.y2 = {
|
|
454
|
+
type: 'linear',
|
|
455
|
+
display: true,
|
|
456
|
+
position: 'right',
|
|
457
|
+
grid: {
|
|
458
|
+
drawOnChartArea: false
|
|
459
|
+
},
|
|
460
|
+
stacked: y2Stacked
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
type: 'line',
|
|
465
|
+
data: data,
|
|
466
|
+
options: {
|
|
467
|
+
scales,
|
|
468
|
+
responsive: true,
|
|
469
|
+
plugins: {
|
|
470
|
+
legend: {
|
|
471
|
+
position: 'bottom',
|
|
472
|
+
},
|
|
473
|
+
title: {
|
|
474
|
+
display: true,
|
|
475
|
+
text: name + trellisName
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
if (trellisAxis === 'shared') {
|
|
483
|
+
// Do a second iteration to implement bounds
|
|
484
|
+
graphData.forEach(trellisGraph => {
|
|
485
|
+
Object.keys(bounds).forEach(bound => {
|
|
486
|
+
let axis = isY2(bound) ? 'y2' : 'y'
|
|
487
|
+
if (bound === '_time') axis = 'x';
|
|
488
|
+
|
|
489
|
+
const thisAxis = trellisGraph.options.scales[axis]
|
|
490
|
+
const { min, max } = bounds[bound]
|
|
491
|
+
if (!thisAxis.min) {
|
|
492
|
+
thisAxis.min = min
|
|
493
|
+
thisAxis.max = max
|
|
494
|
+
}
|
|
495
|
+
if (min < thisAxis.min) thisAxis.min = min
|
|
496
|
+
if (max > thisAxis.max) thisAxis.max = max
|
|
497
|
+
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = JSON.stringify(graphData)
|
|
299
504
|
const lastData = this.visualisationData.at(-1)
|
|
300
505
|
|
|
301
506
|
if (lastData !== data) this.visualisationData.push(data)
|
|
302
507
|
this.visualisations.push([name, type, visualisationOptions, this.visualisationData.length - 1, this.graphFlags[this.graphFlags.length - 1]])
|
|
303
508
|
|
|
304
|
-
if (visualisationOptions.tab && !this.tabs.includes(visualisationOptions.tab))
|
|
509
|
+
if (visualisationOptions.tab && !this.tabs.includes(visualisationOptions.tab)) {
|
|
510
|
+
this.tabs.push(visualisationOptions.tab)
|
|
511
|
+
this.tabs = this.tabs.sort((a, b) => a.localeCompare(b))
|
|
512
|
+
}
|
|
305
513
|
|
|
306
514
|
return this;
|
|
307
515
|
}
|
|
@@ -309,8 +517,8 @@ class Vaporous {
|
|
|
309
517
|
checkpoint(operation, name) {
|
|
310
518
|
|
|
311
519
|
const operations = {
|
|
312
|
-
create: () => this.checkpoints[name] = this.events,
|
|
313
|
-
retrieve: () => this.events = this.checkpoints[name],
|
|
520
|
+
create: () => this.checkpoints[name] = structuredClone(this.events),
|
|
521
|
+
retrieve: () => this.events = structuredClone(this.checkpoints[name]),
|
|
314
522
|
delete: () => delete this.checkpoints[name]
|
|
315
523
|
}
|
|
316
524
|
|
|
@@ -322,10 +530,11 @@ class Vaporous {
|
|
|
322
530
|
const arr = []
|
|
323
531
|
this.events.forEach(event => {
|
|
324
532
|
if (!event[target]) return arr.push(event)
|
|
325
|
-
event[target].forEach((item) => {
|
|
533
|
+
event[target].forEach((item, i) => {
|
|
326
534
|
arr.push({
|
|
327
535
|
...event,
|
|
328
|
-
[target]: item
|
|
536
|
+
[target]: item,
|
|
537
|
+
[`_mvExpand_${target}`]: i
|
|
329
538
|
})
|
|
330
539
|
})
|
|
331
540
|
})
|
|
@@ -334,53 +543,102 @@ class Vaporous {
|
|
|
334
543
|
return this;
|
|
335
544
|
}
|
|
336
545
|
|
|
337
|
-
|
|
546
|
+
writeFile(title) {
|
|
547
|
+
fs.writeFileSync('./' + title, JSON.stringify(this.events))
|
|
548
|
+
return this;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
toGraph(x, y, series, trellis = false) {
|
|
552
|
+
|
|
553
|
+
if (!(y instanceof Array)) y = [y]
|
|
554
|
+
|
|
555
|
+
const yAggregations = y.map(item => [
|
|
556
|
+
new Aggregation(item, 'list', item),
|
|
557
|
+
]).flat()
|
|
338
558
|
|
|
339
559
|
this.stats(
|
|
340
|
-
|
|
560
|
+
...yAggregations,
|
|
341
561
|
new Aggregation(series, 'list', series),
|
|
342
562
|
new Aggregation(trellis, 'values', 'trellis'),
|
|
343
563
|
new By(x), trellis ? new By(trellis) : null
|
|
344
564
|
)
|
|
345
565
|
|
|
346
|
-
const trellisMap = {}
|
|
566
|
+
const trellisMap = {}, columnDefinitions = {}
|
|
347
567
|
|
|
348
568
|
this.table(event => {
|
|
569
|
+
const _time = event[x]
|
|
570
|
+
if (_time === null || _time === undefined) throw new Error(`To graph operation with params ${x}, ${y.join(',')} looks corrupt. x value resolves to null - the graph will not render`)
|
|
349
571
|
const obj = {
|
|
350
|
-
_time
|
|
572
|
+
_time
|
|
351
573
|
}
|
|
352
|
-
|
|
574
|
+
|
|
575
|
+
event[series].forEach((series, i) => {
|
|
576
|
+
y.forEach(item => {
|
|
577
|
+
let name;
|
|
578
|
+
if (y.length === 1) {
|
|
579
|
+
if (series === undefined) name = item
|
|
580
|
+
else name = series
|
|
581
|
+
} else {
|
|
582
|
+
if (series !== undefined) name = `${series}_${item}`
|
|
583
|
+
else name = item
|
|
584
|
+
}
|
|
585
|
+
obj[name] = event[item][i]
|
|
586
|
+
})
|
|
587
|
+
})
|
|
353
588
|
|
|
354
589
|
if (trellis) {
|
|
355
590
|
const tval = event.trellis[0]
|
|
356
|
-
if (!trellisMap[tval])
|
|
591
|
+
if (!trellisMap[tval]) {
|
|
592
|
+
trellisMap[tval] = []
|
|
593
|
+
columnDefinitions[tval] = {}
|
|
594
|
+
}
|
|
357
595
|
trellisMap[tval].push(obj)
|
|
596
|
+
Object.keys(obj).forEach(key => {
|
|
597
|
+
columnDefinitions[tval][key] = true
|
|
598
|
+
})
|
|
599
|
+
} else {
|
|
600
|
+
Object.keys(obj).forEach(key => {
|
|
601
|
+
columnDefinitions[key] = true;
|
|
602
|
+
})
|
|
358
603
|
}
|
|
359
604
|
|
|
360
605
|
return obj
|
|
361
606
|
})
|
|
362
607
|
|
|
363
608
|
const graphFlags = {}
|
|
609
|
+
|
|
364
610
|
if (trellis) {
|
|
365
611
|
graphFlags.trellis = true;
|
|
366
612
|
graphFlags.trellisName = Object.keys(trellisMap)
|
|
613
|
+
graphFlags.columnDefinitions = Object.keys(trellisMap).map(tval => {
|
|
614
|
+
const adjColumns = ['_time']
|
|
615
|
+
Object.keys(columnDefinitions[tval]).forEach(col => (col !== '_time') ? adjColumns.push(col) : null)
|
|
616
|
+
return adjColumns
|
|
617
|
+
})
|
|
367
618
|
this.events = Object.keys(trellisMap).map(tval => trellisMap[tval])
|
|
619
|
+
} else {
|
|
620
|
+
const adjColumns = ['_time']
|
|
621
|
+
Object.keys(columnDefinitions).forEach(col => (col !== '_time') ? adjColumns.push(col) : null)
|
|
622
|
+
|
|
623
|
+
this.events = [this.events]
|
|
624
|
+
graphFlags.columnDefinitions = [adjColumns]
|
|
368
625
|
}
|
|
369
626
|
|
|
370
|
-
Object.assign(graphFlags, options)
|
|
371
627
|
this.graphFlags.push(graphFlags)
|
|
372
628
|
return this;
|
|
373
629
|
}
|
|
374
630
|
|
|
375
|
-
render() {
|
|
631
|
+
render(location = './Vaporous_generation.html') {
|
|
376
632
|
const classSafe = (name) => name.replace(/[^a-zA-Z0-9]/g, "_")
|
|
377
633
|
|
|
378
|
-
const createElement = (name, type, visualisationOptions, eventData, { trellis,
|
|
634
|
+
const createElement = (name, type, visualisationOptions, eventData, { trellis, trellisName = "" }) => {
|
|
635
|
+
|
|
379
636
|
if (classSafe(visualisationOptions.tab) !== selectedTab) return;
|
|
380
637
|
|
|
381
638
|
eventData = visualisationData[eventData]
|
|
382
|
-
|
|
383
|
-
|
|
639
|
+
|
|
640
|
+
// TODO: migrate trellis functionality from here to tograph
|
|
641
|
+
if (trellis) {
|
|
384
642
|
let pairs = trellisName.map((name, i) => [name, eventData[i]]);
|
|
385
643
|
pairs = pairs.sort((a, b) => a[0].localeCompare(b[0]))
|
|
386
644
|
|
|
@@ -389,84 +647,41 @@ class Vaporous {
|
|
|
389
647
|
eventData = pairs.map(p => p[1]);
|
|
390
648
|
}
|
|
391
649
|
|
|
392
|
-
|
|
393
|
-
const data = new google.visualization.DataTable();
|
|
394
|
-
|
|
395
|
-
const series = {}, axis0 = { targetAxisIndex: 0 }, axis1 = { targetAxisIndex: 1 }
|
|
650
|
+
const columnCount = visualisationOptions.columns || 2
|
|
396
651
|
|
|
397
|
-
|
|
398
|
-
|
|
652
|
+
eventData.forEach((trellisData, i) => {
|
|
653
|
+
const parentHolder = document.createElement('div')
|
|
399
654
|
|
|
400
|
-
// Create columns
|
|
401
|
-
const columns = Object.keys(trellis[0])
|
|
402
|
-
columns.forEach((key, i) => {
|
|
403
|
-
data.addColumn(typeof trellis[0][key], key)
|
|
404
655
|
|
|
405
|
-
if (y2 && i !== 0) {
|
|
406
|
-
let match = false;
|
|
407
|
-
if (y2 instanceof Array) { match = y2.includes(key) }
|
|
408
|
-
else if (y2 instanceof RegExp) { match = y2.test(key) }
|
|
409
656
|
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (!series[1 - i]) series[i - 1] = axis0
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
let rows = trellis.map(event => {
|
|
417
|
-
return columns.map(key => event[key])
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
rows = _sort(sortX, rows, 0)
|
|
421
|
-
|
|
422
|
-
data.addRows(rows);
|
|
423
|
-
|
|
424
|
-
const columnCount = visualisationOptions.columns || 2
|
|
425
|
-
const thisEntity = document.createElement('div')
|
|
426
|
-
thisEntity.className = "parentHolder"
|
|
427
|
-
thisEntity.style = `flex: 1 0 calc(${100 / columns}% - 6px); max-width: calc(${100 / columnCount}% - 6px);`
|
|
657
|
+
document.getElementById('content').appendChild(parentHolder)
|
|
428
658
|
|
|
659
|
+
parentHolder.style = `flex: 0 0 calc(${100 / columnCount}% - 8px); max-width: calc(${100 / columnCount}% - 8px);`
|
|
660
|
+
if (type === 'Table') {
|
|
661
|
+
new Tabulator(parentHolder, { data: trellisData, autoColumns: 'full', layout: "fitDataStretch", })
|
|
662
|
+
} else {
|
|
663
|
+
const graphEntity = document.createElement('canvas')
|
|
664
|
+
parentHolder.appendChild(graphEntity)
|
|
665
|
+
new Chart(graphEntity, trellisData)
|
|
666
|
+
}
|
|
429
667
|
|
|
430
|
-
const thisGraph = document.createElement('div')
|
|
431
|
-
thisGraph.className = "graphHolder"
|
|
432
|
-
thisEntity.appendChild(thisGraph)
|
|
433
|
-
document.getElementById('content').appendChild(thisEntity)
|
|
434
|
-
|
|
435
|
-
const chartElement = new google.visualization[type](thisGraph)
|
|
436
|
-
|
|
437
|
-
google.visualization.events.addListener(chartElement, 'select', (e) => {
|
|
438
|
-
console.log(chartElement.getSelection()[1], chartElement.getSelection()[0])
|
|
439
|
-
tokens[name] = trellis[chartElement.getSelection()[0].row]
|
|
440
|
-
console.log(tokens[name])
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const title = trellis ? name + trellisName[i] : name
|
|
444
|
-
|
|
445
|
-
chartElement.draw(data, {
|
|
446
|
-
series, showRowNumber: false, legend: { position: 'bottom' }, title, isStacked: stacked,
|
|
447
|
-
width: document.body.scrollWidth / columnCount - (type === "LineChart" ? 12 : 24),
|
|
448
|
-
animation: { duration: 500, startup: true },
|
|
449
|
-
chartArea: { width: '85%', height: '75%' },
|
|
450
|
-
vAxis: {
|
|
451
|
-
viewWindow: {
|
|
452
|
-
min: y1Min
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
668
|
})
|
|
457
669
|
}
|
|
458
670
|
|
|
459
|
-
const filePath =
|
|
671
|
+
const filePath = location
|
|
460
672
|
fs.writeFileSync(filePath, `
|
|
461
673
|
<html>
|
|
462
674
|
<head>
|
|
675
|
+
<meta name="viewport" content="width=device-width, initial-scale=0.5">
|
|
463
676
|
<style>
|
|
464
677
|
${styles}
|
|
465
678
|
</style>
|
|
466
|
-
|
|
679
|
+
|
|
680
|
+
<link href="https://unpkg.com/tabulator-tables@6.3.1/dist/css/tabulator.min.css" rel="stylesheet">
|
|
681
|
+
<script type="text/javascript" src="https://unpkg.com/tabulator-tables@6.3.1/dist/js/tabulator.min.js"></script>
|
|
682
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
683
|
+
|
|
467
684
|
<script type="text/javascript">
|
|
468
|
-
google.charts.load('current', {'packages':['table', 'corechart']});
|
|
469
|
-
google.charts.setOnLoadCallback(drawVis);
|
|
470
685
|
|
|
471
686
|
const classSafe = ${classSafe.toString()}
|
|
472
687
|
|
|
@@ -505,7 +720,7 @@ class Vaporous {
|
|
|
505
720
|
</html>
|
|
506
721
|
`)
|
|
507
722
|
|
|
508
|
-
console.log('File ouput created ', filePath)
|
|
723
|
+
console.log('File ouput created ', path.resolve(filePath))
|
|
509
724
|
}
|
|
510
725
|
}
|
|
511
726
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vaporous",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Transition data to different structured states for analytical processing",
|
|
5
5
|
"main": "Vaporous.js",
|
|
6
6
|
"scripts": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"dayjs": "^1.11.18",
|
|
13
|
+
"papaparse": "^5.5.3",
|
|
13
14
|
"split2": "^4.2.0"
|
|
14
15
|
}
|
|
15
16
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Vaporous
|
|
2
|
+
Vaporous provides a chained query syntax for accessing unstructured data and converting it into interpretable analytics
|
|
3
|
+
|
|
4
|
+
The tool is still in its early phases of development and is missing some quality of life features
|
|
5
|
+
|
|
6
|
+
The query syntax is heavily inspired by splunk with more bias towards programmitic functionality
|
|
7
|
+
|
|
8
|
+
## Examples
|
|
9
|
+
|
|
10
|
+
Interactive previews for two datasources are available
|
|
11
|
+
|
|
12
|
+
- [Virtualised temperature sensor data](https://lkashl.github.io/vaporous/pages/temp_sensors.html)
|
|
13
|
+
- [CSV delimited virtruvian data](https://lkashl.github.io/vaporous/pages/gym.html)
|
|
14
|
+
|
|
15
|
+
Examples of the source queries used can be referenced in the [examples folder](https://github.com/lkashl/vaporous/tree/main/examples)
|
|
16
|
+
|
|
17
|
+
## TODO List
|
|
18
|
+
- Support web page embedded Vaporous so clients can use browser folder storage as file input
|
|
19
|
+
- Add an error for if a user tries to generate a graph without first calling toGraph
|
|
20
|
+
- Intercept structual errors earlier and add validation to functions - not necessarily data as this casues overhaead
|
|
21
|
+
- Migrate reponsibility for tabular conversion from create element to the primary library to reduce overhead of graph generation
|
|
22
|
+
|