/* eslint-disable arrow-body-style */ import { AxisOptions, LineGraphOptions, TooltipEventParams } from '../../models'; import AtomComponent from '../../atomComponent'; import { pluginManager } from '../../pluginManager'; import { merge, pointDetector, type } from '../../utils'; import { EventName, SeriesCategory } from '../../constants'; import { GlobalData } from '@constants/globalData'; import { isNumber } from '@utils/type'; interface Point { x: number; y: number | null; } export default class Line extends AtomComponent { private static readonly defaultOptions: LineGraphOptions = { type: 'line', symbol: 'none', smooth: false, lineStyle: { width: 1, }, }; protected seriesCategory = SeriesCategory.MULTI; private rawSeries: LineGraphOptions[]; private filteredSeries: LineGraphOptions[]; private selectedIndex = -1; private disabledSeriesNameSet = new Set(); private xAxisOptions: AxisOptions; constructor(id: string, chart, series: LineGraphOptions[]) { super({ id, chart }); this.rawSeries = this.processSeries(Line.defaultOptions, series); this.bindInteraction(); this.bindLegendTap(); this.xAxisOptions = this.processOption(); } public render() { this.filteredSeries = this.rawSeries.filter(({ name }) => !this.disabledSeriesNameSet.has(name)); if (!this.filteredSeries.length) return; this.drawLine(); this.drawSymbol(); this.drawMarkLine(); } private drawMarkLine() { const { chart, rawSeries } = this; rawSeries.forEach(({ markLine }) => { if (markLine) { const MarkLine = pluginManager.getComponent('markLine'); const markLineInstance = new MarkLine(chart, markLine); markLineInstance.render(); } }); } private update(dirtyComps = [this]) { this.$emit(EventName.Repaint, { dirtyComps }); } /** * 事件绑定 * tap事件触发相应区域symbol高亮、tooltip显示 */ private bindInteraction() { const { chart, canvas } = this; const { size } = canvas; chart.getInteration().on(['tap', 'pan'], (e) => { if (!this.filteredSeries.length) return; const { data } = this.filteredSeries[0]; const { size: { width, height }, } = canvas; const xAxisTickGap = size.width / data.length; const { pointsInCartesian: point } = e; if (!(point.x >= 0 && point.x <= width && point.y >= 0 && point.y <= height)) return; const selectedIndex = data.findIndex((_, index) => pointDetector.isInRect(point, { x: 0 + index * xAxisTickGap, y: 0, width: xAxisTickGap, height: size.height, })); if (selectedIndex < 0) { this.$emit(EventName.TooltipClose); } if (this.selectedIndex !== selectedIndex) { this.selectedIndex = selectedIndex; const tooltipEventParams: TooltipEventParams = { centerX: selectedIndex * xAxisTickGap + xAxisTickGap / 2, data: this.filteredSeries.map(seriesItem => ({ ...(seriesItem.data[selectedIndex] as any), color: this.computeStrokeColor(seriesItem.name), seriesName: seriesItem.name, })), }; this.$emit(EventName.TooltipOpen, tooltipEventParams); } else { // this.selectedIndex = -1; // this.$emit(EventName.TooltipClose); } this.update(); }); } private bindLegendTap() { this.$on(EventName.LegendTap, ({ componentName, enabled, dirtyComp }) => { this.$emit(EventName.TooltipClose); this.selectedIndex = -1; const controlledSeries = this.rawSeries.find(({ name }) => name === componentName); if (enabled) { this.disabledSeriesNameSet.delete(controlledSeries.name); } else { this.disabledSeriesNameSet.add(controlledSeries.name); } this.update([dirtyComp, this]); }); } private generatePoints(seriesItem: LineGraphOptions | any): Point[] { const { chart, canvas } = this; const { size } = canvas; const { data } = seriesItem; const { xAxis, yAxis } = chart.getOption(); const [xAxisOptions] = [xAxis[0], yAxis[0]]; // ?下标又问题 const boundaryGap = !type.isEmpty(xAxisOptions) ? xAxisOptions.boundaryGap : true; const getXAxisTickGap = () => { const xAxisTickGapCount = data.length - (boundaryGap ? 0 : 1); return size.width / xAxisTickGapCount; }; const xAxisTickGap = getXAxisTickGap(); const getAxisInfo = () => { const globalValueAxises = chart.getGlobalData(GlobalData.ValueAxis); const { axis } = globalValueAxises[seriesItem?.yAxisIndex || 0]; // 根据series对应的yAxisIndex来选择对应的axis阈值数据 return axis; }; const axisInfo = getAxisInfo(); const getValueRatio = () => { const { max, min } = axisInfo; return size.height / (max - min); }; const valueRatio = getValueRatio(); // 针对空值如何处理?改为统一置null const handleExceptionValue = (item) => { const isValidNumber = isNumber(item.value); const isValidNull = item.value === null; let value = undefined; if (isValidNumber) value = item.value; // 数值(合法) else if (isValidNull) value = null; // null空值(合法) else value = null; // 非法数值: 统一转换为null空值处理 return value; }; return data .map((item, index) => { // 针对非安全item.value统一置0 const value = handleExceptionValue(item); // 返回安全数据项 const dataItem = { x: index * xAxisTickGap + (boundaryGap ? xAxisTickGap / 2 : 0), y: value !== null ? (value - axisInfo?.min) * valueRatio : null, // bugfix: 以axis.min为基准计算物理y值 }; return dataItem; }); } private drawSymbol() { const { filteredSeries } = this; filteredSeries .filter(seriesItem => seriesItem.symbol !== 'none') .forEach((seriesItem) => { const points = this.generatePoints(seriesItem); const strokeStyle = this.computeStrokeColor(seriesItem.name); points.forEach(({ x, y }, index) => { if (y === null) return; switch (seriesItem.symbol) { case 'rect': case 'circle': default: this.painter.draw('Circle', { x, y, r: 5, fillStyle: '#fff', strokeStyle: '#fff', }); this.painter.draw('Circle', { x, y, r: 3, fillStyle: this.selectedIndex === index ? strokeStyle : '#fff', strokeStyle, }); break; } }); }); } private drawLine() { const { canvas, filteredSeries } = this; const { ctx, size } = canvas; filteredSeries.forEach((seriesItem) => { const points = this.generatePoints(seriesItem); // FIXME 暂时不支持values存在空值/异常值的case,后续修复 // FIXME: 面积图 valueAxis需要支持从0开始展示,后续修复 if (seriesItem.areaStyle) { const exteaPoints = [ { x: points[points.length - 1].x, y: 0, }, { x: points[0].x, y: 0, }, ]; const minY = Math.min(...points.map(point => point.y)); const minYPoint = points.find(point => point.y === minY); const line = ctx.createLinearGradient(minYPoint.x, minYPoint.y, minYPoint.x, minYPoint.y + size.height); line.addColorStop(0, 'rgba(0,82,217,0.2)'); line.addColorStop(1, 'rgba(0,82,217,0)'); // 画面积 this.painter.draw('Area', { points: points.concat(exteaPoints.map(point => ({ ...point, selected: false }))), fillStyle: line, }); } const strokeStyle = this.computeStrokeColor(seriesItem.name); // 画线 TODO:在drawPolyLine时实现调过空值绘制能力 this.painter.draw('PolyLine', { points, smooth: seriesItem.smooth, stroke: strokeStyle, lineWidth: seriesItem.lineStyle?.width || 1, }); }); } private processOption() { return this.chart?.getOption()?.axis?.x; } private processStyle(seriesIndex, options) { const { itemStyle: rawItemStyle, lineStyle: rawLineStyle, areaStyle: rawAreaStyle } = options; // 颜色选择逻辑 const { style } = this.chart; const color = style?.colors?.[seriesIndex]; const areaStyle = rawAreaStyle ? { ...rawAreaStyle, color, } : null; const lineStyle = rawLineStyle ? { ...rawLineStyle, color, } : null; return { itemStyle: { ...rawItemStyle, }, lineStyle, areaStyle, }; } private processSeries(defaultOptions, series: LineGraphOptions[]) { return series.map((seriesItem, seriesIndex) => { const options = merge(defaultOptions, seriesItem); return { ...seriesItem, /** * 兼容echart的扁平化配置 * 同时扩展配置项,如:borderStyle\shadowStyle\style */ ...this.processStyle(seriesIndex, options), }; }); } /** * 颜色选择逻辑 * 优先级:用户配置颜色 > 初始化选择配色 * @param name 线名称 * @returns colors 颜色值 */ private computeStrokeColor(name: string) { const { chart, rawSeries } = this; const seriesIndex = rawSeries.findIndex(item => item.name === name); if (rawSeries[seriesIndex]?.itemStyle?.color) { return rawSeries[seriesIndex]?.itemStyle.color; } return chart.style.colors[seriesIndex]; } }