diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 02cb2ab53608..115eb0b65b65 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -484,6 +484,7 @@ export type ConditionalFormattingConfig = { colorScheme?: string; toAllRow?: boolean; toTextColor?: boolean; + useGradient?: boolean; }; export type ColorFormatters = { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts index ccf16208721c..3bdb9a0ad6a5 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts @@ -69,6 +69,7 @@ export const getColorFunction = ( targetValueLeft, targetValueRight, colorScheme, + useGradient, }: ConditionalFormattingConfig, columnValues: number[] | string[] | (boolean | null)[], alpha?: boolean, @@ -256,6 +257,13 @@ export const getColorFunction = ( const compareResult = comparatorFunction(value, columnValues); if (compareResult === false) return undefined; const { cutoffValue, extremeValue } = compareResult; + + // If useGradient is explicitly false, return solid color + if (useGradient === false) { + return colorScheme; + } + + // Otherwise apply gradient (default behavior for backward compatibility) if (alpha === undefined || alpha) { return addAlpha( colorScheme, diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts index 3ceab82ef3cb..3671ba37d547 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts @@ -596,6 +596,104 @@ test('correct column string config', () => { expect(colorFormatters[3].getColorFromValue('Carlos')).toEqual('#FF0000FF'); }); +test('getColorFunction with useGradient false returns solid color', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.GreaterOrEqual, + targetValue: 50, + colorScheme: '#FF0000', + column: 'count', + useGradient: false, + }, + countValues, + ); + // When useGradient is false, should return solid color without opacity + expect(colorFunction(50)).toEqual('#FF0000'); + expect(colorFunction(100)).toEqual('#FF0000'); + expect(colorFunction(0)).toBeUndefined(); +}); + +test('getColorFunction with useGradient true returns gradient color', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.GreaterOrEqual, + targetValue: 50, + colorScheme: '#FF0000', + column: 'count', + useGradient: true, + }, + countValues, + ); + // When useGradient is true, should return gradient color with opacity + expect(colorFunction(50)).toEqual('#FF00000D'); + expect(colorFunction(100)).toEqual('#FF0000FF'); + expect(colorFunction(0)).toBeUndefined(); +}); + +test('getColorFunction with useGradient undefined defaults to gradient (backward compatibility)', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.GreaterOrEqual, + targetValue: 50, + colorScheme: '#FF0000', + column: 'count', + // useGradient is undefined + }, + countValues, + ); + // When useGradient is undefined, should default to gradient for backward compatibility + expect(colorFunction(50)).toEqual('#FF00000D'); + expect(colorFunction(100)).toEqual('#FF0000FF'); + expect(colorFunction(0)).toBeUndefined(); +}); + +test('getColorFunction with useGradient false and None operator returns solid color', () => { + const colorFunction = getColorFunction( + { + operator: Comparator.None, + colorScheme: '#FF0000', + column: 'count', + useGradient: false, + }, + countValues, + ); + // When useGradient is false, all matching values should return solid color + expect(colorFunction(20)).toBeUndefined(); + expect(colorFunction(50)).toEqual('#FF0000'); + expect(colorFunction(75)).toEqual('#FF0000'); + expect(colorFunction(100)).toEqual('#FF0000'); + expect(colorFunction(120)).toBeUndefined(); +}); + +test('getColorFormatters with useGradient flag', () => { + const columnConfig = [ + { + operator: Comparator.GreaterThan, + targetValue: 50, + colorScheme: '#FF0000', + column: 'count', + useGradient: false, + }, + { + operator: Comparator.GreaterThan, + targetValue: 50, + colorScheme: '#00FF00', + column: 'count', + useGradient: true, + }, + ]; + const colorFormatters = getColorFormatters(columnConfig, mockData); + expect(colorFormatters.length).toEqual(2); + + // First formatter with useGradient: false should return solid color + expect(colorFormatters[0].column).toEqual('count'); + expect(colorFormatters[0].getColorFromValue(100)).toEqual('#FF0000'); + + // Second formatter with useGradient: true should return gradient color + expect(colorFormatters[1].column).toEqual('count'); + expect(colorFormatters[1].getColorFromValue(100)).toEqual('#00FF00FF'); +}); + test('correct column boolean config', () => { const columnConfigBoolean = [ { diff --git a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx index 495c5f5cf22e..fd5b8f68dfb6 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -1216,6 +1216,136 @@ describe('plugin-chart-table', () => { ); }); + test('render color with useGradient false returns solid color', () => { + render( + ProviderWrapper({ + children: ( + ', + targetValue: 2467, + useGradient: false, + }, + ], + }, + })} + /> + ), + }), + ); + + // When useGradient is false, should return solid color (no opacity variation) + // The color should be the same for all matching values + expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( + 'rgb(172, 225, 196)', + ); + expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); + }); + + test('render color with useGradient true returns gradient color', () => { + render( + ProviderWrapper({ + children: ( + ', + targetValue: 2467, + useGradient: true, + }, + ], + }, + })} + /> + ), + }), + ); + + // When useGradient is true, should return gradient color with opacity + expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); + }); + + test('render color with useGradient undefined defaults to gradient (backward compatibility)', () => { + render( + ProviderWrapper({ + children: ( + ', + targetValue: 2467, + // useGradient is undefined + }, + ], + }, + })} + /> + ), + }), + ); + + // When useGradient is undefined, should default to gradient for backward compatibility + expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); + }); + + test('render color with useGradient false and None operator returns solid color', () => { + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + + // When useGradient is false with None operator, all values should have solid color + expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( + 'rgb(172, 225, 196)', + ); + expect(getComputedStyle(screen.getByTitle('2467')).background).toBe( + 'rgb(172, 225, 196)', + ); + }); + it('recalculates totals when user filters data', async () => { const formDataWithTotals = { ...testData.basic.formData, diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx index 29b9fe7b26f6..1aedbf99d169 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx @@ -203,3 +203,53 @@ test('Not displays the toAllRow and toTextColor flags', () => { expect(screen.queryByText('To entire row')).not.toBeInTheDocument(); expect(screen.queryByText('To text color')).not.toBeInTheDocument(); }); + +test('displays Use gradient checkbox', () => { + render( + , + ); + + expect(screen.getByText('Use gradient')).toBeInTheDocument(); +}); + +// Helper function to find the "Use gradient" checkbox +// The checkbox and text are in sibling columns within the same row +const findUseGradientCheckbox = (): HTMLInputElement => { + const useGradientText = screen.getByText('Use gradient'); + // Find the common parent row that contains both the text and checkbox + let rowElement: HTMLElement | null = useGradientText.parentElement; + while (rowElement) { + const checkbox = rowElement.querySelector('input[type="checkbox"]'); + if (checkbox && rowElement.textContent?.includes('Use gradient')) { + return checkbox as HTMLInputElement; + } + rowElement = rowElement.parentElement; + } + throw new Error('Could not find Use gradient checkbox'); +}; + +test('Use gradient checkbox defaults to checked', () => { + render( + , + ); + + const checkbox = findUseGradientCheckbox(); + expect(checkbox).toBeChecked(); +}); + +test('Use gradient checkbox can be toggled', async () => { + render( + , + ); + + const checkbox = findUseGradientCheckbox(); + expect(checkbox).toBeChecked(); + + // Uncheck the checkbox + fireEvent.click(checkbox); + expect(checkbox).not.toBeChecked(); + + // Check the checkbox again + fireEvent.click(checkbox); + expect(checkbox).toBeChecked(); +}); diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index 1156872c447e..0a299484b453 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -292,6 +292,9 @@ export const FormattingPopoverContent = ({ const [toTextColor, setToTextColor] = useState(() => Boolean(config?.toTextColor), ); + const [useGradient, setUseGradient] = useState(() => + config?.useGradient !== undefined ? config.useGradient : true, + ); const useConditionalFormattingFlag = ( flagKey: 'toAllRowCheck' | 'toColorTextCheck', @@ -406,6 +409,23 @@ export const FormattingPopoverContent = ({ + + + + setUseGradient(event.target.checked)} + checked={useGradient} + /> + + + + {t('Use gradient')} + + {showOperatorFields ? ( (props: GetFieldValue) => renderOperatorFields(props, columnType) diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts index 242925dce97e..e7de31a16b44 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts @@ -31,6 +31,7 @@ export type ConditionalFormattingConfig = { colorScheme?: string; toAllRow?: boolean; toTextColor?: boolean; + useGradient?: boolean; }; export type ConditionalFormattingControlProps = ControlComponentProps<