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 { 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<Field>;
@ -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);
}

View File

@ -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',

View File

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

View File

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