diff --git a/cypress/fixtures/flows/dashboard-chart-timestamp.json b/cypress/fixtures/flows/dashboard-chart-timestamp.json new file mode 100644 index 000000000..9c0179139 --- /dev/null +++ b/cypress/fixtures/flows/dashboard-chart-timestamp.json @@ -0,0 +1,267 @@ +[ + { + "id": "node-red-tab-charts", + "type": "tab", + "label": "UI Chart", + "disabled": false, + "info": "", + "env": [] + }, + { + "id": "button-clear", + "type": "ui-button", + "z": "node-red-tab-charts", + "group": "dashboard-ui-group", + "name": "", + "label": "Button - Clear", + "order": 0, + "width": 0, + "height": 0, + "emulateClick": false, + "tooltip": "", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "iconPosition": "left", + "payload": "[]", + "payloadType": "json", + "topic": "topic", + "topicType": "msg", + "buttonColor": "", + "textColor": "", + "iconColor": "", + "enablePointerdown": false, + "pointerdownPayload": "", + "pointerdownPayloadType": "str", + "enablePointerup": false, + "pointerupPayload": "", + "pointerupPayloadType": "str", + "x": 370, + "y": 120, + "wires": [ + [ + "line-chart-timestamp", + "line-chart-empty-xProp" + ] + ] + }, + { + "id": "button-send-payload", + "type": "ui-button", + "z": "node-red-tab-charts", + "group": "dashboard-ui-group", + "name": "", + "label": "Button - Number", + "order": 0, + "width": 0, + "height": 0, + "emulateClick": false, + "tooltip": "", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "iconPosition": "left", + "payload": "2", + "payloadType": "num", + "topic": "topic", + "topicType": "msg", + "buttonColor": "", + "textColor": "", + "iconColor": "", + "enablePointerdown": false, + "pointerdownPayload": "", + "pointerdownPayloadType": "str", + "enablePointerup": false, + "pointerupPayload": "", + "pointerupPayloadType": "str", + "x": 370, + "y": 1120, + "wires": [ + [ + "line-chart-timestamp", + "line-chart-empty-xProp" + ] + ] + }, + { + "id": "line-chart-timestamp", + "type": "ui-chart", + "z": "node-red-tab-charts", + "group": "dashboard-ui-group", + "name": "", + "label": "chart", + "order": 9007199254740991, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisType": "time", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "xmin": "", + "xmax": "", + "yAxisLabel": "", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "ymin": "", + "ymax": "", + "bins": 10, + "action": "append", + "stackSeries": false, + "pointShape": "circle", + "pointRadius": 4, + "showLegend": true, + "removeOlder": 1, + "removeOlderUnit": "3600", + "removeOlderPoints": "", + "colors": [ + "#0095ff", + "#ff0000", + "#ff7f0e", + "#2ca02c", + "#a347e1", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "width": 6, + "height": 8, + "className": "", + "x": 510, + "y": 1120, + "wires": [ + [] + ] + }, + { + "id": "line-chart-empty-xProp", + "type": "ui-chart", + "z": "node-red-tab-charts", + "group": "dashboard-ui-group", + "name": "", + "label": "chart", + "order": 9007199254740991, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "", + "xAxisProperty": "", + "xAxisPropertyType": "property", + "xAxisType": "time", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "xmin": "", + "xmax": "", + "yAxisLabel": "", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "ymin": "", + "ymax": "", + "bins": 10, + "action": "append", + "stackSeries": false, + "pointShape": "circle", + "pointRadius": 4, + "showLegend": true, + "removeOlder": 1, + "removeOlderUnit": "3600", + "removeOlderPoints": "", + "colors": [ + "#0095ff", + "#ff0000", + "#ff7f0e", + "#2ca02c", + "#a347e1", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "width": 6, + "height": 8, + "className": "", + "x": 510, + "y": 120, + "wires": [ + [] + ] + }, + { + "id": "dashboard-ui-group", + "type": "ui-group", + "name": "Group 1", + "page": "dashboard-ui-page-1", + "width": "12", + "height": "1", + "order": -1, + "showTitle": true, + "className": "", + "visible": "true", + "disabled": "false" + }, + { + "id": "dashboard-ui-page-1", + "type": "ui-page", + "name": "Page 1", + "ui": "dashboard-ui-base", + "path": "/page1", + "icon": "", + "layout": "grid", + "theme": "dashboard-ui-theme", + "order": -1, + "className": "", + "visible": "true", + "disabled": false + }, + { + "id": "dashboard-ui-base", + "type": "ui-base", + "name": "UI Name", + "path": "/dashboard", + "includeClientData": true, + "acceptsClientConfig": [ + "ui-notification", + "ui-control" + ] + }, + { + "id": "dashboard-ui-theme", + "type": "ui-theme", + "name": "Default", + "colors": { + "surface": "#ffffff", + "primary": "#6771f4", + "bgPage": "#e5dcdc", + "groupBg": "#ffffff", + "groupOutline": "#d39292" + }, + "sizes": { + "pagePadding": "6px", + "groupGap": "12px", + "groupBorderRadius": "4px", + "widgetGap": "6px", + "density": "default" + } + } +] \ No newline at end of file diff --git a/cypress/tests/widgets/chart.spec.js b/cypress/tests/widgets/chart.spec.js index 347269838..295407448 100644 --- a/cypress/tests/widgets/chart.spec.js +++ b/cypress/tests/widgets/chart.spec.js @@ -151,4 +151,55 @@ describe('Node/-RED Dashboard 2.0 - Chart - Data Sets', () => { checkSeries(barChart.chart.config.data.datasets[3], 'Q4', [[2021, 163], [2022, 210], [2023, 138]]) }) }) + + it('renders charts correctly when x is set to "timestamp"', () => { + cy.deployFixture('dashboard-chart-timestamp') + cy.visit('/dashboard/page1') + + // new (from 1.18.1) + cy.get('#nrdb-ui-widget-line-chart-timestamp > div > canvas').should('exist') + + // legacy charts pre 1.18.1, which rendered time when xAxisProperty was empty + cy.get('#nrdb-ui-widget-line-chart-empty-xProp > div > canvas').should('exist') + + // Clear both charts + cy.clickAndWait(cy.get('button').contains('Button - Clear')) + + // Add 3 data points to BOTH charts, 200 milliseconds apart + cy.clickAndWait(cy.get('button').contains('Button - Number'), 200) + cy.clickAndWait(cy.get('button').contains('Button - Number'), 200) + cy.clickAndWait(cy.get('button').contains('Button - Number'), 200) + + // eslint-disable-next-line promise/catch-or-return, promise/always-return + cy.window().then(win => { + should(win.uiCharts).is.not.empty() + const timestampXChart = win.uiCharts['line-chart-timestamp'] + const emptyXChart = win.uiCharts['line-chart-empty-xProp'] + + // New Charts + should(timestampXChart.chart.config.data).be.an.Object() + should(timestampXChart.chart.config.data.datasets).be.an.Array() + + // Legacy Charts + should(emptyXChart.chart.config.data).be.an.Object() + should(emptyXChart.chart.config.data.datasets).be.an.Array() + + // Check data populated correctly + // New Charts + should(timestampXChart.chart.config.data.datasets[0].data).be.an.Array().and.have.length(3) + + // Legacy Charts + should(emptyXChart.chart.config.data.datasets[0].data).be.an.Array().and.have.length(3) + // loop over the three data points + // eslint-disable-next-line promise/always-return + for (let i = 0; i < 3; i++) { + // New Charts + should(timestampXChart.chart.config.data.datasets[0].data[i]).have.property('x') + should(timestampXChart.chart.config.data.datasets[0].data[i]).have.property('y', 2) + // Legacy Charts + should(emptyXChart.chart.config.data.datasets[0].data[i]).have.property('x') + should(emptyXChart.chart.config.data.datasets[0].data[i]).have.property('y', 2) + } + }) + }) }) diff --git a/docs/nodes/widgets/ui-chart.md b/docs/nodes/widgets/ui-chart.md index cc3c0413b..5b920fc77 100644 --- a/docs/nodes/widgets/ui-chart.md +++ b/docs/nodes/widgets/ui-chart.md @@ -19,8 +19,8 @@ props: X-Axis Limit: Any data that is before the specific time limit (for time charts) or where there are more data points than the limit specified will be removed from the chart. Properties: Series: Controls how you want to set the Series of data stream into this widget. The default is msg.topic, where separate topics will render to a new line/bar in their respective plots.
- X: Only available for Line & Scatter Charts. This defines the key (which can be nested) of the value that should be plotted onto the x-axis. If left blank, the x-value will be calculated as the current timestamp.
- Y: Defines the key (which can be nested, e.g. 'nested.value') of the value that should be plotted onto the x-axis. This value is ignored if injecting single numerical values into the chart. + X: Defines which data to use when rendering the x-value of any data point.
+ Y: Defines how to render the y-value of any data point. Text Color: Option to override Chart.Js default color for text. At moment overrides the text color for Chart Title, Ticks Text, Axis Title and Legend Text
It is possible to return to Chart.Js defaults by using the checkbox Use ChartJs Default Text Colors @@ -99,8 +99,8 @@ To map your data to the chart, the most important properties to configure are: _Example key mapping config for UI Chart_ - **Series**: Controls how you want to group your data. On a line chart, different series result in different lines for example, on a bar chart, different series result in different bars for a single x-value (stacked or grouped side-by-side). -- **X**: Define where to read the value to plot on the x-axis. If left blank, the x-value will be calculated as the current timestamp. -- **Y**: Define where to read the value to plot on the y-axis. If left blank, the y-value will be read from `msg.payload` directly, and assume it to be a number. +- **X**: Defines where to read the value to plot on the x-axis. This can be read from the `msg` object, as a `key` on an object, or generate a new `timestamp` for each data point being received to the node. +- **Y**: Define where to read the value to plot on the y-axis. This can be read as a property on the `msg` object, or as a `key` on objects in an array of data. The next most important properties to configure are the "Chart Type" and "X-Axis Type". diff --git a/nodes/widgets/locales/en-US/ui_chart.html b/nodes/widgets/locales/en-US/ui_chart.html index 21bb2dfb2..8fc1784ca 100644 --- a/nodes/widgets/locales/en-US/ui_chart.html +++ b/nodes/widgets/locales/en-US/ui_chart.html @@ -26,8 +26,8 @@

Properties

Properties string

Series: Controls how you want to set the Series of data stream into this widget. The default is msg.topic, where separate topics will render to a new line/bar in their respective plots. You can also provide a JSON array, which will plot multiple data points from a single msg object.

-

X: Only available for Line & Scatter Charts. This defines the key (which can be nested) of the value that should be plotted onto the X-axis. If left blank, the X-value will be calculated as the current timestamp.

-

Y: Defines the key (which can be nested, e.g. 'nested.value') of the value that should be plotted onto the X-axis. This value is ignored if injecting single numerical values into the chart.

+

X: Defines which data to use when rendering the x-value of any data point.

+

Y: Defines how to render the y-value of any data point.

Input

diff --git a/nodes/widgets/locales/en-US/ui_chart.json b/nodes/widgets/locales/en-US/ui_chart.json index 3b0e33a55..9818669b1 100644 --- a/nodes/widgets/locales/en-US/ui_chart.json +++ b/nodes/widgets/locales/en-US/ui_chart.json @@ -52,6 +52,7 @@ "properties": "Properties", "series": "Series:", "key": "key:", + "timestamp": "timestamp", "propertyPlaceholder": "property", "pointStyle": "Point Style", "pointShape": "Shape", diff --git a/nodes/widgets/ui_chart.html b/nodes/widgets/ui_chart.html index 511f3a071..8512ded08 100644 --- a/nodes/widgets/ui_chart.html +++ b/nodes/widgets/ui_chart.html @@ -43,6 +43,7 @@ } const noneType = { value: 'none', label: RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.label.none'), hasValue: false } const keyType = { value: 'property', label: RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.label.key') } + const timestampType = { value: 'timestamp', icon: 'fa fa-clock-o', label: RED._('@flowfuse/node-red-dashboard/ui-chart:ui-chart.label.timestamp'), hasValue: false } function hexToRgb (hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) @@ -84,7 +85,7 @@ categoryType: { value: 'msg' }, xAxisLabel: { value: '' }, xAxisProperty: { value: null }, - xAxisPropertyType: { value: 'property' }, + xAxisPropertyType: { value: 'timestamp' }, xAxisType: { value: 'time' }, xAxisFormat: { value: '' }, xAxisFormatType: { value: 'auto' }, @@ -103,8 +104,8 @@ } }, yAxisLabel: { value: '' }, - yAxisProperty: { value: null }, - yAxisPropertyType: { value: 'property' }, + yAxisProperty: { value: 'payload' }, + yAxisPropertyType: { value: 'msg' }, ymin: { value: '', validate: function (value) { return numberValidator(value, true) } }, ymax: { value: '', validate: function (value) { return numberValidator(value, true) } }, bins: { @@ -260,16 +261,22 @@ typeField: $('#node-input-categoryType'), types: getSeriesTypes($('#node-input-xAxisType').val()) }) + $('#node-input-xAxisProperty').typedInput({ - default: keyType.value, + default: timestampType.value, typeField: $('#node-input-xAxisPropertyType'), - types: ['msg', 'str', keyType] + types: ['msg', keyType, 'str', timestampType] }) $('#node-input-yAxisProperty').typedInput({ - default: keyType.value, typeField: $('#node-input-yAxisPropertyType'), types: ['msg', keyType] // makes no sense for the y-axis to be a string }) + if (typeof node.yAxisPropertyType === 'undefined') { + // older chart nodes had no yAxisPropertyType, so need to default "key" + node.yAxisPropertyType = keyType.value + // make selection via jQuery + $('#node-input-yAxisProperty').typedInput('type', keyType.value) + } const updateXAxisTypeOptions = function (options, defaultValue) { const $el = $('#node-input-xAxisType') diff --git a/nodes/widgets/ui_chart.js b/nodes/widgets/ui_chart.js index f366e9857..3083fc096 100644 --- a/nodes/widgets/ui_chart.js +++ b/nodes/widgets/ui_chart.js @@ -37,11 +37,11 @@ module.exports = function (RED) { } // ensure sane defaults - if (!['msg', 'str', 'property'].includes(config.xAxisPropertyType)) { - config.xAxisPropertyType = 'property' // default to 'key' + if (!['msg', 'str', 'property', 'timestamp'].includes(config.xAxisPropertyType)) { + config.xAxisPropertyType = 'timestamp' // default to 'timestamp' } if (!['msg', 'property'].includes(config.yAxisPropertyType)) { - config.yAxisPropertyType = 'property' // default to 'key' + config.yAxisPropertyType = 'property' // default to 'key' for older chart nodes } if (config.xAxisPropertyType === 'msg' && !config.xAxisProperty) { config.xAxisPropertyType = 'property' // msg needs a property to evaluate, default to 'key' @@ -119,7 +119,7 @@ module.exports = function (RED) { // may have been given an x/y object already // let x = getProperty(payload, config.xAxisProperty) let y = payload.y - if (config.xAxisProperty === '') { + if (config.xAxisPropertyType === 'timestamp' || config.xAxisProperty === '') { // no property defined, therefore use time x = (new Date()).getTime() } else {