From 2da0bd63cdf9b7215e6796b4be4207f54b79db12 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:50:46 +0800 Subject: [PATCH 1/2] feat: expand comparison options --- README.md | 4 +- src/components/ChartContainer.jsx | 19 +++++- src/components/ComparisonControls.jsx | 7 +- .../__tests__/ChartContainer.test.jsx | 34 +++++----- src/components/__tests__/FileList.test.jsx | 64 ++++++++++--------- src/components/__tests__/FileUpload.test.jsx | 26 ++++---- 6 files changed, 88 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 8e3f7d8..bbf37f7 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ gradient_norm:\\s*([\\d.eE+-]+) - **响应式布局**: 根据图表数量自动调整单列/双列布局 ### 🔬 专业对比分析 -- **三种对比模式**: Normal、Absolute、Relative差值分析 -- **统计指标**: Mean Difference、Mean Absolute Error、Mean Relative Error +- **四种对比模式**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute) +- **统计指标**: Mean Difference、Mean Absolute Error、Relative Error、Mean Relative Error - **基准线设置**: 可配置对比基准线,突出显示显著差异 - **差值可视化**: 专门的差值图表,清晰展示训练差异 diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index d2f9365..8b71941 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -213,6 +213,9 @@ export default function ChartContainer({ case 'absolute': diff = Math.abs(v2 - v1); break; + case 'relative-normal': + diff = v1 !== 0 ? (v2 - v1) / v1 : 0; + break; case 'relative': { const ad = Math.abs(v2 - v1); diff = v1 !== 0 ? ad / Math.abs(v1) : 0; @@ -374,7 +377,12 @@ export default function ChartContainer({ const createComparisonChartData = (item1, item2, title) => { const comparisonData = getComparisonData(item1.data, item2.data, compareMode); - const baseline = compareMode === 'relative' ? relativeBaseline : compareMode === 'absolute' ? absoluteBaseline : 0; + const baseline = + compareMode === 'relative' || compareMode === 'relative-normal' + ? relativeBaseline + : compareMode === 'absolute' + ? absoluteBaseline + : 0; const datasets = [ { label: `${title} 差值`, @@ -396,7 +404,7 @@ export default function ChartContainer({ animations: { colors: false, x: false, y: false }, }, ]; - if (baseline > 0 && (compareMode === 'relative' || compareMode === 'absolute')) { + if (baseline > 0 && (compareMode === 'relative' || compareMode === 'relative-normal' || compareMode === 'absolute')) { const baselineData = comparisonData.map(p => ({ x: p.x, y: baseline })); datasets.push({ label: 'Baseline', @@ -477,11 +485,17 @@ export default function ChartContainer({ 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) }; } @@ -528,6 +542,7 @@ export default function ChartContainer({

Mean Difference: {stats.meanNormal.toFixed(6)}

Mean Absolute Error: {stats.meanAbsolute.toFixed(6)}

+

Relative Error: {stats.relativeError.toFixed(6)}

Mean Relative Error: {stats.meanRelative.toFixed(6)}

