Skip to content

Commit 50ae9ba

Browse files
committed
feat: align logs using step keyword
1 parent 7d88e19 commit 50ae9ba

2 files changed

Lines changed: 114 additions & 44 deletions

File tree

src/App.jsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ function App() {
4040
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
4141
const [maxStep, setMaxStep] = useState(0);
4242
const [sidebarVisible, setSidebarVisible] = useState(true);
43+
const [useStepKeyword, setUseStepKeyword] = useState(false);
44+
const [stepKeyword, setStepKeyword] = useState('step:');
4345

4446
const handleFilesUploaded = useCallback((files) => {
4547
const filesWithDefaults = files.map(file => ({
@@ -395,6 +397,36 @@ function App() {
395397
</div>
396398
</div>
397399
</div>
400+
<div className="border-t pt-3">
401+
<h4 className="text-xs font-medium text-gray-700 mb-2">Step 关键词对齐</h4>
402+
<div className="flex items-center gap-2">
403+
<input
404+
id="use-step-keyword"
405+
type="checkbox"
406+
checked={useStepKeyword}
407+
onChange={(e) => setUseStepKeyword(e.target.checked)}
408+
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
409+
/>
410+
<label htmlFor="use-step-keyword" className="text-xs text-gray-700">
411+
使用关键词对齐步数
412+
</label>
413+
</div>
414+
{useStepKeyword && (
415+
<div className="mt-2">
416+
<label htmlFor="step-keyword" className="block text-xs font-medium text-gray-700 mb-1">
417+
关键词
418+
</label>
419+
<input
420+
id="step-keyword"
421+
type="text"
422+
value={stepKeyword}
423+
onChange={(e) => setStepKeyword(e.target.value)}
424+
className="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"
425+
placeholder="step:"
426+
/>
427+
</div>
428+
)}
429+
</div>
398430
</div>
399431
</section>
400432
</aside>
@@ -414,6 +446,8 @@ function App() {
414446
xRange={xRange}
415447
onXRangeChange={setXRange}
416448
onMaxStepChange={setMaxStep}
449+
useStepKeyword={useStepKeyword}
450+
stepKeyword={stepKeyword}
417451
/>
418452
</section>
419453
</main>

src/components/ChartContainer.jsx

Lines changed: 80 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
4040
...options,
4141
onHover: (event, activeElements) => {
4242
if (activeElements.length > 0) {
43-
const step = activeElements[0].index;
44-
onSyncHover(step, chartId);
43+
const { datasetIndex, index } = activeElements[0];
44+
const point = chartRef.current?.data?.datasets?.[datasetIndex]?.data?.[index];
45+
const step = point?.x;
46+
onSyncHover(step ?? null, chartId);
4547
} else {
4648
onSyncHover(null, chartId);
4749
}
@@ -68,7 +70,9 @@ export default function ChartContainer({
6870
absoluteBaseline = 0.005,
6971
xRange = { min: undefined, max: undefined },
7072
onXRangeChange,
71-
onMaxStepChange
73+
onMaxStepChange,
74+
useStepKeyword = false,
75+
stepKeyword = 'step:'
7276
}) {
7377
const chartRefs = useRef(new Map());
7478
const registerChart = useCallback((id, inst) => {
@@ -85,8 +89,9 @@ export default function ChartContainer({
8589
} else if (id !== sourceId) {
8690
const activeElements = [];
8791
chart.data.datasets.forEach((dataset, datasetIndex) => {
88-
if (dataset.data && dataset.data.length > step) {
89-
activeElements.push({ datasetIndex, index: step });
92+
const index = dataset.data.findIndex(p => p.x === step);
93+
if (index !== -1) {
94+
activeElements.push({ datasetIndex, index });
9095
}
9196
});
9297
chart.setActiveElements(activeElements);
@@ -103,39 +108,46 @@ export default function ChartContainer({
103108
const lines = file.content.split('\n');
104109
const metricsData = {};
105110

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-
}
111+
metrics.forEach(metric => {
112+
metricsData[metric.name || metric.keyword] = [];
113+
});
114+
115+
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
116+
const stepKeyLower = stepKeyword.toLowerCase();
117+
118+
lines.forEach(line => {
119+
let stepVal;
120+
if (useStepKeyword) {
121+
const idxStep = line.toLowerCase().indexOf(stepKeyLower);
122+
if (idxStep !== -1) {
123+
const afterStep = line.substring(idxStep + stepKeyword.length);
124+
const mStep = afterStep.match(/[+-]?\d+/);
125+
if (mStep) stepVal = parseInt(mStep[0]);
118126
}
119-
});
120-
return results;
121-
};
127+
if (stepVal === undefined) return;
128+
}
122129

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+
metrics.forEach(metric => {
131+
let value;
132+
if (metric.mode === 'keyword') {
133+
const idx = line.toLowerCase().indexOf(metric.keyword.toLowerCase());
134+
if (idx !== -1) {
135+
const after = line.substring(idx + metric.keyword.length);
136+
const m = after.match(numberRegex);
137+
if (m) value = parseFloat(m[0]);
138+
}
139+
} else if (metric.regex) {
140+
const reg = new RegExp(metric.regex);
130141
reg.lastIndex = 0;
131142
const m = reg.exec(line);
132-
if (m && m[1]) {
133-
const v = parseFloat(m[1]);
134-
if (!isNaN(v)) values.push(v);
135-
}
136-
});
137-
}
138-
metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v }));
143+
if (m && m[1]) value = parseFloat(m[1]);
144+
}
145+
if (value !== undefined && !isNaN(value)) {
146+
const arr = metricsData[metric.name || metric.keyword];
147+
const x = useStepKeyword ? stepVal : arr.length;
148+
arr.push({ x, y: value });
149+
}
150+
});
139151
});
140152

141153
const range = file.config?.dataRange;
@@ -147,15 +159,15 @@ export default function ChartContainer({
147159
const endIndex = Math.min(data.length, end);
148160
return data.slice(start, endIndex);
149161
};
150-
const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y }));
162+
const reindex = data => data.map((p, idx) => (useStepKeyword ? { x: p.x, y: p.y } : { x: idx, y: p.y }));
151163
Object.keys(metricsData).forEach(k => {
152164
metricsData[k] = reindex(applyRange(metricsData[k]));
153165
});
154166
}
155167

156168
return { ...file, metricsData };
157169
});
158-
}, [files, metrics]);
170+
}, [files, metrics, useStepKeyword, stepKeyword]);
159171

