import AtomComponent from '../../atomComponent'; import { BarGraphOptions, TooltipEventParams } from '../../models'; import { merge, pointDetector, type } from '../../utils'; import { EventName, SeriesCategory, AxisTypeEnum } from '../../constants'; import Chart from '../../core/chart'; import { GlobalData } from '@constants/globalData'; // const uniqueArray = (array: Record[], uniqueKey: string) => { // const map = new Map(); // return array.filter((item) => !map.has(item[uniqueKey]) && map.set(item[uniqueKey], 1)); // }; interface BgBarOptions { seriesIndex: number; // series下标 index: number; // 数据项下标 } enum DIRECTIONES { COLUMN = 'column', ROW = 'row', } export default class Bar extends AtomComponent { private static readonly defaultOptions: BarGraphOptions = { type: 'bar', showBackground: false, backgroundStyle: { color: 'rgba(180, 180, 180, 0.2)', }, }; private static readonly barWidthWhenOnlyOne = 28; private static readonly barAndBarGap = 4; protected seriesCategory = SeriesCategory.MULTI; private rawSeries: BarGraphOptions[]; private filteredSeries: BarGraphOptions[]; private selectedIndex = -1; private disabledSeriesNameSet = new Set(); constructor(id: string, chart: Chart, series: Array) { super({ id, chart }); this.rawSeries = this.processSeries(Bar.defaultOptions, series); this.bindInteraction(); this.bindLegendTap(); } render() { this.filteredSeries = this.rawSeries.filter(({ name }) => !this.disabledSeriesNameSet.has(name)); if (!this.series.length) return; this.drawBar(); } private update(dirtyComps = [this]) { this.$emit(EventName.Repaint, { dirtyComps }); } /** * 事件绑定 * bar各自绑定tap事件,点击触发阴影遮罩; * 再次点击相同bar关闭遮罩; * 点击其他bar触发阴影遮罩并关闭前bar遮罩; */ 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 { pointsInCartesian: point } = e; const { size: { width, height }, } = canvas; if (!(point.x >= 0 && point.x <= width && point.y >= 0 && point.y <= height)) return; const xAxisTickGap = size.width / data.length; const selectedIndex = data.findIndex((_, index) => pointDetector.isInRect(point, { x: 0 + index * xAxisTickGap, y: 0, width: xAxisTickGap, height: size.height, })); // 如果当前没有被点击的bar,则什么都不干 if (selectedIndex < 0) { this.$emit(EventName.TooltipClose); } if (this.selectedIndex !== selectedIndex) { this.selectedIndex = selectedIndex; const tooltipEventParams: TooltipEventParams = { centerX: selectedIndex * xAxisTickGap + xAxisTickGap / 2, data: this.series.map(seriesItem => ({ ...(seriesItem.data[selectedIndex] as any), color: this.computeBarColor(seriesItem.name), seriesName: seriesItem.name, })), selectedIndex }; 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 judgeDirection() { const { yAxis: memoedTAxisOptions } = this.chart?.getOption(); const yAxisOptions = memoedTAxisOptions[0]; // 此处和chart.js中getOption()管理方式有关,需要存在两层数组,后期考虑降为一层 const yAxisType = yAxisOptions[0]?.type; return yAxisType === AxisTypeEnum.VALUE; } private drawBar() { const { filteredSeries } = this; filteredSeries.forEach((seriesItem, seriesIndex) => { const { name: serieName, data, showBackground, barRadius, stack, yAxisIndex, itemStyle } = seriesItem; data.forEach((item, index) => { // 画剩余区域柱子 const params = { seriesIndex, index, }; showBackground && this.drawBgBar(params); // 画柱子 const basicBarParams = { seriesIndex, index, item, serieName, barRadius, stack, yAxisIndex, itemStyle, }; this.drawBasicBar(basicBarParams); }); }); return; } private drawBasicBar(basicBarParams) { const { chart, canvas, filteredSeries, dataset, barCount } = this; const { size } = canvas; const { seriesIndex, index, item, serieName, barRadius, stack, yAxisIndex } = basicBarParams; const decal = basicBarParams?.itemStyle?.decal; const computeLastSameStackIndex = () => { if (!stack || seriesIndex === 0) return -1; let lastSameStackIndex = 0; for (let i = 0; i < seriesIndex; i++) { const series = filteredSeries[i]; if (series.name === serieName) { continue; } else { if (series.stack === stack) { lastSameStackIndex = i; } } } return lastSameStackIndex; }; const lastSameStackIndex = computeLastSameStackIndex(); const aaa = this.uniqueSeriesByStack.findIndex(item => item.name === serieName); const computeValueTickRatio = (direction: DIRECTIONES) => { const globalValueAxis = chart.getGlobalData(GlobalData.ValueAxis); const valueAxis = globalValueAxis[yAxisIndex || 0]; const { max, min } = valueAxis?.axis; if (direction === DIRECTIONES.COLUMN) { return size.height / (max - min); } return size.width / (max - min); }; const { radiusLT = 0, radiusRT = 0 } = barRadius || {}; const isYAxisValue = this.judgeDirection(); const direction = isYAxisValue ? DIRECTIONES.COLUMN : DIRECTIONES.ROW; const valueTickRatio = computeValueTickRatio(direction); const computeLastSameStackBarHeight = () => { if (lastSameStackIndex === -1) return 0; let height = 0; let i = 0; while (i <= lastSameStackIndex) { height += (dataset[i][index] as any).value * valueTickRatio; i += 1; } return height; }; const chartOption = this.chart?.getOption?.(); // 纵向 if (direction === DIRECTIONES.COLUMN) { const xAxisOptions = chartOption?.xAxis?.[0]; const boundaryGap = !type.isEmpty(xAxisOptions) ? xAxisOptions.boundaryGap : true; const xAxisTickGapCount = (dataset[0].length - (boundaryGap ? 0 : 1)) || 1; const xAxisTickGap = size.width / xAxisTickGapCount; const barWidth = this.computeBarWidth(DIRECTIONES.COLUMN); this.painter.draw('Rect', { x: index * xAxisTickGap + (xAxisTickGap - barWidth * barCount - Bar.barAndBarGap * (barCount - 1)) / 2 + (barWidth + Bar.barAndBarGap) * (barCount === 1 ? 0 : lastSameStackIndex > -1 ? lastSameStackIndex : aaa) - (boundaryGap ? 0 : (xAxisTickGap) / 2), y: computeLastSameStackBarHeight(), width: barWidth, height: item.value * valueTickRatio, radius: [radiusLT, radiusRT], //柱子颜色配置 fillStyle: item?.itemStyle?.color || this.computeBarColor(serieName), decal, }); } else { const yAxisOptionObj = chartOption?.yAxis?.[0]; const yAxisOptions = yAxisOptionObj instanceof Array ? yAxisOptionObj[0] : yAxisOptionObj; const boundaryGap = !type.isEmpty(yAxisOptions) ? yAxisOptions.boundaryGap : true; const yAxisTickGapCount = (filteredSeries[0].data.length - (boundaryGap ? 0 : 1)) || 1; const yAxisTickGap = size.height / yAxisTickGapCount; const barHeight = this.computeBarWidth(DIRECTIONES.ROW); this.painter.draw('Rect', { x: 0, y: index * yAxisTickGap + (yAxisTickGap - barHeight * barCount - Bar.barAndBarGap * (barCount - 1)) / 2 + (barHeight + Bar.barAndBarGap) * (barCount === 1 ? 0 : seriesIndex) - (boundaryGap ? 0 : (yAxisTickGap) / 2), width: item.value * valueTickRatio, height: barHeight, radius: [radiusLT, radiusRT], fillStyle: item?.itemStyle?.color ||this.computeBarColor(serieName), decal, }); } } private drawBgBar(bgBarOptions: BgBarOptions) { const { canvas, filteredSeries } = this; const { size } = canvas; const { seriesIndex, index } = bgBarOptions; const isYAxisValue = this.judgeDirection(); const direction = isYAxisValue ? DIRECTIONES.COLUMN : DIRECTIONES.ROW; // 纵向 if (direction === DIRECTIONES.COLUMN) { const xAxisTickGap = size.width / filteredSeries[0].data.length; const barWidth = this.computeBarWidth(DIRECTIONES.COLUMN); const barCount = filteredSeries.length; const x = index * xAxisTickGap + (xAxisTickGap - barWidth * barCount - Bar.barAndBarGap * (barCount - 1)) / 2 + (barWidth + Bar.barAndBarGap) * (barCount === 1 ? 0 : seriesIndex); this.painter.draw('Rect', { x, y: 0, width: barWidth, height: size.height, fillStyle: filteredSeries[seriesIndex].backgroundStyle?.color, }); } else { const yAxisTickGap = size.height / filteredSeries[0].data.length; const barWidth = this.computeBarWidth(DIRECTIONES.ROW); const barCount = filteredSeries.length; // TODO: Orz 改不动了... 命名看不懂 const y = index * yAxisTickGap + (yAxisTickGap - barWidth * barCount - Bar.barAndBarGap * (barCount - 1)) / 2 + (barWidth + Bar.barAndBarGap) * (barCount === 1 ? 0 : seriesIndex); this.painter.draw('Rect', { x: 0, y, width: barWidth, height: size.height, fillStyle: filteredSeries[seriesIndex]?.backgroundStyle?.color, }); } } private processStyle({ itemStyle: rawItemStyle, backgroundStyle }) { return { itemStyle: { ...rawItemStyle, }, backgroundStyle, }; } private processSeries(defaultOptions, series: BarGraphOptions[]) { return series.map((seriesItem) => { const options = merge(defaultOptions, seriesItem); return { ...seriesItem, /** * 兼容echart的扁平化配置 * 同时扩展配置项,如:borderStyle\shadowStyle\style */ ...this.processStyle(options), }; }); } private computeBarColor(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]; } private computeBarWidth(direction) { const { canvas, filteredSeries, barCount } = this; const { size } = canvas; // 竖向 if (direction === 'column') { const xAxisTickGap = size.width / filteredSeries[0].data.length; // if (barCount === 1) { // return Bar.barWidthWhenOnlyOne; // } return (xAxisTickGap - Math.max(2, 14 - barCount * 2) - Bar.barAndBarGap * (barCount - 1)) / barCount; } // 横向 const yAxisTickGap = size.height / filteredSeries[0].data.length; // if (barCount === 1) { // return Bar.barWidthWhenOnlyOne; // } return (yAxisTickGap - Math.max(2, 14 - barCount * 2) - Bar.barAndBarGap * (barCount - 1)) / barCount; } private get barCount() { const { filteredSeries } = this; const stacks = filteredSeries.map((item, index) => item.stack ?? index); return new Set(stacks).size; } private get uniqueSeriesByStack() { const { filteredSeries } = this; const map = new Map(); return filteredSeries.filter((item, index) => !map.has(item.stack) && map.set(item.stack ?? index, 1)); } }