@@ -59,6 +59,7 @@ import {
5959} from './labelLayoutHelper' ;
6060import { labelInner , animateLabelValue } from './labelStyle' ;
6161import { normalizeRadian } from 'zrender/src/contain/util' ;
62+ import { throttle } from '../util/throttle' ;
6263
6364interface LabelDesc {
6465 label : ZRText
@@ -194,16 +195,153 @@ function extendWithKeys(target: Dictionary<any>, source: Dictionary<any>, keys:
194195
195196const 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+
197333class 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 /**
0 commit comments