diff --git a/src/components/ComparisonControls.jsx b/src/components/ComparisonControls.jsx index e2661ee..d844575 100644 --- a/src/components/ComparisonControls.jsx +++ b/src/components/ComparisonControls.jsx @@ -6,9 +6,10 @@ export function ComparisonControls({ onCompareModeChange }) { const modes = [ - { value: 'normal', label: '📊 Normal', description: '原始差值' }, - { value: 'absolute', label: '📈 Absolute', description: '绝对差值' }, - { value: 'relative', label: '📉 Relative', description: '相对误差' } + { value: 'normal', label: '📊 平均误差 (normal)', description: '未取绝对值的平均误差' }, + { value: 'absolute', label: '📈 平均误差 (absolute)', description: '绝对值差值的平均' }, + { value: 'relative-normal', label: '📉 相对误差 (normal)', description: '不取绝对值的相对误差' }, + { value: 'relative', label: '📊 平均相对误差 (absolute)', description: '绝对相对误差的平均' } ]; return ( diff --git a/src/components/__tests__/ChartContainer.test.jsx b/src/components/__tests__/ChartContainer.test.jsx index bb7039d..26543e1 100644 --- a/src/components/__tests__/ChartContainer.test.jsx +++ b/src/components/__tests__/ChartContainer.test.jsx @@ -52,23 +52,25 @@ function renderComponent(props = {}) { return { ...result, onXRangeChange, onMaxStepChange }; } -it('shows empty message when no files', () => { - renderComponent(); - expect(screen.getByText('📊 暂无数据')).toBeInTheDocument(); -}); +describe('ChartContainer', () => { + it('shows empty message when no files', () => { + renderComponent(); + expect(screen.getByText('📊 暂无数据')).toBeInTheDocument(); + }); -it('shows metric selection message when no metrics', () => { - renderComponent({ files: [sampleFile] }); - expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument(); -}); + it('shows metric selection message when no metrics', () => { + renderComponent({ files: [sampleFile] }); + expect(screen.getByText('🎯 请选择要显示的图表')).toBeInTheDocument(); + }); -it('renders charts and triggers callbacks', async () => { - const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [sampleFile], metrics: [metric] }); - expect(await screen.findByText('📊 loss')).toBeInTheDocument(); - await waitFor(() => { - expect(onMaxStepChange).toHaveBeenCalledWith(1); - expect(onXRangeChange).toHaveBeenCalled(); + it('renders charts and triggers callbacks', async () => { + const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [sampleFile], metrics: [metric] }); + expect(await screen.findByText('📊 loss')).toBeInTheDocument(); + await waitFor(() => { + expect(onMaxStepChange).toHaveBeenCalledWith(1); + expect(onXRangeChange).toHaveBeenCalled(); + }); + const cb = onXRangeChange.mock.calls[0][0]; + expect(cb({})).toEqual({ min: 0, max: 1 }); }); - const cb = onXRangeChange.mock.calls[0][0]; - expect(cb({})).toEqual({ min: 0, max: 1 }); }); diff --git a/src/components/__tests__/FileList.test.jsx b/src/components/__tests__/FileList.test.jsx index c65f91f..73a78bd 100644 --- a/src/components/__tests__/FileList.test.jsx +++ b/src/components/__tests__/FileList.test.jsx @@ -10,35 +10,37 @@ afterEach(() => { vi.restoreAllMocks(); }); -it('shows empty state when no files', () => { - render(); - expect(screen.getByText('📂 暂无文件')).toBeInTheDocument(); -}); - -it('renders file and triggers actions', async () => { - const user = userEvent.setup(); - const file = { id: '1', name: 'test.log', enabled: true }; - const onFileRemove = vi.fn(); - const onFileToggle = vi.fn(); - const onFileConfig = vi.fn(); - render(); - - const checkbox = screen.getByRole('checkbox'); - await user.click(checkbox); - expect(onFileToggle).toHaveBeenCalledWith(0, false); - - const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` }); - await user.click(configButton); - expect(onFileConfig).toHaveBeenCalledWith(file); - - const removeButton = screen.getByRole('button', { name: `删除文件 ${file.name}` }); - await user.click(removeButton); - expect(onFileRemove).toHaveBeenCalledWith(0); -}); - -it('disables config when file disabled', () => { - const file = { id: '2', name: 'off.log', enabled: false }; - render(); - const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` }); - expect(configButton).toBeDisabled(); +describe('FileList', () => { + it('shows empty state when no files', () => { + render(); + expect(screen.getByText('📂 暂无文件')).toBeInTheDocument(); + }); + + it('renders file and triggers actions', async () => { + const user = userEvent.setup(); + const file = { id: '1', name: 'test.log', enabled: true }; + const onFileRemove = vi.fn(); + const onFileToggle = vi.fn(); + const onFileConfig = vi.fn(); + render(); + + const checkbox = screen.getByRole('checkbox'); + await user.click(checkbox); + expect(onFileToggle).toHaveBeenCalledWith(0, false); + + const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` }); + await user.click(configButton); + expect(onFileConfig).toHaveBeenCalledWith(file); + + const removeButton = screen.getByRole('button', { name: `删除文件 ${file.name}` }); + await user.click(removeButton); + expect(onFileRemove).toHaveBeenCalledWith(0); + }); + + it('disables config when file disabled', () => { + const file = { id: '2', name: 'off.log', enabled: false }; + render(); + const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` }); + expect(configButton).toBeDisabled(); + }); }); diff --git a/src/components/__tests__/FileUpload.test.jsx b/src/components/__tests__/FileUpload.test.jsx index 0591f90..f8d1ec8 100644 --- a/src/components/__tests__/FileUpload.test.jsx +++ b/src/components/__tests__/FileUpload.test.jsx @@ -10,24 +10,26 @@ function mockFileReader(text) { const readAsText = vi.fn(function () { this.onload({ target: { result: text } }); }); - global.FileReader = vi.fn(() => ({ onload, readAsText })); + globalThis.FileReader = vi.fn(() => ({ onload, readAsText })); } afterEach(() => { vi.restoreAllMocks(); }); -it('uploads files and calls callback', async () => { - const onFilesUploaded = vi.fn(); - mockFileReader('content'); - const file = new File(['content'], 'test.log', { type: 'text/plain' }); - render(); +describe('FileUpload', () => { + it('uploads files and calls callback', async () => { + const onFilesUploaded = vi.fn(); + mockFileReader('content'); + const file = new File(['content'], 'test.log', { type: 'text/plain' }); + render(); - const input = screen.getByLabelText('选择日志文件,支持所有文本格式'); - await fireEvent.change(input, { target: { files: [file] } }); + const input = screen.getByLabelText('选择日志文件,支持所有文本格式'); + await fireEvent.change(input, { target: { files: [file] } }); - await waitFor(() => expect(onFilesUploaded).toHaveBeenCalled()); - const uploaded = onFilesUploaded.mock.calls[0][0][0]; - expect(uploaded.name).toBe('test.log'); - expect(uploaded.content).toBe('content'); + await waitFor(() => expect(onFilesUploaded).toHaveBeenCalled()); + const uploaded = onFilesUploaded.mock.calls[0][0][0]; + expect(uploaded.name).toBe('test.log'); + expect(uploaded.content).toBe('content'); + }); }); From 65acd127ccf702c203068b2a23ea3d3af79e9a32 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:09:42 +0800 Subject: [PATCH 2/2] Update difference stats labels --- README.md | 4 ++-- src/components/ChartContainer.jsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bbf37f7..fb75caf 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ - **📈 Normal模式**: 原始差值分析 (File2 - File1) - **📊 Absolute模式**: 绝对差值分析 |File2 - File1| - **📉 Relative模式**: 相对差值百分比分析 -- **📋 统计指标**: 详细的Mean Difference、Mean Absolute Error、Mean Relative Error +- **📋 统计指标**: 详细的平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute) - **⚖️ 基准线设置**: 可配置相对误差和绝对误差的基准线 ### �️ **灵活的显示控制** @@ -150,7 +150,7 @@ gradient_norm:\\s*([\\d.eE+-]+) ### 🔬 专业对比分析 - **四种对比模式**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute) -- **统计指标**: Mean Difference、Mean Absolute Error、Relative Error、Mean Relative Error +- **统计指标**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute) - **基准线设置**: 可配置对比基准线,突出显示显著差异 - **差值可视化**: 专门的差值图表,清晰展示训练差异 diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index 8b71941..b10f0f5 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -540,10 +540,10 @@ export default function ChartContainer({

{key} 差值统计

-

Mean Difference: {stats.meanNormal.toFixed(6)}

-

Mean Absolute Error: {stats.meanAbsolute.toFixed(6)}

-

Relative Error: {stats.relativeError.toFixed(6)}

-

Mean Relative Error: {stats.meanRelative.toFixed(6)}

+

平均误差 (normal): {stats.meanNormal.toFixed(6)}

+

平均误差 (absolute): {stats.meanAbsolute.toFixed(6)}

+

相对误差 (normal): {stats.relativeError.toFixed(6)}

+

平均相对误差 (absolute): {stats.meanRelative.toFixed(6)}

)}