Skip to content

Commit 9864dee

Browse files
committed
fix(datalabel): hideOverlap not work on emphasis state
1 parent 0919819 commit 9864dee

File tree

3 files changed

+348
-13
lines changed

3 files changed

+348
-13
lines changed

src/label/LabelManager.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
} from './labelLayoutHelper';
6060
import { labelInner, animateLabelValue } from './labelStyle';
6161
import { normalizeRadian } from 'zrender/src/contain/util';
62+
import { throttle } from '../util/throttle';
6263

6364
interface LabelDesc {
6465
label: ZRText
@@ -194,16 +195,153 @@ function extendWithKeys(target: Dictionary<any>, source: Dictionary<any>, keys:
194195

195196
const LABEL_LAYOUT_PROPS = ['x', 'y', 'rotation'];
196197

198+
/**
199+
* Emphasis manager for handling label emphasis state changes
200+
*/
201+
class EmphasisManager {
202+
// eslint-disable-next-line no-undef
203+
private currentEmphasisLabels: Set<Element> = new Set();
204+
private labelsNeedsHideOverlap: LabelLayoutWithGeometry[] = [];
205+
// eslint-disable-next-line no-undef
206+
private labelsNeedsHideOverlapSet: Set<Element> = new Set();
207+
// eslint-disable-next-line no-undef
208+
private originalStates: Map<Element, boolean> = new Map();
209+
210+
setLabelsNeedsHideOverlap(labels: LabelLayoutWithGeometry[]): void {
211+
this.clear();
212+
if (labels.length === 0) {
213+
return;
214+
}
215+
216+
this.labelsNeedsHideOverlap = labels;
217+
labels.forEach(item => {
218+
this.labelsNeedsHideOverlapSet.add(item.label);
219+
});
220+
221+
// Record original ignore states only when needed
222+
labels.forEach(item => {
223+
this.originalStates.set(item.label, item.label.ignore);
224+
if (item.labelLine) {
225+
this.originalStates.set(item.labelLine, item.labelLine.ignore);
226+
}
227+
});
228+
}
229+
230+
handleEmphasisChange(targetLabel: Element, isEnteringEmphasis: boolean): void {
231+
// Early return if no labels need hideOverlap processing
232+
if (this.labelsNeedsHideOverlap.length === 0) {
233+
return;
234+
}
235+
// Only respond to labels that participates in hideOverlap.
236+
if (!this.labelsNeedsHideOverlapSet.has(targetLabel)) {
237+
return;
238+
}
239+
240+
if (isEnteringEmphasis) {
241+
this.currentEmphasisLabels.add(targetLabel);
242+
}
243+
else {
244+
this.currentEmphasisLabels.delete(targetLabel);
245+
}
246+
247+
if (this.currentEmphasisLabels.size === 0) {
248+
// No emphasis labels, restore original state
249+
this.restoreOriginalState();
250+
}
251+
else {
252+
// Re-sort with emphasis labels first and call hideOverlap
253+
this.reorderAndHideOverlap();
254+
}
255+
}
256+
257+
private reorderAndHideOverlap = throttle(() => {
258+
if (this.labelsNeedsHideOverlap.length === 0) {
259+
return;
260+
}
261+
262+
// Create a copy for reordering
263+
const reorderedLabels = [...this.labelsNeedsHideOverlap];
264+
265+
// Sort: emphasis labels first, then by original priority
266+
reorderedLabels.sort((a, b) => {
267+
const aIsEmphasis = this.currentEmphasisLabels.has(a.label) ? 1 : 0;
268+
const bIsEmphasis = this.currentEmphasisLabels.has(b.label) ? 1 : 0;
269+
270+
// Emphasis labels come first
271+
if (aIsEmphasis !== bIsEmphasis) {
272+
return bIsEmphasis - aIsEmphasis;
273+
}
274+
275+
// Then by original priority
276+
return ((b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0))
277+
|| (b.priority - a.priority);
278+
});
279+
280+
// First restore all to show state
281+
reorderedLabels.forEach(item => {
282+
item.label.ignore = false;
283+
const emphasisState = item.label.ensureState('emphasis');
284+
emphasisState.ignore = false;
285+
286+
if (item.labelLine) {
287+
item.labelLine.ignore = false;
288+
const lineEmphasisState = item.labelLine.ensureState('emphasis');
289+
lineEmphasisState.ignore = false;
290+
}
291+
});
292+
293+
// Call hideOverlap with isOrdered = true
294+
hideOverlap(reorderedLabels, true);
295+
}, 16, true);
296+
297+
private restoreOriginalState = throttle(() => {
298+
this.labelsNeedsHideOverlap.forEach(item => {
299+
const originalIgnore = this.originalStates.get(item.label) ?? false;
300+
item.label.ignore = originalIgnore;
301+
302+
// For emphasis state, use the original hideOverlap logic
303+
const emphasisState = item.label.ensureState('emphasis');
304+
emphasisState.ignore = originalIgnore;
305+
306+
if (item.labelLine) {
307+
const originalLineIgnore = this.originalStates.get(item.labelLine) ?? false;
308+
item.labelLine.ignore = originalLineIgnore;
309+
310+
const lineEmphasisState = item.labelLine.ensureState('emphasis');
311+
lineEmphasisState.ignore = originalLineIgnore;
312+
}
313+
});
314+
}, 16, true);
315+
316+
clear(): void {
317+
// Cancel pending throttled tasks to avoid running with stale label references.
318+
this.reorderAndHideOverlap.clear?.();
319+
this.restoreOriginalState.clear?.();
320+
321+
this.currentEmphasisLabels.clear();
322+
this.labelsNeedsHideOverlap = [];
323+
this.labelsNeedsHideOverlapSet.clear();
324+
this.originalStates.clear();
325+
}
326+
}
327+
328+
const hoverStateChangeStore = makeInner<{
329+
originalOnHoverStateChange: ECElement['onHoverStateChange'];
330+
wrapper: ECElement['onHoverStateChange'];
331+
}, ECElement>();
332+
197333
class LabelManager {
198334

199335
private _labelList: LabelDesc[] = [];
200336
private _chartViewList: ChartView[] = [];
337+
private _emphasisManager: EmphasisManager = new EmphasisManager();
201338

202339
constructor() {}
203340

204341
clearLabels() {
205342
this._labelList = [];
206343
this._chartViewList = [];
344+
this._emphasisManager.clear();
207345
}
208346

209347
/**
@@ -323,6 +461,36 @@ class LabelManager {
323461
// Can only attach the text on the element with dataIndex
324462
if (textEl && !(textEl as ECElement).disableLabelLayout) {
325463
this._addLabel(ecData.dataIndex, ecData.dataType, seriesModel, textEl, layoutOption);
464+
// Add emphasis state change listener for hideOverlap labels.
465+
// Avoid repeated wrapping and avoid capturing `textEl` in closure (it may be replaced on rerender).
466+
const hostEl = child as ECElement;
467+
const store = hoverStateChangeStore(hostEl);
468+
if (!store.wrapper) {
469+
const labelManager = this;
470+
store.wrapper = function (this: ECElement, toState: string) {
471+
const original = store.originalOnHoverStateChange;
472+
original && original.call(this, toState);
473+
474+
if (toState === 'emphasis' || toState === 'normal') {
475+
const labelEl = this.getTextContent();
476+
if (labelEl) {
477+
labelManager._emphasisManager.handleEmphasisChange(
478+
labelEl,
479+
toState === 'emphasis'
480+
);
481+
}
482+
}
483+
};
484+
}
485+
486+
// If labelLayout is a callback, hideOverlap might be returned dynamically.
487+
// Install the hook in that case as well. EmphasisManager will ignore irrelevant labels.
488+
const shouldInstall = isFunction(layoutOption) || (layoutOption as LabelLayoutOption).hideOverlap;
489+
if (shouldInstall && hostEl.onHoverStateChange !== store.wrapper) {
490+
// Keep original handler up-to-date but never nest wrappers.
491+
store.originalOnHoverStateChange = hostEl.onHoverStateChange;
492+
hostEl.onHoverStateChange = store.wrapper;
493+
}
326494
}
327495
});
328496
}
@@ -466,6 +634,7 @@ class LabelManager {
466634

467635
restoreIgnore(labelsNeedsHideOverlap);
468636
hideOverlap(labelsNeedsHideOverlap);
637+
this._emphasisManager.setLabelsNeedsHideOverlap(labelsNeedsHideOverlap);
469638
}
470639

471640
/**

src/label/labelLayoutHelper.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -519,25 +519,22 @@ export function restoreIgnore(labelList: LabelLayoutData[]): void {
519519
* PENDING: although currently this method is effectively called in other states in `updateLabelLayout` case,
520520
* the bad case is not noticeable in the zooming scenario.
521521
*/
522-
export function hideOverlap(labelList: LabelLayoutData[]): void {
522+
export function hideOverlap(labelList: LabelLayoutData[], isOrdered?: boolean): void {
523523
const displayedLabels: LabelLayoutWithGeometry[] = [];
524524

525525
// TODO, render overflow visible first, put in the displayedLabels.
526-
labelList.sort(function (a, b) {
527-
return ((b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0))
528-
|| (b.priority - a.priority);
529-
});
526+
if (!isOrdered) {
527+
labelList.sort(function (a, b) {
528+
return ((b.suggestIgnore ? 1 : 0) - (a.suggestIgnore ? 1 : 0))
529+
|| (b.priority - a.priority);
530+
});
531+
}
530532

531533
function hideEl(el: Element) {
532-
if (!el.ignore) {
533-
// Show on emphasis.
534-
const emphasisState = el.ensureState('emphasis');
535-
if (emphasisState.ignore == null) {
536-
emphasisState.ignore = false;
537-
}
538-
}
539-
540534
el.ignore = true;
535+
// Also hide in emphasis state
536+
const emphasisState = el.ensureState('emphasis');
537+
emphasisState.ignore = true;
541538
}
542539

543540
for (let i = 0; i < labelList.length; i++) {

0 commit comments

Comments
 (0)