UNPKG

@rdkmaster/jigsaw-labs

Version:

Jigsaw, the next generation component set for RDK

518 lines (460 loc) 18.5 kB
import {TableDataBase} from "./table-data"; import { EchartLegend, EchartOptions, EchartSeriesItem, EchartTitle, EchartToolbox, EchartTooltip, EchartXAxis, EchartYAxis } from "./echart-types"; import {GraphDataField, GraphDataHeader, GraphDataMatrix} from "./graph-data"; import {aggregate, AggregateAlgorithm, distinct, flat, getColumn, group, Grouped} from "../utils/data-collection-utils"; import {CommonUtils} from "../utils/common-utils"; import {VMAX_GRAPH_THEME} from "../../component/graph/vmax-theme"; import {VMAX_GRAPH_THEME_DARK} from "../../component/graph/vmax-theme-dark"; export abstract class AbstractModeledGraphTemplate { public abstract getInstance(): EchartOptions; public themes? = [ {name: '默认浅色系', theme: VMAX_GRAPH_THEME}, {name: '默认深色系', theme: VMAX_GRAPH_THEME_DARK} ]; public title?: EchartTitle; public tooltip?: EchartTooltip; public toolbox?: EchartToolbox; public legend?: EchartLegend; } export abstract class AbstractModeledGraphData extends TableDataBase { protected abstract createChartOptions(): EchartOptions; /** * 图形的数据,二维数组。 */ public data: GraphDataMatrix; /** * 图形的列字段描述。 */ public header: GraphDataHeader; /** * 图形的列字段。 */ public field: GraphDataField; /** * 一个适合输入给 echarts 的参数,由本类的子类自动构建出来 */ public options: EchartOptions; /** * 图形个关键配置项的模板 */ public template: AbstractModeledGraphTemplate; protected constructor(data: GraphDataMatrix = [], header: GraphDataHeader = [], field: GraphDataField = []) { super(data, field, header); this.data = data; this.field = field; this.header = header; } public getIndex(field: string): number { if (CommonUtils.isUndefined(field)) { return -1; } let idx = this.field.indexOf(field); return idx == -1 ? this.header.indexOf(field) : idx; } protected getRealDimensions(dimField: string, dimensions: Dimension[], usingAllDimensions: boolean): Dimension[] { const dims = []; const dimIndex = this.getIndex(dimField); if (dimIndex == -1) { return dims; } if (usingAllDimensions) { const series = distinct(getColumn(this.data, dimIndex)); if (series) { dims.push(...series.map(s => { const d = dimensions.find(d => d.name === s); return d ? d : new Dimension(s); })); } } else { dims.push(...dimensions); } return dims; } protected pruneData(records: (string | number)[][], dimIndex: number, dimensions: Dimension[], indicators: Indicator[]): string[][] { const aggregateBy = indicators.map(kpi => ({index: kpi.index, algorithm: kpi.aggregateBy})); const dimGroups = group(records, dimIndex); const pruned: string[][] = []; dimensions.forEach(dim => { const dimRecords = dimGroups[dim.name]; if (dimRecords) { pruned.push(dimRecords.length > 1 ? aggregate(dimRecords, aggregateBy) : dimRecords[0]); } else { const row = []; row[dimIndex] = dim.name; indicators.forEach(i => row[i.index] = i.defaultValue); pruned.push(row); } }); return pruned; } } export class Dimension { public name?: string; public yAxisIndex?: 0 | 1 = 0; public stack?: string; public shade?: 'bar' | 'line' | 'area' = 'bar'; constructor(name?: string) { this.name = name; } } export class Indicator { public name?: string; public field?: string; public index?: number = -1; public yAxisIndex?: 0 | 1 = 0; public stack?: string = undefined; public shade?: 'bar' | 'line' | 'area' = 'bar'; public defaultValue?: number = 0; public aggregateBy?: AggregateAlgorithm = 'sum'; constructor(field?: string) { this.field = field; } } // ------------------------------------------------------------------------------------------------ // 直角系图相关数据对象 export abstract class ModeledRectangularTemplate extends AbstractModeledGraphTemplate { xAxis?: EchartXAxis; yAxis1?: EchartYAxis; yAxis2?: EchartYAxis; seriesItem?: EchartSeriesItem; } export class BasicModeledRectangularTemplate extends ModeledRectangularTemplate { getInstance(): EchartOptions { return { title: CommonUtils.extendObjects<EchartTooltip>({}, this.tooltip), tooltip: CommonUtils.extendObjects<EchartTooltip>({}, this.tooltip), toolbox: CommonUtils.extendObjects<EchartToolbox>({}, this.toolbox), legend: CommonUtils.extendObjects<EchartLegend>({}, this.legend), }; } tooltip = { trigger: 'axis', axisPointer: { type: 'cross', crossStyle: { color: '#999' } } }; toolbox = { feature: { dataView: {show: true, readOnly: false}, magicType: {show: true, type: ['line', 'bar']}, restore: {show: true}, saveAsImage: {show: true} } }; legend = { data: null }; xAxis = { type: 'category', axisPointer: { type: 'shadow' } }; yAxis1 = { type: 'value', axisLabel: { formatter: '{value}' } }; yAxis2 = { type: 'value', axisLabel: { formatter: '{value}' } }; seriesItem = { type: 'bar', data: null }; } export class ModeledRectangularGraphData extends AbstractModeledGraphData { public template: ModeledRectangularTemplate = new BasicModeledRectangularTemplate(); public xAxis: { field?: string, style?: EchartXAxis } = {}; public yAxis1: EchartYAxis; public yAxis2: EchartYAxis; public dimensionField: string; public dimensions: Dimension[] = []; public usingAllDimensions: boolean = true; public indicators: Indicator[] = []; public legend: EchartLegend; public title: EchartTitle; constructor(data: GraphDataMatrix = [], header: GraphDataHeader = [], field: GraphDataField = []) { super(data, header, field); } private _options: EchartOptions; get options(): EchartOptions { if (!this._options) { this._options = this.createChartOptions(); } return this._options; } protected createChartOptions(): EchartOptions { if (!this.dimensionField || !this.xAxis || !this.xAxis.field) { return undefined; } if (!this.indicators || this.indicators.length == 0) { return undefined; } if (!this.usingAllDimensions && (!this.dimensions || this.dimensions.length == 0)) { return undefined; } this.indicators.forEach(kpi => kpi.index = this.getIndex(kpi.field)); const dimensions = this.getRealDimensions(this.dimensionField, this.dimensions, this.usingAllDimensions); return dimensions.length > 1 ? this.createMultiDimensionOptions(dimensions) : this.createMultiKPIOptions(dimensions[0]); } /** * 单指标多维度 */ protected createMultiDimensionOptions(dimensions: Dimension[]): EchartOptions { if (dimensions.length == 0) { return undefined; } const xAxisIndex = this.getIndex(this.xAxis.field); if (xAxisIndex == -1) { return undefined; } const xAxisItems = distinct(getColumn(this.data, xAxisIndex)); if (!xAxisItems || xAxisItems.length == 0) { return undefined } const options: EchartOptions = this.template.getInstance(); options.xAxis = [ CommonUtils.extendObjects<EchartXAxis>({}, this.template.xAxis, this.xAxis.style) ]; options.yAxis = [ CommonUtils.extendObjects<EchartYAxis>({}, this.template.yAxis1, this.yAxis1), CommonUtils.extendObjects<EchartYAxis>({}, this.template.yAxis2, this.yAxis2) ]; if (options.legend) { options.legend.data = dimensions.map(d => d.name); } options.xAxis[0].data = xAxisItems; const dimIndex = this.getIndex(this.dimensionField); const pruned = this.pruneAllData(xAxisIndex, dimIndex, dimensions); const groupedByDim = group(flat(pruned), dimIndex); options.series = groupedByDim._$groupItems .map(dimName => ({data: getColumn(groupedByDim[dimName], this.indicators[0].index), name: dimName})) .map(seriesData => CommonUtils.extendObjects<EchartSeriesItem>(seriesData, this.template.seriesItem)); return options; } /** * 确保给定的数据中,每一个给定的维度,都有且只有一个记录,缺少的记录用默认值补齐,多出的记录删除, 重复的记录用聚集算法聚集, * 并且要保证每组中的维度顺序一致。 * * @param xAxisIndex * @param dimIndex * @param dimensions */ protected pruneAllData(xAxisIndex: number, dimIndex: number, dimensions: Dimension[]): Grouped { const aggregateBy = this.indicators.map(kpi => ({index: kpi.index, algorithm: kpi.aggregateBy})); const groups = group(this.data, xAxisIndex); for (let xAxisItem in groups) { const records: string[][] = groups[xAxisItem]; if (records === groups._$groupItems) { continue; } const pruned = this.pruneData(records, dimIndex, dimensions, this.indicators); pruned.forEach(row => row[xAxisIndex] = xAxisItem); groups[xAxisItem] = pruned; } return groups; } protected createMultiKPIOptions(dim: Dimension): EchartOptions { if (!dim) { return undefined; } const xAxisIndex = this.getIndex(this.xAxis.field); if (xAxisIndex == -1) { return undefined; } const dimIndex = this.getIndex(this.dimensionField); if (dimIndex == -1) { return undefined; } const xAxisGroups = group(this.data, xAxisIndex); const pruned: string[][] = []; for (let xAxisItem in xAxisGroups) { const records: string[][] = xAxisGroups[xAxisItem]; if (records === xAxisGroups._$groupItems) { continue; } const prunedRecords = records.filter(r => r[dimIndex] == dim.name); if (prunedRecords.length == 1) { pruned.push(prunedRecords[0]); } else if (prunedRecords.length > 1) { const by = this.indicators.map(kpi => ({index: kpi.index, algorithm: kpi.aggregateBy})); pruned.push(aggregate(prunedRecords, by)); } else { const row = []; row[xAxisIndex] = xAxisItem; row[dimIndex] = dim.name; this.indicators.forEach(i => row[i.index] = i.defaultValue); pruned.push(row); } } const options: EchartOptions = this.template.getInstance(); options.xAxis = [ CommonUtils.extendObjects<EchartXAxis>({}, this.template.xAxis, this.xAxis.style) ]; options.yAxis = [ CommonUtils.extendObjects<EchartYAxis>({}, this.template.yAxis1, this.yAxis1), CommonUtils.extendObjects<EchartYAxis>({}, this.template.yAxis2, this.yAxis2) ]; if (options.legend) { options.legend.data = this.indicators.map(kpi => this.header[kpi.index]); } options.xAxis[0].data = xAxisGroups._$groupItems; options.series = this.indicators .map(kpi => ({name: this.header[kpi.index], data: getColumn(pruned, kpi.index)})) .map(seriesData => CommonUtils.extendObjects<EchartSeriesItem>(seriesData, this.template.seriesItem)); return options; } } // ------------------------------------------------------------------------------------------------ // 饼图相关数据对象 export class PieSeries { public dimensionField: string; public dimensions: Dimension[] = []; public usingAllDimensions: boolean = true; public indicators: Indicator[] = []; public radius: number[]; public center: number[]; public name?: string; public model?: any[]; constructor(name?: string) { this.name = name; } } export abstract class ModeledPieTemplate extends AbstractModeledGraphTemplate { seriesItem?: EchartSeriesItem; } export class BasicModeledPieTemplate extends ModeledRectangularTemplate { getInstance(): EchartOptions { return { title: CommonUtils.extendObjects<EchartTitle>({}, this.title), tooltip: CommonUtils.extendObjects<EchartTooltip>({}, this.tooltip), legend: CommonUtils.extendObjects<EchartLegend>({}, this.legend), }; } title = { x: 'center', textStyle: {}, subtextStyle: {} }; tooltip = { trigger: 'item', formatter: "{a} <br/>{b} : {c} ({d}%)" }; legend = { type: 'scroll', orient: 'vertical', right: 10, top: 20, bottom: 20, data: null }; seriesItem = { type: 'pie', data: null, name: '', radius: ['0%', '80%'], center: ['50%', '50%'], itemStyle: { emphasis: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } }; } export class ModeledPieGraphData extends AbstractModeledGraphData { constructor(data: GraphDataMatrix = [], header: GraphDataHeader = [], field: GraphDataField = []) { super(data, header, field); } public template: ModeledPieTemplate = new BasicModeledPieTemplate(); public series: PieSeries[]; public legend: EchartLegend; public title: EchartTitle; private _options: EchartOptions; get options(): EchartOptions { if (!this._options) { this._options = this.createChartOptions(); } return this._options; } protected createChartOptions(): EchartOptions { if (!this.series) { return undefined; } const options = this.template.getInstance(); if (options.legend) { options.legend.data = []; } options.series = this.series .filter(seriesData => { if (!seriesData.dimensionField) { return false; } if (!seriesData.indicators || seriesData.indicators.length == 0) { return false; } return seriesData.usingAllDimensions || (seriesData.dimensions && seriesData.dimensions.length > 0); }) .map((seriesData, idx) => { const dimensions = this.getRealDimensions(seriesData.dimensionField, seriesData.dimensions, seriesData.usingAllDimensions); const dimIndex = this.getIndex(seriesData.dimensionField); seriesData.indicators.forEach(kpi => kpi.index = this.getIndex(kpi.field)); seriesData.indicators.forEach(kpi => kpi.name = kpi.name ? kpi.name : this.header[kpi.index]); const seriesItem = CommonUtils.extendObjects<EchartSeriesItem>({}, this.template.seriesItem); if (dimensions.length > 1) { // 多维度 this._mergeLegend(options.legend, dimensions); let records; if (seriesData.usingAllDimensions) { records = this.data; } else { records = this.data.filter(row => dimensions.find(d => d.name == row[dimIndex])); } const kpiIndex = seriesData.indicators[0].index; records = records.map(row => [row[dimIndex], row[kpiIndex]]); const indicator: Indicator = CommonUtils.deepCopy(seriesData.indicators[0]); indicator.index = 1; seriesItem.data = this.pruneData(records, 0, dimensions, [indicator]) .map(row => ({name: row[0], value: row[1]})); } else { // 多指标 this._mergeLegend(options.legend, seriesData.indicators); const dim = dimensions[0].name; const records = this.data.filter(row => row[dimIndex] == dim); const pruned = this.pruneData(records, dimIndex, dimensions, seriesData.indicators)[0]; seriesItem.data = seriesData.indicators.map(i => ({name: i.name, value: pruned[i.index]})); } seriesItem.name = seriesData.name ? seriesData.name : 'series' + idx; seriesItem.radius = seriesData.radius.map(r => r + '%'); seriesItem.center = seriesData.center.map(r => r + '%'); return seriesItem; }); return options; } private _mergeLegend(legendObject: EchartLegend, candidates: (Indicator | Dimension)[]): void { if (!legendObject) { return; } const names = candidates.map(can => can.name); legendObject.data.push(...names.filter(legend => legendObject.data.indexOf(legend) == -1)); } public refresh(): void { this._options = undefined; super.refresh(); } }