import { TooltipOptions, TooltipEventParams, TooltipDataItem } from '../../models'; import AtomComponent from '../../atomComponent'; import { EventName, SeriesCategory, TooltipDefaultStyle } from '../../constants'; import { datetime, merge } from '../../utils'; import { domToString } from '../../utils/htmlSnip'; import { hexToRgb } from '../../theme'; import { isNumber } from '@utils/type'; const name = 'tooltip'; export default abstract class Tooltip extends AtomComponent { protected static readonly defaultOptions: TooltipOptions = { show: true, formatMode: 'replace', width: 150, }; private static readonly defaultParams: TooltipEventParams = { coordinate: 'cartesian', }; protected seriesCategory = SeriesCategory.NONE; protected options: TooltipOptions; protected params: TooltipEventParams; /* todo: 后续优化 */ protected title = ''; protected transformedData = [] as any[]; /* todo: 后续优化 */ constructor(chart, options: TooltipOptions) { super({ name, chart }); this.options = merge(Tooltip.defaultOptions, options); this.initEvent(); } protected computeItemWidth(): number { const { ctx } = this.canvas; ctx.font = `${TooltipDefaultStyle.fontSize}px ${TooltipDefaultStyle.fontFamily}`; const itemTextWidths = this.transformedData.map(item => Math.ceil(ctx.measureText(item.text).width)); const maxItemTextWidth = Math.max(...itemTextWidths); return TooltipDefaultStyle.symbolWidth + TooltipDefaultStyle.symbolToTextGap + maxItemTextWidth; } protected computeTooltipX(width: number, needHandlePadding = false) { const { canvas, params } = this; const { padding, size } = canvas; const { centerX, coordinate, point } = params; if (coordinate === 'cartesian') { let x = 0; if (centerX - width / 2 <= 0) { x = 0; } else if (centerX + width / 2 > size.width) { x = size.width - width; } else { x = centerX - width / 2; } return x + (needHandlePadding ? padding.left : 0); } return point.x - TooltipDefaultStyle.padding - TooltipDefaultStyle.symbolWidth / 2 + (needHandlePadding ? padding.left : 0); // return point.x - TooltipDefaultStyle.padding + (needHandlePadding ? padding.left : 0); } protected getHtmlSinp() { const { formatter, formatMode } = this.options; const defaultHtmlSinp = this.getDefaultHtmlSinp(); const formattedHtmlSinp = this.getFormattedHtmlSinp(); if (!formatter) return defaultHtmlSinp; return formatMode === 'append' ? defaultHtmlSinp + formattedHtmlSinp : formattedHtmlSinp; } /** * 当legend居顶布局,需要将tooltip下移动padding.top距离来露出legend */ protected getPaddingTop() { const { legend } = this.chart.getParsedOptions(); if (!legend || !legend?.top) return 0; const { padding } = this.chart.canvas; return isNumber(padding?.top) ? padding?.top : 0; } private getDefaultHtmlSinp() { const { title, transformedData } = this; const { fontSize, fontFamily, color } = TooltipDefaultStyle; const itemWidth = this.computeItemWidth(); return `
${title}
`; } private getFormattedHtmlSinp() { const { options, params } = this; const { formatter } = options; const { data } = params; let formattedHtmlSinp = ''; if (typeof formatter === 'string') { const [item] = data; formattedHtmlSinp = formatter .replace(/\{a\}/, item.seriesName ?? 'series0') .replace(/\{b\}/, item.name ?? '') .replace(/\{c\}/, `${item.value}` ?? '') .replace(/\{d\}/, `${item.percent}` ?? ''); } if (typeof formatter === 'function') { formattedHtmlSinp = domToString(formatter(params.data.map(item => ({ seriesName: item.seriesName, name: item.name, value: item.value, color: hexToRgb(item.color), percent: item.percent, } as any)))); } return formattedHtmlSinp; } private generateText(item: TooltipDataItem) { const { xAxis: xAxisOption } = this.chart.getOption(); if (xAxisOption?.[0]?.type === 'time' && !datetime.isValid(item.name)) { console.warn(`时间字符串【${item.name}】格式异常`); return ''; } return `${item.seriesName ?? ''} ${item.value ?? '-'}`; } private transformData() { const { data } = this.params; return data .map(item => ({ ...item, text: this.generateText(item), })) .filter(item => !item.isFake); } private generateTitle() { const { data } = this.params; if (!data) return ''; const { xAxis: xAxisOption } = this.chart.getOption(); const [item] = data; if (xAxisOption?.[0]?.type === 'time') { if (!datetime.isValid(item.name)) { console.warn(`时间字符串【${item.name}】格式异常`); return ''; } const { formatter } = xAxisOption[0]?.axisLabel; const labelFormatter = formatter as string; const formattedName = datetime.format(item.name, labelFormatter); return formattedName; } return item.name; } private initEvent() { this.$on(EventName.TooltipOpen, this.prepareOpen.bind(this)); this.$on(EventName.TooltipClose, this.close.bind(this)); } private prepareOpen(params) { this.params = merge(Tooltip.defaultParams, params); this.title = this.generateTitle(); this.transformedData = this.transformData(); this.open(true); } abstract render(): void; abstract open(fromEvent?: any); abstract close(); }