vaporous 0.0.3 → 0.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/Vaporous.js +313 -122
- package/package.json +1 -1
- package/readme.md +22 -22
- package/styles.css +82 -78
- package/types/Aggregation.js +2 -2
package/Vaporous.js
CHANGED
|
@@ -14,7 +14,6 @@ const styles = fs.readFileSync(__dirname + '/styles.css')
|
|
|
14
14
|
const Papa = require('papaparse')
|
|
15
15
|
|
|
16
16
|
// These globals allow us to write functions from the HTML page directly without needing to stringify
|
|
17
|
-
class google { }
|
|
18
17
|
const document = {}
|
|
19
18
|
|
|
20
19
|
const keyFromEvent = (event, bys) => bys.map(i => event[i.bySplit]).join('|')
|
|
@@ -35,7 +34,7 @@ const _sort = (order, data, ...keys) => {
|
|
|
35
34
|
|
|
36
35
|
class Vaporous {
|
|
37
36
|
|
|
38
|
-
constructor() {
|
|
37
|
+
constructor({ loggers } = {}) {
|
|
39
38
|
this.events = [];
|
|
40
39
|
this.visualisations = [];
|
|
41
40
|
this.visualisationData = []
|
|
@@ -44,9 +43,50 @@ class Vaporous {
|
|
|
44
43
|
|
|
45
44
|
this.savedMethods = {}
|
|
46
45
|
this.checkpoints = {}
|
|
46
|
+
|
|
47
|
+
this.loggers = loggers
|
|
48
|
+
this.perf = null
|
|
49
|
+
this.totalTime = 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
manageEntry() {
|
|
53
|
+
if (this.loggers?.perf) {
|
|
54
|
+
const [, , method, ...origination] = new Error().stack.split('\n')
|
|
55
|
+
const invokedMethod = method.match(/Vaporous.(.+?) /)
|
|
56
|
+
|
|
57
|
+
let orig = origination.find(orig => {
|
|
58
|
+
const originator = orig.split("/").at(-1)
|
|
59
|
+
return !originator.includes("Vaporous")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
orig = orig.split("/").at(-1)
|
|
63
|
+
const logLine = "(" + orig + " BEGIN " + invokedMethod[1]
|
|
64
|
+
this.loggers.perf('info', logLine)
|
|
65
|
+
this.perf = { time: new Date().valueOf(), logLine }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
manageExit() {
|
|
70
|
+
if (this.loggers?.perf) {
|
|
71
|
+
let { logLine, time } = this.perf;
|
|
72
|
+
const executionTime = new Date() - time
|
|
73
|
+
this.totalTime += executionTime
|
|
74
|
+
|
|
75
|
+
const match = logLine.match(/^.*?BEGIN/);
|
|
76
|
+
const prepend = "END"
|
|
77
|
+
if (match) {
|
|
78
|
+
const toReplace = match[0]; // the matched substring
|
|
79
|
+
const spaces = " ".repeat(toReplace.length - prepend.length); // same length, all spaces
|
|
80
|
+
logLine = spaces + prepend + logLine.replace(toReplace, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.loggers.perf('info', logLine + " (" + executionTime + "ms)")
|
|
84
|
+
}
|
|
85
|
+
return this
|
|
47
86
|
}
|
|
48
87
|
|
|
49
88
|
method(operation, name, options) {
|
|
89
|
+
if (operation != 'retrieve') this.manageEntry()
|
|
50
90
|
const operations = {
|
|
51
91
|
create: () => {
|
|
52
92
|
this.savedMethods[name] = options
|
|
@@ -61,70 +101,91 @@ class Vaporous {
|
|
|
61
101
|
|
|
62
102
|
|
|
63
103
|
operations[operation]()
|
|
64
|
-
return this
|
|
104
|
+
if (operation !== 'retrieve') return this.manageExit()
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
filterIntoCheckpoint(checkpointName, funct, destroy) {
|
|
109
|
+
this.manageEntry()
|
|
110
|
+
const dataCheckpoint = this.events.filter(funct)
|
|
111
|
+
this._checkpoint('create', checkpointName, dataCheckpoint)
|
|
112
|
+
if (destroy) this.events = this.events.filter(event => !funct(event))
|
|
113
|
+
return this.manageExit()
|
|
65
114
|
}
|
|
66
115
|
|
|
67
116
|
filter(...args) {
|
|
117
|
+
this.manageEntry()
|
|
68
118
|
this.events = this.events.filter(...args)
|
|
69
|
-
return this
|
|
119
|
+
return this.manageExit()
|
|
70
120
|
}
|
|
71
121
|
|
|
72
122
|
append(entities) {
|
|
123
|
+
this.manageEntry()
|
|
73
124
|
this.events = this.events.concat(entities)
|
|
74
|
-
return this
|
|
125
|
+
return this.manageExit()
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
eval(modifier) {
|
|
129
|
+
this.manageEntry()
|
|
78
130
|
this.events.forEach(event => {
|
|
79
131
|
const vals = modifier(event)
|
|
80
132
|
if (vals) Object.assign(event, vals)
|
|
81
133
|
})
|
|
82
|
-
return this
|
|
134
|
+
return this.manageExit()
|
|
83
135
|
}
|
|
84
136
|
|
|
85
|
-
|
|
137
|
+
_table(modifier) {
|
|
86
138
|
this.events = this.events.map(event => {
|
|
87
139
|
const vals = modifier(event)
|
|
88
140
|
return vals;
|
|
89
141
|
})
|
|
90
|
-
|
|
142
|
+
}
|
|
143
|
+
table(modifier) {
|
|
144
|
+
this.manageEntry()
|
|
145
|
+
this._table(modifier)
|
|
146
|
+
return this.manageExit()
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
rename(...entities) {
|
|
150
|
+
this.manageEntry()
|
|
94
151
|
this.events.forEach(event => {
|
|
95
152
|
entities.forEach(([from, to]) => {
|
|
96
153
|
event[to] = event[from]
|
|
97
154
|
delete event[from]
|
|
98
155
|
})
|
|
99
156
|
})
|
|
100
|
-
return this
|
|
157
|
+
return this.manageExit()
|
|
101
158
|
}
|
|
102
159
|
|
|
103
160
|
parseTime(value, customFormat) {
|
|
161
|
+
this.manageEntry()
|
|
104
162
|
this.events.forEach(event => {
|
|
105
163
|
event[value] = dayjs(event[value], customFormat).valueOf()
|
|
106
164
|
})
|
|
107
|
-
return this
|
|
165
|
+
return this.manageExit()
|
|
108
166
|
}
|
|
109
167
|
|
|
110
168
|
bin(value, span) {
|
|
169
|
+
this.manageEntry()
|
|
111
170
|
this.events.forEach(event => {
|
|
112
171
|
event[value] = Math.floor(event[value] / span) * span
|
|
113
172
|
})
|
|
114
|
-
return this
|
|
173
|
+
return this.manageExit()
|
|
115
174
|
}
|
|
116
175
|
|
|
117
176
|
fileScan(directory) {
|
|
177
|
+
this.manageEntry()
|
|
118
178
|
const items = fs.readdirSync(directory)
|
|
119
179
|
this.events = items.map(item => {
|
|
120
180
|
return {
|
|
121
181
|
_fileInput: path.resolve(directory, item)
|
|
122
182
|
}
|
|
123
183
|
})
|
|
124
|
-
return this
|
|
184
|
+
return this.manageExit()
|
|
125
185
|
}
|
|
126
186
|
|
|
127
187
|
async csvLoad(parser) {
|
|
188
|
+
this.manageEntry()
|
|
128
189
|
const tasks = this.events.map(obj => {
|
|
129
190
|
const content = []
|
|
130
191
|
|
|
@@ -151,10 +212,11 @@ class Vaporous {
|
|
|
151
212
|
})
|
|
152
213
|
|
|
153
214
|
await Promise.all(tasks)
|
|
154
|
-
return this
|
|
215
|
+
return this.manageExit()
|
|
155
216
|
}
|
|
156
217
|
|
|
157
218
|
async fileLoad(delim, parser) {
|
|
219
|
+
this.manageEntry()
|
|
158
220
|
const tasks = this.events.map(obj => {
|
|
159
221
|
const content = []
|
|
160
222
|
|
|
@@ -178,10 +240,11 @@ class Vaporous {
|
|
|
178
240
|
})
|
|
179
241
|
|
|
180
242
|
await Promise.all(tasks)
|
|
181
|
-
return this
|
|
243
|
+
return this.manageExit()
|
|
182
244
|
}
|
|
183
245
|
|
|
184
246
|
output(...args) {
|
|
247
|
+
this.manageEntry()
|
|
185
248
|
if (args.length) {
|
|
186
249
|
console.log(this.events.map(event => {
|
|
187
250
|
return args.map(item => event[item])
|
|
@@ -190,10 +253,11 @@ class Vaporous {
|
|
|
190
253
|
console.log(this.events)
|
|
191
254
|
}
|
|
192
255
|
|
|
193
|
-
return this
|
|
256
|
+
return this.manageExit()
|
|
194
257
|
}
|
|
195
258
|
|
|
196
259
|
flatten() {
|
|
260
|
+
this.manageEntry()
|
|
197
261
|
const arraySize = this.events.reduce((acc, obj) => acc + obj._raw.length, 0)
|
|
198
262
|
let flattened = new Array(arraySize)
|
|
199
263
|
let i = 0
|
|
@@ -211,7 +275,7 @@ class Vaporous {
|
|
|
211
275
|
|
|
212
276
|
})
|
|
213
277
|
this.events = flattened;
|
|
214
|
-
return this
|
|
278
|
+
return this.manageExit()
|
|
215
279
|
}
|
|
216
280
|
|
|
217
281
|
_stats(args, events) {
|
|
@@ -245,11 +309,19 @@ class Vaporous {
|
|
|
245
309
|
const arr = Object.keys(map).map(key => {
|
|
246
310
|
const result = map[key]
|
|
247
311
|
|
|
312
|
+
let sortedCache = {}
|
|
248
313
|
aggregations.forEach(aggregation => {
|
|
249
|
-
|
|
314
|
+
const outputField = aggregation.outputField
|
|
315
|
+
const reference = map[key]._statsRaw[aggregation.field]
|
|
316
|
+
|
|
317
|
+
if (aggregation.sortable) {
|
|
318
|
+
sortedCache[aggregation.field] = reference.slice().sort((a, b) => a - b)
|
|
319
|
+
result[outputField] = aggregation.calculate(sortedCache[aggregation.field])
|
|
320
|
+
} else {
|
|
321
|
+
result[outputField] = aggregation.calculate(reference)
|
|
322
|
+
}
|
|
323
|
+
|
|
250
324
|
|
|
251
|
-
const aggregationField = aggregation.outputField
|
|
252
|
-
result[aggregationField] = aggregation.calculate(map[key])
|
|
253
325
|
})
|
|
254
326
|
|
|
255
327
|
delete map[key]._statsRaw
|
|
@@ -260,11 +332,13 @@ class Vaporous {
|
|
|
260
332
|
}
|
|
261
333
|
|
|
262
334
|
stats(...args) {
|
|
335
|
+
this.manageEntry()
|
|
263
336
|
this.events = this._stats(args, this.events).arr
|
|
264
|
-
return this
|
|
337
|
+
return this.manageExit()
|
|
265
338
|
}
|
|
266
339
|
|
|
267
340
|
eventstats(...args) {
|
|
341
|
+
this.manageEntry()
|
|
268
342
|
const stats = this._stats(args, this.events)
|
|
269
343
|
|
|
270
344
|
this.events.forEach(event => {
|
|
@@ -276,7 +350,7 @@ class Vaporous {
|
|
|
276
350
|
return this
|
|
277
351
|
}
|
|
278
352
|
|
|
279
|
-
|
|
353
|
+
_streamstats(...args) {
|
|
280
354
|
const backwardIterate = (event, i, by, maxBoundary = 0) => {
|
|
281
355
|
let backwardIndex = 0
|
|
282
356
|
const thisKey = keyFromEvent(event, by)
|
|
@@ -324,32 +398,186 @@ class Vaporous {
|
|
|
324
398
|
delete event._streamstats
|
|
325
399
|
})
|
|
326
400
|
|
|
327
|
-
return this
|
|
401
|
+
return this
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
streamstats(...args) {
|
|
405
|
+
this.manageEntry()
|
|
406
|
+
this._streamstats(...args)
|
|
407
|
+
return this.manageExit()
|
|
328
408
|
}
|
|
329
409
|
|
|
330
410
|
delta(field, remapField, ...bys) {
|
|
331
|
-
this.
|
|
332
|
-
|
|
411
|
+
this.manageEntry()
|
|
412
|
+
this._streamstats(new Aggregation(field, 'range', remapField), new Window(2), ...bys)
|
|
413
|
+
return this.manageExit()
|
|
333
414
|
}
|
|
334
415
|
|
|
335
416
|
sort(order, ...keys) {
|
|
417
|
+
this.manageEntry()
|
|
336
418
|
this.events = _sort(order, this.events, ...keys)
|
|
337
|
-
return this
|
|
419
|
+
return this.manageExit()
|
|
338
420
|
}
|
|
339
421
|
|
|
340
422
|
assert(funct) {
|
|
423
|
+
this.manageEntry()
|
|
341
424
|
const expect = (funct) => { if (!funct) throw new Error('Assertion failed') }
|
|
342
425
|
this.events.forEach((event, i) => {
|
|
343
426
|
funct(event, i, { expect })
|
|
344
427
|
})
|
|
345
|
-
return this
|
|
428
|
+
return this.manageExit()
|
|
346
429
|
}
|
|
347
430
|
|
|
348
|
-
build(name, type, { tab = 'Default', columns = 2 } = {}) {
|
|
431
|
+
build(name, type, { tab = 'Default', columns = 2, y2, y1Type, y2Type, y1Stacked, y2Stacked, sortX = 'asc', xTicks = false, trellisAxis = "shared", legend } = {}) {
|
|
432
|
+
this.manageEntry()
|
|
349
433
|
|
|
350
434
|
const visualisationOptions = { tab, columns }
|
|
351
435
|
|
|
352
|
-
|
|
436
|
+
|
|
437
|
+
let bounds = {}
|
|
438
|
+
|
|
439
|
+
const isY2 = (data) => {
|
|
440
|
+
let y2Mapped = false;
|
|
441
|
+
|
|
442
|
+
if (y2 instanceof Array) {
|
|
443
|
+
y2Mapped = y2.includes(data)
|
|
444
|
+
}
|
|
445
|
+
else if (y2 instanceof RegExp) {
|
|
446
|
+
y2Mapped = y2.test(data)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return y2Mapped
|
|
450
|
+
}
|
|
451
|
+
const graphData = this.events.map((trellis, i) => {
|
|
452
|
+
if (type === 'Table') {
|
|
453
|
+
return trellis;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const dataOptions = {}
|
|
457
|
+
|
|
458
|
+
// For every event in this trellis restructure to chart.js
|
|
459
|
+
if (sortX) trellis = _sort(sortX, trellis, '_time')
|
|
460
|
+
|
|
461
|
+
const trellisName = this.graphFlags.at(-1).trellisName?.[i] || ""
|
|
462
|
+
const columnDefinitions = this.graphFlags.at(-1).columnDefinitions[i]
|
|
463
|
+
|
|
464
|
+
trellis.forEach(event => {
|
|
465
|
+
columnDefinitions.forEach(prop => {
|
|
466
|
+
if (!dataOptions[prop]) dataOptions[prop] = []
|
|
467
|
+
const val = event[prop]
|
|
468
|
+
dataOptions[prop].push(val)
|
|
469
|
+
|
|
470
|
+
if (!bounds[prop]) bounds[prop] = {
|
|
471
|
+
min: val,
|
|
472
|
+
max: val
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (val < bounds[prop].min) bounds[prop].min = val;
|
|
476
|
+
if (val > bounds[prop].max) bounds[prop].max = val
|
|
477
|
+
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
const _time = dataOptions._time
|
|
483
|
+
delete dataOptions._time
|
|
484
|
+
|
|
485
|
+
let y2WasMapped = false
|
|
486
|
+
const data = {
|
|
487
|
+
labels: _time,
|
|
488
|
+
datasets: Object.keys(dataOptions).map(data => {
|
|
489
|
+
const y2Mapped = isY2(data)
|
|
490
|
+
if (y2Mapped) y2WasMapped = y2Mapped
|
|
491
|
+
|
|
492
|
+
const base = {
|
|
493
|
+
label: data,
|
|
494
|
+
yAxisID: y2Mapped ? 'y2' : undefined,
|
|
495
|
+
data: dataOptions[data],
|
|
496
|
+
type: y2Mapped ? y2Type : y1Type,
|
|
497
|
+
// borderColor: 'red',
|
|
498
|
+
// backgroundColor: 'red',
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (type === 'Scatter') {
|
|
502
|
+
base.showLine = false
|
|
503
|
+
base.pointRadius = 8
|
|
504
|
+
base.pointStyle = 'rect'
|
|
505
|
+
} else if (type === 'Area') {
|
|
506
|
+
base.fill = 'origin'
|
|
507
|
+
} else if (type === 'Line') {
|
|
508
|
+
base.pointRadius = 0;
|
|
509
|
+
}
|
|
510
|
+
return base
|
|
511
|
+
})
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const scales = {
|
|
515
|
+
y: {
|
|
516
|
+
type: 'linear',
|
|
517
|
+
display: true,
|
|
518
|
+
position: 'left',
|
|
519
|
+
stacked: y1Stacked
|
|
520
|
+
},
|
|
521
|
+
x: {
|
|
522
|
+
type: 'linear',
|
|
523
|
+
ticks: {
|
|
524
|
+
display: xTicks
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (y2WasMapped) scales.y2 = {
|
|
530
|
+
type: 'linear',
|
|
531
|
+
display: true,
|
|
532
|
+
position: 'right',
|
|
533
|
+
grid: {
|
|
534
|
+
drawOnChartArea: false
|
|
535
|
+
},
|
|
536
|
+
stacked: y2Stacked
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
type: 'line',
|
|
541
|
+
data: data,
|
|
542
|
+
options: {
|
|
543
|
+
scales,
|
|
544
|
+
responsive: true,
|
|
545
|
+
plugins: {
|
|
546
|
+
legend: {
|
|
547
|
+
display: legend || true,
|
|
548
|
+
position: 'bottom',
|
|
549
|
+
},
|
|
550
|
+
title: {
|
|
551
|
+
display: true,
|
|
552
|
+
text: name + trellisName
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
if (trellisAxis === 'shared') {
|
|
560
|
+
// Do a second iteration to implement bounds
|
|
561
|
+
graphData.forEach(trellisGraph => {
|
|
562
|
+
Object.keys(bounds).forEach(bound => {
|
|
563
|
+
let axis = isY2(bound) ? 'y2' : 'y'
|
|
564
|
+
if (bound === '_time') axis = 'x';
|
|
565
|
+
|
|
566
|
+
const thisAxis = trellisGraph.options.scales[axis]
|
|
567
|
+
const { min, max } = bounds[bound]
|
|
568
|
+
if (!thisAxis.min) {
|
|
569
|
+
thisAxis.min = min
|
|
570
|
+
thisAxis.max = max
|
|
571
|
+
}
|
|
572
|
+
if (min < thisAxis.min) thisAxis.min = min
|
|
573
|
+
if (max > thisAxis.max) thisAxis.max = max
|
|
574
|
+
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const data = JSON.stringify(graphData)
|
|
353
581
|
const lastData = this.visualisationData.at(-1)
|
|
354
582
|
|
|
355
583
|
if (lastData !== data) this.visualisationData.push(data)
|
|
@@ -360,22 +588,29 @@ class Vaporous {
|
|
|
360
588
|
this.tabs = this.tabs.sort((a, b) => a.localeCompare(b))
|
|
361
589
|
}
|
|
362
590
|
|
|
363
|
-
return this
|
|
591
|
+
return this.manageExit()
|
|
364
592
|
}
|
|
365
593
|
|
|
366
|
-
|
|
594
|
+
_checkpoint(operation, name, data) {
|
|
367
595
|
|
|
368
596
|
const operations = {
|
|
369
|
-
create: () => this.checkpoints[name] = structuredClone(
|
|
597
|
+
create: () => this.checkpoints[name] = structuredClone(data),
|
|
370
598
|
retrieve: () => this.events = structuredClone(this.checkpoints[name]),
|
|
371
599
|
delete: () => delete this.checkpoints[name]
|
|
372
600
|
}
|
|
373
601
|
|
|
374
602
|
operations[operation]()
|
|
375
|
-
return this
|
|
603
|
+
return this
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
checkpoint(operation, name) {
|
|
607
|
+
this.manageEntry()
|
|
608
|
+
this._checkpoint(operation, name, this.events)
|
|
609
|
+
return this.manageExit()
|
|
376
610
|
}
|
|
377
611
|
|
|
378
612
|
mvexpand(target) {
|
|
613
|
+
this.manageEntry()
|
|
379
614
|
const arr = []
|
|
380
615
|
this.events.forEach(event => {
|
|
381
616
|
if (!event[target]) return arr.push(event)
|
|
@@ -389,31 +624,33 @@ class Vaporous {
|
|
|
389
624
|
})
|
|
390
625
|
|
|
391
626
|
this.events = arr
|
|
392
|
-
return this
|
|
627
|
+
return this.manageExit()
|
|
393
628
|
}
|
|
394
629
|
|
|
395
630
|
writeFile(title) {
|
|
631
|
+
this.manageEntry()
|
|
396
632
|
fs.writeFileSync('./' + title, JSON.stringify(this.events))
|
|
397
|
-
return this
|
|
633
|
+
return this.manageExit()
|
|
398
634
|
}
|
|
399
635
|
|
|
400
|
-
toGraph(x, y, series, trellis = false
|
|
401
|
-
|
|
636
|
+
toGraph(x, y, series, trellis = false) {
|
|
637
|
+
this.manageEntry()
|
|
402
638
|
if (!(y instanceof Array)) y = [y]
|
|
403
|
-
if (options.y2 instanceof RegExp) options.y2 = options.y2.toString()
|
|
404
639
|
|
|
405
|
-
const yAggregations = y.map(item =>
|
|
640
|
+
const yAggregations = y.map(item => [
|
|
641
|
+
new Aggregation(item, 'list', item),
|
|
642
|
+
]).flat()
|
|
406
643
|
|
|
407
|
-
this.
|
|
644
|
+
this.events = this._stats([
|
|
408
645
|
...yAggregations,
|
|
409
646
|
new Aggregation(series, 'list', series),
|
|
410
647
|
new Aggregation(trellis, 'values', 'trellis'),
|
|
411
|
-
new By(x), trellis ? new By(trellis) : null
|
|
412
|
-
)
|
|
648
|
+
new By(x), trellis ? new By(trellis) : null], this.events
|
|
649
|
+
).arr
|
|
413
650
|
|
|
414
651
|
const trellisMap = {}, columnDefinitions = {}
|
|
415
652
|
|
|
416
|
-
this.
|
|
653
|
+
this._table(event => {
|
|
417
654
|
const _time = event[x]
|
|
418
655
|
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`)
|
|
419
656
|
const obj = {
|
|
@@ -422,7 +659,14 @@ class Vaporous {
|
|
|
422
659
|
|
|
423
660
|
event[series].forEach((series, i) => {
|
|
424
661
|
y.forEach(item => {
|
|
425
|
-
|
|
662
|
+
let name;
|
|
663
|
+
if (y.length === 1) {
|
|
664
|
+
if (series === undefined) name = item
|
|
665
|
+
else name = series
|
|
666
|
+
} else {
|
|
667
|
+
if (series !== undefined) name = `${series}_${item}`
|
|
668
|
+
else name = item
|
|
669
|
+
}
|
|
426
670
|
obj[name] = event[item][i]
|
|
427
671
|
})
|
|
428
672
|
})
|
|
@@ -447,6 +691,7 @@ class Vaporous {
|
|
|
447
691
|
})
|
|
448
692
|
|
|
449
693
|
const graphFlags = {}
|
|
694
|
+
|
|
450
695
|
if (trellis) {
|
|
451
696
|
graphFlags.trellis = true;
|
|
452
697
|
graphFlags.trellisName = Object.keys(trellisMap)
|
|
@@ -464,23 +709,15 @@ class Vaporous {
|
|
|
464
709
|
graphFlags.columnDefinitions = [adjColumns]
|
|
465
710
|
}
|
|
466
711
|
|
|
467
|
-
Object.assign(graphFlags, options)
|
|
468
712
|
this.graphFlags.push(graphFlags)
|
|
469
|
-
return this
|
|
713
|
+
return this.manageExit()
|
|
470
714
|
}
|
|
471
715
|
|
|
472
|
-
render() {
|
|
716
|
+
render(location = './Vaporous_generation.html') {
|
|
717
|
+
this.manageEntry()
|
|
473
718
|
const classSafe = (name) => name.replace(/[^a-zA-Z0-9]/g, "_")
|
|
474
719
|
|
|
475
|
-
const createElement = (name, type, visualisationOptions, eventData, { trellis,
|
|
476
|
-
|
|
477
|
-
if (typeof y2 === 'string') {
|
|
478
|
-
y2 = y2.split("/")
|
|
479
|
-
const flags = y2.at(-1)
|
|
480
|
-
y2.pop()
|
|
481
|
-
const content = y2.splice(1).join("/")
|
|
482
|
-
y2 = new RegExp(content, flags)
|
|
483
|
-
}
|
|
720
|
+
const createElement = (name, type, visualisationOptions, eventData, { trellis, trellisName = "" }) => {
|
|
484
721
|
|
|
485
722
|
if (classSafe(visualisationOptions.tab) !== selectedTab) return;
|
|
486
723
|
|
|
@@ -496,88 +733,41 @@ class Vaporous {
|
|
|
496
733
|
eventData = pairs.map(p => p[1]);
|
|
497
734
|
}
|
|
498
735
|
|
|
499
|
-
|
|
500
|
-
const data = new google.visualization.DataTable();
|
|
501
|
-
|
|
502
|
-
const series = {}, axis0 = { targetAxisIndex: 0 }, axis1 = { targetAxisIndex: 1 }
|
|
503
|
-
|
|
504
|
-
if (y1Type) axis0.type = y1Type
|
|
505
|
-
if (y2Type) axis1.type = y2Type
|
|
506
|
-
|
|
507
|
-
// Create columns
|
|
508
|
-
const columns = columnDefinitions[i]
|
|
509
|
-
|
|
510
|
-
columns.forEach((key, i) => {
|
|
511
|
-
// TODO: we might have to iterate the dataseries to find this information - most likely update the column definition references
|
|
512
|
-
const colType = typeof trellisData[0][key]
|
|
513
|
-
data.addColumn(colType === 'undefined' ? "number" : colType, key)
|
|
514
|
-
|
|
515
|
-
if (y2 && i !== 0) {
|
|
516
|
-
let match = false;
|
|
517
|
-
if (y2 instanceof Array) { match = y2.includes(key) }
|
|
518
|
-
else if (y2 instanceof RegExp) { match = y2.test(key) }
|
|
519
|
-
|
|
520
|
-
if (match) series[i - 1] = axis1
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (!series[i - 1]) series[i - 1] = axis0
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
let rows = trellisData.map(event => {
|
|
527
|
-
return columns.map(key => event[key])
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
rows = _sort(sortX, rows, 0)
|
|
531
|
-
|
|
532
|
-
data.addRows(rows);
|
|
533
|
-
|
|
534
|
-
const columnCount = visualisationOptions.columns || 2
|
|
535
|
-
const thisEntity = document.createElement('div')
|
|
536
|
-
thisEntity.className = "parentHolder"
|
|
537
|
-
thisEntity.style = `flex: 1 0 calc(${100 / columns}% - 6px); max-width: calc(${100 / columnCount}% - 6px);`
|
|
736
|
+
const columnCount = visualisationOptions.columns || 2
|
|
538
737
|
|
|
738
|
+
eventData.forEach((trellisData, i) => {
|
|
739
|
+
const parentHolder = document.createElement('div')
|
|
539
740
|
|
|
540
|
-
const thisGraph = document.createElement('div')
|
|
541
|
-
thisGraph.className = "graphHolder"
|
|
542
|
-
thisEntity.appendChild(thisGraph)
|
|
543
|
-
document.getElementById('content').appendChild(thisEntity)
|
|
544
741
|
|
|
545
|
-
const chartElement = new google.visualization[type](thisGraph)
|
|
546
742
|
|
|
547
|
-
|
|
548
|
-
console.log(chartElement.getSelection()[1], chartElement.getSelection()[0])
|
|
549
|
-
tokens[name] = trellisData[chartElement.getSelection()[0].row]
|
|
550
|
-
console.log(tokens[name])
|
|
551
|
-
});
|
|
743
|
+
document.getElementById('content').appendChild(parentHolder)
|
|
552
744
|
|
|
553
|
-
|
|
745
|
+
parentHolder.style = `flex: 0 0 calc(${100 / columnCount}% - 8px); max-width: calc(${100 / columnCount}% - 8px);`
|
|
746
|
+
if (type === 'Table') {
|
|
747
|
+
new Tabulator(parentHolder, { data: trellisData, autoColumns: 'full', layout: "fitDataStretch", })
|
|
748
|
+
} else {
|
|
749
|
+
const graphEntity = document.createElement('canvas')
|
|
750
|
+
parentHolder.appendChild(graphEntity)
|
|
751
|
+
new Chart(graphEntity, trellisData)
|
|
752
|
+
}
|
|
554
753
|
|
|
555
|
-
chartElement.draw(data, {
|
|
556
|
-
series, showRowNumber: false, legend: { position: 'bottom' }, title, isStacked: stacked,
|
|
557
|
-
width: document.body.scrollWidth / columnCount - (type === "LineChart" ? 12 : 24),
|
|
558
|
-
animation: { duration: 500, startup: true },
|
|
559
|
-
chartArea: { width: '85%', height: '75%' },
|
|
560
|
-
vAxis: {
|
|
561
|
-
viewWindow: {
|
|
562
|
-
min: y1Min
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
|
-
pointSize: type === 'ScatterChart' ? 2 : undefined
|
|
566
|
-
})
|
|
567
754
|
})
|
|
568
755
|
}
|
|
569
756
|
|
|
570
|
-
const filePath =
|
|
757
|
+
const filePath = location
|
|
571
758
|
fs.writeFileSync(filePath, `
|
|
572
759
|
<html>
|
|
573
760
|
<head>
|
|
761
|
+
<meta name="viewport" content="width=device-width, initial-scale=0.5">
|
|
574
762
|
<style>
|
|
575
763
|
${styles}
|
|
576
764
|
</style>
|
|
577
|
-
|
|
765
|
+
|
|
766
|
+
<link href="https://unpkg.com/tabulator-tables@6.3.1/dist/css/tabulator.min.css" rel="stylesheet">
|
|
767
|
+
<script type="text/javascript" src="https://unpkg.com/tabulator-tables@6.3.1/dist/js/tabulator.min.js"></script>
|
|
768
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
769
|
+
|
|
578
770
|
<script type="text/javascript">
|
|
579
|
-
google.charts.load('current', {'packages':['table', 'corechart']});
|
|
580
|
-
google.charts.setOnLoadCallback(drawVis);
|
|
581
771
|
|
|
582
772
|
const classSafe = ${classSafe.toString()}
|
|
583
773
|
|
|
@@ -615,8 +805,9 @@ class Vaporous {
|
|
|
615
805
|
</body>
|
|
616
806
|
</html>
|
|
617
807
|
`)
|
|
618
|
-
|
|
619
808
|
console.log('File ouput created ', path.resolve(filePath))
|
|
809
|
+
if (this.totalTime) console.log("File completed in " + this.totalTime)
|
|
810
|
+
return this.manageExit()
|
|
620
811
|
}
|
|
621
812
|
}
|
|
622
813
|
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,22 +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 quality of life features
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Examples
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- [
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
+
|
package/styles.css
CHANGED
|
@@ -1,79 +1,83 @@
|
|
|
1
|
-
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap');
|
|
2
|
-
|
|
3
|
-
body {
|
|
4
|
-
font-family: 'Roboto', Arial, sans-serif;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/* Chart-specific styles */
|
|
8
|
-
.chart,
|
|
9
|
-
.chart-container,
|
|
10
|
-
canvas,
|
|
11
|
-
svg,
|
|
12
|
-
.chartjs-render-monitor {
|
|
13
|
-
font-family: 'Roboto', Arial, sans-serif !important;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
body {
|
|
17
|
-
margin: 0;
|
|
18
|
-
padding:
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.tabBar {
|
|
22
|
-
display: flex;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
margin-bottom: 8px;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
border:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap');
|
|
2
|
+
|
|
3
|
+
body {
|
|
4
|
+
font-family: 'Roboto', Arial, sans-serif;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* Chart-specific styles */
|
|
8
|
+
.chart,
|
|
9
|
+
.chart-container,
|
|
10
|
+
canvas,
|
|
11
|
+
svg,
|
|
12
|
+
.chartjs-render-monitor {
|
|
13
|
+
font-family: 'Roboto', Arial, sans-serif !important;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 16px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.tabBar {
|
|
22
|
+
display: flex;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
|
|
25
|
+
min-height: 32px;
|
|
26
|
+
margin-bottom: 8px;
|
|
27
|
+
margin-top: 8px;
|
|
28
|
+
margin-left: 8px;
|
|
29
|
+
margin-right: 8px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.tabs {
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
padding: 8px 20px;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
background: none;
|
|
37
|
+
border: none;
|
|
38
|
+
outline: none;
|
|
39
|
+
font-size: 1rem;
|
|
40
|
+
color: #555;
|
|
41
|
+
transition: background 0.3s, color 0.3s;
|
|
42
|
+
text-align: center;
|
|
43
|
+
margin: 12px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.tabs:not(.selectedTab):hover {
|
|
47
|
+
background: #e0e0e0;
|
|
48
|
+
color: #222;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.selectedTab {
|
|
52
|
+
background: #1976d2;
|
|
53
|
+
color: #fff;
|
|
54
|
+
font-weight: bold;
|
|
55
|
+
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.15);
|
|
56
|
+
transition: background 0.3s, color 0.3s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#content {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-wrap: wrap;
|
|
62
|
+
padding: 0px;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
justify-content: flex-start;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.parentHolder {
|
|
68
|
+
display: flex;
|
|
69
|
+
margin: 2px;
|
|
70
|
+
border: 1px solid #d3d3d3;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.parentHolder::after {
|
|
74
|
+
border: 2px solid red;
|
|
75
|
+
/* Change color and width as needed */
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
z-index: 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.tabContent {
|
|
81
|
+
opacity: 1;
|
|
82
|
+
transition: opacity 0.3s,
|
|
79
83
|
}
|