Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

catchart

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

catchart - npm Package Compare versions

Comparing version 0.0.3 to 1.0.0

program.js

12

catchart.js
#!/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++]
}
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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc