Skip to content

Commit 9b8c680

Browse files
authored
feat: add baseline and pairwise multi-file comparison (#48)
* feat: support multi-file comparisons * feat: refine comparison controls and stats
1 parent b592eb9 commit 9b8c680

File tree

6 files changed

+278
-104
lines changed

6 files changed

+278
-104
lines changed

public/locales/en/translation.json

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@
1212
"fileList.disabled": "Disabled",
1313
"fileList.config": "Configure file {{name}}",
1414
"fileList.delete": "Remove file {{name}}",
15-
"comparison.title": "⚖️ Compare Mode",
15+
"comparison.title": "Compare Mode",
1616
"comparison.select": "Select comparison mode",
17-
"comparison.normal": "📊 Mean Error (normal)",
17+
"comparison.multiFileMode": "Multi-file comparison mode",
18+
"comparison.modeBaseline": "Baseline vs others",
19+
"comparison.modePairwise": "Pairwise comparisons",
20+
"comparison.baselineFile": "Baseline file",
21+
"comparison.normal": "Mean Error (normal)",
1822
"comparison.normalDesc": "Mean error without absolute value",
19-
"comparison.absolute": "📈 Mean Error (absolute)",
23+
"comparison.absolute": "Mean Error (absolute)",
2024
"comparison.absoluteDesc": "Mean of absolute differences",
21-
"comparison.relativeNormal": "📉 Relative Error (normal)",
25+
"comparison.relativeNormal": "Relative Error (normal)",
2226
"comparison.relativeNormalDesc": "Relative error without absolute value",
23-
"comparison.relative": "📊 Mean Relative Error (absolute)",
27+
"comparison.relative": "Mean Relative Error (absolute)",
2428
"comparison.relativeDesc": "Mean of absolute relative error",
2529
"themeToggle.aria": "Toggle theme",
2630
"chart.noData": "📊 No data",
@@ -61,11 +65,18 @@
6165
"chart.area": "Chart display area",
6266
"chart.actions": "Chart action buttons",
6367
"chart.diffLabel": "{{title}} difference",
64-
"comparison.panelTitle": "⚖️ {{key}} comparison ({{mode}})",
68+
"comparison.panelTitle": "{{key}} comparison ({{mode}})",
6569
"comparison.meanNormal": "Mean error (normal): {{value}}",
6670
"comparison.meanAbsolute": "Mean error (absolute): {{value}}",
6771
"comparison.relativeError": "Relative error (normal): {{value}}",
6872
"comparison.meanRelative": "Mean relative error (absolute): {{value}}",
73+
"comparison.pair": "Pair",
74+
"comparison.meanNormalLabel": "Mean error (normal)",
75+
"comparison.meanAbsoluteLabel": "Mean error (absolute)",
76+
"comparison.relativeErrorLabel": "Relative error (normal)",
77+
"comparison.meanRelativeLabel": "Mean relative error (absolute)",
78+
"comparison.maxAbsoluteLabel": "Max absolute error",
79+
"comparison.maxRelativeLabel": "Max relative error",
6980
"regex.preset": "Preset",
7081
"regex.selectPreset": "Select preset",
7182
"regex.mode": "Match Mode",

public/locales/zh/translation.json

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@
1212
"fileList.disabled": "已禁用",
1313
"fileList.config": "配置文件 {{name}}",
1414
"fileList.delete": "删除文件 {{name}}",
15-
"comparison.title": "⚖️ 对比模式",
15+
"comparison.title": "对比模式",
1616
"comparison.select": "选择数据对比模式",
17-
"comparison.normal": "📊 平均误差 (normal)",
17+
"comparison.multiFileMode": "多文件对比模式",
18+
"comparison.modeBaseline": "基准文件对比",
19+
"comparison.modePairwise": "成对比较",
20+
"comparison.baselineFile": "基准文件",
21+
"comparison.normal": "平均误差 (normal)",
1822
"comparison.normalDesc": "未取绝对值的平均误差",
19-
"comparison.absolute": "📈 平均误差 (absolute)",
23+
"comparison.absolute": "平均误差 (absolute)",
2024
"comparison.absoluteDesc": "绝对值差值的平均",
21-
"comparison.relativeNormal": "📉 相对误差 (normal)",
25+
"comparison.relativeNormal": "相对误差 (normal)",
2226
"comparison.relativeNormalDesc": "不取绝对值的相对误差",
23-
"comparison.relative": "📊 平均相对误差 (absolute)",
27+
"comparison.relative": "平均相对误差 (absolute)",
2428
"comparison.relativeDesc": "绝对相对误差的平均",
2529
"themeToggle.aria": "切换主题",
2630
"chart.noData": "📊 暂无数据",
@@ -61,11 +65,18 @@
6165
"chart.area": "图表显示区域",
6266
"chart.actions": "图表操作按钮",
6367
"chart.diffLabel": "{{title}} 差值",
64-
"comparison.panelTitle": "⚖️ {{key}} 对比分析 ({{mode}})",
68+
"comparison.panelTitle": "{{key}} 对比分析 ({{mode}})",
6569
"comparison.meanNormal": "平均误差 (normal): {{value}}",
6670
"comparison.meanAbsolute": "平均误差 (absolute): {{value}}",
6771
"comparison.relativeError": "相对误差 (normal): {{value}}",
6872
"comparison.meanRelative": "平均相对误差 (absolute): {{value}}",
73+
"comparison.pair": "文件对",
74+
"comparison.meanNormalLabel": "平均误差 (normal)",
75+
"comparison.meanAbsoluteLabel": "平均误差 (absolute)",
76+
"comparison.relativeErrorLabel": "相对误差 (normal)",
77+
"comparison.meanRelativeLabel": "平均相对误差 (absolute)",
78+
"comparison.maxAbsoluteLabel": "最大绝对误差",
79+
"comparison.maxRelativeLabel": "最大相对误差",
6980
"regex.preset": "预设",
7081
"regex.selectPreset": "选择预设",
7182
"regex.mode": "匹配模式",

src/App.jsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ function App() {
4545
});
4646

