import { TooltipDefaultStyle } from '../../constants'; import { getEnv } from '../../utils'; import { htmlSnipToSvg } from '../../utils/htmlSnip'; import Tooltip from './tooltip'; export default class CanvasTooltip extends Tooltip { private isShow = false; render() { this.isShow && this.open(); } open(fromEvent = false) { this.isShow = true; if (!fromEvent) { const { coordinate } = this.params; if (coordinate === 'cartesian') { this.drawTooltipInCartesian(); } else { this.drawTooltipInPolar(); } } } close() { this.isShow = false; } private async drawTooltipInCartesian() { const { canvas, options, dataset, title, transformedData, params } = this; const { ctx, padding, size } = canvas; const texts = transformedData.map(item => item.text); const colors = transformedData.map(item => item.color); ctx.font = `${TooltipDefaultStyle.fontSize}px ${TooltipDefaultStyle.fontFamily}`; const itemWidth = this.computeItemWidth(); function computePerLineTooltipCount(texts: any[]) { let perLineTooltipCount = 1; for (let i = 1; i <= texts.length; i++) { const rectWidth = TooltipDefaultStyle.padding * 2 + itemWidth * i + TooltipDefaultStyle.itemHorizontalGap * (i - 1); if (rectWidth > size.width) { break; } perLineTooltipCount = i; } return perLineTooltipCount; } const perLineTooltipCount = computePerLineTooltipCount(texts); function computeRectWidth() { const { width: titleWidth } = ctx.measureText(title); const rowTooltipWidth = itemWidth * perLineTooltipCount + TooltipDefaultStyle.itemHorizontalGap * (perLineTooltipCount - 1); return TooltipDefaultStyle.padding * 2 + (titleWidth > rowTooltipWidth ? titleWidth : rowTooltipWidth); } const rectWidth = computeRectWidth(); const generateTooltips = () => { const tooltips = []; let i = 0; while (i < texts.length) { tooltips.push(texts.slice(i, i + perLineTooltipCount)); i += perLineTooltipCount; } return tooltips; }; const tooltips = generateTooltips(); const rectHeight = TooltipDefaultStyle.fontSize * tooltips.length + TooltipDefaultStyle.itemVerticalGap * (tooltips.length - 1) + TooltipDefaultStyle.padding * 2 + (title ? TooltipDefaultStyle.fontSize + TooltipDefaultStyle.titleToListGap : 0); const rectX = this.computeTooltipX(rectWidth); const rectY = size.height + padding.top - rectHeight - 4 - this.getPaddingTop(); if (options.formatter) { return this.drawTooltipWithImage(); } this.painter.draw('Rect', { x: rectX, y: rectY, width: rectWidth, height: rectHeight, radius: [2, 2, 2, 2], boxShadow: { offsetX: 0, offsetY: 0, blur: 16, color: 'rgba(0,0,0,0.3)', }, fillStyle: '#fff', }); const drawTitle = () => { this.painter.draw('Text', { text: title, x: rectX + TooltipDefaultStyle.padding, y: rectY + rectHeight - TooltipDefaultStyle.padding, textBaseline: 'top', fontSize: TooltipDefaultStyle.fontSize, fontStyle: TooltipDefaultStyle.color, fontFamily: TooltipDefaultStyle.fontFamily, }); }; title && drawTitle(); const drawTooltip = (texts: string[], lineIndex: number) => { const lineCenterY = rectY + rectHeight - lineIndex * (TooltipDefaultStyle.fontSize + TooltipDefaultStyle.itemVerticalGap) - TooltipDefaultStyle.padding - (TooltipDefaultStyle.fontSize - TooltipDefaultStyle.symbolWidth) / 2 - TooltipDefaultStyle.symbolWidth / 2 - (title ? TooltipDefaultStyle.fontSize + TooltipDefaultStyle.titleToListGap : 0); // 当前选中序号 const currentData = dataset[0] || []; const selectedIndex = params?.selectedIndex || 0; texts.forEach((text, index) => { const tooltipX = rectX + TooltipDefaultStyle.padding + index * (itemWidth + TooltipDefaultStyle.itemHorizontalGap); //自定义配置tootlip颜色 const customColor = currentData[selectedIndex]?.itemStyle?.color || colors[index + lineIndex * perLineTooltipCount]; this.painter.draw('Circle', { x: tooltipX + TooltipDefaultStyle.symbolWidth / 2, y: lineCenterY, r: TooltipDefaultStyle.symbolWidth / 2, fillStyle: customColor, strokeStyle: '#fff', }); this.painter.draw('Text', { text, x: tooltipX + TooltipDefaultStyle.symbolWidth + TooltipDefaultStyle.symbolToTextGap, y: lineCenterY, textBaseline: 'middle', fontSize: TooltipDefaultStyle.fontSize, fontStyle: TooltipDefaultStyle.color, fontFamily: TooltipDefaultStyle.fontFamily, }); }); }; tooltips.forEach((texts, index) => { drawTooltip(texts, index); }); } private drawTooltipInPolar() { const { canvas: { ctx }, params, painter, transformedData, } = this; const { data, point: { x, y }, } = params; const { fontFamily, padding, symbolWidth, symbolToTextGap } = TooltipDefaultStyle; const { text } = transformedData[0]; const fontSize = data.length === 1 ? 16 : 12; ctx.font = `${fontSize}px ${fontFamily}`; const textWidth = Number.parseFloat(ctx.measureText(text).width.toFixed(2)); const rectHeight = 40; // 蒙层背景 painter.draw('Rect', { x: x - padding - symbolWidth / 2, y: y - fontSize / 2 - (rectHeight - fontSize) / 2, width: padding + symbolWidth + textWidth + padding * 2 + symbolWidth + symbolToTextGap, height: rectHeight, radius: [2, 2, 2, 2], fillStyle: 'rgba(255,255,255,0.8)', strokeStyle: 'rgba(255,255,255,0.8)', boxShadow: { offsetX: 2, offsetY: 4, blur: 16, color: 'rgba(0,0,0,0.1)', }, filter: 'blur(20px)', }); const { color } = data[0]; this.painter.draw('Circle', { x: x + symbolWidth / 2, y, r: symbolWidth / 2, fillStyle: color || '#eee', strokeStyle: color || '#eee', }); // // key // // value painter.draw('Text', { text, x: x + padding + symbolWidth + symbolToTextGap, y, fontSize, fontFamily, fontStyle: TooltipDefaultStyle.color, textBaseline: 'middle', }); } private drawTooltipWithImage() { const htmlSnip = this.getHtmlSinp(); const image = htmlSnipToSvg(htmlSnip, this.options.width); switch (getEnv()) { case 'web': return this.drawTooltipWithImageInWeb(image); case 'mp': return this.drawTooltipWithImageInMp(image); default: console.log('该环境暂不支持自定义tooltip'); } } private drawTooltipWithImageInWeb(imageSrc: string) { const { canvas } = this; const { padding, size } = canvas; const image = new Image(); image.onload = () => { this.painter.draw('Image', { image, x: this.computeTooltipX(this.options.width), y: size.height + padding.top - 4 - this.getPaddingTop(), }); }; image.onerror = () => {}; image.src = imageSrc; } private drawTooltipWithImageInMp(imageSrc: string) { const { canvas, options: { width }, } = this; /** * 模拟器上好使 * 真机上无法走到onload,这个坑网上一大堆,但是不知道为什么 * 初步怀疑是createImage()返回的image对象的src属性不支持渲染svg const img = canvas.element.createImage(); img.onload = function () { resolve(img); }; img.onerror = function (error) { reject(error); }; img.src = imageSrc; */ canvas.customTooltip.setData({ src: imageSrc, x: this.computeTooltipX(this.options.width, true), width, }); } }