diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts index 977729f25..c77725573 100644 --- a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts @@ -7,6 +7,7 @@ import { DEFAULT_CHART_CONFIG, DEFAULT_COLUMN_LABEL_FORMAT, } from '@buster/server-shared/metrics'; +import { fa } from '@faker-js/faker'; import type { CartesianScaleTypeRegistry, Scale, @@ -20,7 +21,7 @@ import type { BusterChartProps } from '../../../BusterChart.types'; import { formatYAxisLabel, yAxisSimilar } from '../../../commonHelpers'; import { useY2AxisTitle } from './axisHooks/useY2AxisTitle'; -export const DEFAULT_Y2_AXIS_COUNT = 7; +export const DEFAULT_Y2_AXIS_COUNT = 9; export const useY2Axis = ({ columnLabelFormats, @@ -47,6 +48,7 @@ export const useY2Axis = ({ const selectedAxis = selectedAxisProp as ComboChartAxis; const y2AxisKeys = selectedAxis.y2 || []; const yAxisMinValue = yAxis?.min; + const yAxisMaxValue = yAxis?.max; const y2AxisKeysString = useMemo(() => { return y2AxisKeys.join(','); @@ -116,6 +118,7 @@ export const useY2Axis = ({ includeBounds: true, }, min: yAxisMinValue, + max: yAxisMaxValue, grid: { drawOnChartArea: false, // only want the grid lines for one axis to show up }, diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts index 3d3ee41b3..cf395fe44 100644 --- a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts @@ -1,11 +1,14 @@ /** biome-ignore-all lint/style/noNonNullAssertion: false positive */ + +import round from 'lodash/round'; import { useMemo } from 'react'; import type { BusterChartProps } from '../../../BusterChart.types'; -const MIN_PERCENT_DIFFERENCE = 1.5; +const MIN_PERCENT_DIFFERENCE = 2; const MAX_PERCENT_DIFFERENCE = 2; const MIN_OFFSET = 1.1; const MAX_OFFSET = 1.1; +const PERCENTAGE_STEP = 5; export const useYTickValues = ({ hasY2Axis, @@ -25,23 +28,34 @@ export const useYTickValues = ({ const shouldUseMinAndMaxValues = hasY2Axis && columnMetadata && selectedChartType === 'combo'; const checkValues = useMemo(() => { + if (!shouldUseMinAndMaxValues) return []; return [...yAxisKeys, ...y2AxisKeys]; - }, [yAxisKeys, y2AxisKeys]); + }, [yAxisKeys, y2AxisKeys, shouldUseMinAndMaxValues]); + + const hasOnePercentageValue = useMemo(() => { + if (!shouldUseMinAndMaxValues) return false; + return checkValues.some((key) => { + const columnFormat = columnLabelFormats[key]; + return columnFormat?.style === 'percent'; + }); + }, [checkValues, columnLabelFormats, shouldUseMinAndMaxValues]); const allYValuesArePercentage = useMemo(() => { + if (!shouldUseMinAndMaxValues) return false; return checkValues.every((key) => { const columnFormat = columnLabelFormats[key]; return columnFormat?.style === 'percent'; }); - }, [checkValues, columnLabelFormats]); + }, [checkValues, columnLabelFormats, shouldUseMinAndMaxValues]); const columnMap = useMemo(() => { - if (!columnMetadata) return new Map(); + if (!columnMetadata || !shouldUseMinAndMaxValues) return new Map(); return new Map(columnMetadata.map((col) => [col.name, col])); - }, [columnMetadata]); + }, [columnMetadata, shouldUseMinAndMaxValues]); // Calculate min/max ranges for y-axis columns const yAxisRange = useMemo(() => { + if (!shouldUseMinAndMaxValues) return { min: Infinity, max: -Infinity }; return yAxisKeys.reduce( (acc, key) => { const column = columnMap.get(key); @@ -55,10 +69,11 @@ export const useYTickValues = ({ }, { min: Infinity, max: -Infinity } ); - }, [yAxisKeys, columnMap]); + }, [yAxisKeys, columnMap, shouldUseMinAndMaxValues]); // Calculate min/max ranges for y2-axis columns const y2AxisRange = useMemo(() => { + if (!shouldUseMinAndMaxValues) return { min: Infinity, max: -Infinity }; return y2AxisKeys.reduce( (acc, key) => { const column = columnMap.get(key); @@ -72,7 +87,7 @@ export const useYTickValues = ({ }, { min: Infinity, max: -Infinity } ); - }, [y2AxisKeys, columnMap]); + }, [y2AxisKeys, columnMap, shouldUseMinAndMaxValues]); const minTickValue: number | undefined = useMemo(() => { if (!shouldUseMinAndMaxValues) return undefined; @@ -110,9 +125,16 @@ export const useYTickValues = ({ // If both min and max values are similar, use the lowest min value if (minValuesAreSimilar && maxValuesAreSimilar) { - return Math.min(yAxisRange.min, y2AxisRange.min) * MIN_OFFSET; + if (hasOnePercentageValue) { + // If there's at least one percentage value, round the min to the nearest lower multiple of 5 + const min = Math.min(yAxisRange.min, y2AxisRange.min) * MIN_OFFSET; + return Math.floor(min / PERCENTAGE_STEP) * PERCENTAGE_STEP; + } + + return round(Math.min(yAxisRange.min, y2AxisRange.min) * MIN_OFFSET, 0); } }, [ + hasOnePercentageValue, columnLabelFormats, yAxisRange, y2AxisRange, @@ -122,7 +144,60 @@ export const useYTickValues = ({ allYValuesArePercentage, ]); - const maxTickValue: number | undefined = undefined; + const maxTickValue: number | undefined = useMemo(() => { + if (!shouldUseMinAndMaxValues) return undefined; + + // If all Y values are percentages, return the highest value + if (allYValuesArePercentage) { + const highestValue = checkValues.reduce((max, key) => { + const column = columnMap.get(key); + return Math.max(max, Number(column?.max_value ?? 0)); + }, -Infinity); + if (highestValue === -Infinity) return undefined; + if (highestValue < 1) return 1; + return highestValue; + } + + // Reset infinities if no valid data found + if (yAxisRange.min === Infinity) yAxisRange.min = 0; + if (yAxisRange.max === -Infinity) yAxisRange.max = 0; + if (y2AxisRange.min === Infinity) y2AxisRange.min = 0; + if (y2AxisRange.max === -Infinity) y2AxisRange.max = 0; + + // Check if min values are within 150% of each other + const minValuesAreSimilar = + Math.abs(yAxisRange.min) > 0 && Math.abs(y2AxisRange.min) > 0 + ? Math.max(yAxisRange.min, y2AxisRange.min) / Math.min(yAxisRange.min, y2AxisRange.min) <= + MIN_PERCENT_DIFFERENCE + : yAxisRange.min === y2AxisRange.min; + + // Check if max values are within 200% of each other + const maxValuesAreSimilar = + Math.abs(yAxisRange.max) > 0 && Math.abs(y2AxisRange.max) > 0 + ? Math.max(yAxisRange.max, y2AxisRange.max) / Math.min(yAxisRange.max, y2AxisRange.max) <= + MAX_PERCENT_DIFFERENCE + : yAxisRange.max === y2AxisRange.max; + + // If both min and max values are similar, use the highest max value + if (minValuesAreSimilar && maxValuesAreSimilar) { + if (hasOnePercentageValue) { + // If there's at least one percentage value, round the max to the nearest higher multiple of 5 + const max = Math.max(yAxisRange.max, y2AxisRange.max) * MAX_OFFSET; + return Math.ceil(max / PERCENTAGE_STEP) * PERCENTAGE_STEP; + } + + return round(Math.max(yAxisRange.max, y2AxisRange.max) * MAX_OFFSET, 0); + } + }, [ + hasOnePercentageValue, + columnLabelFormats, + yAxisRange, + y2AxisRange, + shouldUseMinAndMaxValues, + columnMap, + checkValues, + allYValuesArePercentage, + ]); return { minTickValue,