diff --git a/examples/bar-chart.json b/examples/bar-chart.json index 631bc4519..67d4de3c4 100644 --- a/examples/bar-chart.json +++ b/examples/bar-chart.json @@ -6,17 +6,23 @@ "group": "", "name": "", "label": "Sales in Million", - "order": 9007199254740991, + "order": 3, "chartType": "bar", "category": "location", "categoryType": "property", + "xAxisLabel": "", "xAxisProperty": "", - "xAxisPropertyType": "msg", + "xAxisPropertyType": "str", "xAxisType": "category", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "", "yAxisProperty": "sales_millions", + "yAxisPropertyType": "property", "ymin": "", "ymax": "", "action": "append", + "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": false, @@ -71,5 +77,35 @@ "2a23595f05d3331e" ] ] + }, + { + "id": "f4c5f4c74fbd2db2", + "type": "inject", + "z": "0758321f1687e812", + "name": "Clear", + "props": [ + { + "p": "payload" + }, + { + "p": "action", + "v": "replace", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "[]", + "payloadType": "json", + "x": 210, + "y": 800, + "wires": [ + [ + "2a23595f05d3331e" + ] + ] } ] \ No newline at end of file diff --git a/examples/line-chart.json b/examples/line-chart.json index 4bc12b67e..89cd16150 100644 --- a/examples/line-chart.json +++ b/examples/line-chart.json @@ -4,19 +4,25 @@ "type": "ui-chart", "z": "0758321f1687e812", "group": "", - "name": "", + "name": "Line Chart", "label": "chart", "order": 9007199254740991, "chartType": "line", "category": "location", "categoryType": "property", + "xAxisLabel": "", "xAxisProperty": "datestamp", - "xAxisPropertyType": "msg", + "xAxisPropertyType": "property", "xAxisType": "time", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "", "yAxisProperty": "temp", + "yAxisPropertyType": "property", "ymin": "", "ymax": "", "action": "append", + "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, @@ -71,5 +77,35 @@ "efccfdf502300871" ] ] + }, + { + "id": "33ee5762cce0ee32", + "type": "inject", + "z": "862ec766f2af6f61", + "name": "Clear", + "props": [ + { + "p": "payload" + }, + { + "p": "action", + "v": "replace", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "[]", + "payloadType": "json", + "x": 290, + "y": 1100, + "wires": [ + [ + "efccfdf502300871" + ] + ] } ] \ No newline at end of file diff --git a/examples/scatter-chart.json b/examples/scatter-chart.json index 8ebd76eae..ab8cd47f5 100644 --- a/examples/scatter-chart.json +++ b/examples/scatter-chart.json @@ -4,19 +4,25 @@ "type": "ui-chart", "z": "0758321f1687e812", "group": "", - "name": "", + "name": "Scatter Chart", "label": "chart", - "order": 9007199254740991, + "order": 4, "chartType": "scatter", "category": "", "categoryType": "str", + "xAxisLabel": "", "xAxisProperty": "x", - "xAxisPropertyType": "msg", + "xAxisPropertyType": "property", "xAxisType": "linear", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "", "yAxisProperty": "y", + "yAxisPropertyType": "property", "ymin": "", "ymax": "", "action": "replace", + "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, @@ -47,7 +53,7 @@ "id": "937b42a40fdcf424", "type": "inject", "z": "0758321f1687e812", - "name": "", + "name": "Arc data", "props": [ { "p": "payload" @@ -71,5 +77,35 @@ "91ced026339a3eeb" ] ] + }, + { + "id": "60e4dfb81bdbcb03", + "type": "inject", + "z": "0758321f1687e812", + "name": "Clear", + "props": [ + { + "p": "payload" + }, + { + "p": "action", + "v": "replace", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "[]", + "payloadType": "json", + "x": 250, + "y": 860, + "wires": [ + [ + "91ced026339a3eeb" + ] + ] } ] \ No newline at end of file diff --git a/nodes/widgets/ui_chart.html b/nodes/widgets/ui_chart.html index 05e05c42f..48b1e0a9f 100644 --- a/nodes/widgets/ui_chart.html +++ b/nodes/widgets/ui_chart.html @@ -42,7 +42,7 @@ return RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.' + property) } const noneType = { value: 'none', label: RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.label.none'), hasValue: false } - const propertyType = { value: 'property', label: RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.label.key') } + const keyType = { value: 'property', label: RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.label.key') } function hexToRgb (hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) @@ -57,9 +57,9 @@ function getSeriesTypes (xAxisType) { if (xAxisType === 'category') { - return [noneType, 'msg', 'str', 'json', propertyType] + return [noneType, 'msg', 'str', 'json', keyType] } else { - return [noneType, 'msg', 'str', 'json', propertyType] + return [noneType, 'msg', 'str', 'json', keyType] } } @@ -82,6 +82,7 @@ xAxisFormatType: { value: 'auto' }, yAxisLabel: { value: '' }, yAxisProperty: { value: null }, + yAxisPropertyType: { value: 'property' }, ymin: { value: '', validate: function (value) { return value === '' || RED.validators.number() } }, ymax: { value: '', validate: function (value) { return value === '' || RED.validators.number() } }, action: { value: 'append' }, @@ -224,14 +225,14 @@ types: getSeriesTypes($('#node-input-xAxisType').val()) }) $('#node-input-xAxisProperty').typedInput({ - default: propertyType.value, + default: keyType.value, typeField: $('#node-input-xAxisPropertyType'), - types: ['msg', 'str', propertyType] + types: ['msg', 'str', keyType] }) $('#node-input-yAxisProperty').typedInput({ - default: propertyType.value, + default: keyType.value, typeField: $('#node-input-yAxisPropertyType'), - types: [propertyType] + types: ['msg', keyType] // makes no sense for the y-axis to be a string }) const updateXAxisTypeOptions = function (options, defaultValue) { @@ -298,9 +299,9 @@ } }) - // handle event when chart's type is changed + // handle event when chart's series property is changed $('#node-input-category').change((evt) => { - const categoryType = $(evt.target).val() + const categoryType = $('#node-input-category').typedInput('type') if (categoryType === 'json') { // hide y-axis property setting as category will now control that @@ -313,8 +314,8 @@ // ensure the xAxis PropertyType isn't an invalid value if (this.xAxisPropertyType === 'msg' && this.xAxisProperty === '') { // auto-fix it for the user - this.xAxisPropertyType = propertyType.value - $('#node-input-xAxisProperty').typedInput('type', propertyType.value) + this.xAxisPropertyType = keyType.value + $('#node-input-xAxisProperty').typedInput('type', keyType.value) } // Stack/Group-By Options - convert from bool to string values for diff --git a/nodes/widgets/ui_chart.js b/nodes/widgets/ui_chart.js index 42fa3d864..bbf94eaff 100644 --- a/nodes/widgets/ui_chart.js +++ b/nodes/widgets/ui_chart.js @@ -28,6 +28,22 @@ module.exports = function (RED) { return value } + // ensure sane defaults + if (!['msg', 'str', 'property'].includes(config.xAxisPropertyType)) { + config.xAxisPropertyType = 'property' // default to 'key' + } + if (!['msg', 'property'].includes(config.yAxisPropertyType)) { + config.yAxisPropertyType = 'property' // default to 'key' + } + if (config.xAxisPropertyType === 'msg' && !config.xAxisProperty) { + config.xAxisPropertyType = 'property' // msg needs a property to evaluate, default to 'key' + } + if (config.yAxisPropertyType === 'msg' && !config.yAxisProperty) { + config.yAxisPropertyType = 'property' // msg needs a property to evaluate, default to 'key' + } + config.xAxisProperty = config.xAxisProperty || '' + config.yAxisProperty = config.yAxisProperty || '' + 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 @@ -67,32 +83,36 @@ module.exports = function (RED) { } } + function evaluateNodePropertyWithKey (node, msg, payload, property, propertyType) { + if (propertyType === 'property' /* AKA key */) { + return RED.util.evaluateNodeProperty(property, 'msg', node, payload) + } + return RED.util.evaluateNodeProperty(property, propertyType, node, msg) + } + // 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() + // since key would attempt to evaluate a property on a number, we don't do evaluation when + // x-axis is type is 'key' (only when it's 'msg' or 'str') + datapoint.x = config.xAxisPropertyType === 'msg' || config.xAxisPropertyType === 'str' + ? evaluateNodePropertyWithKey(node, msg, payload, config.xAxisProperty, config.xAxisPropertyType) + : (new Date()).getTime() datapoint.y = payload } else if (typeof payload === 'object') { + let x = evaluateNodePropertyWithKey(node, msg, payload, config.xAxisProperty, config.xAxisPropertyType) // 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)) { + let y if (series.length > 1) { y = series.map((s) => { return getProperty(payload, s) @@ -100,9 +120,11 @@ module.exports = function (RED) { } else { y = getProperty(payload, series[0]) } + datapoint.y = y + } else { + datapoint.y = evaluateNodePropertyWithKey(node, msg, payload, config.yAxisProperty, config.yAxisPropertyType) } datapoint.x = x - datapoint.y = y } return datapoint } diff --git a/ui/src/widgets/ui-chart/UIChart.vue b/ui/src/widgets/ui-chart/UIChart.vue index 03cf6f90e..6cdad5c98 100644 --- a/ui/src/widgets/ui-chart/UIChart.vue +++ b/ui/src/widgets/ui-chart/UIChart.vue @@ -21,6 +21,7 @@ export default { }, data () { return { + /** @type {Chart} */ chart: null, hasData: false, chartUpdateDebounceTimeout: null @@ -73,7 +74,7 @@ export default { parsing.xAxisKey = this.props.xAxisProperty } - if (this.props.categoryType !== 'json' && this.props.yAxisProperty) { + if (this.props.categoryType !== 'json' && this.props.yAxisProperty && this.props.yAxisPropertyType === 'property') { parsing.yAxisKey = this.props.yAxisProperty } } else { @@ -353,9 +354,12 @@ export default { this.chartUpdate() }, addPoints (payload, datapoint, label) { - const d = { - ...datapoint, - ...payload + const d = { ...datapoint, ...payload } + if (!this.chart.config?.options?.parsing?.xAxisKey) { + d.x = datapoint.x // if there is no mapping key, ensure server side computed datapoint.x is used + } + if (!this.chart.config?.options?.parsing?.yAxisKey) { + d.y = datapoint.y // if there is no mapping key, ensure server side computed datapoint.y is used } if (Array.isArray(label) && label.length > 0) { @@ -366,19 +370,15 @@ export default { } dd.category = d.category[i] dd.y = d.y[i] - this.addPoint(payload, dd, label[i]) + this.addToChart(d, label) + this.commit(payload, dd, label[i]) } } else { - this.addPoint(payload, datapoint, label) + this.addToChart(d, label) + this.commit(payload, datapoint, label) } }, - addPoint (payload, datapoint, label) { - const d = { - ...datapoint, - ...payload - } - this.addToChart(d, label) - + commit (payload, datapoint, label) { // APPEND our latest data point to the store this.$store.commit('data/append', { widgetId: this.id,