Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ function App() {
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
const [maxStep, setMaxStep] = useState(0);
const [sidebarVisible, setSidebarVisible] = useState(true);
const [useStepKeyword, setUseStepKeyword] = useState(false);
const [stepKeyword, setStepKeyword] = useState('step:');

const handleFilesUploaded = useCallback((files) => {
const filesWithDefaults = files.map(file => ({
Expand Down Expand Up @@ -395,6 +397,36 @@ function App() {
</div>
</div>
</div>
<div className="border-t pt-3">
<h4 className="text-xs font-medium text-gray-700 mb-2">Step 关键词对齐</h4>
<div className="flex items-center gap-2">
<input
id="use-step-keyword"
type="checkbox"
checked={useStepKeyword}
onChange={(e) => setUseStepKeyword(e.target.checked)}
className="h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
<label htmlFor="use-step-keyword" className="text-xs text-gray-700">
使用关键词对齐步数
</label>
</div>
{useStepKeyword && (
<div className="mt-2">
<label htmlFor="step-keyword" className="block text-xs font-medium text-gray-700 mb-1">
关键词
</label>
<input
id="step-keyword"
type="text"
value={stepKeyword}
onChange={(e) => setStepKeyword(e.target.value)}
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"
placeholder="step:"
/>
</div>
)}
</div>
</div>
</section>
</aside>
Expand All @@ -414,6 +446,8 @@ function App() {
xRange={xRange}
onXRangeChange={setXRange}
onMaxStepChange={setMaxStep}
useStepKeyword={useStepKeyword}
stepKeyword={stepKeyword}
/>
</section>
</main>
Expand Down
124 changes: 80 additions & 44 deletions src/components/ChartContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
...options,
onHover: (event, activeElements) => {
if (activeElements.length > 0) {
const step = activeElements[0].index;
onSyncHover(step, chartId);
const { datasetIndex, index } = activeElements[0];
const point = chartRef.current?.data?.datasets?.[datasetIndex]?.data?.[index];
const step = point?.x;
onSyncHover(step ?? null, chartId);
} else {
onSyncHover(null, chartId);
}
Expand All @@ -68,7 +70,9 @@ export default function ChartContainer({
absoluteBaseline = 0.005,
xRange = { min: undefined, max: undefined },
onXRangeChange,
onMaxStepChange
onMaxStepChange,
useStepKeyword = false,
stepKeyword = 'step:'
}) {
const chartRefs = useRef(new Map());
const registerChart = useCallback((id, inst) => {
Expand All @@ -85,8 +89,9 @@ export default function ChartContainer({
} else if (id !== sourceId) {
const activeElements = [];
chart.data.datasets.forEach((dataset, datasetIndex) => {
if (dataset.data && dataset.data.length > step) {
activeElements.push({ datasetIndex, index: step });
const index = dataset.data.findIndex(p => p.x === step);
if (index !== -1) {
activeElements.push({ datasetIndex, index });
}
});
chart.setActiveElements(activeElements);
Expand All @@ -103,39 +108,46 @@ export default function ChartContainer({
const lines = file.content.split('\n');
const metricsData = {};

const extractByKeyword = (content, keyword) => {
const results = [];
const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
content.split('\n').forEach(line => {
const idx = line.toLowerCase().indexOf(keyword.toLowerCase());
if (idx !== -1) {
const after = line.substring(idx + keyword.length);
const match = after.match(numberRegex);
if (match) {
const v = parseFloat(match[0]);
if (!isNaN(v)) results.push(v);
}
metrics.forEach(metric => {
metricsData[metric.name || metric.keyword] = [];
});

const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
const stepKeyLower = stepKeyword.toLowerCase();

lines.forEach(line => {
let stepVal;
if (useStepKeyword) {
const idxStep = line.toLowerCase().indexOf(stepKeyLower);
if (idxStep !== -1) {
const afterStep = line.substring(idxStep + stepKeyword.length);
const mStep = afterStep.match(/[+-]?\d+/);
if (mStep) stepVal = parseInt(mStep[0]);
}
});
return results;
};
if (stepVal === undefined) return;
}

metrics.forEach(metric => {
let values = [];
if (metric.mode === 'keyword') {
values = extractByKeyword(file.content, metric.keyword);
} else if (metric.regex) {
const reg = new RegExp(metric.regex);
lines.forEach(line => {
metrics.forEach(metric => {
let value;
if (metric.mode === 'keyword') {
const idx = line.toLowerCase().indexOf(metric.keyword.toLowerCase());
if (idx !== -1) {
const after = line.substring(idx + metric.keyword.length);
const m = after.match(numberRegex);
if (m) value = parseFloat(m[0]);
}
} else if (metric.regex) {
const reg = new RegExp(metric.regex);
reg.lastIndex = 0;
const m = reg.exec(line);
if (m && m[1]) {
const v = parseFloat(m[1]);
if (!isNaN(v)) values.push(v);
}
});
}
metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v }));
if (m && m[1]) value = parseFloat(m[1]);
}
if (value !== undefined && !isNaN(value)) {
const arr = metricsData[metric.name || metric.keyword];
const x = useStepKeyword ? stepVal : arr.length;
arr.push({ x, y: value });
}
});
});

const range = file.config?.dataRange;
Expand All @@ -147,15 +159,15 @@ export default function ChartContainer({
const endIndex = Math.min(data.length, end);
return data.slice(start, endIndex);
};
const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y }));
const reindex = data => data.map((p, idx) => (useStepKeyword ? { x: p.x, y: p.y } : { x: idx, y: p.y }));
Object.keys(metricsData).forEach(k => {
metricsData[k] = reindex(applyRange(metricsData[k]));
});
}

return { ...file, metricsData };
});
}, [files, metrics]);
}, [files, metrics, useStepKeyword, stepKeyword]);

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

useEffect(() => {
const minSteps = getMinSteps(parsedData);
if (minSteps > 0) {
onXRangeChange(prev => {
const next = { min: 0, max: minSteps - 1 };
if (prev.min === next.min && prev.max === next.max) return prev;
return next;
});
if (useStepKeyword) {
const minStart = parsedData.reduce((m, f) => {
const localMin = Object.values(f.metricsData).reduce(
(mm, d) => Math.min(mm, d.length > 0 ? d[0].x : Infinity),
Infinity
);
return Math.min(m, localMin);
}, Infinity);
const maxEnd = parsedData.reduce((m, f) => {
const localMax = Object.values(f.metricsData).reduce(
(mm, d) => Math.max(mm, d.length > 0 ? d[d.length - 1].x : -Infinity),
-Infinity
);
return Math.max(m, localMax);
}, -Infinity);
if (minStart !== Infinity && maxEnd !== -Infinity) {
onXRangeChange(prev => {
const next = { min: minStart, max: maxEnd };
if (prev.min === next.min && prev.max === next.max) return prev;
return next;
});
}
} else {
const minSteps = getMinSteps(parsedData);
if (minSteps > 0) {
onXRangeChange(prev => {
const next = { min: 0, max: minSteps - 1 };
if (prev.min === next.min && prev.max === next.max) return prev;
return next;
});
}
}
}, [parsedData, onXRangeChange]);
}, [parsedData, onXRangeChange, useStepKeyword]);

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