Skip to content

Commit 13551ba

Browse files
committed
feat: show step range and align comparison
1 parent 7e64719 commit 13551ba

5 files changed

Lines changed: 130 additions & 57 deletions

File tree

src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ function App() {
451451
onClose={handleConfigClose}
452452
onSave={handleConfigSave}
453453
globalParsingConfig={globalParsingConfig}
454+
stepKeyword={stepKeyword}
454455
/>
455456
</div>
456457
);

src/components/ChartContainer.jsx

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,33 @@ ChartJS.register(
2626
zoomPlugin
2727
);
2828

29+
export const getComparisonData = (data1, data2, mode) => {
30+
const map1 = new Map(data1.map(p => [p.x, p.y]));
31+
const map2 = new Map(data2.map(p => [p.x, p.y]));
32+
const steps = [...map1.keys()].filter(k => map2.has(k)).sort((a, b) => a - b);
33+
return steps.map(step => {
34+
const v1 = map1.get(step);
35+
const v2 = map2.get(step);
36+
let diff;
37+
switch (mode) {
38+
case 'absolute':
39+
diff = Math.abs(v2 - v1);
40+
break;
41+
case 'relative-normal':
42+
diff = v1 !== 0 ? (v2 - v1) / v1 : 0;
43+
break;
44+
case 'relative': {
45+
const ad = Math.abs(v2 - v1);
46+
diff = v1 !== 0 ? ad / Math.abs(v1) : 0;
47+
break;
48+
}
49+
default:
50+
diff = v2 - v1;
51+
}
52+
return { x: step, y: diff };
53+
});
54+
};
55+
2956
const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover }) => {
3057
const chartRef = useRef(null);
3158

@@ -248,58 +275,31 @@ export default function ChartContainer({
248275
}
249276
}, [parsedData, onXRangeChange, useStepKeyword]);
250277

251-
const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#f97316'];
252-
const createChartData = dataArray => ({
253-
datasets: dataArray.map((item, index) => {
254-
const color = colors[index % colors.length];
255-
return {
256-
label: item.name?.replace(/\.(log|txt)$/i, '') || `File ${index + 1}`,
257-
data: item.data,
258-
borderColor: color,
259-
backgroundColor: `${color}33`,
260-
borderWidth: 2,
261-
fill: false,
262-
tension: 0,
263-
pointRadius: 0,
264-
pointHoverRadius: 4,
265-
pointBackgroundColor: color,
266-
pointBorderColor: color,
267-
pointBorderWidth: 1,
268-
pointHoverBackgroundColor: color,
269-
pointHoverBorderColor: color,
270-
pointHoverBorderWidth: 1,
271-
animation: false,
272-
animations: { colors: false, x: false, y: false },
273-
};
274-
})
275-
});
276-
277-
const getComparisonData = (data1, data2, mode) => {
278-
const minLength = Math.min(data1.length, data2.length);
279-
const result = [];
280-
for (let i = 0; i < minLength; i++) {
281-
const v1 = data1[i].y;
282-
const v2 = data2[i].y;
283-
let diff;
284-
switch (mode) {
285-
case 'absolute':
286-
diff = Math.abs(v2 - v1);
287-
break;
288-
case 'relative-normal':
289-
diff = v1 !== 0 ? (v2 - v1) / v1 : 0;
290-
break;
291-
case 'relative': {
292-
const ad = Math.abs(v2 - v1);
293-
diff = v1 !== 0 ? ad / Math.abs(v1) : 0;
294-
break;
295-
}
296-
default:
297-
diff = v2 - v1;
298-
}
299-
result.push({ x: i, y: diff });
300-
}
301-
return result;
302-
};
278+
const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#f97316'];
279+
const createChartData = dataArray => ({
280+
datasets: dataArray.map((item, index) => {
281+
const color = colors[index % colors.length];
282+
return {
283+
label: item.name?.replace(/\.(log|txt)$/i, '') || `File ${index + 1}`,
284+
data: item.data,
285+
borderColor: color,
286+
backgroundColor: `${color}33`,
287+
borderWidth: 2,
288+
fill: false,
289+
tension: 0,
290+
pointRadius: 0,
291+
pointHoverRadius: 4,
292+
pointBackgroundColor: color,
293+
pointBorderColor: color,
294+
pointBorderWidth: 1,
295+
pointHoverBackgroundColor: color,
296+
pointHoverBorderColor: color,
297+
pointHoverBorderWidth: 1,
298+
animation: false,
299+
animations: { colors: false, x: false, y: false },
300+
};
301+
})
302+
});
303303

