Skip to content

Commit 5371231

Browse files
committed
feat: support dynamic metric configs
1 parent 26eb13f commit 5371231

3 files changed

Lines changed: 168 additions & 18 deletions

File tree

src/App.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ function App() {
2121
mode: 'keyword', // 'keyword' | 'regex'
2222
keyword: 'norm:',
2323
regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'
24-
}
24+
},
25+
others: [] // 其他自定义指标解析配置
2526
});
2627

2728
// 兼容旧版本的正则表达式状态(供ChartContainer使用)
@@ -48,6 +49,7 @@ function App() {
4849
// 使用全局解析配置作为默认值
4950
loss: { ...globalParsingConfig.loss },
5051
gradNorm: { ...globalParsingConfig.gradNorm },
52+
others: globalParsingConfig.others.map(o => ({ ...o })),
5153
dataRange: {
5254
start: 0, // 默认从第一个数据点开始
5355
end: undefined, // 默认到最后一个数据点
@@ -129,7 +131,8 @@ function App() {
129131
config: {
130132
...file.config,
131133
loss: { ...newConfig.loss },
132-
gradNorm: { ...newConfig.gradNorm }
134+
gradNorm: { ...newConfig.gradNorm },
135+
others: newConfig.others.map(o => ({ ...o }))
133136
}
134137
})));
135138
}, []);
@@ -440,6 +443,7 @@ function App() {
440443
files={uploadedFiles}
441444
lossRegex={lossRegex}
442445
gradNormRegex={gradNormRegex}
446+
otherConfigs={globalParsingConfig.others}
443447
compareMode={compareMode}
444448
relativeBaseline={relativeBaseline}
445449
absoluteBaseline={absoluteBaseline}

src/components/ChartContainer.jsx

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,10 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
7878
};
7979

8080
export default function ChartContainer({
81-
files,
82-
lossRegex,
83-
gradNormRegex,
81+
files,
82+
lossRegex,
83+
gradNormRegex,
84+
otherConfigs = [],
8485
compareMode,
8586
relativeBaseline = 0.002,
8687
absoluteBaseline = 0.005,
@@ -138,13 +139,35 @@ export default function ChartContainer({
138139
const enabledFiles = files.filter(file => file.enabled !== false);
139140

140141
return enabledFiles.map(file => {
141-
if (!file.content) return { ...file, lossData: [], gradNormData: [] };
142+
if (!file.content) return { ...file, lossData: [], gradNormData: [], othersData: {} };
142143

143144
const lines = file.content.split('\n');
144145
const lossData = [];
145146
const gradNormData = [];
147+
const otherMetricData = {};
146148

147149
try {
150+
// 公用关键词匹配函数
151+
const extractByKeyword = (content, keyword) => {
152+
const results = [];
153+
const lines = content.split('\n');
154+
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
155+
lines.forEach((line) => {
156+
const keywordIndex = line.toLowerCase().indexOf(keyword.toLowerCase());
157+
if (keywordIndex !== -1) {
158+
const afterKeyword = line.substring(keywordIndex + keyword.length);
159+
const numberMatch = afterKeyword.match(numberRegex);
160+
if (numberMatch) {
161+
const value = parseFloat(numberMatch[0]);
162+
if (!isNaN(value)) {
163+
results.push(value);
164+
}
165+
}
166+
}
167+
});
168+
return results;
169+
};
170+
148171
// 使用新的配置格式,同时保持向后兼容
149172
let fileLossConfig, fileGradNormConfig;
150173

@@ -265,6 +288,29 @@ export default function ChartContainer({
265288
}
266289
});
267290
}
291+
292+
// 处理其他自定义指标
293+
if (Array.isArray(file.config?.others)) {
294+
file.config.others.forEach(metric => {
295+
let values = [];
296+
if (metric.mode === 'keyword') {
297+
values = extractByKeyword(file.content, metric.keyword);
298+
} else {
299+
const regexObj = new RegExp(metric.regex);
300+
lines.forEach(line => {
301+
regexObj.lastIndex = 0;
302+
const match = regexObj.exec(line);
303+
if (match && match[1]) {
304+
const value = parseFloat(match[1]);
305+
if (!isNaN(value)) {
306+
values.push(value);
307+
}
308+
}
309+
});
310+
}
311+
otherMetricData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v }));
312+
});
313+
}
268314
} catch (error) {
269315
console.error('Regex error:', error);
270316
}
@@ -291,28 +337,34 @@ export default function ChartContainer({
291337
return slicedData;
292338
};
293339

340+
const reindexData = (data) => data.map((point, index) => ({ x: index, y: point.y }));
341+
294342
const filteredLossData = applyRangeFilter(lossData);
295343
const filteredGradNormData = applyRangeFilter(gradNormData);
344+
const filteredOthers = {};
345+
Object.entries(otherMetricData).forEach(([key, data]) => {
346+
filteredOthers[key] = reindexData(applyRangeFilter(data));
347+
});
296348

297-
// 重新索引数据点
298-
const reindexData = (data) => data.map((point, index) => ({ x: index, y: point.y }));
299-
300-
return {
301-
...file,
302-
lossData: reindexData(filteredLossData),
303-
gradNormData: reindexData(filteredGradNormData)
349+
return {
350+
...file,
351+
lossData: reindexData(filteredLossData),
352+
gradNormData: reindexData(filteredGradNormData),
353+
othersData: filteredOthers
304354
};
305355
}
306356

307-
return { ...file, lossData, gradNormData };
357+
return { ...file, lossData, gradNormData, othersData: otherMetricData };
308358
});
309-
}, [files, lossRegex, gradNormRegex]);
359+
// eslint-disable-next-line react-hooks/exhaustive-deps
360+
}, [files, lossRegex, gradNormRegex, otherConfigs]);
310361

