diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts index 29dcafc45b8..79075b35dbb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts @@ -222,8 +222,20 @@ export default function transformProps( const { setDataMask = () => {}, onContextMenu } = hooks; - const min = minVal ?? calculateMin(transformedData); - const max = maxVal ?? calculateMax(transformedData); + const isValidNumber = ( + val: number | null | undefined | string, + ): val is number => { + if (val == null || val === '') return false; + const num = typeof val === 'string' ? Number(val) : val; + return !Number.isNaN(num) && Number.isFinite(num); + }; + + const min = isValidNumber(minVal) + ? Number(minVal) + : calculateMin(transformedData); + const max = isValidNumber(maxVal) + ? Number(maxVal) + : calculateMax(transformedData); const axisLabels = range(min, max, (max - min) / splitNumber); const axisLabelLength = Math.max( ...axisLabels.map(label => numberFormatter(label).length).concat([1]), diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts index 78298ef72b6..2601464e5ca 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts @@ -281,6 +281,411 @@ describe('Echarts Gauge transformProps', () => { }); }); +describe('Min/Max calculation and axis labels', () => { + const baseFormData: SqlaFormData = { + datasource: '26__table', + viz_type: VizType.Gauge, + metric: 'count', + adhocFilters: [], + rowLimit: 10, + startAngle: 225, + endAngle: -45, + colorScheme: 'SUPERSET_DEFAULT', + fontSize: 14, + numberFormat: 'SMART_NUMBER', + valueFormatter: '{value}', + showPointer: true, + animation: true, + showAxisTick: false, + showSplitLine: false, + splitNumber: 10, + showProgress: true, + overlap: true, + roundCap: false, + groupby: [], + }; + + it('should use provided minVal and maxVal when valid numbers', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: 10, + maxVal: 100, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 50 }, { count: 75 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(10); + expect(series.max).toBe(100); + }); + + it('should calculate min/max from data when minVal is null', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: null, + maxVal: 100, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 20 }, { count: 80 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(100); + }); + + it('should calculate min/max from data when maxVal is null', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: 0, + maxVal: null, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 20 }, { count: 80 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(160); + }); + + it('should calculate min/max from data when both are null', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: null, + maxVal: null, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 15 }, { count: 45 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(90); + }); + + it('should calculate min/max from data when minVal is empty string', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: '' as any, + maxVal: 200, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 30 }, { count: 60 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(200); + }); + + it('should calculate min/max from data when maxVal is invalid string', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: 0, + maxVal: 'invalid' as any, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 25 }, { count: 75 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(150); + }); + + it('should handle negative values in min/max calculation', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: null, + maxVal: null, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: -20 }, { count: 40 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(-40); + expect(series.max).toBe(80); + }); + + it('should generate axis labels correctly based on min, max, and splitNumber', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: 0, + maxVal: 100, + splitNumber: 5, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 50 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(100); + expect(series.splitNumber).toBe(5); + expect(series.axisLabel).toBeDefined(); + expect(series.axisLabel.formatter).toBeDefined(); + }); + + it('should calculate axis label length correctly for different number formats', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: 0, + maxVal: 1000, + splitNumber: 10, + numberFormat: ',d', + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 500 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.axisLabel).toBeDefined(); + expect(series.axisLabel.formatter).toBeDefined(); + expect(typeof series.axisLabel.formatter).toBe('function'); + }); + + it('should integrate interval bounds and colors with calculated min/max', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: null, + maxVal: null, + intervals: '20,60', + intervalColorIndices: '1,2', + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 10 }, { count: 50 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(100); + + const { axisLine } = series; + expect(axisLine.lineStyle.color).toEqual( + expect.arrayContaining([ + expect.arrayContaining([expect.any(Number), expect.any(String)]), + ]), + ); + }); + + it('should handle zero values in data correctly', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: null, + maxVal: null, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 0 }, { count: 0 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(0); + expect(series.max).toBe(0); + }); + + it('should handle string minVal/maxVal that can be converted to numbers', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: '10' as any, + maxVal: '200' as any, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 50 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.min).toBe(10); + expect(series.max).toBe(200); + }); + + it('should handle different splitNumber values', () => { + const formData: SqlaFormData = { + ...baseFormData, + minVal: 0, + maxVal: 100, + splitNumber: 20, + }; + const queriesData = [ + { + colnames: ['count'], + data: [{ count: 50 }], + }, + ]; + + const chartProps = new ChartProps({ + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + }); + + const result = transformProps(chartProps as EchartsGaugeChartProps); + const series = (result.echartOptions as any).series[0]; + + expect(series.splitNumber).toBe(20); + }); +}); + describe('getIntervalBoundsAndColors', () => { it('should generate correct interval bounds and colors', () => { const colorFn = CategoricalColorNamespace.getScale(