211 lines
9.2 KiB
JavaScript

const datastore = require('../store/data.js')
module.exports = function (RED) {
function ChartNode (config) {
const node = this
// create node in Node-RED
RED.nodes.createNode(this, config)
// which group are we rendering this widget
const group = RED.nodes.getNode(config.group)
const base = group.getBase()
node.clearHistory = function () {
const empty = []
datastore.save(base, node, empty)
// emit socket to front end to mimic an incoming message
base.emit('msg-input:' + node.id, { payload: empty }, node)
}
function getProperty (value, property) {
const props = property.split('.')
props.forEach((prop) => {
if (value) {
value = value[prop]
}
})
return value
}
const evts = {
// beforeSend will run before messages are sent client-side, as well as before sending on within Node-RED
// here, we use it to pre-process chart data to format it ready for plotting
beforeSend: function (msg) {
const p = msg.payload
let series = RED.util.evaluateNodeProperty(config.category, config.categoryType, node, msg)
// if receiving a object payload, the series could be a within the payload
if (config.categoryType === 'property') {
series = getProperty(p, config.category)
}
// single point or array of data?
if (Array.isArray(p)) {
// array of data
msg._datapoint = p.map((point) => {
// series available on a msg by msg basis - ensure we check for each msg
if (config.categoryType === 'property') {
series = getProperty(point, config.category)
}
return addToChart(point, series)
})
} else {
// single point
if (config.categoryType === 'json') {
// we can produce multiple datapoints from a single object/value here
const points = []
series.forEach((s) => {
if (s in p) {
const datapoint = addToChart(p, s)
points.push(datapoint)
}
})
msg._datapoint = points
} else {
msg._datapoint = addToChart(p, series)
}
}
// function to process a data point being appended to a line/scatter chart
function addToChart (payload, series) {
const datapoint = {}
// we group/categorize data by "series"
datapoint.category = series
// get our x value, if set
if (config.xAxisPropertyType === 'msg' && config.xAxisProperty === '') {
// handle a missing declaration of x-axis property, and backup to time series
config.xAxisPropertyType = 'property'
}
const x = RED.util.evaluateNodeProperty(config.xAxisProperty, config.xAxisPropertyType, node, msg)
// construct our datapoint
if (typeof payload === 'number') {
// do we have an x-property defined - if not, we're assuming time series
datapoint.x = config.xAxisProperty !== '' ? x : (new Date()).getTime()
datapoint.y = payload
} else if (typeof payload === 'object') {
// may have been given an x/y object already
let x = getProperty(payload, config.xAxisProperty)
let y = payload.y
if (x === undefined || x === null) {
x = (new Date()).getTime()
}
if (Array.isArray(series)) {
if (series.length > 1) {
y = series.map((s) => {
return getProperty(payload, s)
})
} else {
y = getProperty(payload, series[0])
}
}
datapoint.x = x
datapoint.y = y
}
return datapoint
}
return msg
},
onInput: function (msg, send, done) {
// use our own custom onInput in order to store history of msg payloads
if (!datastore.get(node.id)) {
datastore.save(base, node, [])
}
if (Array.isArray(msg.payload) && !msg.payload.length) {
// clear history
datastore.save(base, node, [])
} else {
if (config.action === 'replace') {
// clear our data store as we are replacing data
datastore.save(base, node, [])
}
if (!Array.isArray(msg.payload)) {
// quick clone of msg, and store in history
datastore.append(base, node, {
...msg
})
} else {
// we have an array in msg.payload, let's split them
msg.payload.forEach((p, i) => {
const payload = JSON.parse(JSON.stringify(p))
const d = msg._datapoint ? msg._datapoint[i] : null
const m = {
...msg,
payload,
_datapoint: d
}
datastore.append(base, node, m)
})
}
const maxPoints = parseInt(config.removeOlderPoints)
if (maxPoints && config.removeOlderPoints) {
// account for multiple lines?
// client-side does this for _each_ line
// remove older points
const lineCounts = {}
const _msg = datastore.get(node.id)
// trawl through in reverse order, and only keep the latest points (up to maxPoints) for each label
for (let i = _msg.length - 1; i >= 0; i--) {
const msg = _msg[i]
const label = msg.topic
lineCounts[label] = lineCounts[label] || 0
if (lineCounts[label] >= maxPoints) {
_msg.splice(i, 1)
} else {
lineCounts[label]++
}
}
datastore.save(base, node, _msg)
}
if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
// remove any points older than the specified time
const removeOlder = parseFloat(config.removeOlder)
const removeOlderUnit = parseFloat(config.removeOlderUnit)
const ago = (removeOlder * removeOlderUnit) * 1000 // milliseconds ago
const cutoff = (new Date()).getTime() - ago
const _msg = datastore.get(node.id).filter((msg) => {
let timestamp = msg._datapoint.x
// is x already a millisecond timestamp?
if (typeof (msg._datapoint.x) === 'string') {
timestamp = (new Date(msg._datapoint.x)).getTime()
}
return timestamp > cutoff
})
datastore.save(base, node, _msg)
}
// check sizing limits
}
send(msg)
}
}
// inform the dashboard UI that we are adding this node
group.register(node, config, evts)
}
RED.nodes.registerType('ui-chart', ChartNode)
// Add HTTP Admin endpoint to permit reset of chart history
RED.httpAdmin.post('/dashboard/chart/:id/clear', RED.auth.needsPermission('ui-chart.write'), function (req, res) {
const node = RED.nodes.getNode(req.params.id)
if (node) {
if (node.type === 'ui-chart') {
node.clearHistory()
res.sendStatus(200)
} else {
res.sendStatus(400, 'Requested node is not of type "ui-chart"')
}
} else {
res.sendStatus(404)
}
})
}