Skip to content

Commit 80e65ff

Browse files
authored
Merge pull request #1980 from colinl/1978_chartOption_series_issues
Fix chart options issues
2 parents 670fbc0 + 536ea2c commit 80e65ff

4 files changed

Lines changed: 184 additions & 73 deletions

File tree

docs/en/nodes/widgets/ui-chart.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -502,9 +502,41 @@ The changes are additive, so if it was also desired to move the bottom of the gr
502502
}
503503
}
504504
```
505-
The options set earlier will remain in force.
506-
507-
505+
The options set earlier will remain in force. Generally a message containing `msg.ui_update` should contain no payload or other properties otherwise those will be interpreted as data to go on the chart.
506+
### Setting Series Colour
507+
The settings for each series on the chart can also be set using `msg.ui_update`. Note that when adjusting settings for a series you must include all
508+
customised settings for all series, as each update to series configuration will replace any previous customisation to series.
509+
For example, for a line chart with two lines 'temperature' and 'humidity', to set the colour of the temperature line to red and humidity to green `msg.ui_update` could be sent containing
510+
```
511+
{
512+
"chartOptions": {
513+
"series": [
514+
{
515+
"name": "temperature",
516+
"type": "line",
517+
"lineStyle": {
518+
"color": "red",
519+
},
520+
"itemStyle": {
521+
"color": "red"
522+
},
523+
},
524+
{
525+
"name": "humidity",
526+
"type": "line",
527+
"lineStyle": {
528+
"color": "rgb(0, 255, 0)"
529+
},
530+
"itemStyle": {
531+
"color": "rgb(0, 255, 0)"
532+
},
533+
}
534+
]
535+
}
536+
}
537+
```
538+
Note that this sets the colour for the whole line. It cannot be used to change the colour for just a section of the line.
539+
Any such message should always contain the series name and type for each series.
508540

509541
## Building Custom Charts
510542
For even further customisataion such as rendering charts we don't yet support, you can use a UI Template node.

nodes/widgets/locales/en-US/ui_chart.html

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,46 @@ <h3>Dynamic Properties</h3>
106106
}
107107
}
108108
</pre>
109-
The options set earlier will remain in force.
109+
The options set earlier will remain in force. Generally a message containing `msg.ui_update` should contain
110+
no payload or other properties otherwise those will be interpreted as data to go on the chart.
111+
</dd>
112+
<dd>
113+
The settings for each series on the chart can also be set using `msg.ui_update`. In particular this allows line
114+
colours to be configured. Note that when adjusting settings
115+
for a series you must include all customised settings for all series, as each update to series configuration will
116+
replace any previous customisation to series.
117+
For example, for a line chart with two lines 'temperature' and 'humidity', to set the colour of the temperature line
118+
to red and humidity to green `msg.ui_update` could be sent containing
119+
<pre>
120+
{
121+
"chartOptions": {
122+
"series": [
123+
{
124+
"name": "temperature",
125+
"type": "line",
126+
"lineStyle": {
127+
"color": "red",
128+
},
129+
"itemStyle": {
130+
"color": "red"
131+
},
132+
},
133+
{
134+
"name": "humidity",
135+
"type": "line",
136+
"lineStyle": {
137+
"color": "rgb(0, 255, 0)"
138+
},
139+
"itemStyle": {
140+
"color": "rgb(0, 255, 0)"
141+
},
142+
}
143+
]
144+
}
145+
}
146+
</pre>
147+
Note that this sets the colour for the whole line. It cannot be used to change the colour for just a section of the line.
148+
149+
Any such message should always contain the series name and type for each series.
110150
</dd>
111151
</script>

nodes/widgets/ui_chart.js

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ module.exports = function (RED) {
1414
const group = RED.nodes.getNode(config.group)
1515
const base = group.getBase()
1616

17+
// add a chartOptions object into the config
18+
config.chartOptions = config.chartOptions || {}
19+
1720
// correct typing
1821
if (typeof config.xmin !== 'undefined') {
1922
config.xmin = parseFloat(config.xmin)
@@ -209,61 +212,64 @@ module.exports = function (RED) {
209212
if (!datastore.get(node.id)) {
210213
datastore.save(base, node, [])
211214
}
212-
if (Array.isArray(msg.payload) && !msg.payload.length) {
213-
// clear history
214-
datastore.save(base, node, [])
215-
} else {
216-
// delete old data if a replace is being performed.
217-
// This is the case if msg.action is replace
218-
// or the node is configured for replace and this is not being overriden by msg.action set to append
219-
if (msg.action === 'replace' || (config.action === 'replace' && msg.action !== 'append')) {
220-
// clear our data store as we are replacing data
215+
// To prevent ui_update messages from deleting old data, skip this section if no msg.payload present
216+
if (typeof msg.payload !== 'undefined') {
217+
if (Array.isArray(msg.payload) && !msg.payload.length) {
218+
// clear history
221219
datastore.save(base, node, [])
222-
}
223-
if (!Array.isArray(msg.payload)) {
224-
// quick clone of msg, and store in history
225-
datastore.append(base, node, {
226-
...msg
227-
})
228220
} else {
229-
// we have an array in msg.payload, let's split them
230-
msg.payload.forEach((p, i) => {
231-
const payload = JSON.parse(JSON.stringify(p))
232-
const d = msg._datapoint ? msg._datapoint[i] : null
233-
const m = {
234-
...msg,
235-
payload,
236-
_datapoint: d
237-
}
238-
datastore.append(base, node, m)
239-
})
240-
}
221+
// delete old data if a replace is being performed.
222+
// This is the case if msg.action is replace
223+
// or the node is configured for replace and this is not being overriden by msg.action set to append
224+
if (msg.action === 'replace' || (config.action === 'replace' && msg.action !== 'append')) {
225+
// clear our data store as we are replacing data
226+
datastore.save(base, node, [])
227+
}
228+
if (!Array.isArray(msg.payload)) {
229+
// quick clone of msg, and store in history
230+
datastore.append(base, node, {
231+
...msg
232+
})
233+
} else {
234+
// we have an array in msg.payload, let's split them
235+
msg.payload.forEach((p, i) => {
236+
const payload = JSON.parse(JSON.stringify(p))
237+
const d = msg._datapoint ? msg._datapoint[i] : null
238+
const m = {
239+
...msg,
240+
payload,
241+
_datapoint: d
242+
}
243+
datastore.append(base, node, m)
244+
})
245+
}
241246

242-
const maxPoints = parseInt(config.removeOlderPoints)
247+
const maxPoints = parseInt(config.removeOlderPoints)
243248

244-
if (maxPoints && config.removeOlderPoints) {
245-
// account for multiple lines?
246-
// client-side does this for _each_ line
247-
// remove older points
248-
const lineCounts = {}
249-
const _msg = datastore.get(node.id)
250-
// trawl through in reverse order, and only keep the latest points (up to maxPoints) for each label
251-
for (let i = _msg.length - 1; i >= 0; i--) {
252-
const msg = _msg[i]
253-
const label = msg.topic
254-
lineCounts[label] = lineCounts[label] || 0
255-
if (lineCounts[label] >= maxPoints) {
256-
_msg.splice(i, 1)
257-
} else {
258-
lineCounts[label]++
249+
if (maxPoints && config.removeOlderPoints) {
250+
// account for multiple lines?
251+
// client-side does this for _each_ line
252+
// remove older points
253+
const lineCounts = {}
254+
const _msg = datastore.get(node.id)
255+
// trawl through in reverse order, and only keep the latest points (up to maxPoints) for each label
256+
for (let i = _msg.length - 1; i >= 0; i--) {
257+
const msg = _msg[i]
258+
const label = msg.topic
259+
lineCounts[label] = lineCounts[label] || 0
260+
if (lineCounts[label] >= maxPoints) {
261+
_msg.splice(i, 1)
262+
} else {
263+
lineCounts[label]++
264+
}
259265
}
266+
datastore.save(base, node, _msg)
260267
}
261-
datastore.save(base, node, _msg)
262-
}
263268

264-
if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
265-
// remove any points older than the specified time
266-
clearOldPoints()
269+
if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
270+
// remove any points older than the specified time
271+
clearOldPoints()
272+
}
267273
}
268274
}
269275

ui/src/widgets/ui-chart/UIChart.vue

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export default {
2020
inject: ['$socket', '$dataTracker'],
2121
props: {
2222
id: { type: String, required: true },
23-
props: { type: Object, default: () => ({}) }
23+
props: { type: Object, default: () => ({}) },
24+
state: { type: Object, default: () => ({}) }
2425
},
2526
data () {
2627
return {
@@ -33,6 +34,7 @@ export default {
3334
},
3435
chartUpdateDebounceTimeout: null,
3536
tooltipDataset: [],
37+
dynamicChartOptions: [], // an array of chart options updates received this session
3638
resizeObserver: null
3739
}
3840
},
@@ -62,6 +64,9 @@ export default {
6264
},
6365
interpolation () {
6466
return this.props.interpolation
67+
},
68+
chartOptions () {
69+
return this.getProperty('chartOptions')
6570
}
6671
},
6772
watch: {
@@ -154,7 +159,7 @@ export default {
154159
chart.setOption(options)
155160
156161
// merge in any updates provided via ui_update.chartOptions
157-
const chartOptions = this.props.chartOptions
162+
const chartOptions = this.chartOptions
158163
if (chartOptions) {
159164
// pass the options to the chart
160165
chart.setOption(chartOptions)
@@ -188,6 +193,8 @@ export default {
188193
if (this.chart) {
189194
this.chart.setOption(updates.chartOptions)
190195
}
196+
// add these options to the array of previous updates received this session
197+
this.dynamicChartOptions.push(updates.chartOptions)
191198
}
192199
},
193200
generateChartOptions () {
@@ -399,19 +406,39 @@ export default {
399406
}
400407
},
401408
onMsgInput (msg) {
402-
if (Array.isArray(msg.payload) && !msg.payload.length) {
403-
// clear the chart if msg.payload = [] is received
404-
this.clearChart()
405-
this.clearDataStore()
406-
} else {
407-
if (msg.action === 'replace' || (this.props.action === 'replace' && msg.action !== 'append')) {
408-
// clear the chart
409+
// ignore if payload is empty and msg is not an array (on loading it can be an array)
410+
// ui_update messages are handled by OnDynamicProperties
411+
if (msg.payload !== undefined || Array.isArray(msg)) {
412+
if (Array.isArray(msg.payload) && !msg.payload.length) {
413+
// clear the chart if msg.payload = [] is received
409414
this.clearChart()
410-
// delete messages array in the store
411415
this.clearDataStore()
416+
} else {
417+
if (msg.action === 'replace' || (this.props.action === 'replace' && msg.action !== 'append')) {
418+
// clear the chart
419+
this.clearChart()
420+
// delete messages array in the store
421+
this.clearDataStore()
422+
}
423+
// update the chart
424+
// remember how many series are currently configured
425+
const seriesCount = this.chart.getOption().series.length
426+
this.add(msg)
427+
// if any series have been added, re-apply any chartOptions passed in
428+
// Also re-apply if this is a radial (pie or doughnut) chart as the radius for these are re-calculated
429+
// when adding data, which may remove any radius applied via msg.ui_update.
430+
if (this.chart.getOption().series.length > seriesCount || this.props.xAxisType === 'radial') {
431+
// update the chart first from options applied in previous sessions
432+
const chartOptions = this.chartOptions
433+
if (chartOptions) {
434+
this.chart.setOption(chartOptions)
435+
}
436+
// then from this session
437+
this.dynamicChartOptions.forEach((options) => {
438+
this.chart.setOption(options)
439+
})
440+
}
412441
}
413-
// update the chart
414-
this.add(msg)
415442
}
416443
},
417444
getXDisplayFormats (xAxisFormatType) {
@@ -687,6 +714,10 @@ export default {
687714
}
688715
689716
// Add data point
717+
// ensure the data array exists
718+
if (Array.isArray(options.series[sIndex].data) === false) {
719+
options.series[sIndex].data = []
720+
}
690721
if (this.props.xAxisType === 'category') {
691722
// for categories, we need to update the existing data point for this x-value
692723
const xIndex = options.series[sIndex].data.findIndex(d => d[0] === datapoint.x)
@@ -745,15 +776,17 @@ export default {
745776
if ((cutoff || points) && series.length > 0) {
746777
// loop over each series
747778
for (let i = 0; i < series.length; i++) {
748-
const length = series[i].data.length // check how much data there is in this series
749-
series[i].data = series[i].data.filter((d, i) => {
750-
if (cutoff && d[0] < cutoff) {
751-
return false
752-
} else if (points && (i < length - points)) {
753-
return false
754-
}
755-
return true
756-
})
779+
const length = series[i].data?.length // check how much data there is in this series
780+
if (length) {
781+
series[i].data = series[i].data.filter((d, i) => {
782+
if (cutoff && d[0] < cutoff) {
783+
return false
784+
} else if (points && (i < length - points)) {
785+
return false
786+
}
787+
return true
788+
})
789+
}
757790
}
758791
}
759792
// apply data limtations to the vuex store

0 commit comments

Comments
 (0)