diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/widgets/chart/index.js b/bundles/org.openhab.ui/web/src/assets/definitions/widgets/chart/index.js
index 5a5840337..a4169890e 100644
--- a/bundles/org.openhab.ui/web/src/assets/definitions/widgets/chart/index.js
+++ b/bundles/org.openhab.ui/web/src/assets/definitions/widgets/chart/index.js
@@ -186,7 +186,8 @@ export default {
{ value: 'day', label: 'Hours of day' },
{ value: 'week', label: 'Days of week' },
{ value: 'month', label: 'Days of month' },
- { value: 'year', label: 'Months of year' }
+ { value: 'year', label: 'Months of year' },
+ { value: 'values', label: 'Values' }
]
},
{
@@ -229,6 +230,16 @@ export default {
return configuration.categoryType === 'year'
}
},
+ {
+ name: 'data',
+ label: 'Category Values',
+ type: 'TEXT',
+ description: 'Category values to display',
+ multiple: true,
+ visible: (value, configuration, configDescription, parameters) => {
+ return configuration.categoryType === 'values'
+ }
+ },
gridIndexParameter
]
}
@@ -307,6 +318,31 @@ export default {
}
},
+ 'oh-state-series': {
+ label: 'State Series',
+ props: {
+ parameterGroups: [componentRelationsGroup, actionGroup()],
+ parameters: [
+ ...seriesParameters,
+ {
+ name: 'yValue',
+ label: 'Y Value',
+ type: 'DECIMAL',
+ description: 'The position the state timeline should appear on the Y axis (in graph coordinates). If Y axis is a category axis, this should be the index of the category'
+ },
+ {
+ name: 'yHeight',
+ label: 'Y Height',
+ type: 'DECIMAL',
+ description: 'The height the state timeline bar in graph coordinates (default is 0.6)'
+ },
+ xAxisIndexParameter,
+ yAxisIndexParameter,
+ ...actionParams()
+ ]
+ }
+ },
+
'oh-aggregate-series': {
label: 'Aggregate Series',
docLink: 'https://echarts.apache.org/en/option.html#series',
diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/chart/chart-designer.vue b/bundles/org.openhab.ui/web/src/components/pagedesigner/chart/chart-designer.vue
index 3867b9462..7e9f8b877 100644
--- a/bundles/org.openhab.ui/web/src/components/pagedesigner/chart/chart-designer.vue
+++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/chart/chart-designer.vue
@@ -64,6 +64,9 @@
Add Aggregate Series
+
+ Add State Series
+
@@ -378,7 +381,7 @@ export default {
let firstXAxis = this.context.component.slots.xAxis.find(a => a.config.gridIndex === gridIdx)
let firstYAxis = this.context.component.slots.yAxis.find(a => a.config.gridIndex === gridIdx)
if (!firstXAxis) {
- if (type === 'oh-time-series') {
+ if (type === 'oh-time-series' || type === 'oh-state-series') {
this.addAxis(gridIdx, 'xAxis', 'oh-time-axis')
firstXAxis = this.context.component.slots.xAxis.find(a => a.config.gridIndex === gridIdx)
automaticAxisCreated = true
@@ -392,6 +395,11 @@ export default {
this.addAxis(gridIdx, 'yAxis', 'oh-value-axis')
firstYAxis = this.context.component.slots.yAxis.find(a => a.config.gridIndex === gridIdx)
automaticAxisCreated = true
+ } else if (type === 'oh-state-series') {
+ this.addAxis(gridIdx, 'yAxis', 'oh-category-axis')
+ firstYAxis = this.context.component.slots.yAxis.find(a => a.config.gridIndex === gridIdx)
+ firstYAxis.config.categoryType = 'values'
+ automaticAxisCreated = true
} else {
this.$f7.dialog.alert('Please add at least one X axis and one Y axis')
return
@@ -406,16 +414,27 @@ export default {
}).open()
}
- this.context.component.slots.series.push({
+ let component = {
component: type,
config: {
name: 'Series ' + (this.context.component.slots.series.length + 1),
gridIndex: gridIdx,
xAxisIndex: this.context.component.slots.xAxis.indexOf(firstXAxis),
- yAxisIndex: this.context.component.slots.yAxis.indexOf(firstYAxis),
- type: 'line'
+ yAxisIndex: this.context.component.slots.yAxis.indexOf(firstYAxis)
}
- })
+ }
+
+ if (type === 'oh-state-series') {
+ if (firstYAxis.config.categoryType === 'values') {
+ firstYAxis.config.data = firstYAxis.config.data || []
+ firstYAxis.config.data.unshift(component.config.name)
+ component.config.yValue = firstYAxis.config.data.length - 1
+ }
+ } else {
+ component.config.type = 'line'
+ }
+
+ this.context.component.slots.series.push(component)
},
configureSeries (ev, series, context) {
let el = ev.target
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/chart/axis/oh-category-axis.js b/bundles/org.openhab.ui/web/src/components/widgets/chart/axis/oh-category-axis.js
index 1755e0200..1a485b051 100644
--- a/bundles/org.openhab.ui/web/src/components/widgets/chart/axis/oh-category-axis.js
+++ b/bundles/org.openhab.ui/web/src/components/widgets/chart/axis/oh-category-axis.js
@@ -22,7 +22,7 @@ export default {
let axis = chartWidget.evaluateExpression(ComponentId.get(component), component.config)
axis.type = 'category'
- axis.data = []
+ axis.data = axis.data || []
switch (config.categoryType) {
case 'hour':
axis.name = 'min'
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/chart/chart-mixin.js b/bundles/org.openhab.ui/web/src/components/widgets/chart/chart-mixin.js
index e2299df86..212e9d89c 100644
--- a/bundles/org.openhab.ui/web/src/components/widgets/chart/chart-mixin.js
+++ b/bundles/org.openhab.ui/web/src/components/widgets/chart/chart-mixin.js
@@ -13,6 +13,7 @@ import OhDataSeries from './series/oh-data-series'
import OhTimeSeries from './series/oh-time-series'
import OhAggregateSeries from './series/oh-aggregate-series'
import OhCalendarSeries from './series/oh-calendar-series'
+import OhStateSeries from './series/oh-state-series'
// Other components
import OhChartTooltip from './misc/oh-chart-tooltip'
@@ -35,7 +36,8 @@ const seriesComponents = {
'oh-data-series': OhDataSeries,
'oh-time-series': OhTimeSeries,
'oh-aggregate-series': OhAggregateSeries,
- 'oh-calendar-series': OhCalendarSeries
+ 'oh-calendar-series': OhCalendarSeries,
+ 'oh-state-series': OhStateSeries
}
export default {
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/chart/series/oh-state-series.js b/bundles/org.openhab.ui/web/src/components/widgets/chart/series/oh-state-series.js
new file mode 100644
index 000000000..49fd69a42
--- /dev/null
+++ b/bundles/org.openhab.ui/web/src/components/widgets/chart/series/oh-state-series.js
@@ -0,0 +1,116 @@
+import ComponentId from '../../component-id'
+import Framework7 from 'framework7'
+import { graphic } from 'echarts/core'
+
+function renderState (params, api) {
+ const yValue = api.value(0)
+ const start = api.coord([api.value(1), yValue])
+ const duration = api.value(2)
+ const end = api.coord([api.value(1) + duration, yValue])
+ const state = api.value(3)
+ const yHeight = api.value(4)
+
+ if (state === 'UNDEF' || state === 'NULL') return
+ const height = api.size([0, 1])[1] * yHeight
+ const rectShape = graphic.clipRectByRect(
+ {
+ x: start[0],
+ y: start[1] - height / 2,
+ width: end[0] - start[0],
+ height
+ },
+ {
+ x: params.coordSys.x,
+ y: params.coordSys.y,
+ width: params.coordSys.width,
+ height: params.coordSys.height
+ }
+ )
+ return (
+ rectShape && {
+ type: 'rect',
+ shape: rectShape,
+ style: api.style({})
+ }
+ )
+}
+
+export default {
+ neededItems (component, chart) {
+ let series = chart.evaluateExpression(ComponentId.get(component), component.config)
+ return [
+ series.item
+ ]
+ },
+ get (component, points, startTime, endTime, chart) {
+ let series = chart.evaluateExpression(ComponentId.get(component), component.config)
+ series.type = 'custom'
+ series.renderItem = renderState
+ series.encode = {
+ x: -1, // don't filter by x value when zooming
+ y: 0,
+ tooltip: [3],
+ itemName: 3
+ }
+ series.colorBy = 'data'
+ series.label = series.label || { }
+ if (series.label.show === undefined) series.label.show = true
+ series.label.position = series.label.position || 'insideLeft'
+ series.label.formatter = series.label.formatter || '{@[3]}'
+ series.labelLayout = series.labelLayout || { hideOverlap: true }
+ series.tooltip = series.tooltip || { }
+ if (series.tooltip.formatter === undefined) {
+ series.tooltip.formatter = (params) => {
+ let durationSec = params.value[2] / 1000
+ let hours = Math.floor(durationSec / 3600).toString().padStart(2, '0')
+ let minutes = Math.floor((durationSec - (hours * 3600)) / 60).toString().padStart(2, '0')
+ return params.seriesName + '
' + params.marker + params.name + ' (' + hours + ':' + minutes + ')'
+ }
+ }
+
+ series.data = []
+
+ if (series.item) {
+ let itemPoints = points.find(p => p.name === series.item).data
+
+ if (series.mapState) {
+ for (let i = 0; i < itemPoints.length; i++) {
+ itemPoints[i].state = series.mapState(itemPoints[i].state)
+ }
+ }
+
+ let data = []
+ let itemStartTime = null
+ // fill data array - combine state updates where state does not change
+ for (let i = 0; i < itemPoints.length; i++) {
+ if (itemPoints[i + 1] && itemPoints[i].state === itemPoints[i + 1].state) {
+ itemStartTime = itemStartTime || new Date(itemPoints[i].time)
+ continue
+ }
+
+ itemStartTime = itemStartTime || new Date(itemPoints[i].time)
+ let itemEndTime = new Date(itemPoints[i + 1] ? itemPoints[i + 1].time : endTime)
+ let itemDuration = itemEndTime - itemStartTime
+
+ let stateColor = (series.stateColor) ? series.stateColor[itemPoints[i].state] : null
+ data.push({
+ value: [series.yValue || 0, itemStartTime, itemDuration, itemPoints[i].state, series.yHeight || 0.6],
+ itemStyle: {
+ color: stateColor
+ }
+ })
+ itemStartTime = null
+ }
+
+ series.data = data
+
+ series.id = `oh-state-series#${series.item}#${Framework7.utils.id()}`
+ }
+
+ if (!series.tooltip) {
+ series.tooltip = { show: true }
+ }
+
+ return series
+ }
+}
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/system/oh-chart-component.vue b/bundles/org.openhab.ui/web/src/components/widgets/system/oh-chart-component.vue
index ad702f74c..b4c4e6ca0 100644
--- a/bundles/org.openhab.ui/web/src/components/widgets/system/oh-chart-component.vue
+++ b/bundles/org.openhab.ui/web/src/components/widgets/system/oh-chart-component.vue
@@ -48,16 +48,17 @@ dayjs.extend(LocalizedFormat)
import { use, registerLocale } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
-import { LineChart, BarChart, GaugeChart, HeatmapChart, PieChart, ScatterChart } from 'echarts/charts'
+import { LineChart, BarChart, GaugeChart, HeatmapChart, PieChart, ScatterChart, CustomChart } from 'echarts/charts'
+import { LabelLayout } from 'echarts/features'
import {
TitleComponent, LegendComponent, LegendScrollComponent, GridComponent, SingleAxisComponent, ToolboxComponent, TooltipComponent,
DataZoomComponent, MarkLineComponent, MarkPointComponent, MarkAreaComponent, VisualMapComponent, CalendarComponent
} from 'echarts/components'
import VChart from 'vue-echarts'
-use([CanvasRenderer, LineChart, BarChart, GaugeChart, HeatmapChart, PieChart, ScatterChart, TitleComponent,
+use([CanvasRenderer, LineChart, BarChart, GaugeChart, HeatmapChart, PieChart, ScatterChart, CustomChart, TitleComponent,
LegendComponent, LegendScrollComponent, GridComponent, SingleAxisComponent, ToolboxComponent, TooltipComponent, DataZoomComponent,
- MarkLineComponent, MarkPointComponent, MarkAreaComponent, VisualMapComponent, CalendarComponent])
+ MarkLineComponent, MarkPointComponent, MarkAreaComponent, VisualMapComponent, CalendarComponent, LabelLayout])
const ECHARTS_LOCALE = i18n.locale.split('-')[0].toUpperCase()