160172
useEffect(() => {
161173
const maxStep = parsedData.reduce((m, f) => {
@@ -166,15 +178,39 @@ export default function ChartContainer({
166178
}, [parsedData, onMaxStepChange]);
167179

168180
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;
175-
});
181+
if (useStepKeyword) {
182+
const minStart = parsedData.reduce((m, f) => {
183+
const localMin = Object.values(f.metricsData).reduce(
184+
(mm, d) => Math.min(mm, d.length > 0 ? d[0].x : Infinity),
185+
Infinity
186+
);
187+
return Math.min(m, localMin);
188+
}, Infinity);
189+
const maxEnd = parsedData.reduce((m, f) => {
190+
const localMax = Object.values(f.metricsData).reduce(
191+
(mm, d) => Math.max(mm, d.length > 0 ? d[d.length - 1].x : -Infinity),
192+
-Infinity
193+
);
194+
return Math.max(m, localMax);
195+
}, -Infinity);
196+
if (minStart !== Infinity && maxEnd !== -Infinity) {
197+
onXRangeChange(prev => {
198+
const next = { min: minStart, max: maxEnd };
199+
if (prev.min === next.min && prev.max === next.max) return prev;
200+
return next;
201+
});
202+
}
203+
} else {
204+
const minSteps = getMinSteps(parsedData);
205+
if (minSteps > 0) {
206+
onXRangeChange(prev => {
207+
const next = { min: 0, max: minSteps - 1 };
208+
if (prev.min === next.min && prev.max === next.max) return prev;
209+
return next;
210+
});
211+
}
176212
}
177-
}, [parsedData, onXRangeChange]);
213+
}, [parsedData, onXRangeChange, useStepKeyword]);
178214

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

0 commit comments

Comments
 (0)