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: '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',
|
||||
|
|
|
@ -64,6 +64,9 @@
|
|||
<f7-list-button color="blue" @click="addSeries('oh-aggregate-series', gridIdx)">
|
||||
Add Aggregate Series
|
||||
</f7-list-button>
|
||||
<f7-list-button color="blue" @click="addSeries('oh-state-series', gridIdx)">
|
||||
Add State Series
|
||||
</f7-list-button>
|
||||
</f7-list>
|
||||
</f7-card>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 { 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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue