From 690fea6f4a20204670b6498c385fbf54e7eaeaf4 Mon Sep 17 00:00:00 2001 From: Mohammad Al-Qasem Date: Sun, 23 Nov 2025 20:59:19 -0500 Subject: [PATCH 1/2] feat(table): add gradient toggle for conditional formatting Add a "Use gradient" checkbox to conditional formatting that allows users to switch between gradient colors (varying opacity) and solid colors (full opacity). Changes: - Add useGradient flag to ConditionalFormattingConfig type - Modify getColorFunction to support solid color mode when useGradient is false - Add "Use gradient" checkbox to FormattingPopoverContent UI - Default to checked (true) for backward compatibility with existing configs --- .../superset-ui-chart-controls/src/types.ts | 1 + .../src/utils/getColorFormatters.ts | 8 ++ .../test/utils/getColorFormatters.test.ts | 98 +++++++++++++ .../test/TableChart.test.tsx | 130 ++++++++++++++++++ .../FormattingPopoverContent.test.tsx | 50 +++++++ .../FormattingPopoverContent.tsx | 20 +++ .../ConditionalFormattingControl/types.ts | 1 + 7 files changed, 308 insertions(+) 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 eaca8133d445..b7b73fc016bd 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -480,6 +480,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 dfa48efdf405..7becc99fa97e 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[], alpha?: boolean, @@ -231,6 +232,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 c7d6c99b2cf0..a5831c747a34 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 @@ -532,3 +532,101 @@ test('correct column string config', () => { expect(colorFormatters[3].column).toEqual('name'); 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'); +}); \ No newline at end of file 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 1f19c7944fd7..8b042e4055c4 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -1085,6 +1085,136 @@ describe('plugin-chart-table', () => { 'rgba(172, 225, 196, 1)', ); }); + + 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)', + ); + }); }); }); }); 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 aaa17cd67d3c..0e5f1ceea68d 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx @@ -182,3 +182,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 aa63c778a186..7bb2d7cf69a0 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -261,6 +261,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', @@ -365,6 +368,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< From 5d5944a06e244d0cfaf7111a3af60a00dbcf2ae1 Mon Sep 17 00:00:00 2001 From: Mohammad Al-Qasem Date: Sun, 30 Nov 2025 18:18:47 -0500 Subject: [PATCH 2/2] style(test): add missing newline at end of file --- .../test/utils/getColorFormatters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5831c747a34..355e0bf58c14 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 @@ -629,4 +629,4 @@ test('getColorFormatters with useGradient flag', () => { // Second formatter with useGradient: true should return gradient color expect(colorFormatters[1].column).toEqual('count'); expect(colorFormatters[1].getColorFromValue(100)).toEqual('#00FF00FF'); -}); \ No newline at end of file +});