Skip to content

Commit 234d427

Browse files
committed
feat: align data by step keyword
1 parent 7d88e19 commit 234d427

File tree

4 files changed

+143
-58
lines changed

4 files changed

+143
-58
lines changed

src/App.jsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ function App() {
2727
keyword: 'norm:',
2828
regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'
2929
}
30-
]
30+
],
31+
stepKeyword: 'step:',
32+
useStepKeyword: false
3133
});
3234

3335
const [compareMode, setCompareMode] = useState('normal');
@@ -117,16 +119,18 @@ function App() {
117119

118120
// 全局解析配置变更处理
119121
const handleGlobalParsingConfigChange = useCallback((newConfig) => {
120-
setGlobalParsingConfig(newConfig);
122+
setGlobalParsingConfig(prev => ({ ...prev, ...newConfig }));
121123

122-
// 同步所有文件的解析配置
123-
setUploadedFiles(prev => prev.map(file => ({
124-
...file,
125-
config: {
126-
...file.config,
127-
metrics: newConfig.metrics.map(m => ({ ...m }))
128-
}
129-
})));
124+
// 如果更新了指标配置,同步到所有文件
125+
if (newConfig.metrics) {
126+
setUploadedFiles(prev => prev.map(file => ({
127+
...file,
128+
config: {
129+
...file.config,
130+
metrics: newConfig.metrics.map(m => ({ ...m }))
131+
}
132+
})));
133+
}
130134
}, []);
131135

132136
// 全局拖拽事件处理
@@ -414,6 +418,8 @@ function App() {
414418
xRange={xRange}
415419
onXRangeChange={setXRange}
416420
onMaxStepChange={setMaxStep}
421+
stepKeyword={globalParsingConfig.stepKeyword}
422+
useStepKeyword={globalParsingConfig.useStepKeyword}
417423
/>
418424
</section>
419425
</main>

src/components/ChartContainer.jsx

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
4040
...options,
4141
onHover: (event, activeElements) => {
4242
if (activeElements.length > 0) {
43-
const step = activeElements[0].index;
43+
const el = activeElements[0].element;
44+
const step = el?.$context?.parsed?.x ?? activeElements[0].index;
4445
onSyncHover(step, chartId);
4546
} else {
4647
onSyncHover(null, chartId);
@@ -68,7 +69,9 @@ export default function ChartContainer({
6869
absoluteBaseline = 0.005,
6970
xRange = { min: undefined, max: undefined },
7071
onXRangeChange,
71-
onMaxStepChange
72+
onMaxStepChange,
73+
stepKeyword = 'step:',
74+
useStepKeyword = false
7275
}) {
7376
const chartRefs = useRef(new Map());
7477
const registerChart = useCallback((id, inst) => {
@@ -85,8 +88,9 @@ export default function ChartContainer({
8588
} else if (id !== sourceId) {
8689
const activeElements = [];
8790
chart.data.datasets.forEach((dataset, datasetIndex) => {
88-
if (dataset.data && dataset.data.length > step) {
89-
activeElements.push({ datasetIndex, index: step });
91+
const idx = dataset.data.findIndex(p => p.x === step);
92+
if (idx !== -1) {
93+
activeElements.push({ datasetIndex, index: idx });
9094
}
9195
});
9296
chart.setActiveElements(activeElements);
@@ -100,62 +104,73 @@ export default function ChartContainer({
100104
const enabled = files.filter(f => f.enabled !== false);
101105
return enabled.map(file => {
102106
if (!file.content) return { ...file, metricsData: {} };
107+
103108
const lines = file.content.split('\n');
104109
const metricsData = {};
110+
metrics.forEach(metric => {
111+
metricsData[metric.name || metric.keyword] = [];
112+
});
105113

106-
const extractByKeyword = (content, keyword) => {
107-
const results = [];
108-
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
109-
content.split('\n').forEach(line => {
110-
const idx = line.toLowerCase().indexOf(keyword.toLowerCase());
111-
if (idx !== -1) {
112-
const after = line.substring(idx + keyword.length);
113-
const match = after.match(numberRegex);
114-
if (match) {
115-
const v = parseFloat(match[0]);
116-
if (!isNaN(v)) results.push(v);
117-
}
118-
}
119-
});
120-
return results;
121-
};
114+
const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
115+
const stepReg = useStepKeyword && stepKeyword
116+
? new RegExp(`${escapeRegex(stepKeyword)}\\s*\\[?\\s*(\\d+)`, 'i')
117+
: null;
118+
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
122119

123-
metrics.forEach(metric => {
124-
let values = [];
125-
if (metric.mode === 'keyword') {
126-
values = extractByKeyword(file.content, metric.keyword);
127-
} else if (metric.regex) {
128-
const reg = new RegExp(metric.regex);
129-
lines.forEach(line => {
130-
reg.lastIndex = 0;
120+
lines.forEach(line => {
121+
const stepMatch = stepReg ? stepReg.exec(line) : null;
122+
const stepVal = stepMatch ? parseInt(stepMatch[1]) : null;
123+
124+
metrics.forEach(metric => {
125+
let value;
126+
if (metric.mode === 'keyword' && metric.keyword) {
127+
const idx = line.toLowerCase().indexOf(metric.keyword.toLowerCase());
128+
if (idx !== -1) {
129+
const after = line.substring(idx + metric.keyword.length);
130+
const match = after.match(numberRegex);
131+
if (match) {
132+
const v = parseFloat(match[0]);
133+
if (!isNaN(v)) value = v;
134+
}
135+
}
136+
} else if (metric.regex) {
137+
const reg = new RegExp(metric.regex);
131138
const m = reg.exec(line);
132139
if (m && m[1]) {
133140
const v = parseFloat(m[1]);
134-
if (!isNaN(v)) values.push(v);
141+
if (!isNaN(v)) value = v;
135142
}
136-
});
137-
}
138-
metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v }));
143+
}
144+
145+
if (value !== undefined) {
146+
const arr = metricsData[metric.name || metric.keyword];
147+
if (useStepKeyword) {
148+
if (stepVal !== null) arr.push({ x: stepVal, y: value });
149+
} else {
150+
arr.push({ x: arr.length, y: value });
151+
}
152+
}
153+
});
139154
});
140155

141156
const range = file.config?.dataRange;
142157
if (range && (range.start > 0 || range.end !== undefined)) {
143-
const applyRange = data => {
144-
if (data.length === 0) return data;
158+
Object.keys(metricsData).forEach(k => {
159+
const data = metricsData[k];
160+
if (data.length === 0) return;
145161
const start = Math.max(0, parseInt(range.start) || 0);
146162
const end = range.end !== undefined ? parseInt(range.end) : data.length;
147163
const endIndex = Math.min(data.length, end);
148-
return data.slice(start, endIndex);
149-
};
150-
const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y }));
151-
Object.keys(metricsData).forEach(k => {
152-
metricsData[k] = reindex(applyRange(metricsData[k]));
164+
const sliced = data.slice(start, endIndex);
165+
metricsData[k] = useStepKeyword
166+
? sliced
167+
: sliced.map((p, idx) => ({ x: idx, y: p.y }));
153168
});
154169
}
155170

156171
return { ...file, metricsData };
157172
});
158-
}, [files, metrics]);
173+
}, [files, metrics, stepKeyword, useStepKeyword]);
159174

160175
useEffect(() => {
161176
const maxStep = parsedData.reduce((m, f) => {
@@ -166,15 +181,33 @@ export default function ChartContainer({
166181
}, [parsedData, onMaxStepChange]);
167182

168183
useEffect(() => {
169-
const minSteps = getMinSteps(parsedData);
170-
if (minSteps > 0) {
171-
onXRangeChange(prev => {
172-
const next = { min: 0, max: minSteps - 1 };
173-
if (prev.min === next.min && prev.max === next.max) return prev;
174-
return next;
184+
if (useStepKeyword) {
185+
const ranges = [];
186+
parsedData.forEach(f => {
187+
Object.values(f.metricsData).forEach(d => {
188+
if (d.length > 0) ranges.push({ min: d[0].x, max: d[d.length - 1].x });
189+
});
175190
});
191+
if (ranges.length > 0) {
192+
const globalMin = Math.min(...ranges.map(r => r.min));
193+
const globalMax = Math.max(...ranges.map(r => r.max));
194+
onXRangeChange(prev => {
195+
const next = { min: globalMin, max: globalMax };
196+
if (prev.min === next.min && prev.max === next.max) return prev;
197+
return next;
198+
});
199+
}
200+
} else {
201+
const minSteps = getMinSteps(parsedData);
202+
if (minSteps > 0) {
203+
onXRangeChange(prev => {
204+
const next = { min: 0, max: minSteps - 1 };
205+
if (prev.min === next.min && prev.max === next.max) return prev;
206+
return next;
207+
});
208+
}
176209
}
177-
}, [parsedData, onXRangeChange]);
210+
}, [parsedData, onXRangeChange, useStepKeyword]);
178211

179212
const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#f97316'];
180213
const createChartData = dataArray => ({

src/components/RegexControls.jsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,28 @@ export function RegexControls({
447447
</button>
448448
</div>
449449
</div>
450-
450+
451451
<div className="space-y-4">
452+
<div className="border rounded-lg p-3">
453+
<label className="flex items-center gap-2 text-sm">
454+
<input
455+
type="checkbox"
456+
checked={globalParsingConfig.useStepKeyword || false}
457+
onChange={(e) => onGlobalParsingConfigChange({ useStepKeyword: e.target.checked })}
458+
/>
459+
根据关键词确定步数
460+
</label>
461+
{globalParsingConfig.useStepKeyword && (
462+
<input
463+
type="text"
464+
className="mt-2 w-full px-2 py-1 text-xs border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none"
465+
value={globalParsingConfig.stepKeyword || ''}
466+
onChange={(e) => onGlobalParsingConfigChange({ stepKeyword: e.target.value })}
467+
placeholder="step:"
468+
/>
469+
)}
470+
</div>
471+
452472
{globalParsingConfig.metrics.map((cfg, idx) => (
453473
<div key={idx} className="border rounded-lg p-3 relative">
454474
<button

src/components/__tests__/ChartContainer.test.jsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,30 @@ describe('ChartContainer', () => {
165165
opts.plugins.zoom.pan.onPanComplete({ chart: { scales: { x: { min: 0, max: 10 } } } });
166166
opts.plugins.zoom.zoom.onZoomComplete({ chart: { scales: { x: { min: 2, max: 4 } } } });
167167
});
168+
169+
it('uses step keyword to place data points', () => {
170+
const onXRangeChange = vi.fn(fn => fn({}));
171+
const onMaxStepChange = vi.fn();
172+
const files = [
173+
{ name: 'a.log', enabled: true, content: 'step:[2/350], loss: 1\nstep:[3/350], loss: 2' }
174+
];
175+
render(
176+
<ChartContainer
177+
files={files}
178+
metrics={[{ name: 'loss', keyword: 'loss', mode: 'keyword' }]}
179+
compareMode="normal"
180+
onXRangeChange={onXRangeChange}
181+
onMaxStepChange={onMaxStepChange}
182+
useStepKeyword
183+
stepKeyword="step:"
184+
/>
185+
);
186+
187+
const data = __lineProps[__lineProps.length - 1].data.datasets[0].data;
188+
expect(data[0].x).toBe(2);
189+
expect(data[1].x).toBe(3);
190+
expect(onMaxStepChange).toHaveBeenCalledWith(3);
191+
const lastCall = onXRangeChange.mock.calls[onXRangeChange.mock.calls.length - 1][0];
192+
expect(lastCall({})).toEqual({ min: 2, max: 3 });
193+
});
168194
});

0 commit comments

Comments
 (0)