diff --git a/common/changes/@visactor/vchart/fix-dom-tooltip-content-update_2024-12-11-07-29.json b/common/changes/@visactor/vchart/fix-dom-tooltip-content-update_2024-12-11-07-29.json new file mode 100644 index 0000000000..a566ede14e --- /dev/null +++ b/common/changes/@visactor/vchart/fix-dom-tooltip-content-update_2024-12-11-07-29.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: html tooltip can reuse the dom content and fix the unneed animation\n\n", + "type": "none", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "dingling112@gmail.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vchart/fix-tooltip-formatter_2024-12-11-02-38.json b/common/changes/@visactor/vchart/fix-tooltip-formatter_2024-12-11-02-38.json new file mode 100644 index 0000000000..35a306b89c --- /dev/null +++ b/common/changes/@visactor/vchart/fix-tooltip-formatter_2024-12-11-02-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix tooltip content when only has `valueFormatter` or `keyFormatter`\n\n", + "type": "none", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "dingling112@gmail.com" +} \ No newline at end of file diff --git a/packages/vchart/src/component/tooltip/utils/common.ts b/packages/vchart/src/component/tooltip/utils/common.ts index e8971a1594..c14dcbd5c4 100644 --- a/packages/vchart/src/component/tooltip/utils/common.ts +++ b/packages/vchart/src/component/tooltip/utils/common.ts @@ -78,23 +78,30 @@ function addContentLine( ...spec } : { ...shapeAttrs, ...spec }; - - Object.keys(finalSpec).forEach(k => { - if (k === 'key') { - res.key = getTimeString( - getTooltipContentValue(finalSpec.key, datum, params, finalSpec.keyFormatter), - finalSpec.keyTimeFormat, - finalSpec.keyTimeFormatMode - ); - } else if (k === 'value') { - res.value = getTimeString( - getTooltipContentValue(finalSpec.value, datum, params, finalSpec.valueFormatter), - finalSpec.valueTimeFormat, - finalSpec.valueTimeFormatMode - ); - } else { - (res as any)[k] = getTooltipContentValue((finalSpec as any)[k], datum, params); - } + const { + key, + keyFormatter, + keyTimeFormat, + keyTimeFormatMode, + value, + valueFormatter, + valueTimeFormat, + valueTimeFormatMode, + ...others + } = finalSpec; + + res.key = getTimeString( + getTooltipContentValue(key, datum, params, keyFormatter), + keyTimeFormat, + keyTimeFormatMode + ); + res.value = getTimeString( + getTooltipContentValue(value, datum, params, valueFormatter), + valueTimeFormat, + valueTimeFormatMode + ); + Object.keys(others).forEach(k => { + (res as any)[k] = getTooltipContentValue((finalSpec as any)[k], datum, params); }); if (res.visible !== false && (isValid(res.key) || isValid(res.value))) { result.push(res); diff --git a/packages/vchart/src/plugin/components/tooltip-handler/dom-tooltip-handler.ts b/packages/vchart/src/plugin/components/tooltip-handler/dom-tooltip-handler.ts index 4bc6641950..4ed625db86 100644 --- a/packages/vchart/src/plugin/components/tooltip-handler/dom-tooltip-handler.ts +++ b/packages/vchart/src/plugin/components/tooltip-handler/dom-tooltip-handler.ts @@ -1,6 +1,6 @@ import type { ITooltipActual, ITooltipPositionActual } from '../../../typings/tooltip'; import { BaseTooltipHandler } from './base'; -import { cssToStyleString, getDomStyle, getTextStyle, setStyleToDom } from './utils/style'; +import { getDomStyle, getTextStyle, setStyleToDom } from './utils/style'; import { TOOLTIP_CONTAINER_EL_CLASS_NAME, DEFAULT_TOOLTIP_Z_INDEX, @@ -8,7 +8,7 @@ import { TOOLTIP_CONTENT_BOX_CLASS_NAME, TOOLTIP_TITLE_CLASS_NAME } from './constants'; -import { type Maybe, isNil, isValid } from '@visactor/vutils'; +import { type Maybe, isValid } from '@visactor/vutils'; import type { IContainerSize } from '@visactor/vrender-components'; import { domDocument } from '../../../util/env'; import type { ITooltipSpec, TooltipHandlerParams } from '../../../component/tooltip'; @@ -40,7 +40,6 @@ export class DomTooltipHandler extends BaseTooltipHandler { protected _rootDom?: HTMLElement; protected _tooltipActual?: ITooltipActual; protected declare _container: Maybe; - protected _domString?: string; /** 自定义 tooltip 的位置缓存 */ protected _cacheCustomTooltipPosition: ILayoutPoint; @@ -83,39 +82,47 @@ export class DomTooltipHandler extends BaseTooltipHandler { this._container.classList.add(TOOLTIP_CONTAINER_EL_CLASS_NAME); parentElement.appendChild(this._container); } - const tooltipElement = document.createElement('div'); - const globalTheme = this._chartOption?.getTheme() ?? {}; - - setStyleToDom(tooltipElement, { - left: '0', - top: '0', - pointerEvents: 'none', - padding: '12px', - position: 'absolute', - zIndex: DEFAULT_TOOLTIP_Z_INDEX, - fontFamily: (globalTheme?.fontFamily ?? token.fontFamily) as string, - fontSize: '11px', - borderRadius: '3px', - borderStyle: 'solid', - lineHeight: 'initial', - background: '#fff', - boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', - maxWidth: '100wh', - maxHeight: '100vh', - ...this._domStyle?.panel - } as CSSStyleDeclaration); - - this._container.appendChild(tooltipElement); - this._rootDom = tooltipElement; } } + initRootDom() { + const tooltipSpec = this._component.getSpec() as ITooltipSpec; + const tooltipElement = document.createElement('div'); + const globalTheme = this._chartOption?.getTheme() ?? {}; + + setStyleToDom(tooltipElement, { + left: '0', + top: '0', + pointerEvents: 'none', + padding: '12px', + position: 'absolute', + zIndex: DEFAULT_TOOLTIP_Z_INDEX, + fontFamily: (globalTheme?.fontFamily ?? token.fontFamily) as string, + fontSize: '11px', + borderRadius: '3px', + borderStyle: 'solid', + lineHeight: 'initial', + background: '#fff', + boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', + maxWidth: '100wh', + maxHeight: '100vh', + visibility: 'hidden', + ...this._domStyle.panel + } as CSSStyleDeclaration); + tooltipElement.classList.add(tooltipSpec.className); + tooltipElement.setAttribute('vchart-tooltip-id', `${this.id}`); + this._container.appendChild(tooltipElement); + this._rootDom = tooltipElement; + } + // 计算 tooltip 内容区域的宽高,并缓存结果 protected _getTooltipBoxSize(actualTooltip: ITooltipActual, changePositionOnly: boolean): IContainerSize | undefined { - if (!changePositionOnly || isNil(this._domString)) { + if (!this._rootDom) { + this.initRootDom(); + } + if (!changePositionOnly) { this._updateDomStringByCol(actualTooltip); } - this._rootDom.innerHTML = this._domString ?? ''; this._updateDomStyle('height'); @@ -145,7 +152,7 @@ export class DomTooltipHandler extends BaseTooltipHandler { if (!params.changePositionOnly) { this._tooltipActual = activeTooltipSpec; } - this.setVisibility(visible); + const currentVisible = this.getVisibility(); // 位置 const el = this._rootDom; @@ -166,9 +173,16 @@ export class DomTooltipHandler extends BaseTooltipHandler { // 更新缓存 this._cacheCustomTooltipPosition = position; } else { + if (!currentVisible) { + // 当从隐藏切换到显示的时候,需要先设置一次 transition 为 0ms,防止出现从一个非常远的初始位置进行动画 + this._rootDom.style.transitionDuration = '0ms'; + } else { + this._rootDom.style.transitionDuration = this._domStyle.panel.transitionDuration ?? 'initial'; + } this._updatePosition({ x, y }); } } + this.setVisibility(visible); } } @@ -179,125 +193,159 @@ export class DomTooltipHandler extends BaseTooltipHandler { } protected _updateDomStringByCol(actualTooltip?: ITooltipActual) { - let domString = ''; const { title = {}, content } = actualTooltip; const hasContent = content && content.length; const rowStyle = this._domStyle.row; + const chilren = [...(this._rootDom.children as any)] as HTMLElement[]; + let titleDom = chilren.find(child => child.className.includes(TOOLTIP_TITLE_CLASS_NAME)); + + if (!titleDom && title.visible !== false) { + titleDom = document.createElement('h2'); + const span = document.createElement('span'); + titleDom.appendChild(span); + + titleDom.classList.add(TOOLTIP_TITLE_CLASS_NAME); + this._rootDom.appendChild(titleDom); + } - if (title.visible !== false) { - domString += `

