diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index b60a9ac2b2..071da676c3 100644 --- a/src/label/LabelManager.ts +++ b/src/label/LabelManager.ts @@ -59,6 +59,7 @@ import { } from './labelLayoutHelper'; import { labelInner, animateLabelValue } from './labelStyle'; import { normalizeRadian } from 'zrender/src/contain/util'; +import { throttle } from '../util/throttle'; interface LabelDesc { label: ZRText @@ -194,10 +195,127 @@ function extendWithKeys(target: Dictionary, source: Dictionary, keys: const LABEL_LAYOUT_PROPS = ['x', 'y', 'rotation']; +/** + * Emphasis manager for handling label emphasis state changes + */ +class EmphasisManager { + // eslint-disable-next-line no-undef + private currentEmphasisLabels: Set = new Set(); + private labelsNeedsHideOverlap: LabelLayoutWithGeometry[] = []; + // eslint-disable-next-line no-undef + private originalStates: Map = new Map(); + + setLabelsNeedsHideOverlap(labels: LabelLayoutWithGeometry[]): void { + this.clear(); + if (labels.length === 0) { + return; + } + + this.labelsNeedsHideOverlap = labels; + + // Record original ignore states only when needed + labels.forEach(item => { + this.originalStates.set(item.label, item.label.ignore); + if (item.labelLine) { + this.originalStates.set(item.labelLine, item.labelLine.ignore); + } + }); + } + + handleEmphasisChange(targetLabel: Element, isEnteringEmphasis: boolean): void { + // Early return if no labels need hideOverlap processing + if (this.labelsNeedsHideOverlap.length === 0) { + return; + } + + if (isEnteringEmphasis) { + this.currentEmphasisLabels.add(targetLabel); + } + else { + this.currentEmphasisLabels.delete(targetLabel); + } + + if (this.currentEmphasisLabels.size === 0) { + // No emphasis labels, restore original state + this.restoreOriginalState(); + } + else { + // Re-sort with emphasis labels first and call hideOverlap + this.reorderAndHideOverlap(); + } + } + + private reorderAndHideOverlap = throttle(() => { + if (this.labelsNeedsHideOverlap.length === 0) { + return; + } + + // Create a copy for reordering + const reorderedLabels = [...this.labelsNeedsHideOverlap]; + + // Sort: emphasis labels first, then by original priority + reorderedLabels.sort((a, b) => { + const aIsEmphasis = this.currentEmphasisLabels.has(a.label) ? 1 : 0; + const bIsEmphasis = this.currentEmphasisLabels.has(b.label) ? 1 : 0; + + // Emphasis labels come first + if (aIsEmphasis !== bIsEmphasis) { + return bIsEmphasis - aIsEmphasis; + } + + // Then by original priority + return ((b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0)) + || (b.priority - a.priority); + }); + + // First restore all to show state + reorderedLabels.forEach(item => { + item.label.ignore = false; + const emphasisState = item.label.ensureState('emphasis'); + emphasisState.ignore = false; + + if (item.labelLine) { + item.labelLine.ignore = false; + const lineEmphasisState = item.labelLine.ensureState('emphasis'); + lineEmphasisState.ignore = false; + } + }); + + // Call hideOverlap with isOrdered = true + hideOverlap(reorderedLabels, true); + }, 16, true); + + private restoreOriginalState = throttle(() => { + this.labelsNeedsHideOverlap.forEach(item => { + const originalIgnore = this.originalStates.get(item.label) ?? false; + item.label.ignore = originalIgnore; + + // For emphasis state, use the original hideOverlap logic + const emphasisState = item.label.ensureState('emphasis'); + emphasisState.ignore = originalIgnore; + + if (item.labelLine) { + const originalLineIgnore = this.originalStates.get(item.labelLine) ?? false; + item.labelLine.ignore = originalLineIgnore; + + const lineEmphasisState = item.labelLine.ensureState('emphasis'); + lineEmphasisState.ignore = originalLineIgnore; + } + }); + }, 16, true); + + clear(): void { + this.currentEmphasisLabels.clear(); + this.labelsNeedsHideOverlap = []; + this.originalStates.clear(); + } +} + class LabelManager { private _labelList: LabelDesc[] = []; private _chartViewList: ChartView[] = []; + private _emphasisManager: EmphasisManager = new EmphasisManager(); constructor() {} @@ -323,6 +441,32 @@ class LabelManager { // Can only attach the text on the element with dataIndex if (textEl && !(textEl as ECElement).disableLabelLayout) { this._addLabel(ecData.dataIndex, ecData.dataType, seriesModel, textEl, layoutOption); + // Add emphasis state change listener for hideOverlap labels + const resolvedLayoutOption = isFunction(layoutOption) ? null : layoutOption; + if (resolvedLayoutOption && resolvedLayoutOption.hideOverlap) { + const hostEl = child as ECElement; + const originalOnHoverStateChange = hostEl.onHoverStateChange; + const labelManager = this; + + hostEl.onHoverStateChange = function (toState: string) { + // Call original handler first + if (originalOnHoverStateChange) { + originalOnHoverStateChange.call(this, toState); + } + + // Handle emphasis state change for hideOverlap labels + if (toState === 'emphasis' || toState === 'normal') { + // Find the label element - could be textEl or child itself + const labelElement = textEl || this; + + // Use EmphasisManager to handle the state change + labelManager._emphasisManager.handleEmphasisChange( + labelElement, + toState === 'emphasis' + ); + } + }; + } } }); } @@ -466,6 +610,7 @@ class LabelManager { restoreIgnore(labelsNeedsHideOverlap); hideOverlap(labelsNeedsHideOverlap); + this._emphasisManager.setLabelsNeedsHideOverlap(labelsNeedsHideOverlap); } /** diff --git a/src/label/labelLayoutHelper.ts b/src/label/labelLayoutHelper.ts index 1136bb63d8..4dfcc2d547 100644 --- a/src/label/labelLayoutHelper.ts +++ b/src/label/labelLayoutHelper.ts @@ -519,25 +519,22 @@ export function restoreIgnore(labelList: LabelLayoutData[]): void { * PENDING: although currently this method is effectively called in other states in `updateLabelLayout` case, * the bad case is not noticeable in the zooming scenario. */ -export function hideOverlap(labelList: LabelLayoutData[]): void { +export function hideOverlap(labelList: LabelLayoutData[], isOrdered?: boolean): void { const displayedLabels: LabelLayoutWithGeometry[] = []; // TODO, render overflow visible first, put in the displayedLabels. - labelList.sort(function (a, b) { - return ((b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0)) - || (b.priority - a.priority); - }); + if (!isOrdered) { + labelList.sort(function (a, b) { + return ((b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0)) + || (b.priority - a.priority); + }); + } function hideEl(el: Element) { - if (!el.ignore) { - // Show on emphasis. - const emphasisState = el.ensureState('emphasis'); - if (emphasisState.ignore == null) { - emphasisState.ignore = false; - } - } - el.ignore = true; + // Also hide in emphasis state + const emphasisState = el.ensureState('emphasis'); + emphasisState.ignore = true; } for (let i = 0; i < labelList.length; i++) { diff --git a/test/test-hideoverlap-emphasis-fix.html b/test/test-hideoverlap-emphasis-fix.html new file mode 100644 index 0000000000..4671ea9ee7 --- /dev/null +++ b/test/test-hideoverlap-emphasis-fix.html @@ -0,0 +1,232 @@ + + + + + + + + + + + + + Test: HideOverlap with AxisPointer TriggerEmphasis Fix + + + +
+

Test: HideOverlap with AxisPointer TriggerEmphasis Fix

+
+ +
+ 测试说明: +
    +
  • 这个测试验证了 GitHub issue #20744 的修复效果
  • +
  • 问题:axisPointer triggerEmphasis 时 labelLayout.hideOverlap 不生效
  • +
  • 将鼠标悬停在不同的轴刻度上,观察标签重叠处理是否正确
  • +
  • 期望结果:emphasis 时 hideOverlap 依旧生效,且优先显示当前 hover 图形的标签
  • +
+
+ +
+ +
+ 测试步骤: +
    +
  1. 将鼠标悬停在不同的 x 轴刻度上(特别是 'Tue' 位置)
  2. +
  3. 观察标签是否正确处理重叠
  4. +
  5. 检查 emphasis 状态下的标签是否优先显示
  6. +
  7. 打开浏览器控制台查看事件日志
  8. +
+ 期望行为: +
    +
  • 正常状态下,重叠的标签会被隐藏
  • +
  • 当 axisPointer triggerEmphasis 时,当前 hover 的数据点标签应该优先显示
  • +
  • 其他重叠的标签仍然会被正确隐藏
  • +
+
+ + + +