Colors: default colors for strings and boolean (#33965)

This commit is contained in:
Ryan McKinley 2021-05-12 08:40:43 -07:00 committed by GitHub
parent 765fa675f5
commit ca8f6addab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 82 additions and 8 deletions

View File

@ -8,10 +8,10 @@ import { getValueFormat } from '../valueFormats/valueFormats';
import { getValueMappingResult } from '../utils/valueMappings'; import { getValueMappingResult } from '../utils/valueMappings';
import { dateTime } from '../datetime'; import { dateTime } from '../datetime';
import { KeyValue, TimeZone } from '../types'; import { KeyValue, TimeZone } from '../types';
import { getScaleCalculator } from './scale'; import { getScaleCalculator, ScaleCalculator } from './scale';
import { GrafanaTheme2 } from '../themes/types'; import { GrafanaTheme2 } from '../themes/types';
import { anyToNumber } from '../utils/anyToNumber'; import { anyToNumber } from '../utils/anyToNumber';
import { getColorForTheme } from '../utils/namedColorsPalette'; import { classicColors, getColorForTheme } from '../utils/namedColorsPalette';
interface DisplayProcessorOptions { interface DisplayProcessorOptions {
field: Partial<Field>; field: Partial<Field>;
@ -41,7 +41,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
return toStringProcessor; return toStringProcessor;
} }
const { field } = options; const field = options.field as Field;
const config = field.config ?? {}; const config = field.config ?? {};
let unit = config.unit; let unit = config.unit;
@ -53,7 +53,8 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
} }
const formatFunc = getValueFormat(unit || 'none'); 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) => { return (value: any) => {
const { mappings } = config; const { mappings } = config;
@ -113,7 +114,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
} }
if (!color) { if (!color) {
const scaleResult = scaleFunc(-Infinity); const scaleResult = defaultColor(value);
color = scaleResult.color; color = scaleResult.color;
percent = scaleResult.percent; percent = scaleResult.percent;
} }
@ -132,3 +133,24 @@ export function getRawDisplayProcessor(): DisplayProcessor {
numeric: (null as unknown) as number, 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);
}

View File

@ -684,7 +684,7 @@ describe('applyRawFieldOverrides', () => {
}); });
expect(getDisplayValue(frames, frameIndex, 3)).toEqual({ expect(getDisplayValue(frames, frameIndex, 3)).toEqual({
color: '#808080', color: '#F2495C', // red
numeric: 0, numeric: 0,
percent: expect.any(Number), percent: expect.any(Number),
prefix: undefined, prefix: undefined,
@ -693,9 +693,9 @@ describe('applyRawFieldOverrides', () => {
}); });
expect(getDisplayValue(frames, frameIndex, 4)).toEqual({ expect(getDisplayValue(frames, frameIndex, 4)).toEqual({
color: '#808080', color: '#73BF69', // value from classic pallet
numeric: NaN, numeric: NaN,
percent: 0, percent: 1,
prefix: undefined, prefix: undefined,
suffix: undefined, suffix: undefined,
text: 'A - string', text: 'A - string',

View File

@ -3,6 +3,7 @@ import { sortThresholds } from './thresholds';
import { ArrayVector } from '../vector/ArrayVector'; import { ArrayVector } from '../vector/ArrayVector';
import { getScaleCalculator } from './scale'; import { getScaleCalculator } from './scale';
import { createTheme } from '../themes'; import { createTheme } from '../themes';
import { getColorForTheme } from '../utils';
describe('getScaleCalculator', () => { describe('getScaleCalculator', () => {
it('should return percent, threshold and color', () => { it('should return percent, threshold and color', () => {
@ -26,4 +27,26 @@ describe('getScaleCalculator', () => {
color: '#EAB839', 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,
});
});
}); });

View File

@ -2,6 +2,7 @@ import { isNumber } from 'lodash';
import { GrafanaTheme2 } from '../themes/types'; import { GrafanaTheme2 } from '../themes/types';
import { reduceField, ReducerID } from '../transformations/fieldReducer'; import { reduceField, ReducerID } from '../transformations/fieldReducer';
import { Field, FieldConfig, FieldType, NumericRange, Threshold } from '../types'; import { Field, FieldConfig, FieldType, NumericRange, Threshold } from '../types';
import { getColorForTheme } from '../utils';
import { getFieldColorModeForField } from './fieldColor'; import { getFieldColorModeForField } from './fieldColor';
import { getActiveThresholdForValue } from './thresholds'; import { getActiveThresholdForValue } from './thresholds';
@ -14,6 +15,10 @@ export interface ColorScaleValue {
export type ScaleCalculator = (value: number) => ColorScaleValue; export type ScaleCalculator = (value: number) => ColorScaleValue;
export function getScaleCalculator(field: Field, theme: GrafanaTheme2): ScaleCalculator { export function getScaleCalculator(field: Field, theme: GrafanaTheme2): ScaleCalculator {
if (field.type === FieldType.boolean) {
return getBooleanScaleCalculator(field, theme);
}
const mode = getFieldColorModeForField(field); const mode = getFieldColorModeForField(field);
const getColor = mode.getCalculator(field, theme); const getColor = mode.getCalculator(field, theme);
const info = field.state?.range ?? getMinMaxAndDelta(field); 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 { function getMinMaxAndDelta(field: Field): NumericRange {
if (field.type !== FieldType.number) { if (field.type !== FieldType.number) {
return { min: 0, max: 100, delta: 100 }; return { min: 0, max: 100, delta: 100 };