From 03a481e98ac351ed742d15984b9fcb53d8e67398 Mon Sep 17 00:00:00 2001
From: JavaZero <71128095+JavaZeroo@users.noreply.github.com>
Date: Tue, 26 Aug 2025 20:10:22 +0800
Subject: [PATCH 1/2] feat: support multi-file comparisons
---
public/locales/en/translation.json | 4 +
public/locales/zh/translation.json | 4 +
src/App.jsx | 22 +++-
src/components/ChartContainer.jsx | 114 +++++++++-------
src/components/ComparisonControls.jsx | 123 ++++++++++++------
.../__tests__/ComparisonControls.test.jsx | 28 +++-
6 files changed, 206 insertions(+), 89 deletions(-)
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 1aa629c..12dc8fd 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -14,6 +14,10 @@
"fileList.delete": "Remove file {{name}}",
"comparison.title": "⚖️ Compare Mode",
"comparison.select": "Select comparison mode",
+ "comparison.multiFileMode": "Multi-file comparison mode",
+ "comparison.modeBaseline": "Baseline vs others",
+ "comparison.modePairwise": "Pairwise comparisons",
+ "comparison.baselineFile": "Baseline file",
"comparison.normal": "📊 Mean Error (normal)",
"comparison.normalDesc": "Mean error without absolute value",
"comparison.absolute": "📈 Mean Error (absolute)",
diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json
index 8ae331d..d7a935e 100644
--- a/public/locales/zh/translation.json
+++ b/public/locales/zh/translation.json
@@ -14,6 +14,10 @@
"fileList.delete": "删除文件 {{name}}",
"comparison.title": "⚖️ 对比模式",
"comparison.select": "选择数据对比模式",
+ "comparison.multiFileMode": "多文件对比模式",
+ "comparison.modeBaseline": "基准文件对比",
+ "comparison.modePairwise": "成对比较",
+ "comparison.baselineFile": "基准文件",
"comparison.normal": "📊 平均误差 (normal)",
"comparison.normalDesc": "未取绝对值的平均误差",
"comparison.absolute": "📈 平均误差 (absolute)",
diff --git a/src/App.jsx b/src/App.jsx
index 961d291..4a942e6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -45,6 +45,8 @@ function App() {
});
const [compareMode, setCompareMode] = useState('normal');
+ const [multiFileMode, setMultiFileMode] = useState('baseline');
+ const [baselineFile, setBaselineFile] = useState('');
const [relativeBaseline, setRelativeBaseline] = useState(0.002);
const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005);
const [configModalOpen, setConfigModalOpen] = useState(false);
@@ -55,6 +57,17 @@ function App() {
const [maxStep, setMaxStep] = useState(0);
const [sidebarVisible, setSidebarVisible] = useState(true);
const savingDisabledRef = useRef(false);
+ const enabledFiles = uploadedFiles.filter(file => file.enabled);
+
+ useEffect(() => {
+ if (enabledFiles.length > 0) {
+ if (!enabledFiles.find(f => f.name === baselineFile)) {
+ setBaselineFile(enabledFiles[0].name);
+ }
+ } else {
+ setBaselineFile('');
+ }
+ }, [enabledFiles, baselineFile]);
// Persist configuration to localStorage
useEffect(() => {
@@ -387,10 +400,15 @@ function App() {
onFileConfig={handleFileConfig}
/>
- {uploadedFiles.filter(file => file.enabled).length === 2 && (
+ {enabledFiles.length >= 2 && (
)}
@@ -475,6 +493,8 @@ function App() {
files={uploadedFiles}
metrics={globalParsingConfig.metrics}
compareMode={compareMode}
+ multiFileMode={multiFileMode}
+ baselineFile={baselineFile}
relativeBaseline={relativeBaseline}
absoluteBaseline={absoluteBaseline}
xRange={xRange}
diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx
index 0e84f03..eabfce0 100644
--- a/src/components/ChartContainer.jsx
+++ b/src/components/ChartContainer.jsx
@@ -93,6 +93,8 @@ export default function ChartContainer({
files,
metrics = [],
compareMode,
+ multiFileMode = 'baseline',
+ baselineFile,
relativeBaseline = 0.002,
absoluteBaseline = 0.005,
xRange = { min: undefined, max: undefined },
@@ -531,40 +533,71 @@ export default function ChartContainer({
elements: { point: { radius: 0 } }
}), [xRange, onXRangeChange]);
- const createComparisonChartData = (item1, item2, title) => {
- const comparisonData = getComparisonData(item1.data, item2.data, compareMode);
- const baseline =
+ const buildComparisonChartData = (dataArray) => {
+ const baselineVal =
compareMode === 'relative' || compareMode === 'relative-normal'
? relativeBaseline
: compareMode === 'absolute'
? absoluteBaseline
: 0;
- const datasets = [
- {
- label: t('chart.diffLabel', { title }),
- data: comparisonData,
- borderColor: '#dc2626',
- backgroundColor: '#dc2626',
+ const datasets = [];
+ const stats = [];
+ const addPair = (base, target, colorIdx) => {
+ const diffData = getComparisonData(base.data, target.data, compareMode);
+ const color = colors[colorIdx % colors.length];
+ datasets.push({
+ label: `${target.name} vs ${base.name}`,
+ data: diffData,
+ borderColor: color,
+ backgroundColor: color,
borderWidth: 2,
fill: false,
tension: 0,
pointRadius: 0,
pointHoverRadius: 4,
- pointBackgroundColor: '#dc2626',
- pointBorderColor: '#dc2626',
+ pointBackgroundColor: color,
+ pointBorderColor: color,
pointBorderWidth: 1,
- pointHoverBackgroundColor: '#dc2626',
- pointHoverBorderColor: '#dc2626',
+ pointHoverBackgroundColor: color,
+ pointHoverBorderColor: color,
pointHoverBorderWidth: 1,
animation: false,
animations: { colors: false, x: false, y: false },
- },
- ];
- if (baseline > 0 && (compareMode === 'relative' || compareMode === 'relative-normal' || compareMode === 'absolute')) {
- const baselineData = comparisonData.map(p => ({ x: p.x, y: baseline }));
+ });
+ const normalDiff = getComparisonData(base.data, target.data, 'normal');
+ const absDiff = getComparisonData(base.data, target.data, 'absolute');
+ const relNormalDiff = getComparisonData(base.data, target.data, 'relative-normal');
+ const relDiff = getComparisonData(base.data, target.data, 'relative');
+ const mean = arr => (arr.reduce((s, p) => s + p.y, 0) / arr.length) || 0;
+ stats.push({
+ label: `${target.name} vs ${base.name}`,
+ meanNormal: mean(normalDiff),
+ meanAbsolute: mean(absDiff),
+ relativeError: mean(relNormalDiff),
+ meanRelative: mean(relDiff)
+ });
+ };
+
+ let colorIdx = 0;
+ if (multiFileMode === 'baseline') {
+ const base = dataArray.find(d => d.name === baselineFile) || dataArray[0];
+ dataArray.forEach(item => {
+ if (item.name === base.name) return;
+ addPair(base, item, colorIdx++);
+ });
+ } else {
+ for (let i = 0; i < dataArray.length; i++) {
+ for (let j = i + 1; j < dataArray.length; j++) {
+ addPair(dataArray[i], dataArray[j], colorIdx++);
+ }
+ }
+ }
+
+ if (datasets.length > 0 && baselineVal > 0 && (compareMode === 'relative' || compareMode === 'relative-normal' || compareMode === 'absolute')) {
+ const baseData = datasets[0].data.map(p => ({ x: p.x, y: baselineVal }));
datasets.push({
label: 'Baseline',
- data: baselineData,
+ data: baseData,
borderColor: '#10b981',
backgroundColor: '#10b981',
borderWidth: 2,
@@ -583,7 +616,8 @@ export default function ChartContainer({
animations: { colors: false, x: false, y: false },
});
}
- return { datasets };
+
+ return { datasets, stats };
};
if (parsedData.length === 0) {
@@ -626,7 +660,7 @@ export default function ChartContainer({
const metricElements = metrics.map((metric, idx) => {
const key = metric.name || metric.keyword || `metric${idx + 1}`;
const dataArray = metricDataArrays[key] || [];
- const showComparison = dataArray.length === 2;
+ const showComparison = dataArray.length >= 2;
const yRange = calculateYRange(dataArray);
const options = {
@@ -638,28 +672,11 @@ export default function ChartContainer({
};
let stats = null;
- if (showComparison) {
- const normalDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'normal');
- const absDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'absolute');
- const relNormalDiff = getComparisonData(
- dataArray[0].data,
- dataArray[1].data,
- 'relative-normal'
- );
- const relDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'relative');
- const mean = arr => (arr.reduce((s, p) => s + p.y, 0) / arr.length) || 0;
- stats = {
- meanNormal: mean(normalDiff),
- meanAbsolute: mean(absDiff),
- relativeError: mean(relNormalDiff),
- meanRelative: mean(relDiff)
- };
- }
-
let comparisonChart = null;
if (showComparison) {
- const compData = createComparisonChartData(dataArray[0], dataArray[1], key);
- const compRange = calculateYRange(compData.datasets);
+ const compResult = buildComparisonChartData(dataArray);
+ stats = compResult.stats.length > 0 ? compResult.stats : null;
+ const compRange = calculateYRange(compResult.datasets);
const compOptions = {
...chartOptions,
scales: {
@@ -705,7 +722,7 @@ export default function ChartContainer({
onRegisterChart={registerChart}
onSyncHover={syncHoverToAllCharts}
syncRef={syncLockRef}
- data={compData}
+ data={{ datasets: compResult.datasets }}
options={compOptions}
/>
@@ -762,11 +779,16 @@ export default function ChartContainer({
{stats && (
{key} {t('chart.diffStats')}
-
-
{t('comparison.meanNormal', { value: stats.meanNormal.toFixed(6) })}
-
{t('comparison.meanAbsolute', { value: stats.meanAbsolute.toFixed(6) })}
-
{t('comparison.relativeError', { value: stats.relativeError.toFixed(6) })}
-
{t('comparison.meanRelative', { value: stats.meanRelative.toFixed(6) })}
+
+ {stats.map(s => (
+
+
{s.label}
+
{t('comparison.meanNormal', { value: s.meanNormal.toFixed(6) })}
+
{t('comparison.meanAbsolute', { value: s.meanAbsolute.toFixed(6) })}
+
{t('comparison.relativeError', { value: s.relativeError.toFixed(6) })}
+
{t('comparison.meanRelative', { value: s.meanRelative.toFixed(6) })}
+
+ ))}
)}
diff --git a/src/components/ComparisonControls.jsx b/src/components/ComparisonControls.jsx
index 7e72aa0..6429f3b 100644
--- a/src/components/ComparisonControls.jsx
+++ b/src/components/ComparisonControls.jsx
@@ -2,17 +2,22 @@ import React from 'react';
import { BarChart2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
- export function ComparisonControls({
- compareMode,
- onCompareModeChange
- }) {
- const { t } = useTranslation();
- const modes = [
- { value: 'normal', label: t('comparison.normal'), description: t('comparison.normalDesc') },
- { value: 'absolute', label: t('comparison.absolute'), description: t('comparison.absoluteDesc') },
- { value: 'relative-normal', label: t('comparison.relativeNormal'), description: t('comparison.relativeNormalDesc') },
- { value: 'relative', label: t('comparison.relative'), description: t('comparison.relativeDesc') }
- ];
+export function ComparisonControls({
+ compareMode,
+ onCompareModeChange,
+ files = [],
+ baseline,
+ onBaselineChange,
+ multiFileMode = 'baseline',
+ onMultiFileModeChange
+}) {
+ const { t } = useTranslation();
+ const modes = [
+ { value: 'normal', label: t('comparison.normal'), description: t('comparison.normalDesc') },
+ { value: 'absolute', label: t('comparison.absolute'), description: t('comparison.absoluteDesc') },
+ { value: 'relative-normal', label: t('comparison.relativeNormal'), description: t('comparison.relativeNormalDesc') },
+ { value: 'relative', label: t('comparison.relative'), description: t('comparison.relativeDesc') }
+ ];
return (
@@ -26,40 +31,82 @@ import { useTranslation } from 'react-i18next';
id="comparison-controls-heading"
className="card-title"
>
- {t('comparison.title')}
-
+ {t('comparison.title')}
+
+
+
+
+
+
+
+ {multiFileMode === 'baseline' && files.length > 1 && (
+
+
+
+
+ )}
+