Charts: Add oh-state-series to render state transitions over time (#3179)
This implements a new series type to show state transitions on a timeline diagram. The additional settings in the oh-state-series (in addition to the oh-time-series) are: - yValue (optional) - where the timeline center should be on the y axis. Note, if using categories, it will simply be the index of the category, defaults to 0. - yHeight (optional) - the unit (in relation to the y-axis coordiate system) of the height of the timeline, defaults to .6. - mapState (optional) - a function to classify item states to a set of "string" states - stateColor (optional) - a map of specified colors to use in the graph for each state. --------- Signed-off-by: Jeff James <jeff@james-online.com>pull/3195/head
parent
5d01ce3efc
commit
d17072ae31
|
@ -186,7 +186,8 @@ export default {
|
||||||
{ value: 'day', label: 'Hours of day' },
|
{ value: 'day', label: 'Hours of day' },
|
||||||
{ value: 'week', label: 'Days of week' },
|
{ value: 'week', label: 'Days of week' },
|
||||||
{ value: 'month', label: 'Days of month' },
|
{ 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'
|
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
|
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': {
|
'oh-aggregate-series': {
|
||||||
label: 'Aggregate Series',
|
label: 'Aggregate Series',
|
||||||
docLink: 'https://echarts.apache.org/en/option.html#series',
|
docLink: 'https://echarts.apache.org/en/option.html#series',
|
||||||
|
|
|
@ -64,6 +64,9 @@
|
||||||
<f7-list-button color="blue" @click="addSeries('oh-aggregate-series', gridIdx)">
|
<f7-list-button color="blue" @click="addSeries('oh-aggregate-series', gridIdx)">
|
||||||
Add Aggregate Series
|
Add Aggregate Series
|
||||||
</f7-list-button>
|
</f7-list-button>
|
||||||
|
<f7-list-button color="blue" @click="addSeries('oh-state-series', gridIdx)">
|
||||||
|
Add State Series
|
||||||
|
</f7-list-button>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
</f7-card>
|
</f7-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -378,7 +381,7 @@ export default {
|
||||||
let firstXAxis = this.context.component.slots.xAxis.find(a => a.config.gridIndex === gridIdx)
|
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)
|
let firstYAxis = this.context.component.slots.yAxis.find(a => a.config.gridIndex === gridIdx)
|
||||||
if (!firstXAxis) {
|
if (!firstXAxis) {
|
||||||
if (type === 'oh-time-series') {
|
if (type === 'oh-time-series' || type === 'oh-state-series') {
|
||||||
this.addAxis(gridIdx, 'xAxis', 'oh-time-axis')
|
this.addAxis(gridIdx, 'xAxis', 'oh-time-axis')
|
||||||
firstXAxis = this.context.component.slots.xAxis.find(a => a.config.gridIndex === gridIdx)
|
firstXAxis = this.context.component.slots.xAxis.find(a => a.config.gridIndex === gridIdx)
|
||||||
automaticAxisCreated = true
|
automaticAxisCreated = true
|
||||||
|
@ -392,6 +395,11 @@ export default {
|
||||||
this.addAxis(gridIdx, 'yAxis', 'oh-value-axis')
|
this.addAxis(gridIdx, 'yAxis', 'oh-value-axis')
|
||||||
firstYAxis = this.context.component.slots.yAxis.find(a => a.config.gridIndex === gridIdx)
|
firstYAxis = this.context.component.slots.yAxis.find(a => a.config.gridIndex === gridIdx)
|
||||||
automaticAxisCreated = true
|
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 {
|
} else {
|
||||||
this.$f7.dialog.alert('Please add at least one X axis and one Y axis')
|
this.$f7.dialog.alert('Please add at least one X axis and one Y axis')
|
||||||
return
|
return
|
||||||
|
@ -406,16 +414,27 @@ export default {
|
||||||
}).open()
|
}).open()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.context.component.slots.series.push({
|
let component = {
|
||||||
component: type,
|
component: type,
|
||||||
config: {
|
config: {
|
||||||
name: 'Series ' + (this.context.component.slots.series.length + 1),
|
name: 'Series ' + (this.context.component.slots.series.length + 1),
|
||||||
gridIndex: gridIdx,
|
gridIndex: gridIdx,
|
||||||
xAxisIndex: this.context.component.slots.xAxis.indexOf(firstXAxis),
|
xAxisIndex: this.context.component.slots.xAxis.indexOf(firstXAxis),
|
||||||
yAxisIndex: this.context.component.slots.yAxis.indexOf(firstYAxis),
|
yAxisIndex: this.context.component.slots.yAxis.indexOf(firstYAxis)
|
||||||
type: 'line'
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
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) {
|
configureSeries (ev, series, context) {
|
||||||
let el = ev.target
|
let el = ev.target
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
||||||
let axis = chartWidget.evaluateExpression(ComponentId.get(component), component.config)
|
let axis = chartWidget.evaluateExpression(ComponentId.get(component), component.config)
|
||||||
axis.type = 'category'
|
axis.type = 'category'
|
||||||
|
|
||||||
axis.data = []
|
axis.data = axis.data || []
|
||||||
switch (config.categoryType) {
|
switch (config.categoryType) {
|
||||||
case 'hour':
|
case 'hour':
|
||||||
axis.name = 'min'
|
axis.name = 'min'
|
||||||
|
|
|
@ -13,6 +13,7 @@ import OhDataSeries from './series/oh-data-series'
|
||||||
import OhTimeSeries from './series/oh-time-series'
|
import OhTimeSeries from './series/oh-time-series'
|
||||||
import OhAggregateSeries from './series/oh-aggregate-series'
|
import OhAggregateSeries from './series/oh-aggregate-series'
|
||||||
import OhCalendarSeries from './series/oh-calendar-series'
|
import OhCalendarSeries from './series/oh-calendar-series'
|
||||||
|
import OhStateSeries from './series/oh-state-series'
|
||||||
|
|
||||||
// Other components
|
// Other components
|
||||||
import OhChartTooltip from './misc/oh-chart-tooltip'
|
import OhChartTooltip from './misc/oh-chart-tooltip'
|
||||||
|
@ -35,7 +36,8 @@ const seriesComponents = {
|
||||||
'oh-data-series': OhDataSeries,
|
'oh-data-series': OhDataSeries,
|
||||||
'oh-time-series': OhTimeSeries,
|
'oh-time-series': OhTimeSeries,
|
||||||
'oh-aggregate-series': OhAggregateSeries,
|
'oh-aggregate-series': OhAggregateSeries,
|
||||||
'oh-calendar-series': OhCalendarSeries
|
'oh-calendar-series': OhCalendarSeries,
|
||||||
|
'oh-state-series': OhStateSeries
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -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 + '<br />' + 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,16 +48,17 @@ dayjs.extend(LocalizedFormat)
|
||||||
|
|
||||||
import { use, registerLocale } from 'echarts/core'
|
import { use, registerLocale } from 'echarts/core'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
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 {
|
import {
|
||||||
TitleComponent, LegendComponent, LegendScrollComponent, GridComponent, SingleAxisComponent, ToolboxComponent, TooltipComponent,
|
TitleComponent, LegendComponent, LegendScrollComponent, GridComponent, SingleAxisComponent, ToolboxComponent, TooltipComponent,
|
||||||
DataZoomComponent, MarkLineComponent, MarkPointComponent, MarkAreaComponent, VisualMapComponent, CalendarComponent
|
DataZoomComponent, MarkLineComponent, MarkPointComponent, MarkAreaComponent, VisualMapComponent, CalendarComponent
|
||||||
} from 'echarts/components'
|
} from 'echarts/components'
|
||||||
import VChart from 'vue-echarts'
|
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,
|
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()
|
const ECHARTS_LOCALE = i18n.locale.split('-')[0].toUpperCase()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue