Skip to content

Commit b37ded5

Browse files
authored
Update comparison modes (#25)
* feat: expand comparison options * Update difference stats labels
1 parent 042b3ce commit b37ded5

6 files changed

Lines changed: 92 additions & 70 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
- **📈 Normal模式**: 原始差值分析 (File2 - File1)
4343
- **📊 Absolute模式**: 绝对差值分析 |File2 - File1|
4444
- **📉 Relative模式**: 相对差值百分比分析
45-
- **📋 统计指标**: 详细的Mean Difference、Mean Absolute Error、Mean Relative Error
45+
- **📋 统计指标**: 详细的平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute)
4646
- **⚖️ 基准线设置**: 可配置相对误差和绝对误差的基准线
4747

4848
### �️ **灵活的显示控制**
@@ -149,8 +149,8 @@ gradient_norm:\\s*([\\d.eE+-]+)
149149
- **响应式布局**: 根据图表数量自动调整单列/双列布局
150150

151151
### 🔬 专业对比分析
152-
- **三种对比模式**: Normal、Absolute、Relative差值分析
153-
- **统计指标**: Mean Difference、Mean Absolute Error、Mean Relative Error
152+
- **四种对比模式**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute)
153+
- **统计指标**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute)
154154
- **基准线设置**: 可配置对比基准线,突出显示显著差异
155155
- **差值可视化**: 专门的差值图表,清晰展示训练差异
156156

src/components/ChartContainer.jsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ export default function ChartContainer({
213213
case 'absolute':
214214
diff = Math.abs(v2 - v1);
215215
break;
216+
case 'relative-normal':
217+
diff = v1 !== 0 ? (v2 - v1) / v1 : 0;
218+
break;
216219
case 'relative': {
217220
const ad = Math.abs(v2 - v1);
218221
diff = v1 !== 0 ? ad / Math.abs(v1) : 0;
@@ -374,7 +377,12 @@ export default function ChartContainer({
374377

375378
const createComparisonChartData = (item1, item2, title) => {
376379
const comparisonData = getComparisonData(item1.data, item2.data, compareMode);
377-
const baseline = compareMode === 'relative' ? relativeBaseline : compareMode === 'absolute' ? absoluteBaseline : 0;
380+
const baseline =
381+
compareMode === 'relative' || compareMode === 'relative-normal'
382+
? relativeBaseline
383+
: compareMode === 'absolute'
384+
? absoluteBaseline
385+
: 0;
378386
const datasets = [
379387
{
380388
label: `${title} 差值`,
@@ -396,7 +404,7 @@ export default function ChartContainer({
396404
animations: { colors: false, x: false, y: false },
397405
},
398406
];
399-
if (baseline > 0 && (compareMode === 'relative' || compareMode === 'absolute')) {
407+
if (baseline > 0 && (compareMode === 'relative' || compareMode === 'relative-normal' || compareMode === 'absolute')) {
400408
const baselineData = comparisonData.map(p => ({ x: p.x, y: baseline }));
401409
datasets.push({
402410
label: 'Baseline',
@@ -477,11 +485,17 @@ export default function ChartContainer({
477485
if (showComparison) {
478486
const normalDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'normal');
479487
const absDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'absolute');
488+
const relNormalDiff = getComparisonData(
489+
dataArray[0].data,
490+
dataArray[1].data,
491+
'relative-normal'
492+
);
480493
const relDiff = getComparisonData(dataArray[0].data, dataArray[1].data, 'relative');
481494
const mean = arr => (arr.reduce((s, p) => s + p.y, 0) / arr.length) || 0;
482495
stats = {
483496
meanNormal: mean(normalDiff),
484497
meanAbsolute: mean(absDiff),
498+
relativeError: mean(relNormalDiff),
485499
meanRelative: mean(relDiff)
486500
};
487501
}
@@ -526,9 +540,10 @@ export default function ChartContainer({
526540
<div className="bg-white rounded-lg shadow-md p-3">
527541
<h4 className="text-sm font-medium text-gray-700 mb-1">{key} 差值统计</h4>
528542
<div className="space-y-1 text-xs">
529-
<p>Mean Difference: {stats.meanNormal.toFixed(6)}</p>
530-
<p>Mean Absolute Error: {stats.meanAbsolute.toFixed(6)}</p>
531-
<p>Mean Relative Error: {stats.meanRelative.toFixed(6)}</p>
543+
<p>平均误差 (normal): {stats.meanNormal.toFixed(6)}</p>
544+
<p>平均误差 (absolute): {stats.meanAbsolute.toFixed(6)}</p>
545+
<p>相对误差 (normal): {stats.relativeError.toFixed(6)}</p>
546+
<p>平均相对误差 (absolute): {stats.meanRelative.toFixed(6)}</p>
532547
</div>
533548
</div>
534549
)}

src/components/ComparisonControls.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ export function ComparisonControls({
66
onCompareModeChange
77
}) {
88
const modes = [
9-
{ value: 'normal', label: '📊 Normal', description: '原始差值' },
10-
{ value: 'absolute', label: '📈 Absolute', description: '绝对差值' },
11-
{ value: 'relative', label: '📉 Relative', description: '相对误差' }
9+
{ value: 'normal', label: '📊 平均误差 (normal)', description: '未取绝对值的平均误差' },
10+
{ value: 'absolute', label: '📈 平均误差 (absolute)', description: '绝对值差值的平均' },
11+
{ value: 'relative-normal', label: '📉 相对误差 (normal)', description: '不取绝对值的相对误差' },
12+
{ value: 'relative', label: '📊 平均相对误差 (absolute)', description: '绝对相对误差的平均' }
1213
];
1314

1415
return (

src/components/__tests__/ChartContainer.test.jsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,25 @@ function renderComponent(props = {}) {
5252
return { ...result, onXRangeChange, onMaxStepChange };
5353
}
5454

55-
it('shows empty message when no files', () => {
56-
renderComponent();
57-
expect(screen.getByText('📊 暂无数据')).toBeInTheDocument();
58-
});
55+
describe('ChartContainer', () => {
56+
it('shows empty message when no files', () => {
57+
renderComponent();
58+
expect(screen.getByText('📊 暂无数据')).toBeInTheDocument();
59+
});
5960

60-
it('shows metric selection message when no metrics', () => {
61-
renderComponent({ files: [sampleFile] });
62-
expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument();
63-
});
61+
it('shows metric selection message when no metrics', () => {
62+
renderComponent({ files: [sampleFile] });
63+
expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument();
64+
});
6465

65-
it('renders charts and triggers callbacks', async () => {
66-
const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [sampleFile], metrics: [metric] });
67-
expect(await screen.findByText('📊 loss')).toBeInTheDocument();
68-
await waitFor(() => {
69-
expect(onMaxStepChange).toHaveBeenCalledWith(1);
70-
expect(onXRangeChange).toHaveBeenCalled();
66+
it('renders charts and triggers callbacks', async () => {
67+
const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [sampleFile], metrics: [metric] });
68+
expect(await screen.findByText('📊 loss')).toBeInTheDocument();
69+
await waitFor(() => {
70+
expect(onMaxStepChange).toHaveBeenCalledWith(1);
71+
expect(onXRangeChange).toHaveBeenCalled();
72+
});
73+
const cb = onXRangeChange.mock.calls[0][0];
74+
expect(cb({})).toEqual({ min: 0, max: 1 });
7175
});
72-
const cb = onXRangeChange.mock.calls[0][0];
73-
expect(cb({})).toEqual({ min: 0, max: 1 });
7476
});

src/components/__tests__/FileList.test.jsx

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,37 @@ afterEach(() => {
1010
vi.restoreAllMocks();
1111
});
1212

13-
it('shows empty state when no files', () => {
14-
render(<FileList files={[]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
15-
expect(screen.getByText('📂 暂无文件')).toBeInTheDocument();
16-
});
17-
18-
it('renders file and triggers actions', async () => {
19-
const user = userEvent.setup();
20-
const file = { id: '1', name: 'test.log', enabled: true };
21-
const onFileRemove = vi.fn();
22-
const onFileToggle = vi.fn();
23-
const onFileConfig = vi.fn();
24-
render(<FileList files={[file]} onFileRemove={onFileRemove} onFileToggle={onFileToggle} onFileConfig={onFileConfig} />);
25-
26-
const checkbox = screen.getByRole('checkbox');
27-
await user.click(checkbox);
28-
expect(onFileToggle).toHaveBeenCalledWith(0, false);
29-
30-
const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` });
31-
await user.click(configButton);
32-
expect(onFileConfig).toHaveBeenCalledWith(file);
33-
34-
const removeButton = screen.getByRole('button', { name: `删除文件 ${file.name}` });
35-
await user.click(removeButton);
36-
expect(onFileRemove).toHaveBeenCalledWith(0);
37-
});
38-
39-
it('disables config when file disabled', () => {
40-
const file = { id: '2', name: 'off.log', enabled: false };
41-
render(<FileList files={[file]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
42-
const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` });
43-
expect(configButton).toBeDisabled();
13+
describe('FileList', () => {
14+
it('shows empty state when no files', () => {
15+
render(<FileList files={[]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
16+
expect(screen.getByText('📂 暂无文件')).toBeInTheDocument();
17+
});
18+
19+
it('renders file and triggers actions', async () => {
20+
const user = userEvent.setup();
21+
const file = { id: '1', name: 'test.log', enabled: true };
22+
const onFileRemove = vi.fn();
23+
const onFileToggle = vi.fn();
24+
const onFileConfig = vi.fn();
25+
render(<FileList files={[file]} onFileRemove={onFileRemove} onFileToggle={onFileToggle} onFileConfig={onFileConfig} />);
26+
27+
const checkbox = screen.getByRole('checkbox');
28+
await user.click(checkbox);
29+
expect(onFileToggle).toHaveBeenCalledWith(0, false);
30+
31+
const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` });
32+
await user.click(configButton);
33+
expect(onFileConfig).toHaveBeenCalledWith(file);
34+
35+
const removeButton = screen.getByRole('button', { name: `删除文件 ${file.name}` });
36+
await user.click(removeButton);
37+
expect(onFileRemove).toHaveBeenCalledWith(0);
38+
});
39+
40+
it('disables config when file disabled', () => {
41+
const file = { id: '2', name: 'off.log', enabled: false };
42+
render(<FileList files={[file]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
43+
const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` });
44+
expect(configButton).toBeDisabled();
45+
});
4446
});

src/components/__tests__/FileUpload.test.jsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,26 @@ function mockFileReader(text) {
1010
const readAsText = vi.fn(function () {
1111
this.onload({ target: { result: text } });
1212
});
13-
global.FileReader = vi.fn(() => ({ onload, readAsText }));
13+
globalThis.FileReader = vi.fn(() => ({ onload, readAsText }));
1414
}
1515

1616
afterEach(() => {
1717
vi.restoreAllMocks();
1818
});
1919

20-
it('uploads files and calls callback', async () => {
21-
const onFilesUploaded = vi.fn();
22-
mockFileReader('content');
23-
const file = new File(['content'], 'test.log', { type: 'text/plain' });
24-
render(<FileUpload onFilesUploaded={onFilesUploaded} />);
20+
describe('FileUpload', () => {
21+
it('uploads files and calls callback', async () => {
22+
const onFilesUploaded = vi.fn();
23+
mockFileReader('content');
24+
const file = new File(['content'], 'test.log', { type: 'text/plain' });
25+
render(<FileUpload onFilesUploaded={onFilesUploaded} />);
2526

26-
const input = screen.getByLabelText('选择日志文件,支持所有文本格式');
27-
await fireEvent.change(input, { target: { files: [file] } });
27+
const input = screen.getByLabelText('选择日志文件,支持所有文本格式');
28+
await fireEvent.change(input, { target: { files: [file] } });
2829

29-
await waitFor(() => expect(onFilesUploaded).toHaveBeenCalled());
30-
const uploaded = onFilesUploaded.mock.calls[0][0][0];
31-
expect(uploaded.name).toBe('test.log');
32-
expect(uploaded.content).toBe('content');
30+
await waitFor(() => expect(onFilesUploaded).toHaveBeenCalled());
31+
const uploaded = onFilesUploaded.mock.calls[0][0][0];
32+
expect(uploaded.name).toBe('test.log');
33+
expect(uploaded.content).toBe('content');
34+
});
3335
});

0 commit comments

Comments
 (0)