mirror of https://github.com/apache/superset.git
feat: conditional coloring for big number chart (#23064)
Co-authored-by: Gerold Busch <gerold.busch@valtech.com>
This commit is contained in:
parent
fd3030fc14
commit
61d8a0bd12
|
@ -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,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
Loading…
Reference in New Issue