${title.value ?? ''}

`; + }); + (titleDom.firstChild as HTMLElement).innerHTML = `${title.value ?? ''}`; + } else if (titleDom && title.visible === false) { + titleDom.parentNode.removeChild(titleDom); } - if (hasContent) { - let shapeItems = ''; - let keyItems = ''; - let valueItems = ''; - content.forEach((entry, index) => { - const styleByRow = index === content.length - 1 ? null : rowStyle; - - shapeItems += `
${getSvgHtml( - entry - )}
`; - - keyItems += `
${formatContent(entry.key)}
`; - - valueItems += `
${formatContent(entry.value)}
`; + + let contentDom = chilren.find(child => child.className.includes(TOOLTIP_CONTENT_BOX_CLASS_NAME)); + const columns = ['shape', 'key', 'value']; + + if (!contentDom && hasContent) { + contentDom = document.createElement('div'); + + columns.forEach(col => { + const colDiv = document.createElement('div'); + + colDiv.classList.add(`${TOOLTIP_PREFIX}-column`); + colDiv.classList.add(`${TOOLTIP_PREFIX}-${col}-column`); + colDiv.setAttribute('data-col', col); + contentDom.appendChild(colDiv); }); - domString += `
-
- ${shapeItems} -
-
- ${keyItems} -
-
- ${valueItems} -
-
`; + contentDom.classList.add(TOOLTIP_CONTENT_BOX_CLASS_NAME); + this._rootDom.appendChild(contentDom); } - this._domString = domString; + if (contentDom && hasContent) { + const columnDivs = [...(contentDom.children as any)] as HTMLElement[]; + + columnDivs.forEach((colDiv, index) => { + const colName = colDiv.getAttribute('data-col'); + + if (colName && columns.includes(colName)) { + setStyleToDom(colDiv, { + ...(this._domStyle as any)[colName], + display: 'inline-block', + verticalAlign: 'top' + }); + const rows = [...(colDiv.children as any)] as HTMLElement[]; + + // 删除多余的行 + rows.slice(content.length).forEach(extraRow => { + extraRow.parentNode.removeChild(extraRow); + }); + + content.forEach((entry, index) => { + let row = rows[index]; + + if (!row) { + row = document.createElement('div'); + row.classList.add(`${TOOLTIP_PREFIX}-${colName}`); + colDiv.appendChild(row); + } + // 每次更新,需要更新单元格的高度,防止同步高度的时候没有更新 + let styleByRow = index === content.length - 1 ? { height: 'initial' } : { ...rowStyle, height: 'initial' }; + + if (colName === 'key') { + row.innerHTML = formatContent(entry.key); + if (entry.keyStyle) { + styleByRow = { ...styleByRow, ...getTextStyle(entry.keyStyle) }; + } + } else if (colName === 'value') { + row.innerHTML = formatContent(entry.value); + if (entry.valueStyle) { + styleByRow = { ...styleByRow, ...getTextStyle(entry.valueStyle) }; + } + } else if (colName === 'shape') { + row.innerHTML = getSvgHtml(entry); + } + + setStyleToDom(row, styleByRow); + }); + } + }); + } else if (contentDom && !hasContent) { + contentDom.parentNode.removeChild(contentDom); + } } protected _updateDomStyle(sizeKey: 'width' | 'height' = 'width') { const rootDom = this._rootDom; - if (rootDom) { - const contentDom = rootDom.children[rootDom.children.length - 1]; - - if (contentDom.className.includes(TOOLTIP_CONTENT_BOX_CLASS_NAME)) { - const tooltipSpec = this._component.getSpec() as ITooltipSpec; - const contentStyle: Partial = {}; - - if (isValid(tooltipSpec?.style?.maxContentHeight)) { - const titleDom = rootDom.children[0]; - const titleHeight = - titleDom && titleDom.className.includes(TOOLTIP_TITLE_CLASS_NAME) - ? titleDom.getBoundingClientRect().height + (tooltipSpec.style.spaceRow ?? 0) - : 0; - const viewRect = (this._chartOption as any).getChartViewRect(); - const maxHeight = calcLayoutNumber( - tooltipSpec.style.maxContentHeight, - Math.min(viewRect.height, document.body.clientHeight) - - titleHeight - - (this._domStyle.panelPadding ? this._domStyle.panelPadding[0] + this._domStyle.panelPadding[1] : 0) - ); - - if (maxHeight > 0) { - contentStyle.maxHeight = `${maxHeight}px`; - contentStyle.overflowY = 'auto'; - // todo 让内容宽度往外阔一点,给滚动条留出位置 - contentStyle.width = `calc(100% + ${ - this._domStyle.panelPadding ? this._domStyle.panelPadding[1] + 'px' : '10px' - })`; - - setStyleToDom(contentDom as HTMLElement, contentStyle); - } + const contentDom = rootDom.children[rootDom.children.length - 1]; + + if (contentDom.className.includes(TOOLTIP_CONTENT_BOX_CLASS_NAME)) { + const tooltipSpec = this._component.getSpec() as ITooltipSpec; + const contentStyle: Partial = {}; + + if (isValid(tooltipSpec?.style?.maxContentHeight)) { + const titleDom = rootDom.children[0]; + const titleHeight = + titleDom && titleDom.className.includes(TOOLTIP_TITLE_CLASS_NAME) + ? titleDom.getBoundingClientRect().height + (tooltipSpec.style.spaceRow ?? 0) + : 0; + const viewRect = (this._chartOption as any).getChartViewRect(); + const maxHeight = calcLayoutNumber( + tooltipSpec.style.maxContentHeight, + Math.min(viewRect.height, document.body.clientHeight) - + titleHeight - + (this._domStyle.panelPadding ? this._domStyle.panelPadding[0] + this._domStyle.panelPadding[1] : 0) + ); + + if (maxHeight > 0) { + contentStyle.maxHeight = `${maxHeight}px`; + contentStyle.overflowY = 'auto'; + // todo 让内容宽度往外阔一点,给滚动条留出位置 + contentStyle.width = `calc(100% + ${ + this._domStyle.panelPadding ? this._domStyle.panelPadding[1] + 'px' : '10px' + })`; + + setStyleToDom(contentDom as HTMLElement, contentStyle); } + } - const rows = contentDom.children; - const widthByCol: number[] = []; - if (rows) { - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - const cols = row.children ?? ([] as HTMLElement[]); - - for (let j = 0; j < cols.length; j++) { - const width = cols[j].getBoundingClientRect()[sizeKey]; - if (widthByCol[j] === undefined || widthByCol[j] < width) { - widthByCol[j] = width; - } + const rows = contentDom.children; + const widthByCol: number[] = []; + if (rows) { + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const cols = row.children ?? ([] as HTMLElement[]); + + for (let j = 0; j < cols.length; j++) { + const width = cols[j].getBoundingClientRect()[sizeKey]; + if (widthByCol[j] === undefined || widthByCol[j] < width) { + widthByCol[j] = width; } } + } - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - const cols = row.children ?? ([] as HTMLElement[]); + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const cols = row.children ?? ([] as HTMLElement[]); - for (let j = 0; j < cols.length; j++) { - (cols[j] as HTMLElement).style[sizeKey] = `${widthByCol[j]}px`; - } + for (let j = 0; j < cols.length; j++) { + (cols[j] as HTMLElement).style[sizeKey] = `${widthByCol[j]}px`; } } } diff --git a/packages/vchart/src/plugin/components/tooltip-handler/utils/style.ts b/packages/vchart/src/plugin/components/tooltip-handler/utils/style.ts index 868d67558f..c0380aab3d 100644 --- a/packages/vchart/src/plugin/components/tooltip-handler/utils/style.ts +++ b/packages/vchart/src/plugin/components/tooltip-handler/utils/style.ts @@ -1,4 +1,4 @@ -import { isArray, isValid, isValidNumber, lowerCamelCaseToMiddle, normalizePadding } from '@visactor/vutils'; +import { isArray, isValid, isValidNumber, normalizePadding } from '@visactor/vutils'; import type { ITooltipSpec, ITooltipTextTheme, ITooltipTheme } from '../../../../component/tooltip'; const DEFAULT_SHAPE_SPACING = 8; const DEFAULT_KEY_SPACING = 26; @@ -15,16 +15,35 @@ export const getPixelPropertyStr = (num?: number | number[], defaultStr?: string }; export const getTextStyle = (style: ITooltipTextTheme = {}) => { - const textStyle: Partial = { - color: style.fill ?? style.fontColor, - fontFamily: style.fontFamily, - fontSize: getPixelPropertyStr(style.fontSize as number), - fontWeight: style.fontWeight as string, - textAlign: style.textAlign, - maxWidth: getPixelPropertyStr(style.maxWidth), - whiteSpace: style.multiLine ? 'initial' : 'nowrap', - wordBreak: style.multiLine ? style.wordBreak ?? 'break-word' : 'normal' - }; + const textStyle: Partial = {}; + + if (isValid(style.fontFamily)) { + textStyle.fontFamily = style.fontFamily; + } + const color = style.fill ?? style.fontColor; + + if (isValid(color)) { + textStyle.color = color; + } + if (isValid(style.fontWeight)) { + textStyle.fontWeight = style.fontWeight as string; + } + if (isValid(style.textAlign)) { + textStyle.textAlign = style.textAlign as string; + } + if (isValid(style.fontSize)) { + textStyle.fontSize = getPixelPropertyStr(style.fontSize as number); + } + if (isValid(style.maxWidth)) { + textStyle.maxWidth = getPixelPropertyStr(style.maxWidth as number); + } + if (style.multiLine) { + textStyle.whiteSpace = 'initial'; + textStyle.wordBreak = style.wordBreak ?? 'break-word'; + } else { + textStyle.wordBreak = 'normal'; + textStyle.whiteSpace = 'nowrap'; + } return textStyle; }; @@ -131,16 +150,3 @@ export function setStyleToDom(dom: HTMLElement, style: Partial) { - let str = ''; - - style && - Object.keys(style).forEach(k => { - if (isValid((style as any)[k])) { - str += `${lowerCamelCaseToMiddle(k)}:${(style as any)[k]};`; - } - }); - - return str; -}