311362
useEffect(() => {
312363
const maxStep = parsedData.reduce((max, file) => {
313364
const maxLoss = file.lossData.length > 0 ? file.lossData[file.lossData.length - 1].x : 0;
314365
const maxGrad = file.gradNormData.length > 0 ? file.gradNormData[file.gradNormData.length - 1].x : 0;
315-
return Math.max(max, maxLoss, maxGrad);
366+
const otherMax = Object.values(file.othersData || {}).reduce((m, data) => Math.max(m, data.length > 0 ? data[data.length - 1].x : 0), 0);
367+
return Math.max(max, maxLoss, maxGrad, otherMax);
316368
}, 0);
317369
onMaxStepChange(maxStep);
318370
}, [parsedData, onMaxStepChange]);
@@ -655,6 +707,14 @@ export default function ChartContainer({
655707
.filter(file => file.gradNormData && file.gradNormData.length > 0)
656708
.map(file => ({ name: file.name, data: file.gradNormData }));
657709

710+
const otherMetricKeys = otherConfigs.map(c => c.name || c.keyword);
711+
const otherDataArrays = {};
712+
otherMetricKeys.forEach(key => {
713+
otherDataArrays[key] = parsedData
714+
.filter(file => file.othersData && file.othersData[key] && file.othersData[key].length > 0)
715+
.map(file => ({ name: file.name, data: file.othersData[key] }));
716+
});
717+
658718
// 计算显示的图表数量来决定布局
659719
const enabledFiles = files.filter(file => file.enabled !== false);
660720
const showingLossCharts = showLoss && lossDataArray.length > 0;
@@ -837,6 +897,25 @@ export default function ChartContainer({
837897
</div>
838898
</div>
839899
)}
900+
{otherMetricKeys.length > 0 && (
901+
<div className="col-span-full overflow-x-auto">
902+
<div className="flex gap-3 w-max">
903+
{otherMetricKeys.map((key, idx) => (
904+
<div key={key} className="w-96">
905+
<ResizablePanel title={key} initialHeight={440}>
906+
<ChartWrapper
907+
chartId={`other-${idx}`}
908+
onRegisterChart={registerChart}
909+
onSyncHover={syncHoverToAllCharts}
910+
data={createChartData(otherDataArrays[key])}
911+
options={chartOptions}
912+
/>
913+
</ResizablePanel>
914+
</div>
915+
))}
916+
</div>
917+
</div>
918+
)}
840919
</div>
841920
);
842921
}