4747
const [compareMode, setCompareMode] = useState('normal');
48+
const [multiFileMode, setMultiFileMode] = useState('baseline');
49+
const [baselineFile, setBaselineFile] = useState('');
4850
const [relativeBaseline, setRelativeBaseline] = useState(0.002);
4951
const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005);
5052
const [configModalOpen, setConfigModalOpen] = useState(false);
@@ -55,6 +57,17 @@ function App() {
5557
const [maxStep, setMaxStep] = useState(0);
5658
const [sidebarVisible, setSidebarVisible] = useState(true);
5759
const savingDisabledRef = useRef(false);
60+
const enabledFiles = uploadedFiles.filter(file => file.enabled);
61+
62+
useEffect(() => {
63+
if (enabledFiles.length > 0) {
64+
if (!enabledFiles.find(f => f.name === baselineFile)) {
65+
setBaselineFile(enabledFiles[0].name);
66+
}
67+
} else {
68+
setBaselineFile('');
69+
}
70+
}, [enabledFiles, baselineFile]);
5871

5972
// Persist configuration to localStorage
6073
useEffect(() => {
@@ -387,10 +400,15 @@ function App() {
387400
onFileConfig={handleFileConfig}
388401
/>
389402

390-
{uploadedFiles.filter(file => file.enabled).length === 2 && (
403+
{enabledFiles.length >= 2 && (
391404
<ComparisonControls
392405
compareMode={compareMode}
393406
onCompareModeChange={setCompareMode}
407+
files={enabledFiles}
408+
baseline={baselineFile}
409+
onBaselineChange={setBaselineFile}
410+
multiFileMode={multiFileMode}
411+
onMultiFileModeChange={setMultiFileMode}
394412
/>
395413
)}
396414

@@ -475,6 +493,8 @@ function App() {
475493
files={uploadedFiles}
476494
metrics={globalParsingConfig.metrics}
477495
compareMode={compareMode}
496+
multiFileMode={multiFileMode}
497+
baselineFile={baselineFile}
478498
relativeBaseline={relativeBaseline}
479499
absoluteBaseline={absoluteBaseline}
480500
xRange={xRange}

src/components/ChartContainer.jsx

Lines changed: 87 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export default function ChartContainer({
9393
files,
9494
metrics = [],
9595
compareMode,
96+
multiFileMode = 'baseline',
97+
baselineFile,
9698
relativeBaseline = 0.002,
9799
absoluteBaseline = 0.005,
98100
xRange = { min: undefined, max: undefined },
@@ -531,40 +533,74 @@ export default function ChartContainer({
531533
elements: { point: { radius: 0 } }
532534
}), [xRange, onXRangeChange]);
533535

534-
const createComparisonChartData = (item1, item2, title) => {
535-
const comparisonData = getComparisonData(item1.data, item2.data, compareMode);
536-
const baseline =
536+
const buildComparisonChartData = (dataArray) => {
537+
const baselineVal =
537538
compareMode === 'relative' || compareMode === 'relative-normal'
538539
? relativeBaseline
539540
: compareMode === 'absolute'
540541
? absoluteBaseline
541542
: 0;
542-
const datasets = [
543-
{
544-
label: t('chart.diffLabel', { title }),
545-
data: comparisonData,
546-
borderColor: '#dc2626',
547-
backgroundColor: '#dc2626',
543+
const datasets = [];
544+
const stats = [];
545+
const addPair = (base, target, colorIdx) => {
546+
const diffData = getComparisonData(base.data, target.data, compareMode);
547+
const color = colors[colorIdx % colors.length];
548+
datasets.push({
549+
label: `${target.name} vs ${base.name}`,
550+
data: diffData,
551+
borderColor: color,
552+
backgroundColor: color,
548553
borderWidth: 2,
549554
fill: false,
550555
tension: 0,
551556
pointRadius: 0,
552557
pointHoverRadius: 4,
553-
pointBackgroundColor: '#dc2626',
554-
pointBorderColor: '#dc2626',
558+
pointBackgroundColor: color,
559+
pointBorderColor: color,
555560
pointBorderWidth: 1,
556-
pointHoverBackgroundColor: '#dc2626',
557-
pointHoverBorderColor: '#dc2626',
561+
pointHoverBackgroundColor: color,
562+
pointHoverBorderColor: color,
558563
pointHoverBorderWidth: 1,
559564
animation: false,
560565
animations: { colors: false, x: false, y: false },
561-
},
562-
];
563-
if (baseline > 0 && (compareMode === 'relative' || compareMode === 'relative-normal' || compareMode === 'absolute')) {
564-
const baselineData = comparisonData.map(p => ({ x: p.x, y: baseline }));
566+
});
567+
const normalDiff = getComparisonData(base.data, target.data, 'normal');
568+
const absDiff = getComparisonData(base.data, target.data, 'absolute');
569+
const relNormalDiff = getComparisonData(base.data, target.data, 'relative-normal');
570+
const relDiff = getComparisonData(base.data, target.data, 'relative');
571+
const mean = arr => (arr.reduce((s, p) => s + p.y, 0) / arr.length) || 0;
572+
const max = arr => arr.reduce((m, p) => (p.y > m ? p.y : m), 0);
573+
stats.push({
574+
label: `${target.name} vs ${base.name}`,
575+
meanNormal: mean(normalDiff),
576+
meanAbsolute: mean(absDiff),
577+
relativeError: mean(relNormalDiff),
578+
meanRelative: mean(relDiff),
579+
maxAbsolute: max(absDiff),
580+
maxRelative: max(relDiff)
581+
});
582+
};
583+
584+
let colorIdx = 0;
585+
if (multiFileMode === 'baseline') {
586+
const base = dataArray.find(d => d.name === baselineFile) || dataArray[0];
587+
dataArray.forEach(item => {
588+
if (item.name === base.name) return;
589+
addPair(base, item, colorIdx++);
590+
});
591+
} else {
592+
for (let i = 0; i < dataArray.length; i++) {
593+
for (let j = i + 1; j < dataArray.length; j++) {
594+
addPair(dataArray[i], dataArray[j], colorIdx++);
595+
}
596+
}
597+
}
598+
599+
if (datasets.length > 0 && baselineVal > 0 && (compareMode === 'relative' || compareMode === 'relative-normal' || compareMode === 'absolute')) {
600+
const baseData = datasets[0].data.map(p => ({ x: p.x, y: baselineVal }));
565601
datasets.push({
566602
label: 'Baseline',
567-
data: baselineData,
603+
data: baseData,
568604
borderColor: '#10b981',
569605
backgroundColor: '#10b981',
570606
borderWidth: 2,
@@ -583,7 +619,8 @@ export default function ChartContainer({
583619
animations: { colors: false, x: false, y: false },
584620
});
585621
}
586-
return { datasets };
622+
623+
return { datasets, stats };
587624
};
588625

589626
if (parsedData.length === 0) {
@@ -626,7 +663,7 @@ export default function ChartContainer({
626663
const metricElements = metrics.map((metric, idx) => {
627664
const key = metric.name || metric.keyword || `metric${idx + 1}`;
628665
const dataArray = metricDataArrays[key] || [];
629-
const showComparison = dataArray.length === 2;
666+
const showComparison = dataArray.length >= 2;
630667

631668
const yRange = calculateYRange(dataArray);
632669
const options = {
@@ -638,28 +675,11 @@ export default function ChartContainer({
638675
};
639676

640677
let stats = null;
641-
if (showComparison) {
642-
const normalDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'normal');
643-
const absDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'absolute');
644-
const relNormalDiff = getComparisonData(
645-
dataArray[0].data,
646-
dataArray[1].data,
647-
'relative-normal'
648-
);
649-
const relDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'relative');
650-
const mean = arr => (arr.reduce((s, p) => s + p.y, 0) / arr.length) || 0;
651-
stats = {
652-
meanNormal: mean(normalDiff),
653-
meanAbsolute: mean(absDiff),
654-
relativeError: mean(relNormalDiff),
655-
meanRelative: mean(relDiff)
656-
};
657-
}
658-
659678
let comparisonChart = null;
660679
if (showComparison) {
661-
const compData = createComparisonChartData(dataArray[0], dataArray[1], key);
662-
const compRange = calculateYRange(compData.datasets);
680+
const compResult = buildComparisonChartData(dataArray);
681+
stats = compResult.stats.length > 0 ? compResult.stats : null;
682+
const compRange = calculateYRange(compResult.datasets);
663683
const compOptions = {
664684
...chartOptions,
665685
scales: {
@@ -705,7 +725,7 @@ export default function ChartContainer({
705725
onRegisterChart={registerChart}
706726
onSyncHover={syncHoverToAllCharts}
707727
syncRef={syncLockRef}
708-
data={compData}
728+
data={{ datasets: compResult.datasets }}
709729
options={compOptions}
710730
/>
711731
</ResizablePanel>
@@ -760,14 +780,32 @@ export default function ChartContainer({
760780
</ResizablePanel>
761781
{comparisonChart}
762782
{stats && (
763-
<div className="card">
764-
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{key} {t('chart.diffStats')}</h4>
765-
<div className="space-y-1 text-xs">
766-
<p>{t('comparison.meanNormal', { value: stats.meanNormal.toFixed(6) })}</p>
767-
<p>{t('comparison.meanAbsolute', { value: stats.meanAbsolute.toFixed(6) })}</p>
768-
<p>{t('comparison.relativeError', { value: stats.relativeError.toFixed(6) })}</p>
769-
<p>{t('comparison.meanRelative', { value: stats.meanRelative.toFixed(6) })}</p>
770-
</div>
783+
<div className="card overflow-x-auto">
784+
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{key} {t('chart.diffStats')}</h4>
785+
<table className="min-w-full text-xs">
786+
<thead>
787+
<tr className="text-left">
788+
<th className="pr-2">{t('comparison.pair')}</th>
789+
<th className="text-right">{t('comparison.meanNormalLabel')}</th>
790+
<th className="text-right">{t('comparison.meanAbsoluteLabel')}</th>
791+
<th className="text-right">{t('comparison.maxAbsoluteLabel')}</th>
792+
<th className="text-right">{t('comparison.meanRelativeLabel')}</th>
793+
<th className="text-right">{t('comparison.maxRelativeLabel')}</th>
794+
</tr>
795+
</thead>
796+
<tbody>
797+
{stats.map(s => (
798+
<tr key={s.label} className="border-t border-gray-200 dark:border-gray-700">
799+
<td className="pr-2 py-1">{s.label}</td>
800+
<td className="text-right py-1">{s.meanNormal.toFixed(6)}</td>
801+
<td className="text-right py-1">{s.meanAbsolute.toFixed(6)}</td>
802+
<td className="text-right py-1">{s.maxAbsolute.toFixed(6)}</td>
803+
<td className="text-right py-1">{s.meanRelative.toFixed(6)}</td>
804+
<td className="text-right py-1">{s.maxRelative.toFixed(6)}</td>
805+
</tr>
806+
))}
807+
</tbody>
808+
</table>
771809
</div>
772810
)}
773811
</div>

0 commit comments

Comments
 (0)