Skip to content

Commit 8f935e6

Browse files
committed
feat: refine comparison controls and stats
1 parent 03a481e commit 8f935e6

5 files changed

Lines changed: 118 additions & 61 deletions

File tree

public/locales/en/translation.json

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +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",
1717
"comparison.multiFileMode": "Multi-file comparison mode",
1818
"comparison.modeBaseline": "Baseline vs others",
1919
"comparison.modePairwise": "Pairwise comparisons",
2020
"comparison.baselineFile": "Baseline file",
21-
"comparison.normal": "📊 Mean Error (normal)",
21+
"comparison.normal": "Mean Error (normal)",
2222
"comparison.normalDesc": "Mean error without absolute value",
23-
"comparison.absolute": "📈 Mean Error (absolute)",
23+
"comparison.absolute": "Mean Error (absolute)",
2424
"comparison.absoluteDesc": "Mean of absolute differences",
25-
"comparison.relativeNormal": "📉 Relative Error (normal)",
25+
"comparison.relativeNormal": "Relative Error (normal)",
2626
"comparison.relativeNormalDesc": "Relative error without absolute value",
27-
"comparison.relative": "📊 Mean Relative Error (absolute)",
27+
"comparison.relative": "Mean Relative Error (absolute)",
2828
"comparison.relativeDesc": "Mean of absolute relative error",
2929
"themeToggle.aria": "Toggle theme",
3030
"chart.noData": "📊 No data",
@@ -65,11 +65,18 @@
6565
"chart.area": "Chart display area",
6666
"chart.actions": "Chart action buttons",
6767
"chart.diffLabel": "{{title}} difference",
68-
"comparison.panelTitle": "⚖️ {{key}} comparison ({{mode}})",
68+
"comparison.panelTitle": "{{key}} comparison ({{mode}})",
6969
"comparison.meanNormal": "Mean error (normal): {{value}}",
7070
"comparison.meanAbsolute": "Mean error (absolute): {{value}}",
7171
"comparison.relativeError": "Relative error (normal): {{value}}",
7272
"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",
7380
"regex.preset": "Preset",
7481
"regex.selectPreset": "Select preset",
7582
"regex.mode": "Match Mode",

public/locales/zh/translation.json

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@
1212
"fileList.disabled": "已禁用",
1313
"fileList.config": "配置文件 {{name}}",
1414
"fileList.delete": "删除文件 {{name}}",
15-
"comparison.title": "⚖️ 对比模式",
15+
"comparison.title": "对比模式",
1616
"comparison.select": "选择数据对比模式",
1717
"comparison.multiFileMode": "多文件对比模式",
1818
"comparison.modeBaseline": "基准文件对比",
1919
"comparison.modePairwise": "成对比较",
2020
"comparison.baselineFile": "基准文件",
21-
"comparison.normal": "📊 平均误差 (normal)",
21+
"comparison.normal": "平均误差 (normal)",
2222
"comparison.normalDesc": "未取绝对值的平均误差",
23-
"comparison.absolute": "📈 平均误差 (absolute)",
23+
"comparison.absolute": "平均误差 (absolute)",
2424
"comparison.absoluteDesc": "绝对值差值的平均",
25-
"comparison.relativeNormal": "📉 相对误差 (normal)",
25+
"comparison.relativeNormal": "相对误差 (normal)",
2626
"comparison.relativeNormalDesc": "不取绝对值的相对误差",
27-
"comparison.relative": "📊 平均相对误差 (absolute)",
27+
"comparison.relative": "平均相对误差 (absolute)",
2828
"comparison.relativeDesc": "绝对相对误差的平均",
2929
"themeToggle.aria": "切换主题",
3030
"chart.noData": "📊 暂无数据",
@@ -65,11 +65,18 @@
6565
"chart.area": "图表显示区域",
6666
"chart.actions": "图表操作按钮",
6767
"chart.diffLabel": "{{title}} 差值",
68-
"comparison.panelTitle": "⚖️ {{key}} 对比分析 ({{mode}})",
68+
"comparison.panelTitle": "{{key}} 对比分析 ({{mode}})",
6969
"comparison.meanNormal": "平均误差 (normal): {{value}}",
7070
"comparison.meanAbsolute": "平均误差 (absolute): {{value}}",
7171
"comparison.relativeError": "相对误差 (normal): {{value}}",
7272
"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": "最大相对误差",
7380
"regex.preset": "预设",
7481
"regex.selectPreset": "选择预设",
7582
"regex.mode": "匹配模式",

