Skip to content

Commit 93ccf00

Browse files
committed
Restore sync pan and comparison stats
1 parent a600ff6 commit 93ccf00

File tree

1 file changed

+183
-39
lines changed

1 file changed

+183
-39
lines changed

src/components/ChartContainer.jsx

Lines changed: 183 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)