From 3656ee586ca380d3d5597e3250bdaa73d3cca47c Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 9 May 2025 14:54:12 -0600 Subject: [PATCH] scatter x axis formatter --- .../useOptions/useXAxis/useXAxis.test.ts | 128 ++++++++++++++++++ .../hooks/useOptions/useXAxis/useXAxis.ts | 37 +++-- web/src/lib/columnFormatter.ts | 4 +- 3 files changed, 160 insertions(+), 9 deletions(-) diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.test.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.test.ts index 8a16d0d56..fb30993ba 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.test.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.test.ts @@ -284,4 +284,132 @@ describe('useXAxis', () => { expect(result.current?.title?.display).toBe(false); expect(result.current?.title?.text).toBe(''); }); + + it('should correctly set xAxisColumnFormats and firstXColumnLabelFormat', () => { + // Test with default category column + const { result } = renderHook(() => useXAxis(defaultProps)); + + // Get the internal state to verify xAxisColumnFormats and firstXColumnLabelFormat + // We need to access the useXAxis hook results + expect(result.current).toBeDefined(); + + // For scatter chart + const scatterProps = { + ...defaultProps, + selectedChartType: ChartType.Scatter, + selectedAxis: { + x: ['numeric_column'], + y: ['numeric_column'] + } as ChartEncodes + }; + + const { result: scatterResult } = renderHook(() => useXAxis(scatterProps)); + expect(scatterResult.current).toBeDefined(); + expect(scatterResult.current?.type).toBe('linear'); + + // For multiple x-axis columns + const multipleColumnsProps = { + ...defaultProps, + selectedAxis: { + x: ['category_column', 'numeric_column'], + y: ['numeric_column'] + } as ChartEncodes + }; + + const { result: multiResult } = renderHook(() => useXAxis(multipleColumnsProps)); + expect(multiResult.current).toBeDefined(); + expect(multiResult.current?.type).toBe('category'); + + // For unsupported chart type (pie) + const pieProps = { + ...defaultProps, + selectedChartType: ChartType.Pie + }; + + const { result: pieResult } = renderHook(() => useXAxis(pieProps)); + expect(pieResult.current).toBeUndefined(); + }); + + it('should correctly handle firstXColumnLabelFormat for scatter chart', () => { + // Specifically test the firstXColumnLabelFormat logic for scatter charts + const scatterProps = { + ...defaultProps, + selectedChartType: ChartType.Scatter, + selectedAxis: { + x: ['numeric_column'], + y: ['numeric_column'] + } as ChartEncodes + }; + + const { result } = renderHook(() => useXAxis(scatterProps)); + + // Verify that scatter charts get proper formatting with minimumFractionDigits and maximumFractionDigits set to 0 + expect(result.current).toBeDefined(); + expect(result.current?.type).toBe('linear'); + + // Test the tick callback formatting + // We can't directly access the firstXColumnLabelFormat from outside the hook, + // but we can test its effects through the tick callback behavior + expect(result.current?.ticks?.callback).toBeDefined(); + + // Verify custom column label formats are applied + const customFormatProps = { + ...scatterProps, + columnLabelFormats: { + ...defaultProps.columnLabelFormats, + numeric_column: { + ...DEFAULT_COLUMN_LABEL_FORMAT, + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const, + minimumFractionDigits: 3, + maximumFractionDigits: 3 + } + } + }; + + const { result: customResult } = renderHook(() => useXAxis(customFormatProps)); + expect(customResult.current).toBeDefined(); + expect(customResult.current?.type).toBe('linear'); + }); + + it('should apply correct column formats for date columns with different formats', () => { + // Test with custom date format + const customDateFormatProps = { + ...defaultProps, + selectedChartType: ChartType.Line, + selectedAxis: { + x: ['date_column'], + y: ['numeric_column'] + } as ChartEncodes, + columnLabelFormats: { + ...defaultProps.columnLabelFormats, + date_column: { + ...defaultProps.columnLabelFormats.date_column, + dateFormat: 'YYYY-MM-DD' + } + } + }; + + const { result } = renderHook(() => useXAxis(customDateFormatProps)); + expect(result.current).toBeDefined(); + expect(result.current?.type).toBe('time'); + expect(result.current?.ticks?.callback).toBeDefined(); + + // Test with auto date format + const autoDateFormatProps = { + ...customDateFormatProps, + columnLabelFormats: { + ...defaultProps.columnLabelFormats, + date_column: { + ...defaultProps.columnLabelFormats.date_column, + dateFormat: 'auto' + } + } + }; + + const { result: autoResult } = renderHook(() => useXAxis(autoDateFormatProps)); + expect(autoResult.current).toBeDefined(); + expect(autoResult.current?.type).toBe('time'); + expect(autoResult.current?.ticks?.callback).toBeDefined(); + }); }); diff --git a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts index eab7a7541..f7b1bc19a 100644 --- a/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts +++ b/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useXAxis/useXAxis.ts @@ -67,6 +67,17 @@ export const useXAxis = ({ }, {}); }, [selectedAxis.x, columnLabelFormats, isSupportedType]); + const firstXColumnLabelFormat = useMemo(() => { + if (isScatterChart) { + return { + ...xAxisColumnFormats[selectedAxis.x[0]], + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }; + } + return xAxisColumnFormats[selectedAxis.x[0]]; + }, [isScatterChart, xAxisColumnFormats, selectedAxis.x]); + const stacked = useIsStacked({ selectedChartType, lineGroupType, barGroupType }); const grid: DeepPartial | undefined = useMemo(() => { @@ -79,10 +90,9 @@ export const useXAxis = ({ const type: DeepPartial['scales']['x']['type']> = useMemo(() => { const xAxisKeys = Object.keys(xAxisColumnFormats); const xAxisKeysLength = xAxisKeys.length; - const firstXKey = xAxisKeys[0]; if (xAxisKeysLength === 1) { - const xIsDate = xAxisColumnFormats[firstXKey].columnType === 'date'; + const xIsDate = firstXColumnLabelFormat.columnType === 'date'; if ((isLineChart || isScatterChart) && xIsDate) { return 'time'; @@ -101,17 +111,24 @@ export const useXAxis = ({ } if (isScatterChart && xAxisKeysLength === 1) { - const isNumeric = isNumericColumnType(xAxisColumnFormats[firstXKey]?.columnType); + const isNumeric = isNumericColumnType(firstXColumnLabelFormat?.columnType); if (isNumeric) return 'linear'; } return 'category'; - }, [isScatterChart, isComboChart, isLineChart, columnSettings, xAxisColumnFormats]); + }, [ + isScatterChart, + isComboChart, + isLineChart, + columnSettings, + xAxisColumnFormats, + firstXColumnLabelFormat + ]); const derivedTimeUnit = useMemo(() => { if (type !== 'time') return false; - const fmt = xAxisColumnFormats[selectedAxis.x[0]].dateFormat; + const fmt = firstXColumnLabelFormat.dateFormat; if (!fmt || fmt === 'auto') return false; // look for patterns in your DATE_FORMATS keys @@ -122,7 +139,7 @@ export const useXAxis = ({ if (/H{1,2}/.test(fmt)) return 'hour'; // fall back return false; - }, [xAxisColumnFormats, selectedAxis.x]); + }, [firstXColumnLabelFormat]); const title = useXAxisTitle({ xAxis: selectedAxis.x, @@ -141,8 +158,7 @@ export const useXAxis = ({ const rawValue = this.getLabelForValue(value as number); if (type === 'time' || isDate(rawValue)) { - const xKey = selectedAxis.x[0]; - const xColumnLabelFormat = xAxisColumnFormats[xKey]; + const xColumnLabelFormat = firstXColumnLabelFormat; const isAutoFormat = xColumnLabelFormat.dateFormat === 'auto'; if (isAutoFormat) { const unit = (this.chart.scales['x'] as TimeScale)._unit as @@ -162,6 +178,11 @@ export const useXAxis = ({ return truncateText(res, 24); } + if (isScatterChart) { + //raw value does not work for scatter charts, it returns the value as a string + return formatLabel(value, firstXColumnLabelFormat); + } + return DEFAULT_X_AXIS_TICK_CALLBACK.call(this, value, index, this.getLabels() as any); }); diff --git a/web/src/lib/columnFormatter.ts b/web/src/lib/columnFormatter.ts index c1e21614c..45df4de80 100644 --- a/web/src/lib/columnFormatter.ts +++ b/web/src/lib/columnFormatter.ts @@ -81,7 +81,9 @@ export const formatLabel = ( if (style === 'currency') { formattedText = formatNumber(roundedNumber, { currency, - compact: compactNumbers + compact: compactNumbers, + minimumFractionDigits: Math.min(minimumFractionDigits, maximumFractionDigits), + maximumFractionDigits: Math.max(minimumFractionDigits, maximumFractionDigits) }); } else { formattedText = formatNumber(roundedNumber, {