src/components/RegexControls.jsx

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export function RegexControls({
210210

211211
// 预览匹配结果
212212
const previewMatches = useCallback(() => {
213-
const results = { loss: [], gradNorm: [] };
213+
const results = { loss: [], gradNorm: [], others: {} };
214214

215215
uploadedFiles.forEach(file => {
216216
if (file.content) {
@@ -247,6 +247,17 @@ export function RegexControls({
247247
format: m.format
248248
}))
249249
});
250+
251+
globalParsingConfig.others.forEach((cfg, idx) => {
252+
const matches = extractValues(
253+
file.content,
254+
cfg.mode,
255+
cfg
256+
);
257+
const key = cfg.name || `metric${idx+1}`;
258+
if (!results.others[key]) results.others[key] = [];
259+
results.others[key].push({ fileName: file.name, count: matches.length });
260+
});
250261
}
251262
});
252263

@@ -329,6 +340,28 @@ export function RegexControls({
329340
onGlobalParsingConfigChange(newConfig);
330341
};
331342

343+
const handleOtherConfigChange = (index, field, value) => {
344+
const newOthers = [...globalParsingConfig.others];
345+
newOthers[index] = { ...newOthers[index], [field]: value };
346+
const newConfig = { ...globalParsingConfig, others: newOthers };
347+
onGlobalParsingConfigChange(newConfig);
348+
};
349+
350+
const addMetric = () => {
351+
const newOthers = [...globalParsingConfig.others, {
352+
name: `metric${globalParsingConfig.others.length + 1}`,
353+
mode: 'keyword',
354+
keyword: '',
355+
regex: ''
356+
}];
357+
onGlobalParsingConfigChange({ ...globalParsingConfig, others: newOthers });
358+
};
359+
360+
const removeMetric = (index) => {
361+
const newOthers = globalParsingConfig.others.filter((_, i) => i !== index);
362+
onGlobalParsingConfigChange({ ...globalParsingConfig, others: newOthers });
363+
};
364+
332365
const handleXRangeChange = (field, value) => {
333366
const newRange = { ...xRange, [field]: value === '' ? undefined : Number(value) };
334367
onXRangeChange(newRange);
@@ -337,9 +370,20 @@ export function RegexControls({
337370
// 渲染配置项的函数
338371
const renderConfigPanel = (type, config, onConfigChange) => {
339372
const ModeIcon = MODE_CONFIG[config.mode].icon;
340-
373+
341374
return (
342375
<div className="space-y-2">
376+
{type.startsWith('other') && (
377+
<div>
378+
<label className="block text-xs font-medium text-gray-700 mb-1">指标名称</label>
379+
<input
380+
type="text"
381+
value={config.name}
382+
onChange={(e) => onConfigChange('name', e.target.value)}
383+
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none"
384+
/>
385+
</div>
386+
)}
343387
{/* 模式选择 */}
344388
<div>
345389
<label className="block text-xs font-medium text-gray-700 mb-1">
@@ -458,6 +502,29 @@ export function RegexControls({
458502
{renderConfigPanel('gradnorm', globalParsingConfig.gradNorm, (field, value) => handleConfigChange('gradNorm', field, value))}
459503
</div>
460504

505+
{globalParsingConfig.others.map((cfg, idx) => (
506+
<div key={idx} className="border rounded-lg p-3 relative">
507+
<button
508+
onClick={() => removeMetric(idx)}
509+
className="absolute top-1 right-1 text-red-500"
510+
title="删除配置"
511+
>
512+
×
513+
</button>
514+
<h4 className="text-sm font-medium text-gray-800 mb-2 flex items-center gap-1">
515+
<span className="w-3 h-3 bg-blue-500 rounded-full"></span>
516+
{cfg.name || `Metric ${idx+1}`} 解析配置
517+
</h4>
518+
{renderConfigPanel(`other-${idx}`, cfg, (field, value) => handleOtherConfigChange(idx, field, value))}
519+
</div>
520+
))}
521+
<button
522+
onClick={addMetric}
523+
className="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200"
524+
>
525+
+ 添加指标
526+
</button>
527+
461528
<div className="border rounded-lg p-3">
462529
<div className="flex items-center gap-2 mb-2">
463530
<ZoomIn

0 commit comments

Comments
 (0)