304304
const calculateYRange = useCallback((dataArray) => {
305305
let min = Infinity;

src/components/FileConfigModal.jsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function getMetricTitle(metric, index) {
3434
return `Metric ${index + 1}`;
3535
}
3636

37-
export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingConfig }) {
37+
export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingConfig, stepKeyword = 'step:' }) {
3838
const [config, setConfig] = useState({
3939
metrics: [],
4040
dataRange: {
@@ -43,6 +43,7 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo
4343
useRange: false // 保留用于向后兼容
4444
}
4545
});
46+
const [stepRange, setStepRange] = useState(null);
4647

4748
useEffect(() => {
4849
if (file && isOpen) {
@@ -56,8 +57,38 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo
5657
useRange: false
5758
}
5859
});
60+
61+
// 计算日志文件包含的 step 范围
62+
if (file.content) {
63+
const lines = file.content.split('\n');
64+
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
65+
const keywordLower = stepKeyword.toLowerCase();
66+
let min = Infinity;
67+
let max = -Infinity;
68+
lines.forEach(line => {
69+
const idx = line.toLowerCase().indexOf(keywordLower);
70+
if (idx !== -1) {
71+
const after = line.substring(idx + stepKeyword.length);
72+
const match = after.match(numberRegex);
73+
if (match) {
74+
const v = parseFloat(match[0]);
75+
if (!isNaN(v)) {
76+
if (v < min) min = v;
77+
if (v > max) max = v;
78+
}
79+
}
80+
}
81+
});
82+
if (min !== Infinity && max !== -Infinity) {
83+
setStepRange({ start: min, end: max });
84+
} else {
85+
setStepRange(null);
86+
}
87+
} else {
88+
setStepRange(null);
89+
}
5990
}
60-
}, [file, isOpen, globalParsingConfig]);
91+
}, [file, isOpen, globalParsingConfig, stepKeyword]);
6192

6293
const handleSave = () => {
6394
onSave(file.id, config);
@@ -256,7 +287,12 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo
256287
<p className="text-sm text-gray-600">
257288
配置要显示的数据点范围。默认显示全部数据(从第一个到最后一个数据点)。
258289
</p>
259-
290+
{stepRange && (
291+
<p className="text-sm text-blue-600">
292+
当前日志包含步骤: {stepRange.start} - {stepRange.end}
293+
</p>
294+
)}
295+
260296
<div className="bg-gray-50 p-4 rounded-lg border">
261297
<div className="grid grid-cols-2 gap-4">
262298
<div>

src/components/__tests__/ChartContainer.test.jsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ vi.mock('chart.js', () => {
2626

2727
vi.mock('chartjs-plugin-zoom', () => ({ default: {} }));
2828

29-
import ChartContainer from '../ChartContainer.jsx';
29+
import ChartContainer, { getComparisonData } from '../ChartContainer.jsx';
3030

3131
const sampleFile = {
3232
name: 'test.log',
@@ -74,7 +74,7 @@ describe('ChartContainer', () => {
7474
expect(cb({})).toEqual({ min: 0, max: 1 });
7575
});
7676

77-
it('uses step keyword for x positions when enabled', async () => {
77+
it('uses step keyword for x positions when enabled', async () => {
7878
const file = { name: 's.log', id: '2', content: 'step: 2 loss: 1\nstep: 4 loss: 2' };
7979
const { onXRangeChange, onMaxStepChange } = renderComponent({ files: [file], metrics: [metric], useStepKeyword: true, stepKeyword: 'step:' });
8080
await waitFor(() => {
@@ -84,4 +84,14 @@ describe('ChartContainer', () => {
8484
const cb = onXRangeChange.mock.calls[0][0];
8585
expect(cb({})).toEqual({ min: 2, max: 4 });
8686
});
87+
88+
it('computes comparison only on overlapping steps', () => {
89+
const d1 = [{ x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }];
90+
const d2 = [{ x: 2, y: 2 }, { x: 3, y: 4 }, { x: 4, y: 5 }];
91+
const res = getComparisonData(d1, d2, 'normal');
92+
expect(res).toEqual([
93+
{ x: 2, y: 0 },
94+
{ x: 3, y: 1 }
95+
]);
96+
});
8797
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect } from 'vitest';
4+
import '@testing-library/jest-dom/vitest';
5+
import { FileConfigModal } from '../FileConfigModal.jsx';
6+
7+
describe('FileConfigModal', () => {
8+
it('displays step range for log file', () => {
9+
const file = {
10+
id: '1',
11+
name: 'a.log',
12+
content: 'step: 70 loss: 1\nstep: 210 loss: 2'
13+
};
14+
render(
15+
<FileConfigModal
16+
file={file}
17+
isOpen={true}
18+
onClose={() => {}}
19+
onSave={() => {}}
20+
globalParsingConfig={{ metrics: [] }}
21+
stepKeyword="step:"
22+
/>
23+
);
24+
expect(screen.getByText('当前日志包含步骤: 70 - 210')).toBeInTheDocument();
25+
});
26+
});

0 commit comments

Comments
 (0)