From fdac7f38e896d7e351716c41957f5e6cc92ee86b Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:36:12 +0800 Subject: [PATCH 1/2] feat: derive y-axis precision from data --- src/components/ChartContainer.jsx | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index c24e04d..7733bae 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -416,6 +416,11 @@ export default function ChartContainer({ if (min === Infinity || max === -Infinity) { return { min: 0, max: 1 }; } + + // Always include zero on the axis for better readability + min = Math.min(min, 0); + max = Math.max(max, 0); + if (min === max) { return { min: min - 1, max: max + 1 }; } @@ -423,6 +428,25 @@ export default function ChartContainer({ return { min: min - pad, max: max + pad }; }, [xRange]); + const yDecimalPlaces = useMemo(() => { + let maxDecimals = 0; + parsedData.forEach(file => { + Object.values(file.metricsData).forEach(points => { + points.forEach(p => { + const str = p.y.toString(); + const decimalPart = str.split('.')[1]; + if (decimalPart) { + const decimals = decimalPart.replace(/e.+/i, '').length; + if (decimals > maxDecimals) { + maxDecimals = decimals; + } + } + }); + }); + }); + return maxDecimals; + }, [parsedData]); + const chartOptions = useMemo(() => ({ responsive: true, maintainAspectRatio: false, @@ -503,7 +527,7 @@ export default function ChartContainer({ return `Step ${context[0].parsed.x}`; }, label: function (context) { - const value = Number(context.parsed.y.toPrecision(4)); + const value = Number(context.parsed.y.toFixed(yDecimalPlaces)); const label = context.dataset?.label || 'Dataset'; return ` ${label}: ${value}`; }, @@ -539,13 +563,13 @@ export default function ChartContainer({ bounds: 'data', ticks: { callback: function (value) { - return Number(value.toPrecision(2)); + return Number(value.toFixed(yDecimalPlaces)); } } } }, elements: { point: { radius: 0 } } - }), [xRange, onXRangeChange]); + }), [xRange, onXRangeChange, yDecimalPlaces]); const buildComparisonChartData = (dataArray) => { const baselineVal = From 862438db6b790fce56b126c3f8b63ee78b8ee4f1 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:11:11 +0800 Subject: [PATCH 2/2] Refine y-axis tick formatting --- src/components/ChartContainer.jsx | 15 +++++++++++---- src/components/__tests__/ChartContainer.test.jsx | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index 7733bae..2da6f84 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -447,6 +447,8 @@ export default function ChartContainer({ return maxDecimals; }, [parsedData]); + const tickStep = useMemo(() => Math.pow(10, -yDecimalPlaces), [yDecimalPlaces]); + const chartOptions = useMemo(() => ({ responsive: true, maintainAspectRatio: false, @@ -562,6 +564,7 @@ export default function ChartContainer({ title: { display: true, text: 'Value' }, bounds: 'data', ticks: { + stepSize: tickStep, callback: function (value) { return Number(value.toFixed(yDecimalPlaces)); } @@ -569,7 +572,7 @@ export default function ChartContainer({ } }, elements: { point: { radius: 0 } } - }), [xRange, onXRangeChange, yDecimalPlaces]); + }), [xRange, onXRangeChange, yDecimalPlaces, tickStep]); const buildComparisonChartData = (dataArray) => { const baselineVal = @@ -704,11 +707,13 @@ export default function ChartContainer({ const showComparison = dataArray.length >= 2; const yRange = calculateYRange(dataArray); + const yMin = Math.floor(yRange.min / tickStep) * tickStep; + const yMax = Math.ceil(yRange.max / tickStep) * tickStep; const options = { ...chartOptions, scales: { ...chartOptions.scales, - y: { ...chartOptions.scales.y, min: yRange.min, max: yRange.max } + y: { ...chartOptions.scales.y, min: yMin, max: yMax } } }; @@ -717,12 +722,14 @@ export default function ChartContainer({ if (showComparison) { const compResult = buildComparisonChartData(dataArray); stats = compResult.stats.length > 0 ? compResult.stats : null; - const compRange = calculateYRange(compResult.datasets); + const compRange = calculateYRange(compResult.datasets); + const compMin = Math.floor(compRange.min / tickStep) * tickStep; + const compMax = Math.ceil(compRange.max / tickStep) * tickStep; const compOptions = { ...chartOptions, scales: { ...chartOptions.scales, - y: { ...chartOptions.scales.y, min: compRange.min, max: compRange.max } + y: { ...chartOptions.scales.y, min: compMin, max: compMax } } }; const compActions = ( diff --git a/src/components/__tests__/ChartContainer.test.jsx b/src/components/__tests__/ChartContainer.test.jsx index 04b1785..ac524b7 100644 --- a/src/components/__tests__/ChartContainer.test.jsx +++ b/src/components/__tests__/ChartContainer.test.jsx @@ -109,6 +109,7 @@ describe('ChartContainer', () => { expect(opts.interaction.axis).toBe('x'); expect(opts.plugins.tooltip.mode).toBe('nearest'); expect(opts.plugins.tooltip.axis).toBe('x'); + expect(opts.scales.y.ticks.stepSize).toBe(0.1); // simulate hover to trigger sync const hover = __lineProps[0].options.onHover; @@ -140,6 +141,7 @@ describe('ChartContainer', () => { {} ]; + const startIndex = __lineProps.length; render( { screen.getByText(/metric3/); // data range applied (start 1 end 3 => 2 points for loss) - const currentProps = __lineProps.slice(-5); + const currentProps = __lineProps.slice(startIndex); expect(currentProps[0].data.datasets[0].data).toHaveLength(2); + expect(currentProps[0].options.scales.y.ticks.stepSize).toBe(1); // trigger container mouse leave const container = screen.getAllByTestId('line-chart')[0].parentElement;