vaporous 0.0.2 → 0.0.3

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 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, method) {
49
+ method(operation, name, options) {
53
50
  const operations = {
54
51
  create: () => {
55
- this.savedMethods[name] = method
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
- const event = parser(line)
139
- if (event !== null) content.push(event)
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
- console.log(this.events)
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
 
@@ -238,51 +277,63 @@ class Vaporous {
238
277
  }
239
278
 
240
279
  streamstats(...args) {
280
+ const backwardIterate = (event, i, by, maxBoundary = 0) => {
281
+ let backwardIndex = 0
282
+ const thisKey = keyFromEvent(event, by)
283
+ const byKey = thisKey
284
+
285
+ while (true) {
286
+ const target = i - backwardIndex
287
+
288
+ if (target < 0 || target < maxBoundary) break
289
+
290
+ const newKey = keyFromEvent(this.events[target], by)
291
+ if (thisKey !== newKey) break
292
+ backwardIndex++
293
+ }
294
+
295
+ return { byKey, start: i - backwardIndex + 1 }
296
+ }
297
+
298
+
241
299
  const window = args.filter(i => i instanceof Window)
242
300
  const by = args.filter(i => i instanceof By)
243
301
 
244
302
  // Perform some validation
245
303
  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
304
 
248
305
  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
- }
306
+ let start, byKey = "";
271
307
 
272
- backwardIndex++
273
- }
274
- start = Math.max(i - backwardIndex + 1, 0)
275
- }
308
+ // Refine to window size
309
+ if (window.length > 0) start = Math.max(i - window[0].size + 1, 0)
310
+ if (by.length !== 0) ({ start, byKey } = backwardIterate(event, i, by, start))
276
311
 
277
312
  const eventRange = this.events.slice(start, i + 1)
278
- Object.assign(event, this._stats(args, eventRange).map[byKey])
313
+ const embed = this._stats(args, eventRange).map[byKey]
314
+ Object.assign(event, {
315
+ _streamstats: embed
316
+ })
317
+ })
318
+
319
+ // We need to assign to a separate streamstats object to avoid collusions
320
+ // As streamstats iteratively updates the data but rlies on previous samples
321
+ // Modifying data in place corrupts the results of the query
322
+ this.events.forEach(event => {
323
+ Object.assign(event, event._streamstats)
324
+ delete event._streamstats
279
325
  })
280
326
 
281
327
  return this;
282
328
  }
283
329
 
330
+ delta(field, remapField, ...bys) {
331
+ this.streamstats(new Aggregation(field, 'range', remapField), new Window(2), ...bys)
332
+ return this;
333
+ }
334
+
284
335
  sort(order, ...keys) {
285
- this._events = _sort(order, this.events, keys)
336
+ this.events = _sort(order, this.events, ...keys)
286
337
  return this;
287
338
  }
288
339
 
@@ -294,14 +345,20 @@ class Vaporous {
294
345
  return this;
295
346
  }
296
347
 
