From 61d8a0bd1206ffc96ea2f9284e4c238241fcca79 Mon Sep 17 00:00:00 2001 From: Gerold Busch Date: Mon, 1 May 2023 17:44:46 +0200 Subject: [PATCH] feat: conditional coloring for big number chart (#23064) Co-authored-by: Gerold Busch --- .../src/utils/getColorFormatters.ts | 14 +++++-- .../test/utils/getColorFormatters.test.ts | 19 +++++++++ .../BigNumber/BigNumberTotal/controlPanel.ts | 42 ++++++++++++++++++- .../BigNumberTotal/transformProps.ts | 12 ++++++ .../src/BigNumber/BigNumberViz.tsx | 22 +++++++++- .../src/BigNumber/types.ts | 2 + .../FormattingPopoverContent.tsx | 9 ++-- 7 files changed, 111 insertions(+), 9 deletions(-) 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 f613beb074..1a6aa140a6 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 @@ -62,6 +62,7 @@ export const getColorFunction = ( colorScheme, }: ConditionalFormattingConfig, columnValues: number[], + alpha?: boolean, ) => { let minOpacity = MIN_OPACITY_BOUNDED; const maxOpacity = MAX_OPACITY; @@ -176,10 +177,13 @@ export const getColorFunction = ( const compareResult = comparatorFunction(value, columnValues); if (compareResult === false) return undefined; const { cutoffValue, extremeValue } = compareResult; - return addAlpha( - colorScheme, - getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity), - ); + if (alpha === undefined || alpha) { + return addAlpha( + colorScheme, + getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity), + ); + } + return colorScheme; }; }; @@ -187,6 +191,7 @@ export const getColorFormatters = memoizeOne( ( columnConfig: ConditionalFormattingConfig[] | undefined, data: DataRecord[], + alpha?: boolean, ) => columnConfig?.reduce( (acc: ColorFormatters, config: ConditionalFormattingConfig) => { @@ -204,6 +209,7 @@ export const getColorFormatters = memoizeOne( getColorFromValue: getColorFunction( config, data.map(row => row[config.column!] as number), + alpha, ), }); } 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 4b957f628c..7daa3f968d 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 @@ -188,6 +188,25 @@ describe('getColorFunction()', () => { expect(colorFunction(150)).toBeUndefined(); }); + it('getColorFunction BETWEEN_OR_EQUAL without opacity', () => { + const colorFunction = getColorFunction( + { + operator: COMPARATOR.BETWEEN_OR_EQUAL, + targetValueLeft: 50, + targetValueRight: 100, + colorScheme: '#FF0000', + column: 'count', + }, + countValues, + false, + ); + expect(colorFunction(25)).toBeUndefined(); + expect(colorFunction(50)).toEqual('#FF0000'); + expect(colorFunction(75)).toEqual('#FF0000'); + expect(colorFunction(100)).toEqual('#FF0000'); + expect(colorFunction(125)).toBeUndefined(); + }); + it('getColorFunction BETWEEN_OR_LEFT_EQUAL', () => { const colorFunction = getColorFunction( { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts index daacaa283a..abe4ce215f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts @@ -16,11 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { smartDateFormatter, t } from '@superset-ui/core'; +import { GenericDataType, smartDateFormatter, t } from '@superset-ui/core'; import { ControlPanelConfig, D3_FORMAT_DOCS, D3_TIME_FORMAT_OPTIONS, + Dataset, getStandardizedControls, sections, } from '@superset-ui/chart-controls'; @@ -89,6 +90,45 @@ export default { }, }, ], + [ + { + name: 'conditional_formatting', + config: { + type: 'ConditionalFormattingControl', + renderTrigger: true, + label: t('Conditional Formatting'), + description: t('Apply conditional color formatting to metric'), + shouldMapStateToProps() { + return true; + }, + mapStateToProps(explore, _, chart) { + const verboseMap = explore?.datasource?.hasOwnProperty( + 'verbose_map', + ) + ? (explore?.datasource as Dataset)?.verbose_map + : explore?.datasource?.columns ?? {}; + const { colnames, coltypes } = + chart?.queriesResponse?.[0] ?? {}; + const numericColumns = + Array.isArray(colnames) && Array.isArray(coltypes) + ? colnames + .filter( + (colname: string, index: number) => + coltypes[index] === GenericDataType.NUMERIC, + ) + .map(colname => ({ + value: colname, + label: verboseMap[colname] ?? colname, + })) + : []; + return { + columnOptions: numericColumns, + verboseMap, + }; + }, + }, + }, + ], ], }, ], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts index e690b1ef52..8624e5bc54 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts @@ -16,6 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import { + ColorFormatters, + getColorFormatters, +} from '@superset-ui/chart-controls'; import { getNumberFormatter, GenericDataType, @@ -40,6 +44,7 @@ export default function transformProps( forceTimestampFormatting, timeFormat, yAxisFormat, + conditionalFormatting, } = formData; const refs: Refs = {}; const { data = [], coltypes = [] } = queriesData[0]; @@ -71,6 +76,12 @@ export default function transformProps( const { onContextMenu } = hooks; + const defaultColorFormatters = [] as ColorFormatters; + + const colorThresholdFormatters = + getColorFormatters(conditionalFormatting, data, false) ?? + defaultColorFormatters; + return { width, height, @@ -81,5 +92,6 @@ export default function transformProps( subheader: formattedSubheader, onContextMenu, refs, + colorThresholdFormatters, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx index 4762a789d0..112e21657f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -128,10 +128,29 @@ class BigNumberVis extends React.PureComponent { } renderHeader(maxHeight: number) { - const { bigNumber, headerFormatter, width } = this.props; + const { bigNumber, headerFormatter, width, colorThresholdFormatters } = + this.props; // @ts-ignore const text = bigNumber === null ? t('No data') : headerFormatter(bigNumber); + const hasThresholdColorFormatter = + Array.isArray(colorThresholdFormatters) && + colorThresholdFormatters.length > 0; + + let numberColor; + if (hasThresholdColorFormatter) { + colorThresholdFormatters!.forEach(formatter => { + const formatterResult = bigNumber + ? formatter.getColorFromValue(bigNumber as number) + : false; + if (formatterResult) { + numberColor = formatterResult; + } + }); + } else { + numberColor = 'black'; + } + const container = this.createTemporaryContainer(); document.body.append(container); const fontSize = computeMaxFontSize({ @@ -156,6 +175,7 @@ class BigNumberVis extends React.PureComponent { style={{ fontSize, height: maxHeight, + color: numberColor, }} onContextMenu={onContextMenu} > diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts index f0a17e708b..c517fcc0b9 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts @@ -27,6 +27,7 @@ import { QueryFormMetric, TimeFormatter, } from '@superset-ui/core'; +import { ColorFormatters } from '@superset-ui/chart-controls'; import { BaseChartProps, Refs } from '../types'; export interface BigNumberDatum { @@ -94,4 +95,5 @@ export type BigNumberVizProps = { xValueFormatter?: TimeFormatter; formData?: BigNumberWithTrendlineFormData; refs: Refs; + colorThresholdFormatters?: ColorFormatters; }; diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index d50e71608b..154255eee7 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -39,9 +39,12 @@ const JustifyEnd = styled.div` `; const colorSchemeOptions = (theme: SupersetTheme) => [ - { value: theme.colors.success.light1, label: t('green') }, - { value: theme.colors.alert.light1, label: t('yellow') }, - { value: theme.colors.error.light1, label: t('red') }, + { value: theme.colors.success.light1, label: t('success') }, + { value: theme.colors.alert.light1, label: t('alert') }, + { value: theme.colors.error.light1, label: t('error') }, + { value: theme.colors.success.dark1, label: t('success dark') }, + { value: theme.colors.alert.dark1, label: t('alert dark') }, + { value: theme.colors.error.dark1, label: t('error dark') }, ]; const operatorOptions = [