diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 1aa629c..9b51484 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -12,15 +12,19 @@
"fileList.disabled": "Disabled",
"fileList.config": "Configure file {{name}}",
"fileList.delete": "Remove file {{name}}",
- "comparison.title": "⚖️ Compare Mode",
+ "comparison.title": "Compare Mode",
"comparison.select": "Select comparison mode",
- "comparison.normal": "📊 Mean Error (normal)",
+ "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)",
+ "comparison.absolute": "Mean Error (absolute)",
"comparison.absoluteDesc": "Mean of absolute differences",
- "comparison.relativeNormal": "📉 Relative Error (normal)",
+ "comparison.relativeNormal": "Relative Error (normal)",
"comparison.relativeNormalDesc": "Relative error without absolute value",
- "comparison.relative": "📊 Mean Relative Error (absolute)",
+ "comparison.relative": "Mean Relative Error (absolute)",
"comparison.relativeDesc": "Mean of absolute relative error",
"themeToggle.aria": "Toggle theme",
"chart.noData": "📊 No data",
@@ -61,11 +65,18 @@
"chart.area": "Chart display area",
"chart.actions": "Chart action buttons",
"chart.diffLabel": "{{title}} difference",
- "comparison.panelTitle": "⚖️ {{key}} comparison ({{mode}})",
+ "comparison.panelTitle": "{{key}} comparison ({{mode}})",
"comparison.meanNormal": "Mean error (normal): {{value}}",
"comparison.meanAbsolute": "Mean error (absolute): {{value}}",
"comparison.relativeError": "Relative error (normal): {{value}}",
"comparison.meanRelative": "Mean relative error (absolute): {{value}}",
+ "comparison.pair": "Pair",
+ "comparison.meanNormalLabel": "Mean error (normal)",
+ "comparison.meanAbsoluteLabel": "Mean error (absolute)",
+ "comparison.relativeErrorLabel": "Relative error (normal)",
+ "comparison.meanRelativeLabel": "Mean relative error (absolute)",
+ "comparison.maxAbsoluteLabel": "Max absolute error",
+ "comparison.maxRelativeLabel": "Max relative error",
"regex.preset": "Preset",
"regex.selectPreset": "Select preset",
"regex.mode": "Match Mode",
diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json
index 8ae331d..200b4ab 100644
--- a/public/locales/zh/translation.json
+++ b/public/locales/zh/translation.json
@@ -12,15 +12,19 @@
"fileList.disabled": "已禁用",
"fileList.config": "配置文件 {{name}}",
"fileList.delete": "删除文件 {{name}}",
- "comparison.title": "⚖️ 对比模式",
+ "comparison.title": "对比模式",
"comparison.select": "选择数据对比模式",
- "comparison.normal": "📊 平均误差 (normal)",
+ "comparison.multiFileMode": "多文件对比模式",
+ "comparison.modeBaseline": "基准文件对比",
+ "comparison.modePairwise": "成对比较",
+ "comparison.baselineFile": "基准文件",
+ "comparison.normal": "平均误差 (normal)",
"comparison.normalDesc": "未取绝对值的平均误差",
- "comparison.absolute": "📈 平均误差 (absolute)",
+ "comparison.absolute": "平均误差 (absolute)",
"comparison.absoluteDesc": "绝对值差值的平均",
- "comparison.relativeNormal": "📉 相对误差 (normal)",
+ "comparison.relativeNormal": "相对误差 (normal)",
"comparison.relativeNormalDesc": "不取绝对值的相对误差",
- "comparison.relative": "📊 平均相对误差 (absolute)",
+ "comparison.relative": "平均相对误差 (absolute)",
"comparison.relativeDesc": "绝对相对误差的平均",
"themeToggle.aria": "切换主题",
"chart.noData": "📊 暂无数据",
@@ -61,11 +65,18 @@
"chart.area": "图表显示区域",
"chart.actions": "图表操作按钮",
"chart.diffLabel": "{{title}} 差值",
- "comparison.panelTitle": "⚖️ {{key}} 对比分析 ({{mode}})",
+ "comparison.panelTitle": "{{key}} 对比分析 ({{mode}})",
"comparison.meanNormal": "平均误差 (normal): {{value}}",
"comparison.meanAbsolute": "平均误差 (absolute): {{value}}",
"comparison.relativeError": "相对误差 (normal): {{value}}",
"comparison.meanRelative": "平均相对误差 (absolute): {{value}}",
+ "comparison.pair": "文件对",
+ "comparison.meanNormalLabel": "平均误差 (normal)",
+ "comparison.meanAbsoluteLabel": "平均误差 (absolute)",
+ "comparison.relativeErrorLabel": "相对误差 (normal)",
+ "comparison.meanRelativeLabel": "平均相对误差 (absolute)",
+ "comparison.maxAbsoluteLabel": "最大绝对误差",
+ "comparison.maxRelativeLabel": "最大相对误差",
"regex.preset": "预设",
"regex.selectPreset": "选择预设",
"regex.mode": "匹配模式",
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..8a6c5e3 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,74 @@ 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;
+ const max = arr => arr.reduce((m, p) => (p.y > m ? p.y : m), 0);
+ stats.push({
+ label: `${target.name} vs ${base.name}`,
+ meanNormal: mean(normalDiff),
+ meanAbsolute: mean(absDiff),
+ relativeError: mean(relNormalDiff),
+ meanRelative: mean(relDiff),
+ maxAbsolute: max(absDiff),
+ maxRelative: max(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 +619,8 @@ export default function ChartContainer({
animations: { colors: false, x: false, y: false },
});
}
- return { datasets };
+
+ return { datasets, stats };
};
if (parsedData.length === 0) {
@@ -626,7 +663,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 +675,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 +725,7 @@ export default function ChartContainer({
onRegisterChart={registerChart}
onSyncHover={syncHoverToAllCharts}
syncRef={syncLockRef}
- data={compData}
+ data={{ datasets: compResult.datasets }}
options={compOptions}
/>
@@ -760,14 +780,32 @@ export default function ChartContainer({
{comparisonChart}
{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) })}
-
+
+
{key} {t('chart.diffStats')}
+
+
+
+ | {t('comparison.pair')} |
+ {t('comparison.meanNormalLabel')} |
+ {t('comparison.meanAbsoluteLabel')} |
+ {t('comparison.maxAbsoluteLabel')} |
+ {t('comparison.meanRelativeLabel')} |
+ {t('comparison.maxRelativeLabel')} |
+
+
+
+ {stats.map(s => (
+
+ | {s.label} |
+ {s.meanNormal.toFixed(6)} |
+ {s.meanAbsolute.toFixed(6)} |
+ {s.maxAbsolute.toFixed(6)} |
+ {s.meanRelative.toFixed(6)} |
+ {s.maxRelative.toFixed(6)} |
+
+ ))}
+
+
)}
diff --git a/src/components/ComparisonControls.jsx b/src/components/ComparisonControls.jsx
index 7e72aa0..8b651c6 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,109 @@ import { useTranslation } from 'react-i18next';
id="comparison-controls-heading"
className="card-title"
>
- {t('comparison.title')}
-
+ {t('comparison.title')}
+
+
+
+
+
+
+ {t('comparison.multiFileMode')}
+
+
+
+
+
+ {multiFileMode === 'baseline' && files.length > 1 && (
+
+
+ {t('comparison.baselineFile')}
+
+
+ {files.map(f => (
+
+ ))}
+
+
+ )}
+