Skip to content

Commit a151e73

Browse files
committed
feat: support step keyword for x-axis
1 parent 7d88e19 commit a151e73

7 files changed

Lines changed: 208 additions & 113 deletions

File tree

src/App.jsx

Lines changed: 9 additions & 3 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+
useStepKeyword: false,
32+
stepKeyword: 'step:'
3133
});
3234

3335
const [compareMode, setCompareMode] = useState('normal');
@@ -52,7 +54,9 @@ function App() {
5254
start: 0, // 默认从第一个数据点开始
5355
end: undefined, // 默认到最后一个数据点
5456
useRange: false // 保留这个字段用于向后兼容,但默认不启用
55-
}
57+
},
58+
useStepKeyword: globalParsingConfig.useStepKeyword,
59+
stepKeyword: globalParsingConfig.stepKeyword
5660
}
5761
}));
5862
setUploadedFiles(prev => mergeFilesWithReplacement(prev, filesWithDefaults));
@@ -124,7 +128,9 @@ function App() {
124128
...file,
125129
config: {
126130
...file.config,
127-
metrics: newConfig.metrics.map(m => ({ ...m }))
131+
metrics: newConfig.metrics.map(m => ({ ...m })),
132+
useStepKeyword: newConfig.useStepKeyword,
133+
stepKeyword: newConfig.stepKeyword
128134
}
129135
})));
130136
}, []);

src/components/ChartContainer.jsx

Lines changed: 95 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
3838

3939
const enhancedOptions = {
4040
...options,
41-
onHover: (event, activeElements) => {
42-
if (activeElements.length > 0) {
43-
const step = activeElements[0].index;
44-
onSyncHover(step, chartId);
45-
} else {
46-
onSyncHover(null, chartId);
47-
}
48-
},
41+
onHover: (event, activeElements) => {
42+
if (activeElements.length > 0 && chartRef.current) {
43+
const { datasetIndex, index } = activeElements[0];
44+
const dataset = chartRef.current.data.datasets[datasetIndex];
45+
const point = dataset.data[index];
46+
const step = point.x;
47+
onSyncHover(step, chartId);
48+
} else {
49+
onSyncHover(null, chartId);
50+
}
51+
},
4952
events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'],
5053
};
5154

@@ -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);
@@ -96,47 +100,72 @@ export default function ChartContainer({
96100
});
97101
}, []);
98102

99-
const parsedData = useMemo(() => {
100-
const enabled = files.filter(f => f.enabled !== false);
101-
return enabled.map(file => {
102-
if (!file.content) return { ...file, metricsData: {} };
103-
const lines = file.content.split('\n');
104-
const metricsData = {};
103+
const parsedData = useMemo(() => {
104+
const enabled = files.filter(f => f.enabled !== false);
105+
return enabled.map(file => {
106+
if (!file.content) return { ...file, metricsData: {} };
107+
const lines = file.content.split('\n');
108+
const metricsData = {};
105109

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());
110+
const stepCfg = {
111+
enabled: file.config?.useStepKeyword,
112+
keyword: file.config?.stepKeyword || 'step:'
113+
};
114+
115+
const extractStep = (line) => {
116+
if (!stepCfg.enabled) return null;
117+
const idx = line.toLowerCase().indexOf(stepCfg.keyword.toLowerCase());
111118
if (idx !== -1) {
112-
const after = line.substring(idx + keyword.length);
113-
const match = after.match(numberRegex);
119+
const after = line.substring(idx + stepCfg.keyword.length);
120+
const match = after.match(/[+-]?\d+/);
114121
if (match) {
115-
const v = parseFloat(match[0]);
116-
if (!isNaN(v)) results.push(v);
122+
const s = parseInt(match[0], 10);
123+
if (!isNaN(s)) return s;
117124
}
118125
}
119-
});
120-
return results;
121-
};
126+
return null;
127+
};
122128

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;
131-
const m = reg.exec(line);
132-
if (m && m[1]) {
133-
const v = parseFloat(m[1]);
134-
if (!isNaN(v)) values.push(v);
129+
const extractByKeyword = (linesArr, keyword) => {
130+
const results = [];
131+
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
132+
linesArr.forEach(line => {
133+
const idx = line.toLowerCase().indexOf(keyword.toLowerCase());
134+
if (idx !== -1) {
135+
const after = line.substring(idx + keyword.length);
136+
const match = after.match(numberRegex);
137+
if (match) {
138+
const v = parseFloat(match[0]);
139+
if (!isNaN(v)) {
140+
const step = extractStep(line);
141+
results.push({ x: step !== null ? step : results.length, y: v });
142+
}
143+
}
135144
}
136145
});
137-
}
138-
metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v }));
139-
});
146+
return results;
147+
};
148+
149+
metrics.forEach(metric => {
150+
let points = [];
151+
if (metric.mode === 'keyword') {
152+
points = extractByKeyword(lines, metric.keyword);
153+
} else if (metric.regex) {
154+
const reg = new RegExp(metric.regex);
155+
lines.forEach(line => {
156+
reg.lastIndex = 0;
157+
const m = reg.exec(line);
158+
if (m && m[1]) {
159+
const v = parseFloat(m[1]);
160+
if (!isNaN(v)) {
161+
const step = extractStep(line);
162+
points.push({ x: step !== null ? step : points.length, y: v });
163+
}
164+
}
165+
});
166+
}
167+
metricsData[metric.name || metric.keyword] = points;
168+
});
140169

