Comparing version 0.0.3 to 1.0.0
#!/usr/bin/env node | ||
const chartcat = require('./index') | ||
const config = require('./config') | ||
const StreamSlicer = require('stream-slicer') | ||
const pump = require('pump') | ||
const program = require('./program') | ||
chartcat(pump(process.stdin, new StreamSlicer()), config) | ||
let slicer = new StreamSlicer({ sliceBy: program.rowSeparator }) | ||
let chartStream = chartcat(program) | ||
pump(process.stdin, slicer, chartStream, err => { | ||
if (err) { | ||
return console.error(err) | ||
} | ||
}) |
124
client.js
@@ -1,14 +0,5 @@ | ||
// uncomment when I manage to figure out why this particular css | ||
// is indigestible for browserify-css | ||
//require('frappe-charts/dist/frappe-charts.min.css') | ||
const Chart = require('frappe-charts/dist/frappe-charts.min.cjs.js') | ||
const Chart = require('chart.js') | ||
const domReady = require('domready') | ||
const pattern = require('patternomaly') | ||
let colorIndex = 0 | ||
const colors = [ | ||
'green', 'blue', 'violet', 'red', 'orange', 'yellow', 'light-blue', | ||
'light-green', 'purple', 'magenta', 'grey', 'dark-grey' | ||
] | ||
domReady(main) | ||
@@ -19,22 +10,40 @@ | ||
const data = { | ||
labels: [], | ||
datasets: [] | ||
const chartData = { | ||
datasets: [], | ||
labels: [] | ||
} | ||
for (let i = 0; i < context.datasetCount; i++) { | ||
console.log(i) | ||
data.datasets.push({ | ||
title: `dataset #${i + 1}`, | ||
color: pickColor(), | ||
values: [] | ||
for (let i = 0; i < context.fieldCount; i++) { | ||
let borderColor = pickColor() | ||
let patternName = pickPattern() | ||
let backgroundColor | ||
if (context.usePatterns) { | ||
backgroundColor = pattern.draw(patternName, borderColor.toString(0.2)) | ||
} else { | ||
backgroundColor = borderColor.toString(0.2) | ||
} | ||
if (context.noFill) { | ||
backgroundColor = new Color(0, 0, 0, 0) | ||
} | ||
chartData.datasets.push({ | ||
label: `dataset #${i + 1}`, | ||
borderColor: borderColor.toString(), | ||
backgroundColor, | ||
borderWidth: 1, | ||
data: [] | ||
}) | ||
} | ||
const chart = new Chart({ | ||
parent: '#chart', // or a DOM element | ||
title: context.title, | ||
data, | ||
type: context.chartType, // or 'line', 'scatter', 'pie', 'percentage' | ||
height: 250 | ||
const chart = new Chart('myChart', { | ||
data: chartData, | ||
options: { | ||
title: { | ||
display: true, | ||
text: context.title | ||
} | ||
}, | ||
type: context.chartType | ||
}) | ||
@@ -53,16 +62,55 @@ | ||
ws.onmessage = event => { | ||
let shouldTrim = count++ > context.windowSize | ||
let entry = JSON.parse(event.data) | ||
for (let i = 0; i < entry.values.length; i++) { | ||
let data = chartData.datasets[i].data | ||
data.push(parseFloat(entry.values[i])) | ||
let data = JSON.parse(event.data) | ||
console.log(data) | ||
chart.add_data_point( | ||
data.value, | ||
data.label | ||
) | ||
if (shouldTrim) { | ||
data.shift() | ||
} | ||
} | ||
if (count++ > context.windowSize) { | ||
chart.remove_data_point(0) | ||
let labels = chartData.labels | ||
labels.push(entry.label) | ||
if (shouldTrim) { | ||
labels.shift() | ||
} | ||
chart.update() | ||
} | ||
} | ||
class Color { | ||
constructor(r, g, b, a = 1) { | ||
this.r = r | ||
this.g = g | ||
this.b = b | ||
this.a = a | ||
} | ||
toString(overrideAlpha) { | ||
let alpha = overrideAlpha | ||
if (overrideAlpha === undefined) { | ||
alpha = this.a | ||
} | ||
return `rgba(${this.r},${this.g},${this.b},${alpha})` | ||
} | ||
} | ||
let colorIndex = 0 | ||
const colors = [ | ||
new Color(0, 215, 0), new Color(0, 0, 215), new Color(0, 215, 215), new Color(244, 119, 66), new Color(215, 0, 0), | ||
new Color(65, 244, 241), new Color(106, 65, 244), new Color(60, 60, 60), new Color(0, 0, 0) | ||
] | ||
let patternIndex = 0 | ||
const patternNames = [ | ||
'plus', 'cross', 'dash', 'cross-dash', 'dot', 'dot-dash', 'disc', 'ring', 'line', 'line-vertical', | ||
'weave', 'zigzag', 'zigzag-vertical', 'diagonal', 'diagonal-right-left', 'square', 'box', 'triangle', | ||
'triangle-inverted', 'diamond', 'diamond-box' | ||
] | ||
function pickColor() { | ||
@@ -74,2 +122,10 @@ if (colorIndex === colors.length) { | ||
return colors[colorIndex++] | ||
} | ||
function pickPattern() { | ||
if (patternIndex === patternNames.length) { | ||
patternIndex = 0 | ||
} | ||
return patternNames[patternIndex++] | ||
} |
const rc = require('rc') | ||
module.exports = rc('catchart', { | ||
windowSize: 20, | ||
datasetCount: 1, | ||
rowSeparator: '\n', | ||
// json | ||
// csv | ||
// auto - if data starts with { json is selected, otherwise assume csv | ||
inputFormat: 'auto', //json, csv | ||
// when json | ||
// if auto then data is obtained from the "value" or "data" property | ||
// else dataSource is expected to be the name of the data field | ||
// when csv this config option is ignored | ||
dataField: 'auto', | ||
// possible values: 'auto' | [fieldname] for json | [index] for csv | ||
// when json | ||
// if auto select from label from fields: label/title/key | ||
// otherwise select from field specified in labelSource | ||
// when csv | ||
// if auto, use timeSeries | ||
// else if set to "row" (or anything else actually) use the first field | ||
// as label | ||
// when labels cannot be obtained, use timeSeries | ||
labelSource: 'auto', | ||
// do not fill the area under chart lines with color | ||
noFill: false, | ||
// in addition to fill color under chart lines, also apply patterns. | ||
// this is useful for color blind individuals | ||
usePatterns: false, | ||
// size of the buffer | ||
windowSize: 50, | ||
// how many fields are in a row | ||
fieldCount: 'auto', | ||
title: `${new Date()} ::: ${process.cwd()}`, | ||
dataFunction: undefined, // timeSeriesData, keyValueData, multiValueData | ||
chartType: 'line', | ||
// 'line', 'scatter', 'bar', 'percentage', 'heatmap' | ||
chartType: 'line' | ||
// hcat related options | ||
port: 0, | ||
hostname: 'localhost', | ||
contentType: 'text/html' | ||
}) |
@@ -1,12 +0,83 @@ | ||
let count = 1 | ||
setInterval(() => { | ||
console.log(`${next()}\n`) | ||
}, 1000) | ||
const program = require('commander') | ||
function next() { | ||
if (Math.random() > 0.5) { | ||
return count++ | ||
} else { | ||
return count-- | ||
let customLabel = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] | ||
let position = 0 | ||
program.version('0.0.1') | ||
.option('-f, --jsonDataField <jsonDataField>', 'json data field', 'data') | ||
.option('-c, --customLabel', 'output custom labels') | ||
program.command('json') | ||
.action(() => { | ||
let count = 1 | ||
setInterval(() => { | ||
let row = `{ "${program.jsonDataField}": ["${next()}", "${next()}", "foo"]` | ||
if (program.customLabel) { | ||
row += `, "label": "${getCustomLabel()}"` | ||
} | ||
row += '}' | ||
console.error(`emitter: ${row}`) | ||
console.log(row) | ||
}, 1000) | ||
function next() { | ||
if (Math.random() > 0.5) { | ||
return count++ | ||
} else { | ||
return count-- | ||
} | ||
} | ||
}) | ||
program.command('singlecsv') | ||
.action(() => { | ||
let count = 1 | ||
setInterval(() => { | ||
console.log(`${next()}`) | ||
}, 1000) | ||
function next() { | ||
if (Math.random() > 0.5) { | ||
return count++ | ||
} else { | ||
return count-- | ||
} | ||
} | ||
}) | ||
program.command('csv') | ||
.action(() => { | ||
let count = 1 | ||
setInterval(() => { | ||
let row = `${next()},${next()},${next()},${next()}` | ||
if (program.customLabel) { | ||
row = `${getCustomLabel()}, ${row}` | ||
} | ||
console.error(`emitter: ${row}`) | ||
console.log(row) | ||
}, 1000) | ||
function next() { | ||
if (Math.random() > 0.5) { | ||
return count++ | ||
} else { | ||
return count-- | ||
} | ||
} | ||
}) | ||
program.parse(process.argv) | ||
function getCustomLabel() { | ||
if (position === customLabel.length) { | ||
position = 0 | ||
} | ||
return customLabel[position++] | ||
} |
336
index.js
const hcat = require('hcat') | ||
const WebSocketServer = require('ws').Server | ||
const WebSocket = require('ws') | ||
const fs = require('fs') | ||
@@ -7,3 +7,9 @@ const path = require('path') | ||
const LinkedList = require('digital-chain') | ||
const through2 = require('through2') | ||
const pump = require('pump') | ||
const debug = require('debug')('catchart') | ||
const defaultConfig = require('./config') | ||
const { isNullOrUndefined } = require('util') | ||
const timeFormatter = new HumanTime({ | ||
@@ -18,121 +24,287 @@ names: { | ||
const client = fs.readFileSync(path.join(__dirname, 'dist', 'client.js'), 'utf8') | ||
const css = fs.readFileSync(path.join(__dirname, 'node_modules', 'frappe-charts', 'dist', 'frappe-charts.min.css')) | ||
let initialized = false | ||
module.exports = function(stream, config) { | ||
config = config || {} | ||
module.exports = function(config) { | ||
// TODO maybe apply defaults here for programmatic use | ||
config = config || defaultConfig | ||
debug('config is %o', config) | ||
// hcat option | ||
// we don't want the server to die right after first request | ||
// since we're serving websockets, so this is enforced | ||
config.serveOnce = false | ||
// how long we're resting between exec() | ||
let waitTime | ||
const state = { | ||
// how long we're resting between exec() when the buffer is empty | ||
// this number will grow exponentially if the buffer is empty | ||
// and then reset back to 10 when the buffer has something in it | ||
websocketTransmitDelayMillis: 10, | ||
// start time of the first chunk | ||
startTime: undefined, | ||
buff: new LinkedList(), | ||
websocketConnected: false, | ||
inputFormat: config.inputFormat, | ||
dataField: config.dataField, | ||
labelSource: config.labelSource | ||
} | ||
// start time of the first chunk | ||
let startTime | ||
let selectedDataFunction | ||
let buff = new LinkedList() | ||
// start the show | ||
let stream = through2(processInput) | ||
stream.on('finish', shutdown) | ||
stream.once('data', init) | ||
stream.once('data', () => startTime = Date.now()) | ||
stream.on('data', (chunk) => { | ||
buff.push(chunk) | ||
}) | ||
return stream | ||
const dataFunctions = { | ||
keyValueData: data => { | ||
let splitted = data.split(',') | ||
function processInput(chunk, enc, cb) { | ||
if (splitted.length !== 2) { | ||
throw new Error(`invalid data for key / value pair ${data}`) | ||
if (enc !== 'buffer') { | ||
throw new Error(`unexpected encoding ${enc}`) | ||
} | ||
let data = toString(chunk) | ||
if (!initialized) { | ||
init(data) | ||
} | ||
data = state.parseFunction(data) | ||
let entry = { | ||
values: state.dataFunction(data), | ||
label: state.labelFunction(data, state) | ||
} | ||
debug('process input [%o]', entry) | ||
state.buff.push(entry) | ||
cb() | ||
} | ||
function init(data) { | ||
state.startTime = Date.now() | ||
let dataField = state.dataField | ||
let labelSource = state.labelSource | ||
if (state.inputFormat === 'auto') { | ||
debug('inputFormat is "auto"') | ||
if (data.startsWith('{')) { | ||
state.inputFormat = 'json' | ||
} else { | ||
state.inputFormat = 'csv' | ||
} | ||
} | ||
return { label: splitted[0], value: [splitted[1]] } | ||
}, | ||
debug('inputFormat set to "%s"', state.inputFormat) | ||
timeSeriesData: data => { | ||
let timeDiff = Date.now() - startTime | ||
return { | ||
label: timeFormatter.print(timeDiff), | ||
value: [data] | ||
if (state.inputFormat === 'json') { | ||
state.parseFunction = parseFunctions.json | ||
if (dataField === 'auto') { | ||
debug('dataField is "auto"') | ||
dataField = ['value', 'data'] | ||
} | ||
}, | ||
multiValueData: data => { | ||
let timeDiff = Date.now() - startTime | ||
let splitted = data.split(',') | ||
for (let i = 0; i < splitted.length; i++) { | ||
splitted[i] = parseFloat(splitted[i]) | ||
if (!Array.isArray(dataField)) { | ||
dataField = [dataField] | ||
} | ||
return { | ||
label: timeFormatter.print(timeDiff), | ||
value: splitted | ||
debug('dataField set to [%o]', dataField) | ||
if (labelSource === 'auto') { | ||
debug('lableSource is "auto"') | ||
labelSource = ['label', 'title', 'key'] | ||
} | ||
if (!Array.isArray(labelSource)) { | ||
labelSource = [labelSource] | ||
} | ||
debug('labelSource set to [%o]', labelSource) | ||
state.dataFunction = dataFunctions.json(dataField) | ||
state.labelFunction = labelFunctions.json(labelSource) | ||
} else { | ||
state.parseFunction = parseFunctions.arrSplit | ||
debug('labelSource is "%s"', labelSource) | ||
if (labelSource === 'auto') { | ||
state.dataFunction = dataFunctions.arr | ||
state.labelFunction = labelFunctions.timeSeries | ||
} else { | ||
state.dataFunction = dataFunctions.arrSlice(1) | ||
state.labelFunction = labelFunctions.arrFirst | ||
} | ||
} | ||
if (config.fieldCount === 'auto') { | ||
debug('fieldCount is set to "auto"') | ||
let parsed = state.parseFunction(data) | ||
let sliced = state.dataFunction(parsed) | ||
config.fieldCount = sliced.length | ||
debug('setting fieldCount to "%d", deduced from first row of data', sliced.length) | ||
} | ||
const clientContext = { | ||
chartType: config.chartType, | ||
noFill: config.noFill, | ||
usePatterns: config.usePatterns, | ||
windowSize: config.windowSize, | ||
title: config.title, | ||
fieldCount: config.fieldCount | ||
} | ||
debug('client context: %o', clientContext) | ||
state.server = hcat(createClientPage(clientContext), config) | ||
let wss = new WebSocket.Server({ server: state.server }) | ||
wss.on('connection', onIncomingConnection) | ||
initialized = true | ||
} | ||
function init(chunk) { | ||
if (config.dataFunction !== undefined) { | ||
selectedDataFunction = dataFunctions[config.dataFunction] | ||
if (!selectedDataFunction) { | ||
throw new Error(`invalid data function ${config.dataFunction}`) | ||
} | ||
function onIncomingConnection(ws) { | ||
if (state.websocketConnected) { | ||
return ws.close(-1, 'too many connections') | ||
} | ||
let data = toString(chunk).split(',') | ||
state.websocketConnected = true | ||
selectedDataFunction = selectDataFunction(data) | ||
ws.on('error', err => { | ||
console.error('websocket error', err) | ||
}) | ||
if (data.length > 2 && selectedDataFunction === dataFunctions.multiValueData) { | ||
config.datasetCount = data.length | ||
} | ||
const html = ` | ||
<html> | ||
<head> | ||
<style>${css}</style> | ||
<script> | ||
$$context = ${JSON.stringify(config)} | ||
</script> | ||
<script>${client}</script> | ||
</head> | ||
<body> | ||
<div id="chart"></div> | ||
</body> | ||
</html> | ||
` | ||
let server = hcat(html, config) | ||
let wss = new WebSocketServer({ server: server }) | ||
wss.on('connection', ws => exec(ws)) | ||
ws.on('close', () => { | ||
state.websocketConnected = false | ||
}) | ||
exec(ws) | ||
} | ||
function exec(ws) { | ||
if (buff.length > 0) { | ||
waitTime = 10 | ||
let data = selectedDataFunction(toString(buff.shift())) | ||
return ws.send(JSON.stringify(data), () => exec(ws)) | ||
if (ws.readyState === WebSocket.CLOSE) return | ||
if (state.buff.length > 0 && ws.readyState === WebSocket.OPEN) { | ||
state.websocketTransmitDelayMillis = 10 | ||
return ws.send(JSON.stringify(state.buff.shift()), () => exec(ws)) | ||
} | ||
if (waitTime < 2000) { | ||
waitTime *= 2 | ||
// some sort of exponential wait time with an upper cap | ||
// 10 * 2^8 | ||
if (state.websocketTransmitDelayMillis < 2560) { | ||
state.websocketTransmitDelayMillis *= 2 | ||
} | ||
setTimeout(() => exec(ws), waitTime) | ||
setTimeout(() => exec(ws), state.websocketTransmitDelayMillis) | ||
} | ||
function selectDataFunction(data) { | ||
if (data.length === 2) { | ||
return dataFunctions.keyValueData | ||
function shutdown() { | ||
if (state.server) { | ||
state.server.close() | ||
} | ||
} | ||
} | ||
if (data.length > 2) { | ||
return dataFunctions.multiValueData | ||
function toString(chunk) { | ||
return chunk.toString().trim() | ||
} | ||
const labelFunctions = { | ||
timeSeries: (date, state) => { | ||
let timeDiff = Date.now() - state.startTime | ||
return timeFormatter.print(timeDiff) | ||
}, | ||
json: (fields) => { | ||
let extract = extractJson(fields) | ||
return (data, state) => { | ||
let result = extract(data) | ||
if (!result) { | ||
result = labelFunctions.timeSeries(data, state) | ||
} | ||
return result | ||
} | ||
}, | ||
return dataFunctions.timeSeriesData | ||
arrFirst(data) { | ||
return data[0] | ||
} | ||
} | ||
function toString(chunk) { | ||
return chunk.toString().trim() | ||
const dataFunctions = { | ||
arr: data => { | ||
for (let i = 0; i < data.length; i++) { | ||
let number = parseFloat(data[i]) | ||
if (!isNaN(number)) { | ||
data[i] = number | ||
} | ||
} | ||
return data | ||
}, | ||
arrSlice: howMuch => { | ||
return data => { | ||
return dataFunctions.arr(data.slice(howMuch)) | ||
} | ||
}, | ||
json: fields => { | ||
let extract = extractJson(fields) | ||
return data => { | ||
let result = extract(data) | ||
if (!Array.isArray(result)) { | ||
result = [result] | ||
} | ||
result = dataFunctions.arr(result) | ||
return result | ||
} | ||
} | ||
} | ||
const parseFunctions = { | ||
json: JSON.parse, | ||
arrSplit: data => data.split(',') | ||
} | ||
function extractJson(fields) { | ||
return data => { | ||
for (let f of fields) { | ||
let value = data[f] | ||
if (!isNullOrUndefined(value)) { | ||
return value | ||
} | ||
} | ||
} | ||
} | ||
function createClientPage(clientContext) { | ||
let client = fs.readFileSync(path.join(__dirname, 'dist', 'client.js'), 'utf8') | ||
return ` | ||
<html> | ||
<head> | ||
<script> | ||
$$context = ${JSON.stringify(clientContext)} | ||
</script> | ||
<script>${client}</script> | ||
</head> | ||
<body> | ||
<div style="text-align:center"> | ||
<div class="chart-container" style="width: 95%;margin:auto;"> | ||
<canvas id="myChart"></canvas> | ||
</div> | ||
</div> | ||
</body> | ||
</html> | ||
` | ||
} |
{ | ||
"name": "catchart", | ||
"version": "0.0.3", | ||
"version": "1.0.0", | ||
"description": "cat something from command line to a browser chart", | ||
@@ -15,12 +15,16 @@ "license": "MIT", | ||
"dependencies": { | ||
"chart.js": "^2.7.1", | ||
"commander": "^2.13.0", | ||
"custom-human-time": "^1.0.3", | ||
"debug": "^3.1.0", | ||
"digital-chain": "^2.0.0", | ||
"domready": "^1.0.8", | ||
"frappe-charts": "0.0.5", | ||
"hcat": "^2.1.0", | ||
"opn": "^5.1.0", | ||
"patternomaly": "^1.3.0", | ||
"pkg-dir": "^2.0.0", | ||
"pump": "^1.0.2", | ||
"pump": "^2.0.1", | ||
"rc": "^1.2.2", | ||
"stream-slicer": "0.0.6", | ||
"through2": "^2.0.3", | ||
"ws": "^3.3.1" | ||
@@ -27,0 +31,0 @@ }, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
591347
2
69
15
1505
5
+ Addedchart.js@^2.7.1
+ Addedcommander@^2.13.0
+ Addeddebug@^3.1.0
+ Addedpatternomaly@^1.3.0
+ Addedthrough2@^2.0.3
+ Addedchart.js@2.9.4(transitive)
+ Addedchartjs-color@2.4.1(transitive)
+ Addedchartjs-color-string@0.6.0(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.31.1.4(transitive)
+ Addedcommander@2.20.3(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addeddebug@3.2.7(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@1.0.0(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedms@2.1.3(transitive)
+ Addedpatternomaly@1.3.2(transitive)
+ Addedprocess-nextick-args@2.0.1(transitive)
+ Addedpump@2.0.1(transitive)
+ Addedreadable-stream@2.3.8(transitive)
+ Addedstring_decoder@1.1.1(transitive)
+ Addedthrough2@2.0.5(transitive)
+ Addedutil-deprecate@1.0.2(transitive)
+ Addedxtend@4.0.2(transitive)
- Removedfrappe-charts@0.0.5
- Removedfrappe-charts@0.0.5(transitive)
- Removedpump@1.0.3(transitive)
Updatedpump@^2.0.1