Node-Red configuration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ui_chart.js 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. const datastore = require('../store/data.js')
  2. module.exports = function (RED) {
  3. function ChartNode (config) {
  4. const node = this
  5. // create node in Node-RED
  6. RED.nodes.createNode(this, config)
  7. // which group are we rendering this widget
  8. const group = RED.nodes.getNode(config.group)
  9. const base = group.getBase()
  10. node.clearHistory = function () {
  11. const empty = []
  12. datastore.save(base, node, empty)
  13. // emit socket to front end to mimic an incoming message
  14. base.emit('msg-input:' + node.id, { payload: empty }, node)
  15. }
  16. function getProperty (value, property) {
  17. const props = property.split('.')
  18. props.forEach((prop) => {
  19. if (value) {
  20. value = value[prop]
  21. }
  22. })
  23. return value
  24. }
  25. const evts = {
  26. // beforeSend will run before messages are sent client-side, as well as before sending on within Node-RED
  27. // here, we use it to pre-process chart data to format it ready for plotting
  28. beforeSend: function (msg) {
  29. const p = msg.payload
  30. let series = RED.util.evaluateNodeProperty(config.category, config.categoryType, node, msg)
  31. // if receiving a object payload, the series could be a within the payload
  32. if (config.categoryType === 'property') {
  33. series = getProperty(p, config.category)
  34. }
  35. // single point or array of data?
  36. if (Array.isArray(p)) {
  37. // array of data
  38. msg._datapoint = p.map((point) => {
  39. // series available on a msg by msg basis - ensure we check for each msg
  40. if (config.categoryType === 'property') {
  41. series = getProperty(point, config.category)
  42. }
  43. return addToChart(point, series)
  44. })
  45. } else {
  46. // single point
  47. if (config.categoryType === 'json') {
  48. // we can produce multiple datapoints from a single object/value here
  49. const points = []
  50. series.forEach((s) => {
  51. if (s in p) {
  52. const datapoint = addToChart(p, s)
  53. points.push(datapoint)
  54. }
  55. })
  56. msg._datapoint = points
  57. } else {
  58. msg._datapoint = addToChart(p, series)
  59. }
  60. }
  61. // function to process a data point being appended to a line/scatter chart
  62. function addToChart (payload, series) {
  63. const datapoint = {}
  64. // we group/categorize data by "series"
  65. datapoint.category = series
  66. // get our x value, if set
  67. if (config.xAxisPropertyType === 'msg' && config.xAxisProperty === '') {
  68. // handle a missing declaration of x-axis property, and backup to time series
  69. config.xAxisPropertyType = 'property'
  70. }
  71. const x = RED.util.evaluateNodeProperty(config.xAxisProperty, config.xAxisPropertyType, node, msg)
  72. // construct our datapoint
  73. if (typeof payload === 'number') {
  74. // do we have an x-property defined - if not, we're assuming time series
  75. datapoint.x = config.xAxisProperty !== '' ? x : (new Date()).getTime()
  76. datapoint.y = payload
  77. } else if (typeof payload === 'object') {
  78. // may have been given an x/y object already
  79. let x = getProperty(payload, config.xAxisProperty)
  80. let y = payload.y
  81. if (x === undefined || x === null) {
  82. x = (new Date()).getTime()
  83. }
  84. if (Array.isArray(series)) {
  85. if (series.length > 1) {
  86. y = series.map((s) => {
  87. return getProperty(payload, s)
  88. })
  89. } else {
  90. y = getProperty(payload, series[0])
  91. }
  92. }
  93. datapoint.x = x
  94. datapoint.y = y
  95. }
  96. return datapoint
  97. }
  98. return msg
  99. },
  100. onInput: function (msg, send, done) {
  101. // use our own custom onInput in order to store history of msg payloads
  102. if (!datastore.get(node.id)) {
  103. datastore.save(base, node, [])
  104. }
  105. if (Array.isArray(msg.payload) && !msg.payload.length) {
  106. // clear history
  107. datastore.save(base, node, [])
  108. } else {
  109. if (config.action === 'replace') {
  110. // clear our data store as we are replacing data
  111. datastore.save(base, node, [])
  112. }
  113. if (!Array.isArray(msg.payload)) {
  114. // quick clone of msg, and store in history
  115. datastore.append(base, node, {
  116. ...msg
  117. })
  118. } else {
  119. // we have an array in msg.payload, let's split them
  120. msg.payload.forEach((p, i) => {
  121. const payload = JSON.parse(JSON.stringify(p))
  122. const d = msg._datapoint ? msg._datapoint[i] : null
  123. const m = {
  124. ...msg,
  125. payload,
  126. _datapoint: d
  127. }
  128. datastore.append(base, node, m)
  129. })
  130. }
  131. const maxPoints = parseInt(config.removeOlderPoints)
  132. if (maxPoints && config.removeOlderPoints) {
  133. // account for multiple lines?
  134. // client-side does this for _each_ line
  135. // remove older points
  136. const lineCounts = {}
  137. const _msg = datastore.get(node.id)
  138. // trawl through in reverse order, and only keep the latest points (up to maxPoints) for each label
  139. for (let i = _msg.length - 1; i >= 0; i--) {
  140. const msg = _msg[i]
  141. const label = msg.topic
  142. lineCounts[label] = lineCounts[label] || 0
  143. if (lineCounts[label] >= maxPoints) {
  144. _msg.splice(i, 1)
  145. } else {
  146. lineCounts[label]++
  147. }
  148. }
  149. datastore.save(base, node, _msg)
  150. }
  151. if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
  152. // remove any points older than the specified time
  153. const removeOlder = parseFloat(config.removeOlder)
  154. const removeOlderUnit = parseFloat(config.removeOlderUnit)
  155. const ago = (removeOlder * removeOlderUnit) * 1000 // milliseconds ago
  156. const cutoff = (new Date()).getTime() - ago
  157. const _msg = datastore.get(node.id).filter((msg) => {
  158. let timestamp = msg._datapoint.x
  159. // is x already a millisecond timestamp?
  160. if (typeof (msg._datapoint.x) === 'string') {
  161. timestamp = (new Date(msg._datapoint.x)).getTime()
  162. }
  163. return timestamp > cutoff
  164. })
  165. datastore.save(base, node, _msg)
  166. }
  167. // check sizing limits
  168. }
  169. send(msg)
  170. }
  171. }
  172. // inform the dashboard UI that we are adding this node
  173. group.register(node, config, evts)
  174. }
  175. RED.nodes.registerType('ui-chart', ChartNode)
  176. // Add HTTP Admin endpoint to permit reset of chart history
  177. RED.httpAdmin.post('/dashboard/chart/:id/clear', RED.auth.needsPermission('ui-chart.write'), function (req, res) {
  178. const node = RED.nodes.getNode(req.params.id)
  179. if (node) {
  180. if (node.type === 'ui-chart') {
  181. node.clearHistory()
  182. res.sendStatus(200)
  183. } else {
  184. res.sendStatus(400, 'Requested node is not of type "ui-chart"')
  185. }
  186. } else {
  187. res.sendStatus(404)
  188. }
  189. })
  190. }