Skip to content

Commit

Permalink
Merge pull request #304 from FlowFuse/293-series-as-json
Browse files Browse the repository at this point in the history
Support "[ ]" in "Series" property to render multiple points from single data object
  • Loading branch information
joepavitt authored Oct 27, 2023
2 parents abfae4b + 9298245 commit cc0c8ae
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 22 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/theme/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ img + em {
nav#VPSidebarNav p {
pointer-events: none;
}

img + em {
font-size: 0.875rem;
}
65 changes: 60 additions & 5 deletions docs/nodes/widgets/ui-chart.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,25 @@ Most commonly, this is done by wiring a `ui-button` to the `ui-chart` node and c

## Working with Data

`ui-chart` allows you to define which "Properties" from your data you would like to render on the chart.
`ui-chart` allows you to define which "keys" from your data you would like to render on the chart.

![Example keymapping config for UI Chart](/images/node-examples/ui-chart-keymapping.png "Example keymapping config for UI Chart"){data-zoomable}
_Example keymapping config for UI Chart_

In this example, each received datapoint would plot to a fixed "Temperature" series, and UI Chart would read the `.time` value to plot on the x-axis, and the `.temp` value to plot on the y-axis.
### Series

The `Series` property allows you to define how you want to set the Series of data stream into this widget.
The `Series` property allows you to define how you want to control which line/bar (series) data belongs to when streamed into this widget.
The default is `msg.topic`, where separate topics will render to a new line/bar in their respective plots.

If you want to label a single line on your chart, you can set this to a static `string` value, e.g. `my-line`,
If you want to label a single line on your chart, you can set this to a static `string` value, e.g. `Temperature`,
which saves you have to assign `msg.topic` to every data point.

#### Multiple Lines

If you would like to plot multiple lines on the same chart, you can do so by defining the `series` property, which defaults to `msg.topic`.
With this defined, you can assign different data to different satasets that will be plotted:
If you would like to plot multiple lines on the same chart, you can do so by defining the `Series` property, which defaults to `msg.topic`.

With this defined, you can assign different data to different datasets that will be plotted:

```js
msg = {
Expand All @@ -65,6 +70,56 @@ msg = {
}
```

This use case is great if you have multiple sensors feeding data into the same chart. You can assign `msg.topic` accordignly for each sensor.

This behaviour is mimicked here with three sliders, each with a different `msg.topic`:

![Example Line Chart rendering data from different "sensors" using msg.topic](/images/node-examples/ui-chart-msgtopic.png "Example Line Chart rendering data from different \"sensors\" using msg.topic"){data-zoomable}
_Example Line Chart rendering data from different "sensors" using msg.topic_


Alternatively, you can set the `series` property to type `JSON`, and then provide an array of keys (e.g. `["key1", "key2"]`), which will plot a data point for each key provided, from a single data point. For example:

```js
msg.payload = [
{
"x_value": 12,
"value": 17,
"nested": {
"value": 24
}
},
{
"x_value": 17,
"value": 36,
"nested": {
"value": 10
}
},
{
"x_value": 23,
"value": 19,
"nested": {
"value": 75
}
},
{
"x_value": 34,
"value": 12,
"nested": {
"value": 23
}
}
]
```
with a chart config of:

![Example config of a Line Chart to render multiple lines of data from a single data point](/images/node-examples/ui-chart-multiline-config.png "Example config of a Line Chart to render multiple lines of data from a single data point"){data-zoomable}

Would result in:

![Example of a Line Chart rendering multiple data points per injecting payload](/images/node-examples/ui-chart-multiline.png "Example of a Line Chart rendering multiple data points per injecting payload"){data-zoomable}

### Nested Data

It is a common use case that you would have data structured as JSON, and want to plot some of it e.g:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion nodes/widgets/locales/en-US/ui_chart.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h3>Properties</h3>
</dd>
<dt>Properties <span class="property-type">string</span></dt>
<dd>
<p><b>Series:</b> Controls how you want to set the Series of data stream into this widget. The default is <code>msg.topic</code>, where separate topics will render to a new line/bar in their respective plots.</p>
<p><b>Series:</b> Controls how you want to set the Series of data stream into this widget. The default is <code>msg.topic</code>, 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.</p>
<p><b>X:</b> 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.</p>
<p><b>Y:</b> Defines the key (which can be nested, e.g. <code>'nested.value'</code>) of the value that should be plotted onto the x-axis. This value is ignored if injecting single numerical values into the chart.</p>
</dd>
Expand Down
14 changes: 13 additions & 1 deletion nodes/widgets/ui_chart.html
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
$('#node-input-category').typedInput({
default: 'msg',
typeField: $('#node-input-categoryType'),
types: ['msg', 'str', propertyType]
types: ['msg', 'str', 'json', propertyType]
})
$('#node-input-xAxisProperty').typedInput({
default: propertyType.value,
Expand Down Expand Up @@ -178,6 +178,18 @@
$('#x-axis-show').hide()
}
})

// handle event when chart's type is changed
$('#node-input-category').change((evt) => {
const categoryType = $('#node-input-categoryType').val()

if (categoryType === 'json') {
// hide y-axis proepty setting as category will now control that
$('#node-container-yAxisProperty').hide()
} else {
$('#node-container-yAxisProperty').show()
}
})