src/components/ChartContainer.jsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -569,12 +569,15 @@ export default function ChartContainer({
569569
const relNormalDiff = getComparisonData(base.data, target.data, 'relative-normal');
570570
const relDiff = getComparisonData(base.data, target.data, 'relative');
571571
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);
572573
stats.push({
573574
label: `${target.name} vs ${base.name}`,
574575
meanNormal: mean(normalDiff),
575576
meanAbsolute: mean(absDiff),
576577
relativeError: mean(relNormalDiff),
577-
meanRelative: mean(relDiff)
578+
meanRelative: mean(relDiff),
579+
maxAbsolute: max(absDiff),
580+
maxRelative: max(relDiff)
578581
});
579582
};
580583

@@ -777,19 +780,32 @@ export default function ChartContainer({
777780
</ResizablePanel>
778781
{comparisonChart}
779782
{stats && (
780-
<div className="card">
781-
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{key} {t('chart.diffStats')}</h4>
782-
<div className="space-y-2 text-xs">
783-
{stats.map(s => (
784-
<div key={s.label} className="space-y-1">
785-
<p className="font-medium">{s.label}</p>
786-
<p>{t('comparison.meanNormal', { value: s.meanNormal.toFixed(6) })}</p>
787-
<p>{t('comparison.meanAbsolute', { value: s.meanAbsolute.toFixed(6) })}</p>
788-
<p>{t('comparison.relativeError', { value: s.relativeError.toFixed(6) })}</p>
789-
<p>{t('comparison.meanRelative', { value: s.meanRelative.toFixed(6) })}</p>
790-
</div>
791-
))}
792-
</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>
793809
</div>
794810
)}
795811
</div>

src/components/ComparisonControls.jsx

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,41 +37,64 @@ export function ComparisonControls({
3737

3838
<div className="space-y-4">
3939
<div>
40-
<label
41-
htmlFor="multi-file-mode"
42-
className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"
43-
>
40+
<span className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
4441
{t('comparison.multiFileMode')}
45-
</label>
46-
<select
47-
id="multi-file-mode"
48-
value={multiFileMode}
49-
onChange={(e) => onMultiFileModeChange?.(e.target.value)}
50-
className="select"
42+
</span>
43+
<div
44+
role="group"
45+
aria-label={t('comparison.multiFileMode')}
46+
className="inline-flex rounded-md border overflow-hidden"
5147
>
52-
<option value="baseline">{t('comparison.modeBaseline')}</option>
53-
<option value="pairwise">{t('comparison.modePairwise')}</option>
54-
</select>
48+
<button
49+
type="button"
50+
onClick={() => onMultiFileModeChange?.('baseline')}
51+
aria-pressed={multiFileMode === 'baseline'}
52+
className={`px-2 py-1 text-xs font-medium focus:outline-none ${
53+
multiFileMode === 'baseline'
54+
? 'bg-blue-600 text-white'
55+
: 'bg-white text-gray-700 dark:bg-gray-700 dark:text-gray-300'
56+
}`}
57+
>
58+
{t('comparison.modeBaseline')}
59+
</button>
60+
<button
61+
type="button"
62+
onClick={() => onMultiFileModeChange?.('pairwise')}
63+
aria-pressed={multiFileMode === 'pairwise'}
64+
className={`px-2 py-1 text-xs font-medium border-l focus:outline-none ${
65+
multiFileMode === 'pairwise'
66+
? 'bg-blue-600 text-white'
67+
: 'bg-white text-gray-700 dark:bg-gray-700 dark:text-gray-300'
68+
}`}
69+
>
70+
{t('comparison.modePairwise')}
71+
</button>
72+
</div>
5573
</div>
5674

5775
{multiFileMode === 'baseline' && files.length > 1 && (
5876
<div>
59-
<label
60-
htmlFor="baseline-file"
61-
className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1"
62-
>
77+
<span className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
6378
{t('comparison.baselineFile')}
64-
</label>
65-
<select
66-
id="baseline-file"
67-
value={baseline || (files[0]?.name ?? '')}
68-
onChange={(e) => onBaselineChange?.(e.target.value)}
69-
className="select"
70-
>
79+
</span>
80+
<div className="space-y-1">
7181
{files.map(f => (
72-
<option key={f.name} value={f.name}>{f.name}</option>
82+
<label
83+
key={f.name}
84+
className="flex items-center gap-2 cursor-pointer text-xs"
85+
>
86+
<input
87+
type="radio"
88+
name="baseline-file"
89+
value={f.name}
90+
checked={(baseline || files[0]?.name) === f.name}
91+
onChange={(e) => onBaselineChange?.(e.target.value)}
92+
className="radio"
93+
/>
94+
<span className="text-gray-700 dark:text-gray-300">{f.name}</span>
95+
</label>
7396
))}
74-
</select>
97+
</div>
7598
</div>
7699
)}
77100

@@ -80,24 +103,28 @@ export function ComparisonControls({
80103
{modes.map(mode => (
81104
<label
82105
key={mode.value}
83-
className="flex items-center cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 p-1 rounded"
106+
className={`flex items-start gap-2 rounded border p-2 cursor-pointer text-xs ${
107+
compareMode === mode.value
108+
? 'border-blue-500 bg-blue-50 dark:bg-gray-700'
109+
: 'border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
110+
}`}
84111
>
85112
<input
86113
type="radio"
87114
name="compareMode"
88115
value={mode.value}
89116
checked={compareMode === mode.value}
90117
onChange={(e) => onCompareModeChange(e.target.value)}
91-
className="radio"
118+
className="radio mt-0.5"
92119
aria-describedby={`mode-${mode.value}-description`}
93120
/>
94-
<div className="ml-2">
95-
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
121+
<div>
122+
<div className="font-medium text-gray-700 dark:text-gray-300">
96123
{mode.label}
97124
</div>
98125
<div
99126
id={`mode-${mode.value}-description`}
100-
className="text-xs text-gray-500 dark:text-gray-400"
127+
className="text-gray-500 dark:text-gray-400"
101128
>
102129
{mode.description}
103130
</div>

src/components/__tests__/ComparisonControls.test.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ describe('ComparisonControls', () => {
2828
await user.click(absoluteOption);
2929
expect(handleMode).toHaveBeenCalledWith('absolute');
3030

31-
const strategySelect = screen.getByLabelText(i18n.t('comparison.multiFileMode'));
32-
await user.selectOptions(strategySelect, 'pairwise');
31+
const pairwiseButton = screen.getByRole('button', { name: i18n.t('comparison.modePairwise') });
32+
await user.click(pairwiseButton);
3333
expect(handleStrategy).toHaveBeenCalledWith('pairwise');
3434

35-
const baselineSelect = screen.getByLabelText(i18n.t('comparison.baselineFile'));
36-
await user.selectOptions(baselineSelect, 'b.log');
35+
const baselineRadio = screen.getByLabelText('b.log');
36+
await user.click(baselineRadio);
3737
expect(handleBaseline).toHaveBeenCalledWith('b.log');
3838
});
3939
});

0 commit comments

Comments
 (0)