From 9864deee64b6bbd8686447f95dc395405b81ec13 Mon Sep 17 00:00:00 2001 From: Justin-ZS Date: Thu, 28 Aug 2025 11:30:54 +0800 Subject: [PATCH] fix(datalabel): hideOverlap not work on emphasis state --- src/label/LabelManager.ts | 169 ++++++++++++++++++++++++ src/label/labelLayoutHelper.ts | 23 ++-- test/test-hideoverlap-emphasis-fix.html | 169 ++++++++++++++++++++++++ 3 files changed, 348 insertions(+), 13 deletions(-) create mode 100644 test/test-hideoverlap-emphasis-fix.html diff --git a/src/label/LabelManager.ts b/src/label/LabelManager.ts index b60a9ac2b2..777e8a5865 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,16 +195,153 @@ 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 labelsNeedsHideOverlapSet: Set = new Set(); + // 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; + labels.forEach(item => { + this.labelsNeedsHideOverlapSet.add(item.label); + }); + + // 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; + } + // Only respond to labels that participates in hideOverlap. + if (!this.labelsNeedsHideOverlapSet.has(targetLabel)) { + 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 { + // Cancel pending throttled tasks to avoid running with stale label references. + this.reorderAndHideOverlap.clear?.(); + this.restoreOriginalState.clear?.(); + + this.currentEmphasisLabels.clear(); + this.labelsNeedsHideOverlap = []; + this.labelsNeedsHideOverlapSet.clear(); + this.originalStates.clear(); + } +} + +const hoverStateChangeStore = makeInner<{ + originalOnHoverStateChange: ECElement['onHoverStateChange']; + wrapper: ECElement['onHoverStateChange']; +}, ECElement>(); + class LabelManager { private _labelList: LabelDesc[] = []; private _chartViewList: ChartView[] = []; + private _emphasisManager: EmphasisManager = new EmphasisManager(); constructor() {} clearLabels() { this._labelList = []; this._chartViewList = []; + this._emphasisManager.clear(); } /** @@ -323,6 +461,36 @@ 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. + // Avoid repeated wrapping and avoid capturing `textEl` in closure (it may be replaced on rerender). + const hostEl = child as ECElement; + const store = hoverStateChangeStore(hostEl); + if (!store.wrapper) { + const labelManager = this; + store.wrapper = function (this: ECElement, toState: string) { + const original = store.originalOnHoverStateChange; + original && original.call(this, toState); + + if (toState === 'emphasis' || toState === 'normal') { + const labelEl = this.getTextContent(); + if (labelEl) { + labelManager._emphasisManager.handleEmphasisChange( + labelEl, + toState === 'emphasis' + ); + } + } + }; + } + + // If labelLayout is a callback, hideOverlap might be returned dynamically. + // Install the hook in that case as well. EmphasisManager will ignore irrelevant labels. + const shouldInstall = isFunction(layoutOption) || (layoutOption as LabelLayoutOption).hideOverlap; + if (shouldInstall && hostEl.onHoverStateChange !== store.wrapper) { + // Keep original handler up-to-date but never nest wrappers. + store.originalOnHoverStateChange = hostEl.onHoverStateChange; + hostEl.onHoverStateChange = store.wrapper; + } } }); } @@ -466,6 +634,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..b2080b4952 --- /dev/null +++ b/test/test-hideoverlap-emphasis-fix.html @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + +
+
+ + + +