// Series Color Pickers
const setColor = function (id, value) {
Expand Down
40 changes: 38 additions & 2 deletions nodes/widgets/ui_chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module.exports = function (RED) {
}

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

Expand All @@ -46,7 +48,19 @@ module.exports = function (RED) {
})
} else {
// single point
msg._datapoint = addToLine(p, series)
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 = addToLine(p, s)
points.push(datapoint)
}
})
msg._datapoint = points
} else {
msg._datapoint = addToLine(p, series)
}
}
} else if (config.chartType === 'bar') {
// single point or array of data?
Expand All @@ -66,6 +80,7 @@ module.exports = function (RED) {

// function to process a data point being appended to a line/scatter chart
function addToLine (payload, series) {
console.log(payload, series)
const datapoint = {}
datapoint.category = series
// construct our datapoint
Expand All @@ -76,11 +91,21 @@ module.exports = function (RED) {
} 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 = payload.y
datapoint.y = y
}
return datapoint
}
Expand All @@ -92,6 +117,17 @@ module.exports = function (RED) {
if (typeof payload === 'number') {
datapoint.y = payload
}
if (Array.isArray(series)) {
let y = null
if (series.length > 1) {
y = series.map((s) => {
return getProperty(payload, s)
})
} else {
y = getProperty(payload, series[0])
}
datapoint.y = y
}
return datapoint
}

Expand Down
48 changes: 35 additions & 13 deletions ui/src/widgets/ui-chart/UIChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ export default {
if (this.props.xAxisProperty) {
parsing.xAxisKey = this.props.xAxisProperty
}
} else if (this.props.chartType === 'bar') {
} else if (this.props.categoryType !== 'json' && this.props.chartType === 'bar') {
if (this.props.category && this.props.categoryType !== 'msg') {
parsing.xAxisKey = this.props.category
} else {
parsing.xAxisKey = 'category'
}
}
if (this.props.yAxisProperty) {
if (this.props.categoryType !== 'json' && this.props.yAxisProperty) {
parsing.yAxisKey = this.props.yAxisProperty
}
Expand Down Expand Up @@ -107,6 +107,7 @@ export default {
this.chart = shallowRef(chart)
},
methods: {
// given an object, return the value of the category property (which can be nested)
getLabel (value, category) {
if (this.props.categoryType !== 'property') {
return category
Expand Down Expand Up @@ -155,21 +156,21 @@ export default {
const p = m.payload
const d = m._datapoint // server-side we compute a chart friendly format
const label = d.category
this.addPoint(p, d, label)
this.addPoints(p, d, label)
})
} else if (Array.isArray(payload) && msg.payload.length > 0) {
// we have received a message with an array of data points
// and should append each of them
payload.forEach((p, i) => {
const d = msg._datapoint ? msg._datapoint[i] : null // server-side we compute a chart friendly format where required
const label = d.category
this.addPoint(p, d, label)
this.addPoints(p, d, label)
})
} else if (payload !== null && payload !== undefined) {
// we have a single payload value and should append it to the chart
const d = msg._datapoint // server-side we compute a chart friendly format
const label = d.category
this.addPoint(msg.payload, d, label)
this.addPoints(msg.payload, d, label)
} else {
// no payload
console.log('have no payload')
Expand All @@ -179,6 +180,25 @@ export default {
}
this.chart.update()
},
addPoints (payload, datapoint, label) {
const d = {
...datapoint,
...payload
}
if (Array.isArray(label) && label.length > 0) {
// we have an array of series, meaning we plot multiple data points per data object
for (let i = 0; i < label.length; i++) {
const dd = {
...d
}
dd.category = d.category[i]
dd.y = d.y[i]
this.addPoint(payload, dd, label[i])
}
} else {
this.addPoint(payload, datapoint, label)
}
},
addPoint (payload, datapoint, label) {
const d = {
...datapoint,
Expand Down Expand Up @@ -252,23 +272,25 @@ export default {
this.chart.data.labels.push(label)
}
} else if (typeof payload === 'object') {
if (this.chart.data.labels.includes(label)) {
// yes, so we need to find the index of this label
const index = this.chart.data.labels.indexOf(label)
// and update the data at this index
this.chart.data.datasets[0].data[index] = payload
} else {
if (!this.chart.data.labels.includes(label)) {
if (!this.chart.data.datasets.length) {
this.chart.data.datasets.push({
data: [],
backgroundColor: this.props.colors,
borderColor: this.props.colors
})
}
// ChartJS supports objects for Bar Charts, as long as we have xAxisKey and yAxisKey set
this.chart.data.datasets[0].data.push(payload)
this.chart.data.labels.push(label)
}
const index = this.chart.data.labels.indexOf(label)
// ChartJS supports objects for Bar Charts, as long as we have xAxisKey and yAxisKey set
// and update the data at this index
if (this.props.categoryType === 'json' && this.props.category.length > 0) {
// we would have computed these values server-side for multiple series defined
this.chart.data.datasets[0].data[index] = payload.y
} else {
this.chart.data.datasets[0].data[index] = payload
}
} else {
// only support numbers for now
console.log('Unsupported payload type for Bar Chart:', typeof payload)
Expand Down

0 comments on commit cc0c8ae

Please sign in to comment.