297
- build(name, type, visualisationOptions) {
348
+ build(name, type, { tab = 'Default', columns = 2 } = {}) {
349
+
350
+ const visualisationOptions = { tab, columns }
351
+
298
352
  const data = JSON.stringify(this.events)
299
353
  const lastData = this.visualisationData.at(-1)
300
354
 
301
355
  if (lastData !== data) this.visualisationData.push(data)
302
356
  this.visualisations.push([name, type, visualisationOptions, this.visualisationData.length - 1, this.graphFlags[this.graphFlags.length - 1]])
303
357
 
304
- if (visualisationOptions.tab && !this.tabs.includes(visualisationOptions.tab)) this.tabs.push(visualisationOptions.tab)
358
+ if (visualisationOptions.tab && !this.tabs.includes(visualisationOptions.tab)) {
359
+ this.tabs.push(visualisationOptions.tab)
360
+ this.tabs = this.tabs.sort((a, b) => a.localeCompare(b))
361
+ }
305
362
 
306
363
  return this;
307
364
  }
@@ -309,8 +366,8 @@ class Vaporous {
309
366
  checkpoint(operation, name) {
310
367
 
311
368
  const operations = {
312
- create: () => this.checkpoints[name] = this.events,
313
- retrieve: () => this.events = this.checkpoints[name],
369
+ create: () => this.checkpoints[name] = structuredClone(this.events),
370
+ retrieve: () => this.events = structuredClone(this.checkpoints[name]),
314
371
  delete: () => delete this.checkpoints[name]
315
372
  }
316
373
 
@@ -322,10 +379,11 @@ class Vaporous {
322
379
  const arr = []
323
380
  this.events.forEach(event => {
324
381
  if (!event[target]) return arr.push(event)
325
- event[target].forEach((item) => {
382
+ event[target].forEach((item, i) => {
326
383
  arr.push({
327
384
  ...event,
328
- [target]: item
385
+ [target]: item,
386
+ [`_mvExpand_${target}`]: i
329
387
  })
330
388
  })
331
389
  })
@@ -334,27 +392,55 @@ class Vaporous {
334
392
  return this;
335
393
  }
336
394
 
337
- toGraph(x, y, series, trellis, options = {}) {
395
+ writeFile(title) {
396
+ fs.writeFileSync('./' + title, JSON.stringify(this.events))
397
+ return this;
398
+ }
399
+
400
+ toGraph(x, y, series, trellis = false, options = {}) {
401
+
402
+ if (!(y instanceof Array)) y = [y]
403
+ if (options.y2 instanceof RegExp) options.y2 = options.y2.toString()
404
+
405
+ const yAggregations = y.map(item => new Aggregation(item, 'list', item))
338
406
 
339
407
  this.stats(
340
- new Aggregation(y, 'list', y),
408
+ ...yAggregations,
341
409
  new Aggregation(series, 'list', series),
342
410
  new Aggregation(trellis, 'values', 'trellis'),
343
411
  new By(x), trellis ? new By(trellis) : null
344
412
  )
345
413
 
346
- const trellisMap = {}
414
+ const trellisMap = {}, columnDefinitions = {}
347
415
 
348
416
  this.table(event => {
417
+ const _time = event[x]
418
+ 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
419
  const obj = {
350
- _time: event[x]
420
+ _time
351
421
  }
352
- event[series].forEach((series, i) => obj[series] = event[y][i])
422
+
423
+ event[series].forEach((series, i) => {
424
+ y.forEach(item => {
425
+ const name = y.length === 1 ? series : `${series}_${item}`
426
+ obj[name] = event[item][i]
427
+ })
428
+ })
353
429
 
354
430
  if (trellis) {
355
431
  const tval = event.trellis[0]
356
- if (!trellisMap[tval]) trellisMap[tval] = []
432
+ if (!trellisMap[tval]) {
433
+ trellisMap[tval] = []
434
+ columnDefinitions[tval] = {}
435
+ }
357
436
  trellisMap[tval].push(obj)
437
+ Object.keys(obj).forEach(key => {
438
+ columnDefinitions[tval][key] = true
439
+ })
440
+ } else {
441
+ Object.keys(obj).forEach(key => {
442
+ columnDefinitions[key] = true;
443
+ })
358
444
  }
359
445
 
360
446
  return obj
@@ -364,7 +450,18 @@ class Vaporous {
364
450
  if (trellis) {
365
451
  graphFlags.trellis = true;
366
452
  graphFlags.trellisName = Object.keys(trellisMap)
453
+ graphFlags.columnDefinitions = Object.keys(trellisMap).map(tval => {
454
+ const adjColumns = ['_time']
455
+ Object.keys(columnDefinitions[tval]).forEach(col => (col !== '_time') ? adjColumns.push(col) : null)
456
+ return adjColumns
457
+ })
367
458
  this.events = Object.keys(trellisMap).map(tval => trellisMap[tval])
459
+ } else {
460
+ const adjColumns = ['_time']
461
+ Object.keys(columnDefinitions).forEach(col => (col !== '_time') ? adjColumns.push(col) : null)
462
+
463
+ this.events = [this.events]
464
+ graphFlags.columnDefinitions = [adjColumns]
368
465
  }
369
466
 
370
467
  Object.assign(graphFlags, options)
@@ -375,12 +472,22 @@ class Vaporous {
375
472
  render() {
376
473
  const classSafe = (name) => name.replace(/[^a-zA-Z0-9]/g, "_")
377
474
 
378
- const createElement = (name, type, visualisationOptions, eventData, { trellis, y2, sortX, trellisName = "", y2Type, y1Type, stacked, y1Min, y2Min }) => {
475
+ const createElement = (name, type, visualisationOptions, eventData, { trellis, y2, sortX, trellisName = "", y2Type, y1Type, stacked, y1Min, y2Min, columnDefinitions }) => {
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
+ }
484
+
379
485
  if (classSafe(visualisationOptions.tab) !== selectedTab) return;
380
486
 
381
487
  eventData = visualisationData[eventData]
382
- if (!trellis) eventData = [eventData]
383
- else {
488
+
489
+ // TODO: migrate trellis functionality from here to tograph
490
+ if (trellis) {
384
491
  let pairs = trellisName.map((name, i) => [name, eventData[i]]);
385
492
  pairs = pairs.sort((a, b) => a[0].localeCompare(b[0]))
386
493
 
@@ -389,7 +496,7 @@ class Vaporous {
389
496
  eventData = pairs.map(p => p[1]);
390
497
  }
391
498
 
392
- eventData.forEach((trellis, i) => {
499
+ eventData.forEach((trellisData, i) => {
393
500
  const data = new google.visualization.DataTable();
394
501
 
395
502
  const series = {}, axis0 = { targetAxisIndex: 0 }, axis1 = { targetAxisIndex: 1 }
@@ -398,9 +505,12 @@ class Vaporous {
398
505
  if (y2Type) axis1.type = y2Type
399
506
 
400
507
  // Create columns
401
- const columns = Object.keys(trellis[0])
508
+ const columns = columnDefinitions[i]
509
+
402
510
  columns.forEach((key, i) => {
403
- data.addColumn(typeof trellis[0][key], key)
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)
404
514
 
405
515
  if (y2 && i !== 0) {
406
516
  let match = false;
@@ -410,10 +520,10 @@ class Vaporous {
410
520
  if (match) series[i - 1] = axis1
411
521
  }
412
522
 
413
- if (!series[1 - i]) series[i - 1] = axis0
523
+ if (!series[i - 1]) series[i - 1] = axis0
414
524
  })
415
525
 
416
- let rows = trellis.map(event => {
526
+ let rows = trellisData.map(event => {
417
527
  return columns.map(key => event[key])
418
528
  })
419
529
 
@@ -436,7 +546,7 @@ class Vaporous {
436
546
 
437
547
  google.visualization.events.addListener(chartElement, 'select', (e) => {
438
548
  console.log(chartElement.getSelection()[1], chartElement.getSelection()[0])
439
- tokens[name] = trellis[chartElement.getSelection()[0].row]
549
+ tokens[name] = trellisData[chartElement.getSelection()[0].row]
440
550
  console.log(tokens[name])
441
551
  });
442
552
 
@@ -451,7 +561,8 @@ class Vaporous {
451
561
  viewWindow: {
452
562
  min: y1Min
453
563
  }
454
- }
564
+ },
565
+ pointSize: type === 'ScatterChart' ? 2 : undefined
455
566
  })
456
567
  })
457
568
  }
@@ -505,7 +616,7 @@ class Vaporous {
505
616
  </html>
506
617
  `)
507
618
 
508
- console.log('File ouput created ', filePath)
619
+ console.log('File ouput created ', path.resolve(filePath))
509
620
  }
510
621
  }
511
622
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaporous",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
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 quality of life features for query writers
5
+
6
+
7
+
8
+ ## Examples
9
+ You can find example queries [via git in the examples folder](https://github.com/lkashl/vaporous/tree/main/examples)
10
+
11
+ Interactive previews are available here:
12
+
13
+ - [Virtualised temperature sensor data](https://lkashl.github.io/vaporous/pages/temp_sensors.html)
14
+ - [CSV delimited virtruvian data](https://lkashl.github.io/vaporous/pages/gym.html)
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,53 +1,57 @@
1
- class Aggregation {
2
- constructor(field, type, outputField = field, options) {
3
- this.type = type;
4
- this.field = field;
5
- this.outputField = outputField
6
- this.options = options;
7
- this.sortable = ['max', 'min', 'percentile', 'median'].includes(type)
8
- }
9
-
10
- count(values) {
11
- return values.length
12
- }
13
-
14
- distinctCount(values) {
15
- return new Set(values).size
16
- }
17
-
18
- list(values) {
19
- return values;
20
- }
21
-
22
- values(values) {
23
- return [...new Set(values)];
24
- }
25
-
26
- calculate(statObj) {
27
- return this[this.type](statObj._statsRaw[this.field])
28
- }
29
-
30
- max(values) {
31
- return values[values.length - 1]
32
- }
33
-
34
- min(values) {
35
- return values[0]
36
- }
37
-
38
- percentile(values) {
39
- const index = Math.round(this.options / 100 * (values.length - 1));
40
- return values[index]
41
- }
42
-
43
- median(values) {
44
- const index = Math.floor((values.length - 1) / 2);
45
- return values[index]
46
- }
47
-
48
- sum(values) {
49
- return values.reduce((a, b) => a + b, 0)
50
- }
51
- }
52
-
1
+ class Aggregation {
2
+ constructor(field, type, outputField = field, options) {
3
+ this.type = type;
4
+ this.field = field;
5
+ this.outputField = outputField
6
+ this.options = options;
7
+ this.sortable = ['max', 'min', 'percentile', 'median', 'range'].includes(type)
8
+ }
9
+
10
+ count(values) {
11
+ return values.length
12
+ }
13
+
14
+ distinctCount(values) {
15
+ return new Set(values).size
16
+ }
17
+
18
+ list(values) {
19
+ return values;
20
+ }
21
+
22
+ values(values) {
23
+ return [...new Set(values)];
24
+ }
25
+
26
+ calculate(statObj) {
27
+ return this[this.type](statObj._statsRaw[this.field])
28
+ }
29
+
30
+ max(values) {
31
+ return values.at(-1)
32
+ }
33
+
34
+ min(values) {
35
+ return values[0]
36
+ }
37
+
38
+ range(values) {
39
+ return values.at(-1) - values[0]
40
+ }
41
+
42
+ percentile(values) {
43
+ const index = Math.round(this.options / 100 * (values.length - 1));
44
+ return values[index]
45
+ }
46
+
47
+ median(values) {
48
+ const index = Math.floor((values.length - 1) / 2);
49
+ return values[index]
50
+ }
51
+
52
+ sum(values) {
53
+ return values.reduce((a, b) => a + b, 0)
54
+ }
55
+ }
56
+
53
57
  module.exports = Aggregation