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