Skip to content

Commit 62f87b8

Browse files
authored
feat: support manual y-axis range with auto reset (#64)
1 parent 0912970 commit 62f87b8

5 files changed

Lines changed: 86 additions & 5 deletions

File tree

public/locales/en/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,13 @@
9494
"regex.metricConfig": "{{title}} parsing config",
9595
"regex.addMetric": "+ Add Metric",
9696
"regex.xRange": "X-axis range",
97+
"regex.yRange": "Y-axis range",
98+
"regex.min": "Min",
99+
"regex.max": "Max",
100+
"regex.auto": "Auto",
97101
"regex.reset": "Reset",
98102
"regex.xRangeHint": "Hold <0>Shift</0> and drag on the chart to select range, or input values directly.",
103+
"regex.yRangeHint": "Leave blank for auto-scale, or input one/both bounds to manually lock Y-axis range.",
99104
"regex.matchPreview": "Match Preview",
100105
"regex.matchCount": "{{count}} matches",
101106
"regex.lineNumber": "(line {{line}})",

public/locales/zh/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,13 @@
9494
"regex.metricConfig": "{{title}} 解析配置",
9595
"regex.addMetric": "+ 添加指标",
9696
"regex.xRange": "X轴范围",
97+
"regex.yRange": "Y轴范围",
98+
"regex.min": "最小值",
99+
"regex.max": "最大值",
100+
"regex.auto": "自动",
97101
"regex.reset": "复位",
98102
"regex.xRangeHint": "在图表上按住 <0>Shift</0> 键并拖动鼠标可选择范围,或直接输入数值。",
103+
"regex.yRangeHint": "留空即自动缩放,也可以输入一个或两个边界手动固定 Y 轴范围。",
99104
"regex.matchPreview": "匹配预览",
100105
"regex.matchCount": "({{count}} 个匹配)",
101106
"regex.lineNumber": "(第{{line}}行)",

src/App.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function App() {
7272
const [globalDragOver, setGlobalDragOver] = useState(false);
7373
const [, setDragCounter] = useState(0);
7474
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
75+
const [yRange, setYRange] = useState({ min: undefined, max: undefined });
7576
const [maxStep, setMaxStep] = useState(0);
7677
const [sidebarVisible, setSidebarVisible] = useState(true);
7778
const savingDisabledRef = useRef(false);
@@ -535,6 +536,8 @@ function App() {
535536
uploadedFiles={uploadedFiles}
536537
xRange={xRange}
537538
onXRangeChange={setXRange}
539+
yRange={yRange}
540+
onYRangeChange={setYRange}
538541
maxStep={maxStep}
539542
/>
540543

@@ -644,6 +647,7 @@ function App() {
644647
absoluteBaseline={absoluteBaseline}
645648
xRange={xRange}
646649
onXRangeChange={setXRange}
650+
yRange={yRange}
647651
onMaxStepChange={setMaxStep}
648652
/>
649653
</section>

src/components/ChartContainer.jsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export default function ChartContainer({
9898
relativeBaseline = 0.002,
9999
absoluteBaseline = 0.005,
100100
xRange = { min: undefined, max: undefined },
101+
yRange = { min: undefined, max: undefined },
101102
onXRangeChange,
102103
onMaxStepChange
103104
}) {
@@ -393,6 +394,24 @@ export default function ChartContainer({
393394
return { min: niceMin, max: niceMax, step };
394395
}, []);
395396

397+
const getFinalYScale = useCallback((autoScale) => {
398+
const hasManualMin = Number.isFinite(yRange?.min);
399+
const hasManualMax = Number.isFinite(yRange?.max);
400+
401+
const min = hasManualMin ? yRange.min : autoScale.min;
402+
const max = hasManualMax ? yRange.max : autoScale.max;
403+
404+
if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) {
405+
return autoScale;
406+
}
407+
408+
return {
409+
...autoScale,
410+
min,
411+
max
412+
};
413+
}, [yRange]);
414+
396415
const chartOptions = useMemo(() => ({
397416
responsive: true,
398417
maintainAspectRatio: false,
@@ -668,7 +687,8 @@ export default function ChartContainer({
668687
});
669688
});
670689

671-
const yRange = calculateNiceScale(min, max);
690+
const autoYRange = calculateNiceScale(min, max);
691+
const finalYRange = getFinalYScale(autoYRange);
672692

673693
const options = {
674694
...chartOptions,
@@ -690,10 +710,10 @@ export default function ChartContainer({
690710
...chartOptions.scales,
691711
y: {
692712
...chartOptions.scales.y,
693-
min: yRange.min,
694-
max: yRange.max,
713+
min: finalYRange.min,
714+
max: finalYRange.max,
695715
ticks: {
696-
stepSize: yRange.step,
716+
stepSize: finalYRange.step,
697717
callback: (value) => Number(value.toFixed(yDecimals))
698718
}
699719
}
@@ -721,7 +741,8 @@ export default function ChartContainer({
721741
});
722742
});
723743

724-
const compRange = calculateNiceScale(cMin, cMax);
744+
const autoCompRange = calculateNiceScale(cMin, cMax);
745+
const compRange = getFinalYScale(autoCompRange);
725746
const compDecimals = Math.max(4, getMaxDecimals(compResult.datasets)); // Ensure at least 4 for diffs
726747

727748
const compOptions = {

src/components/RegexControls.jsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export function RegexControls({
4545
uploadedFiles = [],
4646
xRange,
4747
onXRangeChange,
48+
yRange,
49+
onYRangeChange,
4850
maxStep
4951
}) {
5052
const [showPreview, setShowPreview] = useState(false);
@@ -182,6 +184,11 @@ export function RegexControls({
182184
onXRangeChange(newRange);
183185
};
184186

187+
const handleYRangeChange = (field, value) => {
188+
const newRange = { ...yRange, [field]: value === '' ? undefined : Number(value) };
189+
onYRangeChange(newRange);
190+
};
191+
185192
// Function to render config panel
186193
const renderConfigPanel = (type, config, onConfigChange, index) => {
187194
const ModeIcon = MODE_CONFIG[config.mode].icon;
@@ -353,6 +360,45 @@ export function RegexControls({
353360
</div>
354361
</div>
355362

363+
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-3">
364+
<div className="flex items-center gap-2 mb-2">
365+
<ZoomIn
366+
size={16}
367+
className="text-gray-600 dark:text-gray-300"
368+
aria-hidden="true"
369+
/>
370+
<h4 className="text-base font-semibold text-gray-800 dark:text-gray-100">
371+
{t('regex.yRange')}
372+
</h4>
373+
</div>
374+
<div className="flex items-center gap-2">
375+
<input
376+
type="number"
377+
placeholder={t('regex.min')}
378+
value={yRange?.min ?? ''}
379+
onChange={(e) => handleYRangeChange('min', e.target.value)}
380+
className="input-field"
381+
/>
382+
<span className="text-gray-500 dark:text-gray-400">-</span>
383+
<input
384+
type="number"
385+
placeholder={t('regex.max')}
386+
value={yRange?.max ?? ''}
387+
onChange={(e) => handleYRangeChange('max', e.target.value)}
388+
className="input-field"
389+
/>
390+
<button
391+
onClick={() => onYRangeChange({ min: undefined, max: undefined })}
392+
className="px-2 py-1 text-xs bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 whitespace-nowrap"
393+
>
394+
{t('regex.auto')}
395+
</button>
396+
</div>
397+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
398+
{t('regex.yRangeHint')}
399+
</p>
400+
</div>
401+
356402
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-3">
357403
<div className="flex items-center gap-2 mb-2">
358404
<ZoomIn

0 commit comments

Comments
 (0)