123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- 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)
- }
- })
- }
|