diff --git a/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.spec.tsx b/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.spec.tsx new file mode 100644 index 00000000000000..3015a705e5358d --- /dev/null +++ b/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.spec.tsx @@ -0,0 +1,53 @@ +import {renderHook} from 'sentry-test/reactTestingLibrary'; + +import type {Series} from 'sentry/types/echarts'; +import {useDetectorChartAxisBounds} from 'sentry/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds'; + +function makeSeries(values: number[]): Series { + return { + seriesName: 'test', + data: values.map((value, i) => ({name: i, value})), + }; +} + +describe('useDetectorChartAxisBounds', () => { + it('anchors the min at 0 and pads the max for non-percentage metrics', () => { + const {result} = renderHook(() => + useDetectorChartAxisBounds({ + series: [makeSeries([450, 500, 550])], + thresholdMaxValue: 300, + aggregate: 'count()', + }) + ); + + // min is anchored at 0 rather than zooming into the clustered data, max gets 10% padding + expect(result.current).toEqual({minValue: 0, maxValue: 605}); + }); + + it('uses the threshold as the max without padding when it exceeds the data', () => { + const {result} = renderHook(() => + useDetectorChartAxisBounds({ + series: [makeSeries([100])], + thresholdMaxValue: 200, + aggregate: 'count()', + }) + ); + + // threshold is the ceiling, so it's used as-is so the threshold line sits at the top edge + expect(result.current.maxValue).toBe(200); + }); + + it('zooms the min into the data range and caps the max at 1 for percentage metrics', () => { + const {result} = renderHook(() => + useDetectorChartAxisBounds({ + series: [makeSeries([0.9, 0.95, 1])], + thresholdMaxValue: 0.5, + aggregate: 'failure_rate()', + }) + ); + + // min zooms to seriesMin - 10% padding, max is capped at 100% + expect(result.current.minValue).toBeCloseTo(0.81); + expect(result.current.maxValue).toBe(1); + }); +}); diff --git a/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.tsx b/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.tsx index 2e75d806ee0e40..c4ef3b2b7f84ea 100644 --- a/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.tsx +++ b/static/app/views/detectors/components/details/metric/utils/useDetectorChartAxisBounds.tsx @@ -41,6 +41,8 @@ export function useDetectorChartAxisBounds({ const seriesMax = Math.max(...allSeriesValues); const seriesMin = Math.min(...allSeriesValues); + const isPercentage = aggregate && aggregateOutputType(aggregate) === 'percentage'; + // Determine the max value: use threshold if it's higher than data, otherwise add padding to data let maxValue: number; if (thresholdMaxValue && thresholdMaxValue >= seriesMax) { @@ -53,14 +55,18 @@ export function useDetectorChartAxisBounds({ } // Cap percentage metrics at 100% (1.0 in 0-1 scale) - const isPercentage = aggregate && aggregateOutputType(aggregate) === 'percentage'; if (isPercentage && maxValue > 1) { maxValue = 1; } - // Add padding to min value - const minPadding = seriesMin * 0.1; - const minValue = Math.max(0, seriesMin - minPadding); + // For percentage metrics, zoom into the data range (e.g. 90% -> 100%) since + // pinning to 0 hides small variations. For all other metrics, anchor the + // axis at 0. + let minValue = 0; + if (isPercentage) { + const minPadding = seriesMin * 0.1; + minValue = Math.max(0, seriesMin - minPadding); + } return { maxValue,