From ca8f6addabb2558e8dc76d6d72c11e40f73061a9 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 12 May 2021 08:40:43 -0700 Subject: [PATCH] Colors: default colors for strings and boolean (#33965) --- .../src/field/displayProcessor.ts | 32 ++++++++++++++++--- .../src/field/fieldOverrides.test.ts | 6 ++-- packages/grafana-data/src/field/scale.test.ts | 23 +++++++++++++ packages/grafana-data/src/field/scale.ts | 29 +++++++++++++++++ 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index de283e96790..606a0ae5883 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -8,10 +8,10 @@ import { getValueFormat } from '../valueFormats/valueFormats'; import { getValueMappingResult } from '../utils/valueMappings'; import { dateTime } from '../datetime'; import { KeyValue, TimeZone } from '../types'; -import { getScaleCalculator } from './scale'; +import { getScaleCalculator, ScaleCalculator } from './scale'; import { GrafanaTheme2 } from '../themes/types'; import { anyToNumber } from '../utils/anyToNumber'; -import { getColorForTheme } from '../utils/namedColorsPalette'; +import { classicColors, getColorForTheme } from '../utils/namedColorsPalette'; interface DisplayProcessorOptions { field: Partial; @@ -41,7 +41,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP return toStringProcessor; } - const { field } = options; + const field = options.field as Field; const config = field.config ?? {}; let unit = config.unit; @@ -53,7 +53,8 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP } const formatFunc = getValueFormat(unit || 'none'); - const scaleFunc = getScaleCalculator(field as Field, options.theme); + const scaleFunc = getScaleCalculator(field, options.theme); + const defaultColor = getDefaultColorFunc(field, scaleFunc, options.theme); return (value: any) => { const { mappings } = config; @@ -113,7 +114,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP } if (!color) { - const scaleResult = scaleFunc(-Infinity); + const scaleResult = defaultColor(value); color = scaleResult.color; percent = scaleResult.percent; } @@ -132,3 +133,24 @@ export function getRawDisplayProcessor(): DisplayProcessor { numeric: (null as unknown) as number, }); } + +function getDefaultColorFunc(field: Field, scaleFunc: ScaleCalculator, theme: GrafanaTheme2) { + if (field.type === FieldType.string) { + return (value: any) => { + const hc = strHashCode(value as string); + return { + color: classicColors[Math.floor(hc % classicColors.length)], + percent: 0, + }; + }; + } + return (value: any) => scaleFunc(-Infinity); +} + +/** + * Converts a string into a numeric value -- we just need it to be different + * enough so that it has a reasonable distribution across a color pallet + */ +function strHashCode(str: string) { + return str.split('').reduce((prevHash, currVal) => ((prevHash << 5) - prevHash + currVal.charCodeAt(0)) | 0, 0); +} diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index c822f360265..dd504f29f4f 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -684,7 +684,7 @@ describe('applyRawFieldOverrides', () => { }); expect(getDisplayValue(frames, frameIndex, 3)).toEqual({ - color: '#808080', + color: '#F2495C', // red numeric: 0, percent: expect.any(Number), prefix: undefined, @@ -693,9 +693,9 @@ describe('applyRawFieldOverrides', () => { }); expect(getDisplayValue(frames, frameIndex, 4)).toEqual({ - color: '#808080', + color: '#73BF69', // value from classic pallet numeric: NaN, - percent: 0, + percent: 1, prefix: undefined, suffix: undefined, text: 'A - string', diff --git a/packages/grafana-data/src/field/scale.test.ts b/packages/grafana-data/src/field/scale.test.ts index fcfa5cbee5c..2e6924ffb8b 100644 --- a/packages/grafana-data/src/field/scale.test.ts +++ b/packages/grafana-data/src/field/scale.test.ts @@ -3,6 +3,7 @@ import { sortThresholds } from './thresholds'; import { ArrayVector } from '../vector/ArrayVector'; import { getScaleCalculator } from './scale'; import { createTheme } from '../themes'; +import { getColorForTheme } from '../utils'; describe('getScaleCalculator', () => { it('should return percent, threshold and color', () => { @@ -26,4 +27,26 @@ describe('getScaleCalculator', () => { color: '#EAB839', }); }); + + it('reasonable boolean values', () => { + const field: Field = { + name: 'test', + config: {}, + type: FieldType.boolean, + values: new ArrayVector([true, false, true]), + }; + + const theme = createTheme(); + const calc = getScaleCalculator(field, theme); + expect(calc(true as any)).toEqual({ + percent: 1, + color: getColorForTheme('green', theme.v1), + threshold: undefined, + }); + expect(calc(false as any)).toEqual({ + percent: 0, + color: getColorForTheme('red', theme.v1), + threshold: undefined, + }); + }); }); diff --git a/packages/grafana-data/src/field/scale.ts b/packages/grafana-data/src/field/scale.ts index 8c916168bdf..16394e0fcbf 100644 --- a/packages/grafana-data/src/field/scale.ts +++ b/packages/grafana-data/src/field/scale.ts @@ -2,6 +2,7 @@ import { isNumber } from 'lodash'; import { GrafanaTheme2 } from '../themes/types'; import { reduceField, ReducerID } from '../transformations/fieldReducer'; import { Field, FieldConfig, FieldType, NumericRange, Threshold } from '../types'; +import { getColorForTheme } from '../utils'; import { getFieldColorModeForField } from './fieldColor'; import { getActiveThresholdForValue } from './thresholds'; @@ -14,6 +15,10 @@ export interface ColorScaleValue { export type ScaleCalculator = (value: number) => ColorScaleValue; export function getScaleCalculator(field: Field, theme: GrafanaTheme2): ScaleCalculator { + if (field.type === FieldType.boolean) { + return getBooleanScaleCalculator(field, theme); + } + const mode = getFieldColorModeForField(field); const getColor = mode.getCalculator(field, theme); const info = field.state?.range ?? getMinMaxAndDelta(field); @@ -35,6 +40,30 @@ export function getScaleCalculator(field: Field, theme: GrafanaTheme2): ScaleCal }; } +function getBooleanScaleCalculator(field: Field, theme: GrafanaTheme2): ScaleCalculator { + const trueValue: ColorScaleValue = { + color: getColorForTheme('green', theme.v1), + percent: 1, + threshold: (undefined as unknown) as Threshold, + }; + + const falseValue: ColorScaleValue = { + color: getColorForTheme('red', theme.v1), + percent: 0, + threshold: (undefined as unknown) as Threshold, + }; + + const mode = getFieldColorModeForField(field); + if (mode.isContinuous && mode.colors) { + trueValue.color = getColorForTheme(mode.colors[mode.colors.length - 1], theme.v1); + falseValue.color = getColorForTheme(mode.colors[0], theme.v1); + } + + return (value: number) => { + return Boolean(value) ? trueValue : falseValue; + }; +} + function getMinMaxAndDelta(field: Field): NumericRange { if (field.type !== FieldType.number) { return { min: 0, max: 100, delta: 100 };