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()