@@ -67,6 +67,8 @@ export default function ChartContainer({
6767 absoluteBaseline = 0.005 ,
6868 showLoss = true ,
6969 showGradNorm = false ,
70+ xRange = { min : undefined , max : undefined } ,
71+ onXRangeChange,
7072 onMaxStepChange
7173} ) {
7274 const chartRefs = useRef ( new Map ( ) ) ;
@@ -214,6 +216,123 @@ export default function ChartContainer({
214216 return result ;
215217 } ;
216218
219+ const chartOptions = useMemo ( ( ) => ( {
220+ responsive : true ,
221+ maintainAspectRatio : false ,
222+ animation : { duration : 0 } ,
223+ animations : { colors : false , x : false , y : false } ,
224+ hover : { animationDuration : 0 } ,
225+ responsiveAnimationDuration : 0 ,
226+ interaction : { mode : 'index' , intersect : false } ,
227+ plugins : {
228+ zoom : {
229+ pan : {
230+ enabled : true ,
231+ mode : 'x' ,
232+ onPanComplete : ( { chart } ) => {
233+ const { min, max } = chart . scales . x ;
234+ onXRangeChange ( { min : Math . round ( min ) , max : Math . round ( max ) } ) ;
235+ }
236+ } ,
237+ zoom : {
238+ drag : {
239+ enabled : true ,
240+ borderColor : 'rgba(225,225,225,0.2)' ,
241+ borderWidth : 1 ,
242+ backgroundColor : 'rgba(225,225,225,0.2)' ,
243+ modifierKey : 'shift'
244+ } ,
245+ wheel : { enabled : true } ,
246+ pinch : { enabled : true } ,
247+ mode : 'x' ,
248+ onZoomComplete : ( { chart } ) => {
249+ const { min, max } = chart . scales . x ;
250+ onXRangeChange ( { min : Math . round ( min ) , max : Math . round ( max ) } ) ;
251+ }
252+ }
253+ } ,
254+ legend : {
255+ position : 'top' ,
256+ labels : {
257+ boxWidth : 40 ,
258+ boxHeight : 2 ,
259+ padding : 10 ,
260+ usePointStyle : false ,
261+ generateLabels : function ( chart ) {
262+ const original = Chart . defaults . plugins . legend . labels . generateLabels ;
263+ const labels = original . call ( this , chart ) ;
264+ labels . forEach ( ( label , index ) => {
265+ const dataset = chart . data . datasets [ index ] ;
266+ if ( dataset && dataset . borderDash && dataset . borderDash . length > 0 ) {
267+ label . lineDash = dataset . borderDash ;
268+ }
269+ } ) ;
270+ return labels ;
271+ }
272+ }
273+ } ,
274+ tooltip : {
275+ mode : 'index' ,
276+ intersect : false ,
277+ animation : false ,
278+ backgroundColor : 'rgba(15, 23, 42, 0.92)' ,
279+ titleColor : '#f1f5f9' ,
280+ bodyColor : '#cbd5e1' ,
281+ borderColor : 'rgba(71, 85, 105, 0.2)' ,
282+ borderWidth : 1 ,
283+ cornerRadius : 6 ,
284+ displayColors : true ,
285+ usePointStyle : true ,
286+ titleFont : { size : 11 , weight : '600' , family : 'Inter, system-ui, sans-serif' } ,
287+ bodyFont : { size : 10 , weight : '400' , family : 'Inter, system-ui, sans-serif' } ,
288+ footerFont : { size : 9 , weight : '300' } ,
289+ padding : { top : 6 , bottom : 6 , left : 8 , right : 8 } ,
290+ caretPadding : 4 ,
291+ caretSize : 4 ,
292+ multiKeyBackground : 'transparent' ,
293+ callbacks : {
294+ title : function ( context ) {
295+ return `Step ${ context [ 0 ] . parsed . x } ` ;
296+ } ,
297+ label : function ( context ) {
298+ const value = Number ( context . parsed . y . toPrecision ( 4 ) ) ;
299+ return ` ${ value } ` ;
300+ } ,
301+ labelColor : function ( context ) {
302+ return {
303+ borderColor : context . dataset . borderColor ,
304+ backgroundColor : context . dataset . borderColor ,
305+ borderWidth : 1 ,
306+ borderRadius : 2
307+ } ;
308+ }
309+ }
310+ }
311+ } ,
312+ scales : {
313+ x : {
314+ type : 'linear' ,
315+ display : true ,
316+ title : { display : true , text : 'Step' } ,
317+ min : xRange . min ,
318+ max : xRange . max ,
319+ bounds : 'data'
320+ } ,
321+ y : {
322+ type : 'linear' ,
323+ display : true ,
324+ title : { display : true , text : 'Value' } ,
325+ bounds : 'data' ,
326+ ticks : {
327+ callback : function ( value ) {
328+ return Number ( value . toPrecision ( 2 ) ) ;
329+ }
330+ }
331+ }
332+ } ,
333+ elements : { point : { radius : 0 } }
334+ } ) , [ xRange , onXRangeChange ] ) ;
335+
217336 const createComparisonChartData = ( item1 , item2 , title ) => {
218337 const comparisonData = getComparisonData ( item1 . data , item2 . data , compareMode ) ;
219338 const baseline = compareMode === 'relative' ? relativeBaseline : compareMode === 'absolute' ? absoluteBaseline : 0 ;
@@ -299,49 +418,74 @@ export default function ChartContainer({
299418 ) ;
300419 }
301420
421+ const stats = [ ] ;
422+
423+ const metricElements = metricsToShow . map ( ( metric , idx ) => {
424+ const key = metric . name || metric . keyword || `metric${ idx + 1 } ` ;
425+ const dataArray = metricDataArrays [ key ] || [ ] ;
426+ const showComparison = dataArray . length === 2 ;
427+
428+ if ( showComparison ) {
429+ const normalDiff = getComparisonData ( dataArray [ 0 ] . data , dataArray [ 1 ] . data , 'normal' ) ;
430+ const absDiff = getComparisonData ( dataArray [ 0 ] . data , dataArray [ 1 ] . data , 'absolute' ) ;
431+ const relDiff = getComparisonData ( dataArray [ 0 ] . data , dataArray [ 1 ] . data , 'relative' ) ;
432+ const mean = arr => ( arr . reduce ( ( s , p ) => s + p . y , 0 ) / arr . length ) || 0 ;
433+ stats . push ( {
434+ label : key ,
435+ meanNormal : mean ( normalDiff ) ,
436+ meanAbsolute : mean ( absDiff ) ,
437+ meanRelative : mean ( relDiff )
438+ } ) ;
439+ }
440+
441+ return (
442+ < div key = { key } className = "min-w-[600px] flex flex-col gap-3" >
443+ < ResizablePanel title = { key } initialHeight = { 440 } >
444+ < ChartWrapper
445+ chartId = { `metric-${ idx } ` }
446+ onRegisterChart = { registerChart }
447+ onSyncHover = { syncHoverToAllCharts }
448+ data = { createChartData ( dataArray ) }
449+ options = { chartOptions }
450+ />
451+ </ ResizablePanel >
452+ { showComparison && (
453+ < ResizablePanel title = { `⚖️ ${ key } 对比分析 (${ compareMode } )` } initialHeight = { 440 } >
454+ < ChartWrapper
455+ chartId = { `metric-comp-${ idx } ` }
456+ onRegisterChart = { registerChart }
457+ onSyncHover = { syncHoverToAllCharts }
458+ data = { createComparisonChartData ( dataArray [ 0 ] , dataArray [ 1 ] , key ) }
459+ options = { chartOptions }
460+ />
461+ </ ResizablePanel >
462+ ) }
463+ </ div >
464+ ) ;
465+ } ) ;
466+
302467 return (
303468 < div className = "overflow-x-auto" >
304469 < div className = "flex gap-3 w-max" >
305- { metricsToShow . map ( ( metric , idx ) => {
306- const key = metric . name || metric . keyword || `metric${ idx + 1 } ` ;
307- const dataArray = metricDataArrays [ key ] || [ ] ;
308- const showComparison = dataArray . length === 2 ;
309- return (
310- < div key = { key } className = "w-96 flex flex-col gap-3" >
311- < ResizablePanel title = { key } initialHeight = { 440 } >
312- < ChartWrapper
313- chartId = { `metric-${ idx } ` }
314- onRegisterChart = { registerChart }
315- onSyncHover = { syncHoverToAllCharts }
316- data = { createChartData ( dataArray ) }
317- options = { {
318- responsive : true ,
319- maintainAspectRatio : false ,
320- scales : { x : { type : 'linear' } } ,
321- plugins : { zoom : { zoom : { enabled : false } , pan : { enabled : false } } }
322- } }
323- />
324- </ ResizablePanel >
325- { showComparison && (
326- < ResizablePanel title = { `⚖️ ${ key } 对比分析 (${ compareMode } )` } initialHeight = { 440 } >
327- < ChartWrapper
328- chartId = { `metric-comp-${ idx } ` }
329- onRegisterChart = { registerChart }
330- onSyncHover = { syncHoverToAllCharts }
331- data = { createComparisonChartData ( dataArray [ 0 ] , dataArray [ 1 ] , key ) }
332- options = { {
333- responsive : true ,
334- maintainAspectRatio : false ,
335- scales : { x : { type : 'linear' } } ,
336- plugins : { zoom : { zoom : { enabled : false } , pan : { enabled : false } } }
337- } }
338- />
339- </ ResizablePanel >
340- ) }
341- </ div >
342- ) ;
343- } ) }
470+ { metricElements }
344471 </ div >
472+ { stats . length > 0 && (
473+ < div className = "bg-white rounded-lg shadow-md p-3 mt-3" >
474+ < h3 className = "text-base font-semibold text-gray-800 mb-2" > 差值分析统计</ h3 >
475+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
476+ { stats . map ( s => (
477+ < div key = { s . label } >
478+ < h4 className = "text-sm font-medium text-gray-700 mb-1" > { s . label } 差值统计</ h4 >
479+ < div className = "space-y-1 text-xs" >
480+ < p > Mean Difference: { s . meanNormal . toFixed ( 6 ) } </ p >
481+ < p > Mean Absolute Error: { s . meanAbsolute . toFixed ( 6 ) } </ p >
482+ < p > Mean Relative Error: { s . meanRelative . toFixed ( 6 ) } </ p >
483+ </ div >
484+ </ div >
485+ ) ) }
486+ </ div >
487+ </ div >
488+ ) }
345489 </ div >
346490 ) ;
347491}
0 commit comments