diff --git a/README.md b/README.md index 8e3f7d8..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) - **⚖️ 基准线设置**: 可配置相对误差和绝对误差的基准线 ### �️ **灵活的显示控制** @@ -149,8 +149,8 @@ gradient_norm:\\s*([\\d.eE+-]+) - **响应式布局**: 根据图表数量自动调整单列/双列布局 ### 🔬 专业对比分析 -- **三种对比模式**: Normal、Absolute、Relative差值分析 -- **统计指标**: Mean Difference、Mean Absolute Error、Mean Relative Error +- **四种对比模式**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute) +- **统计指标**: 平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute) - **基准线设置**: 可配置对比基准线,突出显示显著差异 - **差值可视化**: 专门的差值图表,清晰展示训练差异 diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index d2f9365..b10f0f5 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) }; } @@ -526,9 +540,10 @@ export default function ChartContainer({

{key} 差值统计

-

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

-

Mean Absolute Error: {stats.meanAbsolute.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)}

)} 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'); + }); });