141170
const range = file.config?.dataRange;
142171
if (range && (range.start > 0 || range.end !== undefined)) {
@@ -147,7 +176,7 @@ export default function ChartContainer({
147176
const endIndex = Math.min(data.length, end);
148177
return data.slice(start, endIndex);
149178
};
150-
const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y }));
179+
const reindex = data => stepCfg.enabled ? data : data.map((p, idx) => ({ x: idx, y: p.y }));
151180
Object.keys(metricsData).forEach(k => {
152181
metricsData[k] = reindex(applyRange(metricsData[k]));
153182
});
@@ -203,29 +232,31 @@ export default function ChartContainer({
203232
});
204233

205234
const getComparisonData = (data1, data2, mode) => {
206-
const minLength = Math.min(data1.length, data2.length);
235+
const map2 = new Map(data2.map(p => [p.x, p.y]));
207236
const result = [];
208-
for (let i = 0; i < minLength; i++) {
209-
const v1 = data1[i].y;
210-
const v2 = data2[i].y;
211-
let diff;
212-
switch (mode) {
213-
case 'absolute':
214-
diff = Math.abs(v2 - v1);
215-
break;
216-
case 'relative-normal':
217-
diff = v1 !== 0 ? (v2 - v1) / v1 : 0;
218-
break;
219-
case 'relative': {
220-
const ad = Math.abs(v2 - v1);
221-
diff = v1 !== 0 ? ad / Math.abs(v1) : 0;
222-
break;
237+
data1.forEach(p1 => {
238+
if (map2.has(p1.x)) {
239+
const v1 = p1.y;
240+
const v2 = map2.get(p1.x);
241+
let diff;
242+
switch (mode) {
243+
case 'absolute':
244+
diff = Math.abs(v2 - v1);
245+
break;
246+
case 'relative-normal':
247+
diff = v1 !== 0 ? (v2 - v1) / v1 : 0;
248+
break;
249+
case 'relative': {
250+
const ad = Math.abs(v2 - v1);
251+
diff = v1 !== 0 ? ad / Math.abs(v1) : 0;
252+
break;
253+
}
254+
default:
255+
diff = v2 - v1;
223256
}
224-
default:
225-
diff = v2 - v1;
257+
result.push({ x: p1.x, y: diff });
226258
}
227-
result.push({ x: i, y: diff });
228-
}
259+
});
229260
return result;
230261
};
231262

src/components/FileConfigModal.jsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,34 @@ function getMetricTitle(metric, index) {
3535
}
3636

3737
export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingConfig }) {
38-
const [config, setConfig] = useState({
39-
metrics: [],
40-
dataRange: {
41-
start: 0, // 起始位置,默认为0(第一个数据点)
42-
end: undefined, // 结束位置,默认为undefined(最后一个数据点)
43-
useRange: false // 保留用于向后兼容
44-
}
45-
});
38+
const [config, setConfig] = useState({
39+
metrics: [],
40+
dataRange: {
41+
start: 0, // 起始位置,默认为0(第一个数据点)
42+
end: undefined, // 结束位置,默认为undefined(最后一个数据点)
43+
useRange: false // 保留用于向后兼容
44+
},
45+
useStepKeyword: false,
46+
stepKeyword: 'step:'
47+
});
4648

4749
useEffect(() => {
4850
if (file && isOpen) {
4951
// 如果文件有配置,使用文件配置,否则使用全局配置
5052
const fileConfig = file.config || {};
51-
setConfig({
52-
metrics: fileConfig.metrics || globalParsingConfig.metrics,
53-
dataRange: fileConfig.dataRange || {
54-
start: 0,
55-
end: undefined,
56-
useRange: false
57-
}
58-
});
53+
setConfig({
54+
metrics: fileConfig.metrics || globalParsingConfig.metrics,
55+
dataRange: fileConfig.dataRange || {
56+
start: 0,
57+
end: undefined,
58+
useRange: false
59+
},
60+
useStepKeyword:
61+
fileConfig.useStepKeyword !== undefined
62+
? fileConfig.useStepKeyword
63+
: globalParsingConfig.useStepKeyword,
64+
stepKeyword: fileConfig.stepKeyword || globalParsingConfig.stepKeyword || 'step:'
65+
});
5966
}
6067
}, [file, isOpen, globalParsingConfig]);
6168

@@ -98,7 +105,9 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo
98105
const syncFromGlobal = () => {
99106
setConfig(prev => ({
100107
...prev,
101-
metrics: globalParsingConfig.metrics.map(m => ({ ...m }))
108+
metrics: globalParsingConfig.metrics.map(m => ({ ...m })),
109+
useStepKeyword: globalParsingConfig.useStepKeyword,
110+
stepKeyword: globalParsingConfig.stepKeyword
102111
}));
103112
};
104113

src/components/RegexControls.jsx

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,20 @@ export function RegexControls({
207207
const [showPreview, setShowPreview] = useState(false);
208208
const [previewResults, setPreviewResults] = useState({});
209209

210+
const handleStepToggle = useCallback((checked) => {
211+
onGlobalParsingConfigChange({
212+
...globalParsingConfig,
213+
useStepKeyword: checked
214+
});
215+
}, [globalParsingConfig, onGlobalParsingConfigChange]);
216+
217+
const handleStepKeywordChange = useCallback((value) => {
218+
onGlobalParsingConfigChange({
219+
...globalParsingConfig,
220+
stepKeyword: value
221+
});
222+
}, [globalParsingConfig, onGlobalParsingConfigChange]);
223+
210224
// 提取数值的通用函数
211225
const extractValues = useCallback((content, mode, config) => {
212226
switch (mode) {
@@ -449,28 +463,51 @@ export function RegexControls({
449463
</div>
450464

451465
<div className="space-y-4">
452-
{globalParsingConfig.metrics.map((cfg, idx) => (
453-
<div key={idx} className="border rounded-lg p-3 relative">
454-
<button
455-
onClick={() => removeMetric(idx)}
456-
className="absolute top-1 right-1 text-red-500"
457-
title="删除配置"
458-
>
459-
×
460-
</button>
461-
<h4 className="text-sm font-medium text-gray-800 mb-2 flex items-center gap-1">
462-
<span className="w-3 h-3 bg-blue-500 rounded-full"></span>
463-
{getMetricTitle(cfg, idx)} 解析配置
464-
</h4>
465-
{renderConfigPanel(`metric-${idx}`, cfg, (field, value) => handleMetricChange(idx, field, value), idx)}
466+
{globalParsingConfig.metrics.map((cfg, idx) => (
467+
<div key={idx} className="border rounded-lg p-3 relative">
468+
<button
469+
onClick={() => removeMetric(idx)}
470+
className="absolute top-1 right-1 text-red-500"
471+
title="删除配置"
472+
>
473+
×
474+
</button>
475+
<h4 className="text-sm font-medium text-gray-800 mb-2 flex items-center gap-1">
476+
<span className="w-3 h-3 bg-blue-500 rounded-full"></span>
477+
{getMetricTitle(cfg, idx)} 解析配置
478+
</h4>
479+
{renderConfigPanel(`metric-${idx}`, cfg, (field, value) => handleMetricChange(idx, field, value), idx)}
480+
</div>
481+
))}
482+
<button
483+
onClick={addMetric}
484+
className="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200"
485+
>
486+
+ 添加指标
487+
</button>
488+
489+
<div className="border rounded-lg p-3">
490+
<div className="flex items-center gap-2">
491+
<label className="flex items-center text-xs text-gray-700">
492+
<input
493+
type="checkbox"
494+
className="mr-2"
495+
checked={globalParsingConfig.useStepKeyword || false}
496+
onChange={(e) => handleStepToggle(e.target.checked)}
497+
/>
498+
使用 Step 关键字
499+
</label>
500+
{globalParsingConfig.useStepKeyword && (
501+
<input
502+
type="text"
503+
className="flex-1 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"
504+
value={globalParsingConfig.stepKeyword || 'step:'}
505+
onChange={(e) => handleStepKeywordChange(e.target.value)}
506+
placeholder="step:"
507+
/>
508+
)}
509+
</div>
466510
</div>
467-
))}
468-
<button
469-
onClick={addMetric}
470-
className="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200"
471-
>
472-
+ 添加指标
473-
</button>
474511

475512
<div className="border rounded-lg p-3">
476513
<div className="flex items-center gap-2 mb-2">

0 commit comments

Comments
 (0)