@@ -26,7 +26,7 @@ ChartJS.register(
2626 zoomPlugin
2727) ;
2828
29- const ChartWrapper = ( { data, options, chartId, onRegisterChart, onSyncHover } ) => {
29+ const ChartWrapper = ( { data, options, chartId, onRegisterChart, onSyncHover, syncRef } ) => {
3030 const chartRef = useRef ( null ) ;
3131
3232 const handleChartRef = useCallback ( ( ref ) => {
@@ -38,17 +38,38 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
3838
3939 const enhancedOptions = {
4040 ...options ,
41- onHover : ( event , activeElements ) => {
42- if ( activeElements . length > 0 && chartRef . current ) {
43- const { datasetIndex, index } = activeElements [ 0 ] ;
41+ onHover : ( event , activeElements ) => {
42+ if ( syncRef ?. current ) return ;
43+ if ( activeElements . length > 0 && chartRef . current ) {
44+ // 找到距离鼠标最近的数据点
45+ let closestElement = activeElements [ 0 ] ;
46+ let minDistance = Infinity ;
47+
48+ const canvasRect = chartRef . current . canvas . getBoundingClientRect ( ) ;
49+ const mouseX = event . native ? event . native . clientX - canvasRect . left : event . x ;
50+
51+ activeElements . forEach ( element => {
52+ const { datasetIndex, index } = element ;
4453 const dataset = chartRef . current . data . datasets [ datasetIndex ] ;
4554 const point = dataset . data [ index ] ;
46- const step = point . x ;
47- onSyncHover ( step , chartId ) ;
48- } else {
49- onSyncHover ( null , chartId ) ;
50- }
51- } ,
55+ const pixelX = chartRef . current . scales . x . getPixelForValue ( point . x ) ;
56+ const distance = Math . abs ( mouseX - pixelX ) ;
57+
58+ if ( distance < minDistance ) {
59+ minDistance = distance ;
60+ closestElement = element ;
61+ }
62+ } ) ;
63+
64+ const { datasetIndex, index } = closestElement ;
65+ const dataset = chartRef . current . data . datasets [ datasetIndex ] ;
66+ const point = dataset . data [ index ] ;
67+ const step = point . x ;
68+ onSyncHover ( step , chartId ) ;
69+ } else {
70+ onSyncHover ( null , chartId ) ;
71+ }
72+ } ,
5273 events : [ 'mousemove' , 'mouseout' , 'click' , 'touchstart' , 'touchmove' ] ,
5374 } ;
5475
@@ -74,30 +95,62 @@ export default function ChartContainer({
7495 onMaxStepChange
7596} ) {
7697 const chartRefs = useRef ( new Map ( ) ) ;
98+ const syncLockRef = useRef ( false ) ;
7799 const registerChart = useCallback ( ( id , inst ) => {
78100 chartRefs . current . set ( id , inst ) ;
79101 } , [ ] ) ;
80102
81103 const syncHoverToAllCharts = useCallback ( ( step , sourceId ) => {
104+ if ( syncLockRef . current ) return ;
105+ syncLockRef . current = true ;
82106 chartRefs . current . forEach ( ( chart , id ) => {
83- if ( ! chart ) return ;
107+ if ( ! chart || ! chart . data || ! chart . data . datasets ) return ;
84108 if ( step === null ) {
85109 chart . setActiveElements ( [ ] ) ;
86- chart . tooltip . setActiveElements ( [ ] ) ;
87- chart . update ( 'none' ) ;
110+ chart . tooltip . setActiveElements ( [ ] , { x : 0 , y : 0 } ) ;
111+ chart . draw ( ) ;
88112 } else if ( id !== sourceId ) {
89113 const activeElements = [ ] ;
114+ const seen = new Set ( ) ; // 防止重复添加相同的数据点
90115 chart . data . datasets . forEach ( ( dataset , datasetIndex ) => {
91- const idx = dataset . data . findIndex ( p => p . x === step ) ;
92- if ( idx !== - 1 ) {
93- activeElements . push ( { datasetIndex, index : idx } ) ;
116+ if ( ! dataset || ! dataset . data || ! Array . isArray ( dataset . data ) ) return ;
117+ const idx = dataset . data . findIndex ( p => p && typeof p . x !== 'undefined' && p . x === step ) ;
118+ if ( idx !== - 1 && dataset . data [ idx ] ) {
119+ const elementKey = `${ datasetIndex } -${ idx } ` ;
120+ if ( ! seen . has ( elementKey ) ) {
121+ // 验证元素的有效性
122+ if ( datasetIndex >= 0 && datasetIndex < chart . data . datasets . length &&
123+ idx >= 0 && idx < dataset . data . length ) {
124+ activeElements . push ( { datasetIndex, index : idx } ) ;
125+ seen . add ( elementKey ) ;
126+ }
127+ }
94128 }
95129 } ) ;
96- chart . setActiveElements ( activeElements ) ;
97- chart . tooltip . setActiveElements ( activeElements , { x : 0 , y : 0 } ) ;
98- chart . update ( 'none' ) ;
130+
131+ // 只有当activeElements不为空且所有元素都有效时才设置
132+ if ( activeElements . length > 0 ) {
133+ try {
134+ const pos = { x : chart . scales . x . getPixelForValue ( step ) , y : 0 } ;
135+ chart . setActiveElements ( activeElements ) ;
136+ chart . tooltip . setActiveElements ( activeElements , pos ) ;
137+ chart . draw ( ) ;
138+ } catch ( error ) {
139+ console . warn ( 'Error setting active elements:' , error ) ;
140+ // 如果出错,清除所有activeElements
141+ chart . setActiveElements ( [ ] ) ;
142+ chart . tooltip . setActiveElements ( [ ] , { x : 0 , y : 0 } ) ;
143+ chart . draw ( ) ;
144+ }
145+ } else {
146+ // 如果没有找到有效的activeElements,清除当前的
147+ chart . setActiveElements ( [ ] ) ;
148+ chart . tooltip . setActiveElements ( [ ] , { x : 0 , y : 0 } ) ;
149+ chart . draw ( ) ;
150+ }
99151 }
100152 } ) ;
153+ syncLockRef . current = false ;
101154 } , [ ] ) ;
102155
103156 const parsedData = useMemo ( ( ) => {
@@ -206,30 +259,41 @@ export default function ChartContainer({
206259 } , [ parsedData , onXRangeChange ] ) ;
207260
208261 const colors = [ '#ef4444' , '#3b82f6' , '#10b981' , '#f59e0b' , '#8b5cf6' , '#f97316' ] ;
209- const createChartData = dataArray => ( {
210- datasets : dataArray . map ( ( item , index ) => {
211- const color = colors [ index % colors . length ] ;
212- return {
213- label : item . name ?. replace ( / \. ( l o g | t x t ) $ / i, '' ) || `File ${ index + 1 } ` ,
214- data : item . data ,
215- borderColor : color ,
216- backgroundColor : `${ color } 33` ,
217- borderWidth : 2 ,
218- fill : false ,
219- tension : 0 ,
220- pointRadius : 0 ,
221- pointHoverRadius : 4 ,
222- pointBackgroundColor : color ,
223- pointBorderColor : color ,
224- pointBorderWidth : 1 ,
225- pointHoverBackgroundColor : color ,
226- pointHoverBorderColor : color ,
227- pointHoverBorderWidth : 1 ,
228- animation : false ,
229- animations : { colors : false , x : false , y : false } ,
230- } ;
231- } )
232- } ) ;
262+ const createChartData = dataArray => {
263+ // 确保没有重复的 datasets
264+ const uniqueItems = dataArray . reduce ( ( acc , item ) => {
265+ const exists = acc . find ( existing => existing . name === item . name ) ;
266+ if ( ! exists ) {
267+ acc . push ( item ) ;
268+ }
269+ return acc ;
270+ } , [ ] ) ;
271+
272+ return {
273+ datasets : uniqueItems . map ( ( item , index ) => {
274+ const color = colors [ index % colors . length ] ;
275+ return {
276+ label : item . name ?. replace ( / \. ( l o g | t x t ) $ / i, '' ) || `File ${ index + 1 } ` ,
277+ data : item . data ,
278+ borderColor : color ,
279+ backgroundColor : `${ color } 33` ,
280+ borderWidth : 2 ,
281+ fill : false ,
282+ tension : 0 ,
283+ pointRadius : 0 ,
284+ pointHoverRadius : 4 ,
285+ pointBackgroundColor : color ,
286+ pointBorderColor : color ,
287+ pointBorderWidth : 1 ,
288+ pointHoverBackgroundColor : color ,
289+ pointHoverBorderColor : color ,
290+ pointHoverBorderWidth : 1 ,
291+ animation : false ,
292+ animations : { colors : false , x : false , y : false } ,
293+ } ;
294+ } )
295+ } ;
296+ } ;
233297
234298 const getComparisonData = ( data1 , data2 , mode ) => {
235299 const map2 = new Map ( data2 . map ( p => [ p . x , p . y ] ) ) ;
@@ -291,7 +355,7 @@ export default function ChartContainer({
291355 animations : { colors : false , x : false , y : false } ,
292356 hover : { animationDuration : 0 } ,
293357 responsiveAnimationDuration : 0 ,
294- interaction : { mode : 'x ' , intersect : false } ,
358+ interaction : { mode : 'nearest ' , intersect : false , axis : 'x' } ,
295359 plugins : {
296360 zoom : {
297361 pan : {
@@ -340,8 +404,9 @@ export default function ChartContainer({
340404 }
341405 } ,
342406 tooltip : {
343- mode : 'x ' ,
407+ mode : 'nearest ' ,
344408 intersect : false ,
409+ axis : 'x' ,
345410 animation : false ,
346411 backgroundColor : 'rgba(15, 23, 42, 0.92)' ,
347412 titleColor : '#f1f5f9' ,
@@ -364,7 +429,8 @@ export default function ChartContainer({
364429 } ,
365430 label : function ( context ) {
366431 const value = Number ( context . parsed . y . toPrecision ( 4 ) ) ;
367- return ` ${ value } ` ;
432+ const label = context . dataset ?. label || 'Dataset' ;
433+ return ` ${ label } : ${ value } ` ;
368434 } ,
369435 labelColor : function ( context ) {
370436 return {
@@ -548,6 +614,7 @@ export default function ChartContainer({
548614 chartId = { `metric-comp-${ idx } ` }
549615 onRegisterChart = { registerChart }
550616 onSyncHover = { syncHoverToAllCharts }
617+ syncRef = { syncLockRef }
551618 data = { compData }
552619 options = { compOptions }
553620 />
@@ -562,6 +629,7 @@ export default function ChartContainer({
562629 chartId = { `metric-${ idx } ` }
563630 onRegisterChart = { registerChart }
564631 onSyncHover = { syncHoverToAllCharts }
632+ syncRef = { syncLockRef }
565633 data = { createChartData ( dataArray ) }
566634 options = { options }
567635 />
0 commit comments