Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
- **📈 Normal模式**: 原始差值分析 (File2 - File1)
- **📊 Absolute模式**: 绝对差值分析 |File2 - File1|
- **📉 Relative模式**: 相对差值百分比分析
- **📋 统计指标**: 详细的Mean Difference、Mean Absolute Error、Mean Relative Error
- **📋 统计指标**: 详细的平均误差(normal)、平均误差(absolute)、相对误差(normal)、平均相对误差(absolute)
- **⚖️ 基准线设置**: 可配置相对误差和绝对误差的基准线

### �️ **灵活的显示控制**
Expand Down Expand Up @@ -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)
- **基准线设置**: 可配置对比基准线,突出显示显著差异
- **差值可视化**: 专门的差值图表,清晰展示训练差异

Expand Down
25 changes: 20 additions & 5 deletions src/components/ChartContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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} 差值`,
Expand All @@ -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',
Expand Down Expand Up @@ -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)
};
}
Expand Down Expand Up @@ -526,9 +540,10 @@ export default function ChartContainer({
<div className="bg-white rounded-lg shadow-md p-3">
<h4 className="text-sm font-medium text-gray-700 mb-1">{key} 差值统计</h4>
<div className="space-y-1 text-xs">
<p>Mean Difference: {stats.meanNormal.toFixed(6)}</p>
<p>Mean Absolute Error: {stats.meanAbsolute.toFixed(6)}</p>
<p>Mean Relative Error: {stats.meanRelative.toFixed(6)}</p>
<p>平均误差 (normal): {stats.meanNormal.toFixed(6)}</p>
<p>平均误差 (absolute): {stats.meanAbsolute.toFixed(6)}</p>
<p>相对误差 (normal): {stats.relativeError.toFixed(6)}</p>
<p>平均相对误差 (absolute): {stats.meanRelative.toFixed(6)}</p>
</div>
</div>
)}
Expand Down
7 changes: 4 additions & 3 deletions src/components/ComparisonControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
34 changes: 18 additions & 16 deletions src/components/__tests__/ChartContainer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
64 changes: 33 additions & 31 deletions src/components/__tests__/FileList.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,37 @@ afterEach(() => {
vi.restoreAllMocks();
});

it('shows empty state when no files', () => {
render(<FileList files={[]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
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(<FileList files={[file]} onFileRemove={onFileRemove} onFileToggle={onFileToggle} onFileConfig={onFileConfig} />);

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(<FileList files={[file]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` });
expect(configButton).toBeDisabled();
describe('FileList', () => {
it('shows empty state when no files', () => {
render(<FileList files={[]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
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(<FileList files={[file]} onFileRemove={onFileRemove} onFileToggle={onFileToggle} onFileConfig={onFileConfig} />);

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(<FileList files={[file]} onFileRemove={vi.fn()} onFileToggle={vi.fn()} onFileConfig={vi.fn()} />);
const configButton = screen.getByRole('button', { name: `配置文件 ${file.name}` });
expect(configButton).toBeDisabled();
});
});
26 changes: 14 additions & 12 deletions src/components/__tests__/FileUpload.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<FileUpload onFilesUploaded={onFilesUploaded} />);
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(<FileUpload onFilesUploaded={onFilesUploaded} />);

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