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,
}: 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,
),
});
}

View File

@ -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(
{

View File

@ -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,
};
},
},
},
],
],
},
],

View File

@ -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,
};
}

View File

@ -128,10 +128,29 @@ class BigNumberVis extends React.PureComponent<BigNumberVizProps> {
}
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<BigNumberVizProps> {
style={{
fontSize,
height: maxHeight,
color: numberColor,
}}
onContextMenu={onContextMenu}
>

View File

@ -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;
};

View File

@ -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 = [