Skip to content

Commit 77fdb25

Browse files
committed
feat: Add X-axis range selection and pan/zoom functionality
1 parent 9c7549a commit 77fdb25

File tree

6 files changed

+141
-7
lines changed

6 files changed

+141
-7
lines changed

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@tailwindcss/forms": "^0.5.10",
1616
"autoprefixer": "^10.4.21",
1717
"chart.js": "^4.5.0",
18+
"chartjs-plugin-zoom": "^2.2.0",
1819
"lucide-react": "^0.522.0",
1920
"postcss": "^8.5.6",
2021
"react": "^19.1.0",

src/App.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ function App() {
3737
const [configFile, setConfigFile] = useState(null);
3838
const [globalDragOver, setGlobalDragOver] = useState(false);
3939
const [dragCounter, setDragCounter] = useState(0);
40+
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
41+
const [maxStep, setMaxStep] = useState(0);
4042

4143
const handleFilesUploaded = useCallback((files) => {
4244
const filesWithDefaults = files.map(file => ({
@@ -305,6 +307,9 @@ function App() {
305307
gradNormRegex={gradNormRegex}
306308
onRegexChange={handleRegexChange}
307309
uploadedFiles={uploadedFiles}
310+
xRange={xRange}
311+
onXRangeChange={setXRange}
312+
maxStep={maxStep}
308313
/>
309314

310315
<FileList
@@ -440,6 +445,9 @@ function App() {
440445
absoluteBaseline={absoluteBaseline}
441446
showLoss={showLoss}
442447
showGradNorm={showGradNorm}
448+
xRange={xRange}
449+
onXRangeChange={setXRange}
450+
onMaxStepChange={setMaxStep}
443451
/>
444452
</section>
445453
</main>

src/components/ChartContainer.jsx

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState, useRef, useCallback } from 'react';
1+
import React, { useMemo, useState, useRef, useCallback, useEffect } from 'react';
22
import { Line } from 'react-chartjs-2';
33
import { ResizablePanel } from './ResizablePanel';
44
import {
@@ -12,6 +12,7 @@ import {
1212
Tooltip,
1313
Legend,
1414
} from 'chart.js';
15+
import zoomPlugin from 'chartjs-plugin-zoom';
1516

1617
ChartJS.register(
1718
CategoryScale,
@@ -20,7 +21,8 @@ ChartJS.register(
2021
LineElement,
2122
Title,
2223
Tooltip,
23-
Legend
24+
Legend,
25+
zoomPlugin
2426
);
2527

2628
// Chart wrapper component with error boundary
@@ -83,7 +85,10 @@ export default function ChartContainer({
8385
relativeBaseline = 0.002,
8486
absoluteBaseline = 0.005,
8587
showLoss = true,
86-
showGradNorm = false
88+
showGradNorm = false,
89+
xRange = { min: undefined, max: undefined },
90+
onXRangeChange,
91+
onMaxStepChange
8792
}) {
8893
// 同步hover状态管理
8994
const [syncHoverStep, setSyncHoverStep] = useState(null);
@@ -306,6 +311,15 @@ export default function ChartContainer({
306311
});
307312
}, [files, lossRegex, gradNormRegex]);
308313

314+
useEffect(() => {
315+
const maxStep = parsedData.reduce((max, file) => {
316+
const maxLoss = file.lossData.length > 0 ? file.lossData[file.lossData.length - 1].x : 0;
317+
const maxGrad = file.gradNormData.length > 0 ? file.gradNormData[file.gradNormData.length - 1].x : 0;
318+
return Math.max(max, maxLoss, maxGrad);
319+
}, 0);
320+
onMaxStepChange(maxStep);
321+
}, [parsedData, onMaxStepChange]);
322+
309323
const movingAverage = (data, windowSize = 10) => {
310324
return data.map((point, index) => {
311325
const start = Math.max(0, index - windowSize + 1);
@@ -365,6 +379,36 @@ export default function ChartContainer({
365379
intersect: false,
366380
},
367381
plugins: {
382+
zoom: {
383+
pan: {
384+
enabled: true,
385+
mode: 'x',
386+
onPanComplete: ({chart}) => {
387+
const {min, max} = chart.scales.x;
388+
onXRangeChange({min: Math.round(min), max: Math.round(max)});
389+
}
390+
},
391+
zoom: {
392+
drag: {
393+
enabled: true,
394+
borderColor: 'rgba(225,225,225,0.2)',
395+
borderWidth: 1,
396+
backgroundColor: 'rgba(225,225,225,0.2)',
397+
modifierKey: 'shift',
398+
},
399+
wheel: {
400+
enabled: true,
401+
},
402+
pinch: {
403+
enabled: true
404+
},
405+
mode: 'x',
406+
onZoomComplete: ({chart}) => {
407+
const {min, max} = chart.scales.x;
408+
onXRangeChange({min: Math.round(min), max: Math.round(max)});
409+
}
410+
}
411+
},
368412
legend: {
369413
position: 'top',
370414
labels: {
@@ -449,7 +493,8 @@ export default function ChartContainer({
449493
display: true,
450494
text: 'Step',
451495
},
452-
min: 0,
496+
min: xRange.min,
497+
max: xRange.max,
453498
bounds: 'data'
454499
},
455500
y: {

src/components/ComparisonControls.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import React from 'react';
22
import { BarChart2 } from 'lucide-react';
33

4-
export function ComparisonControls({ compareMode, onCompareModeChange }) {
4+
export function ComparisonControls({
5+
compareMode,
6+
onCompareModeChange
7+
}) {
58
const modes = [
69
{ value: 'normal', label: '📊 Normal', description: '原始差值' },
710
{ value: 'absolute', label: '📈 Absolute', description: '绝对差值' },

src/components/RegexControls.jsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, useCallback } from 'react';
2-
import { Settings, Zap, Eye, ChevronDown, ChevronUp, Target, Code } from 'lucide-react';
2+
import { Settings, Zap, Eye, ChevronDown, ChevronUp, Target, Code, ZoomIn } from 'lucide-react';
33

44
// 匹配模式枚举
55
const MATCH_MODES = {
@@ -190,7 +190,10 @@ export function RegexControls({
190190
lossRegex,
191191
gradNormRegex,
192192
onRegexChange,
193-
uploadedFiles = []
193+
uploadedFiles = [],
194+
xRange,
195+
onXRangeChange,
196+
maxStep
194197
}) {
195198
const [showPreview, setShowPreview] = useState(false);
196199
const [previewResults, setPreviewResults] = useState({ loss: [], gradNorm: [] });
@@ -329,6 +332,12 @@ export function RegexControls({
329332

330333
onGlobalParsingConfigChange(newConfig);
331334
};
335+
336+
const handleXRangeChange = (field, value) => {
337+
const newRange = { ...xRange, [field]: value === '' ? undefined : Number(value) };
338+
onXRangeChange(newRange);
339+
};
340+
332341
// 渲染配置项的函数
333342
const renderConfigPanel = (type, config, onConfigChange) => {
334343
const ModeIcon = MODE_CONFIG[config.mode].icon;
@@ -453,6 +462,45 @@ export function RegexControls({
453462
{renderConfigPanel('gradnorm', globalParsingConfig.gradNorm, (field, value) => handleConfigChange('gradNorm', field, value))}
454463
</div>
455464

465+
<div className="border rounded-lg p-3">
466+
<div className="flex items-center gap-2 mb-2">
467+
<ZoomIn
468+
size={16}
469+
className="text-gray-600"
470+
aria-hidden="true"
471+
/>
472+
<h4 className="text-base font-semibold text-gray-800">
473+
X轴范围
474+
</h4>
475+
</div>
476+
<div className="flex items-center gap-2">
477+
<input
478+
type="number"
479+
placeholder="Min"
480+
value={xRange.min === undefined ? 0 : xRange.min}
481+
onChange={(e) => handleXRangeChange('min', e.target.value)}
482+
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"
483+
/>
484+
<span className="text-gray-500">-</span>
485+
<input
486+
type="number"
487+
placeholder={xRange.max === undefined && maxStep !== undefined ? `${maxStep}` : 'Max'}
488+
value={xRange.max === undefined ? maxStep : xRange.max}
489+
onChange={(e) => handleXRangeChange('max', e.target.value)}
490+
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"
491+
/>
492+
<button
493+
onClick={() => onXRangeChange({ min: undefined, max: undefined })}
494+
className="px-2 py-1 text-xs bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 whitespace-nowrap"
495+
>
496+
复位
497+
</button>
498+
</div>
499+
<p className="text-xs text-gray-500 mt-1">
500+
在图表上按住 <kbd>Shift</kbd> 键并拖动鼠标可选择范围,或直接输入数值。
501+
</p>
502+
</div>
503+
456504
{/* 预览结果 */}
457505
{showPreview && uploadedFiles.length > 0 && (
458506
<div className="mt-3 p-3 bg-blue-50 rounded border border-blue-200">

0 commit comments

Comments
 (0)