feat: conditional coloring for big number chart (#23064)

Co-authored-by: Gerold Busch <gerold.busch@valtech.com>
This commit is contained in:
Gerold Busch 2023-05-01 17:44:46 +02:00 committed by GitHub
parent fd3030fc14
commit 61d8a0bd12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 9 deletions

View File

@ -62,6 +62,7 @@ export const getColorFunction = (
colorScheme, colorScheme,
}: ConditionalFormattingConfig, }: ConditionalFormattingConfig,
columnValues: number[], columnValues: number[],
alpha?: boolean,
) => { ) => {
let minOpacity = MIN_OPACITY_BOUNDED; let minOpacity = MIN_OPACITY_BOUNDED;
const maxOpacity = MAX_OPACITY; const maxOpacity = MAX_OPACITY;
@ -176,10 +177,13 @@ export const getColorFunction = (
const compareResult = comparatorFunction(value, columnValues); const compareResult = comparatorFunction(value, columnValues);
if (compareResult === false) return undefined; if (compareResult === false) return undefined;
const { cutoffValue, extremeValue } = compareResult; const { cutoffValue, extremeValue } = compareResult;
return addAlpha( if (alpha === undefined || alpha) {
colorScheme, return addAlpha(
getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity), colorScheme,
); getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
);
}
return colorScheme;
}; };
}; };
@ -187,6 +191,7 @@ export const getColorFormatters = memoizeOne(
( (
columnConfig: ConditionalFormattingConfig[] | undefined, columnConfig: ConditionalFormattingConfig[] | undefined,
data: DataRecord[], data: DataRecord[],
alpha?: boolean,
) => ) =>
columnConfig?.reduce( columnConfig?.reduce(
(acc: ColorFormatters, config: ConditionalFormattingConfig) => { (acc: ColorFormatters, config: ConditionalFormattingConfig) => {
@ -204,6 +209,7 @@ export const getColorFormatters = memoizeOne(
getColorFromValue: getColorFunction( getColorFromValue: getColorFunction(
config, config,
data.map(row => row[config.column!] as number), data.map(row => row[config.column!] as number),
alpha,
), ),
}); });
} }

View File

@ -188,6 +188,25 @@ describe('getColorFunction()', () => {
expect(colorFunction(150)).toBeUndefined(); 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', () => { it('getColorFunction BETWEEN_OR_LEFT_EQUAL', () => {
const colorFunction = getColorFunction( const colorFunction = getColorFunction(
{ {

View File

@ -16,11 +16,12 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { smartDateFormatter, t } from '@superset-ui/core'; import { GenericDataType, smartDateFormatter, t } from '@superset-ui/core';
import { import {
ControlPanelConfig, ControlPanelConfig,
D3_FORMAT_DOCS, D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS, D3_TIME_FORMAT_OPTIONS,
Dataset,
getStandardizedControls, getStandardizedControls,
sections, sections,
} from '@superset-ui/chart-controls'; } 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,
};
},
},
},
],
], ],
}, },
], ],

View File

@ -16,6 +16,10 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import {
ColorFormatters,
getColorFormatters,
} from '@superset-ui/chart-controls';
import { import {
getNumberFormatter, getNumberFormatter,
GenericDataType, GenericDataType,
@ -40,6 +44,7 @@ export default function transformProps(
forceTimestampFormatting, forceTimestampFormatting,
timeFormat, timeFormat,
yAxisFormat, yAxisFormat,
conditionalFormatting,
} = formData; } = formData;
const refs: Refs = {}; const refs: Refs = {};
const { data = [], coltypes = [] } = queriesData[0]; const { data = [], coltypes = [] } = queriesData[0];
@ -71,6 +76,12 @@ export default function transformProps(
const { onContextMenu } = hooks; const { onContextMenu } = hooks;
const defaultColorFormatters = [] as ColorFormatters;
const colorThresholdFormatters =
getColorFormatters(conditionalFormatting, data, false) ??
defaultColorFormatters;
return { return {
width, width,
height, height,
@ -81,5 +92,6 @@ export default function transformProps(
subheader: formattedSubheader, subheader: formattedSubheader,
onContextMenu, onContextMenu,
refs, refs,
colorThresholdFormatters,
}; };
} }

View File

@ -128,10 +128,29 @@ class BigNumberVis extends React.PureComponent<BigNumberVizProps> {
} }
renderHeader(maxHeight: number) { renderHeader(maxHeight: number) {
const { bigNumber, headerFormatter, width } = this.props; const { bigNumber, headerFormatter, width, colorThresholdFormatters } =
this.props;
// @ts-ignore // @ts-ignore
const text = bigNumber === null ? t('No data') : headerFormatter(bigNumber); 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(); const container = this.createTemporaryContainer();
document.body.append(container); document.body.append(container);
const fontSize = computeMaxFontSize({ const fontSize = computeMaxFontSize({
@ -156,6 +175,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVizProps> {
style={{ style={{
fontSize, fontSize,
height: maxHeight, height: maxHeight,
color: numberColor,
}} }}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
> >

View File

@ -27,6 +27,7 @@ import {
QueryFormMetric, QueryFormMetric,
TimeFormatter, TimeFormatter,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ColorFormatters } from '@superset-ui/chart-controls';
import { BaseChartProps, Refs } from '../types'; import { BaseChartProps, Refs } from '../types';
export interface BigNumberDatum { export interface BigNumberDatum {
@ -94,4 +95,5 @@ export type BigNumberVizProps = {
xValueFormatter?: TimeFormatter; xValueFormatter?: TimeFormatter;
formData?: BigNumberWithTrendlineFormData; formData?: BigNumberWithTrendlineFormData;
refs: Refs; refs: Refs;
colorThresholdFormatters?: ColorFormatters;
}; };

View File

@ -39,9 +39,12 @@ const JustifyEnd = styled.div`
`; `;
const colorSchemeOptions = (theme: SupersetTheme) => [ const colorSchemeOptions = (theme: SupersetTheme) => [
{ value: theme.colors.success.light1, label: t('green') }, { value: theme.colors.success.light1, label: t('success') },
{ value: theme.colors.alert.light1, label: t('yellow') }, { value: theme.colors.alert.light1, label: t('alert') },
{ value: theme.colors.error.light1, label: t('red') }, { 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 = [ const operatorOptions = [