FieldConfig: add thresholds and color modes (#21273)

This commit is contained in:
Ryan McKinley 2019-12-28 17:32:58 -08:00 committed by GitHub
parent 36aad1c101
commit d9e9843a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1598 additions and 729 deletions

View File

@ -52,7 +52,7 @@
"orientation": "horizontal",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -169,7 +169,7 @@
"orientation": "vertical",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -244,7 +244,7 @@
"orientation": "horizontal",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -351,7 +351,7 @@
"orientation": "vertical",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -430,7 +430,7 @@
"orientation": "vertical",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",

View File

@ -58,7 +58,7 @@
"orientation": "vertical",
"showUnfilled": false
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "C",
@ -159,7 +159,7 @@
"orientation": "vertical",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "H",
@ -249,7 +249,7 @@
"orientation": "horizontal",
"showUnfilled": false
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"alias": "Inside",
@ -325,7 +325,7 @@
"orientation": "horizontal",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"alias": "Inside",
@ -401,7 +401,7 @@
"orientation": "horizontal",
"showUnfilled": false
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"alias": "Inside",
@ -477,7 +477,7 @@
"orientation": "horizontal",
"showUnfilled": true
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"alias": "Inside",

View File

@ -69,7 +69,7 @@
"show": true
}
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"alias": "A longer title",
@ -160,7 +160,7 @@
"show": true
}
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"alias": "AB",
@ -251,7 +251,7 @@
"show": true
}
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -336,7 +336,7 @@
"show": true
}
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -424,7 +424,7 @@
"show": true
}
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",
@ -512,7 +512,7 @@
"show": true
}
},
"pluginVersion": "6.6.0-pre",
"pluginVersion": "6.5.0-pre",
"targets": [
{
"refId": "A",

View File

@ -1,7 +1,31 @@
import { getDisplayProcessor, getColorFromThreshold } from './displayProcessor';
import { getDisplayProcessor } from './displayProcessor';
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
import { ValueMapping, MappingType } from '../types/valueMapping';
import { FieldType } from '../types';
import { FieldType, Threshold, GrafanaTheme, Field, FieldConfig, ThresholdsMode } from '../types';
import { getScaleCalculator, sortThresholds } from './scale';
import { ArrayVector } from '../vector';
import { validateFieldConfig } from './fieldOverrides';
function getDisplayProcessorFromConfig(config: FieldConfig) {
return getDisplayProcessor({
field: {
config,
type: FieldType.number,
},
});
}
function getColorFromThreshold(value: number, steps: Threshold[], theme?: GrafanaTheme): string {
const field: Field = {
name: 'test',
config: { thresholds: { mode: ThresholdsMode.Absolute, steps: sortThresholds(steps) } },
type: FieldType.number,
values: new ArrayVector([]),
};
validateFieldConfig(field.config!);
const calc = getScaleCalculator(field, theme);
return calc(value).color!;
}
function assertSame(input: any, processors: DisplayProcessor[], match: DisplayValue) {
processors.forEach(processor => {
@ -20,10 +44,10 @@ describe('Process simple display values', () => {
getDisplayProcessor(),
// Add a simple option that is not used (uses a different base class)
getDisplayProcessor({ config: { min: 0, max: 100 } }),
getDisplayProcessorFromConfig({ min: 0, max: 100 }),
// Add a simple option that is not used (uses a different base class)
getDisplayProcessor({ config: { unit: 'locale' } }),
getDisplayProcessorFromConfig({ unit: 'locale' }),
];
it('support null', () => {
@ -108,7 +132,7 @@ describe('Format value', () => {
it('should return if value isNaN', () => {
const valueMappings: ValueMapping[] = [];
const value = 'N/A';
const instance = getDisplayProcessor({ config: { mappings: valueMappings } });
const instance = getDisplayProcessorFromConfig({ mappings: valueMappings });
const result = instance(value);
@ -119,7 +143,7 @@ describe('Format value', () => {
const valueMappings: ValueMapping[] = [];
const value = '6';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
const result = instance(value);
@ -132,7 +156,7 @@ describe('Format value', () => {
{ id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
];
const value = '10';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
const result = instance(value);
@ -141,20 +165,20 @@ describe('Format value', () => {
it('should set auto decimals, 1 significant', () => {
const value = 3.23;
const instance = getDisplayProcessor({ config: { decimals: null } });
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('3.2');
});
it('should set auto decimals, 2 significant', () => {
const value = 0.0245;
const instance = getDisplayProcessor({ config: { decimals: null } });
const instance = getDisplayProcessorFromConfig({ decimals: null });
expect(instance(value).text).toEqual('0.025');
});
it('should use override decimals', () => {
const value = 100030303;
const instance = getDisplayProcessor({ config: { decimals: 2, unit: 'bytes' } });
const instance = getDisplayProcessorFromConfig({ decimals: 2, unit: 'bytes' });
const disp = instance(value);
expect(disp.text).toEqual('95.40');
expect(disp.suffix).toEqual(' MiB');
@ -166,7 +190,7 @@ describe('Format value', () => {
{ id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
];
const value = '11';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
expect(instance(value).text).toEqual('1-20');
});
@ -176,7 +200,7 @@ describe('Format value', () => {
{ id: 1, operator: '', text: '', type: MappingType.ValueToText, value: '1' },
];
const value = '1';
const instance = getDisplayProcessor({ config: { decimals: 1, mappings: valueMappings } });
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
expect(instance(value).text).toEqual('');
expect(instance(value).numeric).toEqual(1);
@ -188,7 +212,7 @@ describe('Format value', () => {
it('with value 1000 and unit short', () => {
const value = 1000;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.000');
expect(disp.suffix).toEqual(' K');
@ -196,7 +220,7 @@ describe('Format value', () => {
it('with value 1200 and unit short', () => {
const value = 1200;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.200');
expect(disp.suffix).toEqual(' K');
@ -204,7 +228,7 @@ describe('Format value', () => {
it('with value 1250 and unit short', () => {
const value = 1250;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.250');
expect(disp.suffix).toEqual(' K');
@ -212,7 +236,7 @@ describe('Format value', () => {
it('with value 10000000 and unit short', () => {
const value = 1000000;
const instance = getDisplayProcessor({ config: { decimals: null, unit: 'short' } });
const instance = getDisplayProcessorFromConfig({ decimals: null, unit: 'short' });
const disp = instance(value);
expect(disp.text).toEqual('1.000');
expect(disp.suffix).toEqual(' Mil');
@ -222,10 +246,12 @@ describe('Format value', () => {
describe('Date display options', () => {
it('should format UTC dates', () => {
const processor = getDisplayProcessor({
type: FieldType.time,
isUtc: true,
config: {
unit: 'xyz', // ignore non-date formats
field: {
type: FieldType.time,
config: {
unit: 'xyz', // ignore non-date formats
},
},
});
expect(processor(0).text).toEqual('1970-01-01 00:00:00');
@ -233,10 +259,12 @@ describe('Date display options', () => {
it('should pick configured time format', () => {
const processor = getDisplayProcessor({
type: FieldType.time,
isUtc: true,
config: {
unit: 'dateTimeAsUS', // A configurable date format
field: {
type: FieldType.time,
config: {
unit: 'dateTimeAsUS', // ignore non-date formats
},
},
});
expect(processor(0).text).toEqual('01/01/1970 12:00:00 am');
@ -244,10 +272,12 @@ describe('Date display options', () => {
it('respect the configured date format', () => {
const processor = getDisplayProcessor({
type: FieldType.time,
isUtc: true,
config: {
unit: 'time:YYYY',
field: {
type: FieldType.time,
config: {
unit: 'time:YYYY', // ignore non-date formats
},
},
});
expect(processor(0).text).toEqual('1970');

View File

@ -1,22 +1,18 @@
// Libraries
import _ from 'lodash';
// Utils
import { getColorFromHexRgbOrName } from '../utils/namedColorsPalette';
// Types
import { FieldConfig, FieldType } from '../types/dataFrame';
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
import { Field, FieldType } from '../types/dataFrame';
import { GrafanaTheme } from '../types/theme';
import { DisplayProcessor, DisplayValue, DecimalCount, DecimalInfo } from '../types/displayValue';
import { getValueFormat } from '../valueFormats/valueFormats';
import { getMappedValue } from '../utils/valueMappings';
import { Threshold } from '../types/threshold';
import { DEFAULT_DATE_TIME_FORMAT } from '../datetime';
import { KeyValue } from '../types';
import { getScaleCalculator } from './scale';
interface DisplayProcessorOptions {
type?: FieldType;
config?: FieldConfig;
field: Partial<Field>;
// Context
isUtc?: boolean;
@ -31,80 +27,81 @@ const timeFormats: KeyValue<boolean> = {
};
export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor {
if (options && !_.isEmpty(options)) {
const field = options.config ? options.config : {};
if (!options || _.isEmpty(options) || !options.field) {
return toStringProcessor;
}
const { field } = options;
const config = field.config ?? {};
if (options.type === FieldType.time) {
if (field.unit && timeFormats[field.unit]) {
// Currently selected unit is valid for time fields
} else if (field.unit && field.unit.startsWith('time:')) {
// Also OK
} else {
field.unit = `time:${DEFAULT_DATE_TIME_FORMAT}`;
if (field.type === FieldType.time) {
if (config.unit && timeFormats[config.unit]) {
// Currently selected unit is valid for time fields
} else if (config.unit && config.unit.startsWith('time:')) {
// Also OK
} else {
config.unit = `time:${DEFAULT_DATE_TIME_FORMAT}`;
}
}
const formatFunc = getValueFormat(config.unit || 'none');
const scaleFunc = getScaleCalculator(field as Field, options.theme);
return (value: any) => {
const { mappings } = config;
let text = _.toString(value);
let numeric = toNumber(value);
let prefix: string | undefined = undefined;
let suffix: string | undefined = undefined;
let shouldFormat = true;
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, value);
if (mappedValue) {
text = mappedValue.text;
const v = toNumber(text);
if (!isNaN(v)) {
numeric = v;
}
shouldFormat = false;
}
}
const formatFunc = getValueFormat(field.unit || 'none');
if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, config.decimals);
const v = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
text = v.text;
suffix = v.suffix;
prefix = v.prefix;
return (value: any) => {
const { theme } = options;
const { mappings, thresholds } = field;
let color;
let text = _.toString(value);
let numeric = toNumber(value);
let prefix: string | undefined = undefined;
let suffix: string | undefined = undefined;
let shouldFormat = true;
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, value);
if (mappedValue) {
text = mappedValue.text;
const v = toNumber(text);
if (!isNaN(v)) {
numeric = v;
}
shouldFormat = false;
}
}
if (!isNaN(numeric)) {
if (shouldFormat && !_.isBoolean(value)) {
const { decimals, scaledDecimals } = getDecimalsForValue(value, field.decimals);
const v = formatFunc(numeric, decimals, scaledDecimals, options.isUtc);
text = v.text;
suffix = v.suffix;
prefix = v.prefix;
// Check if the formatted text mapped to a different value
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, text);
if (mappedValue) {
text = mappedValue.text;
}
// Check if the formatted text mapped to a different value
if (mappings && mappings.length > 0) {
const mappedValue = getMappedValue(mappings, text);
if (mappedValue) {
text = mappedValue.text;
}
}
if (thresholds && thresholds.length) {
color = getColorFromThreshold(numeric, thresholds, theme);
}
}
if (!text) {
if (field && field.noValue) {
text = field.noValue;
} else {
text = ''; // No data?
}
// Return the value along with scale info
if (text) {
return { text, numeric, prefix, suffix, ...scaleFunc(numeric) };
}
return { text, numeric, color, prefix, suffix };
};
}
}
return toStringProcessor;
if (!text) {
if (config.noValue) {
text = config.noValue;
} else {
text = ''; // No data?
}
}
return { text, numeric, prefix, suffix };
};
}
/** Will return any value as a number or NaN */
@ -125,29 +122,6 @@ function toStringProcessor(value: any): DisplayValue {
return { text: _.toString(value), numeric: toNumber(value) };
}
export function getColorFromThreshold(value: number, thresholds: Threshold[], theme?: GrafanaTheme): string {
const themeType = theme ? theme.type : GrafanaThemeType.Dark;
if (thresholds.length === 1) {
return getColorFromHexRgbOrName(thresholds[0].color, themeType);
}
const atThreshold = thresholds.filter(threshold => value === threshold.value)[0];
if (atThreshold) {
return getColorFromHexRgbOrName(atThreshold.color, themeType);
}
const belowThreshold = thresholds.filter(threshold => value > threshold.value);
if (belowThreshold.length > 0) {
const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
return getColorFromHexRgbOrName(nearestThreshold.color, themeType);
}
// Use the first threshold as the default color
return getColorFromHexRgbOrName(thresholds[0].color, themeType);
}
export function getDecimalsForValue(value: number, decimalOverride?: DecimalCount): DecimalInfo {
if (_.isNumber(decimalOverride)) {
// It's important that scaledDecimals is null here

View File

@ -2,9 +2,9 @@ import merge from 'lodash/merge';
import { getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { toDataFrame } from '../dataframe/processDataFrame';
import { ReducerID } from '../transformations/fieldReducer';
import { Threshold } from '../types/threshold';
import { ThresholdsMode } from '../types/thresholds';
import { GrafanaTheme } from '../types/theme';
import { MappingType } from '../types';
import { MappingType, FieldConfig } from '../types';
import { setFieldConfigDefaults } from './fieldOverrides';
describe('FieldDisplay', () => {
@ -63,34 +63,37 @@ describe('FieldDisplay', () => {
});
it('should restore -Infinity value for base threshold', () => {
const field = {
thresholds: [
({
color: '#73BF69',
value: null,
} as unknown) as Threshold,
{
color: '#F2495C',
value: 50,
},
],
const config: FieldConfig = {
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{
color: '#73BF69',
value: (null as any) as number, // -Infinity becomes null in JSON
},
{
color: '#F2495C',
value: 50,
},
],
},
};
setFieldConfigDefaults(field);
expect(field.thresholds!.length).toEqual(2);
expect(field.thresholds![0].value).toBe(-Infinity);
setFieldConfigDefaults(config);
expect(config.thresholds!.steps.length).toEqual(2);
expect(config.thresholds!.steps[0].value).toBe(-Infinity);
});
it('Should return field thresholds when there is no data', () => {
const options = createEmptyDisplayOptions({
fieldOptions: {
defaults: {
thresholds: [{ color: '#F2495C', value: 50 }],
thresholds: { steps: [{ color: '#F2495C', value: 50 }] },
},
},
});
const display = getFieldDisplayValues(options);
expect(display[0].field.thresholds!.length).toEqual(1);
expect(display[0].field.thresholds!.steps!.length).toEqual(1);
expect(display[0].display.numeric).toEqual(0);
});

View File

@ -124,9 +124,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
const display =
field.display ??
getDisplayProcessor({
config,
field,
theme: options.theme,
type: field.type,
});
const title = config.title ? config.title : defaultTitle;
@ -245,9 +244,11 @@ function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): Fiel
const { defaults } = fieldOptions;
const displayProcessor = getDisplayProcessor({
config: defaults,
field: {
type: FieldType.other,
config: defaults,
},
theme: options.theme,
type: FieldType.other,
});
const display = displayProcessor(null);

View File

@ -8,12 +8,16 @@ import {
Field,
FieldType,
FieldConfigSource,
ThresholdsMode,
FieldColorMode,
ColorScheme,
} from '../types';
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
import { FieldMatcher } from '../types/transformations';
import isNumber from 'lodash/isNumber';
import toNumber from 'lodash/toNumber';
import { getDisplayProcessor } from './displayProcessor';
import { guessFieldTypeForField } from '../dataframe';
interface OverrideProps {
match: FieldMatcher;
@ -112,6 +116,32 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
}
}
// Try harder to set a real value that is not 'other'
let type = field.type;
if (!type || type === FieldType.other) {
const t = guessFieldTypeForField(field);
if (t) {
type = t;
}
}
// Some units have an implied range
if (config.unit === 'percent') {
if (!isNumber(config.min)) {
config.min = 0;
}
if (!isNumber(config.max)) {
config.max = 100;
}
} else if (config.unit === 'percentunit') {
if (!isNumber(config.min)) {
config.min = 0;
}
if (!isNumber(config.max)) {
config.max = 1;
}
}
// Set the Min/Max value automatically
if (options.autoMinMax && field.type === FieldType.number) {
if (!isNumber(config.min) || !isNumber(config.max)) {
@ -127,19 +157,15 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
}
}
return {
// Overwrite the configs
const f: Field = {
...field,
// Overwrite the configs
config,
// Set the display processor
display: getDisplayProcessor({
type: field.type,
config: config,
theme: options.theme,
}),
type,
};
// and set the display processor using it
f.display = getDisplayProcessor({ field: f, theme: options.theme });
return f;
});
return {
@ -206,9 +232,47 @@ export function setFieldConfigDefaults(config: FieldConfig, props?: FieldConfig)
}
}
// First value is always -Infinity
if (config.thresholds && config.thresholds.length) {
config.thresholds[0].value = -Infinity;
validateFieldConfig(config);
}
/**
* This checks that all options on FieldConfig make sense. It mutates any value that needs
* fixed. In particular this makes sure that the first threshold value is -Infinity (not valid in JSON)
*/
export function validateFieldConfig(config: FieldConfig) {
const { thresholds } = config;
if (thresholds) {
if (!thresholds.mode) {
thresholds.mode = ThresholdsMode.Absolute;
}
if (!thresholds.steps) {
thresholds.steps = [];
} else if (thresholds.steps.length) {
// First value is always -Infinity
// JSON saves it as null
thresholds.steps[0].value = -Infinity;
}
}
if (!config.color) {
if (thresholds) {
config.color = {
mode: FieldColorMode.Thresholds,
};
}
// No Color settings
} else if (!config.color.mode) {
// Without a mode, skip color altogether
delete config.color;
} else {
const { color } = config;
if (color.mode === FieldColorMode.Scheme) {
if (!color.schemeName) {
color.schemeName = ColorScheme.BrBG;
}
} else {
delete color.schemeName;
}
}
// Verify that max > min (swap if necessary)

View File

@ -1,4 +1,5 @@
export * from './fieldDisplay';
export * from './displayProcessor';
export * from './scale';
export { applyFieldOverrides } from './fieldOverrides';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';

View File

@ -0,0 +1,195 @@
import { Field, FieldType, ColorScheme, ThresholdsConfig, ThresholdsMode, FieldColorMode, FieldConfig } from '../types';
import { sortThresholds, getScaleCalculator, getActiveThreshold } from './scale';
import { ArrayVector } from '../vector';
import { validateFieldConfig } from './fieldOverrides';
describe('scale', () => {
test('sort thresholds', () => {
const thresholds: ThresholdsConfig = {
steps: [
{ color: 'TEN', value: 10 },
{ color: 'HHH', value: 100 },
{ color: 'ONE', value: 1 },
],
mode: ThresholdsMode.Absolute,
};
const sorted = sortThresholds(thresholds.steps).map(t => t.value);
expect(sorted).toEqual([1, 10, 100]);
const config: FieldConfig = { thresholds };
// Mutates and sorts the
validateFieldConfig(config);
expect(getActiveThreshold(10, thresholds.steps).color).toEqual('TEN');
});
test('find active', () => {
const thresholds: ThresholdsConfig = {
steps: [
{ color: 'ONE', value: 1 },
{ color: 'TEN', value: 10 },
{ color: 'HHH', value: 100 },
],
mode: ThresholdsMode.Absolute,
};
const config: FieldConfig = { thresholds };
// Mutates and sets ONE to -Infinity
validateFieldConfig(config);
expect(getActiveThreshold(-1, thresholds.steps).color).toEqual('ONE');
expect(getActiveThreshold(1, thresholds.steps).color).toEqual('ONE');
expect(getActiveThreshold(5, thresholds.steps).color).toEqual('ONE');
expect(getActiveThreshold(10, thresholds.steps).color).toEqual('TEN');
expect(getActiveThreshold(11, thresholds.steps).color).toEqual('TEN');
expect(getActiveThreshold(99, thresholds.steps).color).toEqual('TEN');
expect(getActiveThreshold(100, thresholds.steps).color).toEqual('HHH');
expect(getActiveThreshold(1000, thresholds.steps).color).toEqual('HHH');
});
test('absolute thresholds', () => {
const thresholds: ThresholdsConfig = {
steps: [
// Colors are ignored when 'scheme' exists
{ color: '#F00', state: 'LowLow', value: -Infinity },
{ color: '#F00', state: 'Low', value: -50 },
{ color: '#F00', state: 'OK', value: 0 },
{ color: '#F00', state: 'High', value: 50 },
{ color: '#F00', state: 'HighHigh', value: 100 },
],
mode: ThresholdsMode.Absolute,
};
const field: Field<number> = {
name: 'test',
type: FieldType.number,
config: {
min: -100, // explicit range
max: 100, // note less then range of actual data
thresholds,
color: {
mode: FieldColorMode.Scheme,
schemeName: ColorScheme.Greens,
},
},
values: new ArrayVector([
-1000,
-100,
-75,
-50,
-25,
0, // middle
25,
50,
75,
100,
1000,
]),
};
validateFieldConfig(field.config);
const calc = getScaleCalculator(field);
const mapped = field.values.toArray().map(v => {
return calc(v);
});
expect(mapped).toMatchInlineSnapshot(`
Array [
Object {
"color": "rgb(247, 252, 245)",
"percent": -4.5,
"threshold": Object {
"color": "#F00",
"state": "LowLow",
"value": -Infinity,
},
},
Object {
"color": "rgb(247, 252, 245)",
"percent": 0,
"threshold": Object {
"color": "#F00",
"state": "LowLow",
"value": -Infinity,
},
},
Object {
"color": "rgb(227, 244, 222)",
"percent": 0.125,
"threshold": Object {
"color": "#F00",
"state": "LowLow",
"value": -Infinity,
},
},
Object {
"color": "rgb(198, 232, 191)",
"percent": 0.25,
"threshold": Object {
"color": "#F00",
"state": "Low",
"value": -50,
},
},
Object {
"color": "rgb(160, 216, 155)",
"percent": 0.375,
"threshold": Object {
"color": "#F00",
"state": "Low",
"value": -50,
},
},
Object {
"color": "rgb(115, 195, 120)",
"percent": 0.5,
"threshold": Object {
"color": "#F00",
"state": "OK",
"value": 0,
},
},
Object {
"color": "rgb(69, 170, 93)",
"percent": 0.625,
"threshold": Object {
"color": "#F00",
"state": "OK",
"value": 0,
},
},
Object {
"color": "rgb(34, 139, 69)",
"percent": 0.75,
"threshold": Object {
"color": "#F00",
"state": "High",
"value": 50,
},
},
Object {
"color": "rgb(6, 107, 45)",
"percent": 0.875,
"threshold": Object {
"color": "#F00",
"state": "High",
"value": 50,
},
},
Object {
"color": "rgb(0, 68, 27)",
"percent": 1,
"threshold": Object {
"color": "#F00",
"state": "HighHigh",
"value": 100,
},
},
Object {
"color": "rgb(0, 68, 27)",
"percent": 5.5,
"threshold": Object {
"color": "#F00",
"state": "HighHigh",
"value": 100,
},
},
]
`);
});
});

View File

@ -0,0 +1,122 @@
import { Field, Threshold, GrafanaTheme, GrafanaThemeType, ThresholdsMode, FieldColorMode } from '../types';
import { reduceField, ReducerID } from '../transformations';
import { getColorFromHexRgbOrName } from '../utils/namedColorsPalette';
import * as d3 from 'd3-scale-chromatic';
import isNumber from 'lodash/isNumber';
export interface ScaledValue {
percent?: number; // 0-1
threshold?: Threshold; // the selected step
color?: string; // Selected color (may be range based on threshold)
}
export type ScaleCalculator = (value: number) => ScaledValue;
/**
* @param t Number in the range [0, 1].
*/
type colorInterpolator = (t: number) => string;
export function getScaleCalculator(field: Field, theme?: GrafanaTheme): ScaleCalculator {
const themeType = theme ? theme.type : GrafanaThemeType.Dark;
const config = field.config || {};
const { thresholds, color } = config;
const fixedColor =
color && color.mode === FieldColorMode.Fixed && color.fixedColor
? getColorFromHexRgbOrName(color.fixedColor, themeType)
: undefined;
// Should we calculate the percentage
const percentThresholds = thresholds && thresholds.mode === ThresholdsMode.Percentage;
const useColorScheme = color && color.mode === FieldColorMode.Scheme;
if (percentThresholds || useColorScheme) {
// Calculate min/max if required
let min = config.min;
let max = config.max;
if (!isNumber(min) || !isNumber(max)) {
if (field.values && field.values.length) {
const stats = reduceField({ field, reducers: [ReducerID.min, ReducerID.max] });
if (!isNumber(min)) {
min = stats[ReducerID.min];
}
if (!isNumber(max)) {
max = stats[ReducerID.max];
}
} else {
min = 0;
max = 100;
}
}
const delta = max! - min!;
// Use a d3 color scale
let interpolator: colorInterpolator | undefined;
if (useColorScheme) {
interpolator = (d3 as any)[`interpolate${color!.schemeName}`] as colorInterpolator;
}
return (value: number) => {
const percent = (value - min!) / delta;
const threshold = thresholds
? getActiveThreshold(percentThresholds ? percent * 100 : value, thresholds.steps)
: undefined; // 0-100
let color = fixedColor;
if (interpolator) {
color = interpolator(percent);
} else if (threshold) {
color = getColorFromHexRgbOrName(threshold!.color, themeType);
}
return {
percent,
threshold,
color,
};
};
}
if (thresholds) {
return (value: number) => {
const threshold = getActiveThreshold(value, thresholds.steps);
const color = fixedColor ?? (threshold ? getColorFromHexRgbOrName(threshold.color, themeType) : undefined);
return {
threshold,
color,
};
};
}
// Constant color
if (fixedColor) {
return (value: number) => {
return { color: fixedColor };
};
}
// NO-OP
return (value: number) => {
return {};
};
}
export function getActiveThreshold(value: number, thresholds: Threshold[]): Threshold {
let active = thresholds[0];
for (const threshold of thresholds) {
if (value >= threshold.value) {
active = threshold;
} else {
break;
}
}
return active;
}
/**
* Sorts the thresholds
*/
export function sortThresholds(thresholds: Threshold[]) {
return thresholds.sort((t1, t2) => {
return t1.value - t2.value;
});
}

View File

@ -1,10 +1,11 @@
import { Threshold } from './threshold';
import { ThresholdsConfig } from './thresholds';
import { ValueMapping } from './valueMapping';
import { QueryResultBase, Labels, NullValueMode } from './data';
import { DisplayProcessor } from './displayValue';
import { DataLink } from './dataLink';
import { Vector } from './vector';
import { FieldCalcs } from '../transformations/fieldReducer';
import { FieldColor } from './fieldColor';
export enum FieldType {
time = 'time', // or date
@ -32,8 +33,11 @@ export interface FieldConfig {
// Convert input values into a display string
mappings?: ValueMapping[];
// Must be sorted by 'value', first value is always -Infinity
thresholds?: Threshold[];
// Map numeric values to states
thresholds?: ThresholdsConfig;
// Map values to a display color
color?: FieldColor;
// Used when reducing field values
nullValueMode?: NullValueMode;
@ -44,8 +48,7 @@ export interface FieldConfig {
// Alternative to empty string
noValue?: string;
color?: string;
// Panel Specific Values
custom?: Record<string, any>;
}

View File

@ -4,6 +4,7 @@ export type DisplayProcessor = (value: any) => DisplayValue;
export interface DisplayValue extends FormattedValue {
numeric: number; // Use isNaN to check if it is a real number
percent?: number; // 0-1 between min & max
color?: string; // color based on configs or Threshold
title?: string;
}

View File

@ -0,0 +1,52 @@
export enum FieldColorMode {
Thresholds = 'thresholds',
Scheme = 'scheme',
Fixed = 'fixed',
}
export interface FieldColor {
mode: FieldColorMode;
schemeName?: ColorScheme;
fixedColor?: string;
}
// https://github.com/d3/d3-scale-chromatic
export enum ColorScheme {
BrBG = 'BrBG',
PRGn = 'PRGn',
PiYG = 'PiYG',
PuOr = 'PuOr',
RdBu = 'RdBu',
RdGy = 'RdGy',
RdYlBu = 'RdYlBu',
RdYlGn = 'RdYlGn',
Spectral = 'Spectral',
BuGn = 'BuGn',
BuPu = 'BuPu',
GnBu = 'GnBu',
OrRd = 'OrRd',
PuBuGn = 'PuBuGn',
PuBu = 'PuBu',
PuRd = 'PuRd',
RdPu = 'RdPu',
YlGnBu = 'YlGnBu',
YlGn = 'YlGn',
YlOrBr = 'YlOrBr',
YlOrRd = 'YlOrRd',
Blues = 'Blues',
Greens = 'Greens',
Greys = 'Greys',
Purples = 'Purples',
Reds = 'Reds',
Oranges = 'Oranges',
// interpolateCubehelix
// interpolateRainbow,
// interpolateWarm
// interpolateCool
// interpolateSinebow
// interpolateViridis
// interpolateMagma
// interpolateInferno
// interpolatePlasma
}

View File

@ -5,7 +5,7 @@ export * from './logs';
export * from './navModel';
export * from './select';
export * from './time';
export * from './threshold';
export * from './thresholds';
export * from './utils';
export * from './valueMapping';
export * from './displayValue';
@ -18,6 +18,8 @@ export * from './app';
export * from './datasource';
export * from './panel';
export * from './plugin';
export * from './thresholds';
export * from './fieldColor';
export * from './theme';
export * from './orgs';

View File

@ -1,4 +0,0 @@
export interface Threshold {
value: number;
color: string;
}

View File

@ -0,0 +1,17 @@
export interface Threshold {
value: number;
color: string;
state?: string; // Warning, Error, LowLow, Low, OK, High, HighHigh etc
}
export enum ThresholdsMode {
Absolute = 'absolute',
Percentage = 'percentage', // between 0 and 1 (based on min/max)
}
export interface ThresholdsConfig {
mode: ThresholdsMode;
// Must be sorted by 'value', first value is always -Infinity
steps: Threshold[];
}

View File

@ -5,7 +5,6 @@ export * from './logs';
export * from './labels';
export * from './labels';
export * from './object';
export * from './thresholds';
export * from './namedColorsPalette';
export * from './series';

View File

@ -1,22 +0,0 @@
import { Threshold } from '../types';
export function getActiveThreshold(value: number, thresholds: Threshold[]): Threshold {
let active = thresholds[0];
for (const threshold of thresholds) {
if (value >= threshold.value) {
active = threshold;
} else {
break;
}
}
return active;
}
/**
* Sorts the thresholds
*/
export function sortThresholds(thresholds: Threshold[]) {
return thresholds.sort((t1, t2) => {
return t1.value - t2.value;
});
}

View File

@ -1,7 +1,7 @@
import { storiesOf } from '@storybook/react';
import { number, text } from '@storybook/addon-knobs';
import { BarGauge, Props, BarGaugeDisplayMode } from './BarGauge';
import { VizOrientation } from '@grafana/data';
import { VizOrientation, ThresholdsMode, Field, FieldType, getDisplayProcessor } from '@grafana/data';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
@ -35,6 +35,23 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
threshold2Value,
} = getKnobs();
const field: Partial<Field> = {
type: FieldType.number,
config: {
min: minValue,
max: maxValue,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: threshold1Value, color: threshold1Color },
{ value: threshold2Value, color: threshold2Color },
],
},
},
};
field.display = getDisplayProcessor({ field });
const props: Props = {
theme: {} as any,
width: 300,
@ -44,15 +61,10 @@ function addBarGaugeStory(name: string, overrides: Partial<Props>) {
title: title,
numeric: value,
},
minValue: minValue,
maxValue: maxValue,
orientation: VizOrientation.Vertical,
displayMode: BarGaugeDisplayMode.Basic,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: threshold1Value, color: threshold1Color },
{ value: threshold2Value, color: threshold2Color },
],
field: field.config!,
display: field.display!,
};
Object.assign(props, overrides);

View File

@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DisplayValue } from '@grafana/data';
import { DisplayValue, VizOrientation, ThresholdsMode, Field, FieldType, getDisplayProcessor } from '@grafana/data';
import {
BarGauge,
Props,
@ -11,29 +11,38 @@ import {
getValuePercent,
BarGaugeDisplayMode,
} from './BarGauge';
import { VizOrientation } from '@grafana/data';
import { getTheme } from '../../themes';
const green = '#73BF69';
const orange = '#FF9830';
function getProps(propOverrides?: Partial<Props>): Props {
const field: Partial<Field> = {
type: FieldType.number,
config: {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 70, color: 'orange' },
{ value: 90, color: 'red' },
],
},
},
};
const theme = getTheme();
field.display = getDisplayProcessor({ field, theme });
const props: Props = {
maxValue: 100,
minValue: 0,
displayMode: BarGaugeDisplayMode.Basic,
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: 70, color: 'orange' },
{ value: 90, color: 'red' },
],
field: field.config!,
display: field.display!,
height: 300,
width: 300,
value: {
text: '25',
numeric: 25,
},
theme: getTheme(),
value: field.display(25),
theme,
orientation: VizOrientation.Horizontal,
};
@ -59,11 +68,13 @@ function getValue(value: number, title?: string): DisplayValue {
describe('BarGauge', () => {
describe('Get value color', () => {
it('should get the threshold color if value is same as a threshold', () => {
const props = getProps({ value: getValue(70) });
const props = getProps();
props.value = props.display(70);
expect(getValueColor(props)).toEqual(orange);
});
it('should get the base threshold', () => {
const props = getProps({ value: getValue(-10) });
const props = getProps();
props.value = props.display(-10);
expect(getValueColor(props)).toEqual(green);
});
});

View File

@ -1,14 +1,17 @@
// Library
import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import tinycolor from 'tinycolor2';
import * as d3 from 'd3-scale-chromatic';
import {
Threshold,
TimeSeriesValue,
getActiveThreshold,
DisplayValue,
formattedValueToString,
FormattedValue,
DisplayValueAlignmentFactors,
ThresholdsMode,
DisplayProcessor,
FieldConfig,
FieldColorMode,
} from '@grafana/data';
// Compontents
@ -33,10 +36,9 @@ const VALUE_LEFT_PADDING = 10;
export interface Props extends Themeable {
height: number;
width: number;
thresholds: Threshold[];
field: FieldConfig;
display: DisplayProcessor;
value: DisplayValue;
maxValue: number;
minValue: number;
orientation: VizOrientation;
itemSpacing?: number;
lcdCellWidth?: number;
@ -55,8 +57,6 @@ export enum BarGaugeDisplayMode {
export class BarGauge extends PureComponent<Props> {
static defaultProps: Partial<Props> = {
maxValue: 100,
minValue: 0,
lcdCellWidth: 12,
value: {
text: '100',
@ -64,7 +64,14 @@ export class BarGauge extends PureComponent<Props> {
},
displayMode: BarGaugeDisplayMode.Gradient,
orientation: VizOrientation.Horizontal,
thresholds: [],
field: {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [],
},
},
itemSpacing: 10,
showUnfilled: true,
};
@ -116,7 +123,7 @@ export class BarGauge extends PureComponent<Props> {
}
getCellColor(positionValue: TimeSeriesValue): CellColors {
const { thresholds, theme, value } = this.props;
const { value, display } = this.props;
if (positionValue === null) {
return {
background: 'gray',
@ -124,10 +131,8 @@ export class BarGauge extends PureComponent<Props> {
};
}
const activeThreshold = getActiveThreshold(positionValue, thresholds);
if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
const color = display(positionValue).color;
if (color) {
// if we are past real value the cell is not "on"
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
return {
@ -160,7 +165,7 @@ export class BarGauge extends PureComponent<Props> {
}
renderRetroBars(): ReactNode {
const { maxValue, minValue, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
const { field, value, itemSpacing, alignmentFactors, orientation, lcdCellWidth } = this.props;
const {
valueHeight,
valueWidth,
@ -169,6 +174,8 @@ export class BarGauge extends PureComponent<Props> {
wrapperWidth,
wrapperHeight,
} = calculateBarAndValueDimensions(this.props);
const minValue = field.min!;
const maxValue = field.max!;
const isVert = isVertical(orientation);
const valueRange = maxValue - minValue;
@ -402,10 +409,10 @@ export function getValuePercent(value: number, minValue: number, maxValue: numbe
* Only exported to for unit test
*/
export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles {
const { displayMode, maxValue, minValue, value, alignmentFactors, orientation, theme } = props;
const { displayMode, field, value, alignmentFactors, orientation, theme } = props;
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
const valuePercent = getValuePercent(value.numeric, field.min!, field.max!);
const valueColor = getValueColor(props);
const valueToBaseSizeOn = alignmentFactors ? alignmentFactors : value;
@ -495,26 +502,56 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
* Only exported to for unit test
*/
export function getBarGradient(props: Props, maxSize: number): string {
const { minValue, maxValue, thresholds, value, orientation } = props;
const { field, value, orientation } = props;
const cssDirection = isVertical(orientation) ? '0deg' : '90deg';
const minValue = field.min!;
const maxValue = field.max!;
let gradient = '';
let lastpos = 0;
for (let i = 0; i < thresholds.length; i++) {
const threshold = thresholds[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
if (field.color && field.color.mode === FieldColorMode.Scheme) {
const schemeSet = (d3 as any)[`scheme${field.color.schemeName}`] as any[];
if (!schemeSet) {
// Error: unknown scheme
const color = '#F00';
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
gradient += ` ${maxSize}px, ${color}`;
return gradient + ')';
}
// Get the scheme with as many steps as possible
const scheme = schemeSet[schemeSet.length - 1] as string[];
for (let i = 0; i < scheme.length; i++) {
const color = scheme[i];
const valuePercent = i / (scheme.length - 1);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
}
}
} else {
const thresholds = field.thresholds!;
for (let i = 0; i < thresholds.steps.length; i++) {
const threshold = thresholds.steps[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
} else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
gradient += ` ${offset}px, ${color}`;
}
}
}
@ -525,14 +562,10 @@ export function getBarGradient(props: Props, maxSize: number): string {
* Only exported to for unit test
*/
export function getValueColor(props: Props): string {
const { thresholds, theme, value } = props;
const activeThreshold = getActiveThreshold(value.numeric, thresholds);
if (activeThreshold !== null) {
return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
const { theme, value } = props;
if (value.color) {
return value.color;
}
return getColorFromHexRgbOrName('gray', theme.type);
}

View File

@ -40,8 +40,15 @@ exports[`BarGauge Render with basic options should render 1`] = `
}
value={
Object {
"color": "#73BF69",
"numeric": 25,
"prefix": undefined,
"suffix": undefined,
"text": "25",
"threshold": Object {
"color": "green",
"value": -Infinity,
},
}
}
/>

View File

@ -3,20 +3,29 @@ import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
import { getTheme } from '../../themes';
import { ThresholdsMode, FieldConfig } from '@grafana/data';
jest.mock('jquery', () => ({
plot: jest.fn(),
}));
const setup = (propOverrides?: object) => {
const setup = (propOverrides?: FieldConfig) => {
const field: FieldConfig = {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [{ value: -Infinity, color: '#7EB26D' }],
},
};
Object.assign(field, propOverrides);
const props: Props = {
maxValue: 100,
minValue: 0,
showThresholdMarkers: true,
showThresholdLabels: false,
thresholds: [{ value: -Infinity, color: '#7EB26D' }],
height: 300,
field,
width: 300,
height: 300,
value: {
text: '25',
numeric: 25,
@ -24,8 +33,6 @@ const setup = (propOverrides?: object) => {
theme: getTheme(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<Gauge {...props} />);
const instance = wrapper.instance() as Gauge;
@ -37,7 +44,9 @@ const setup = (propOverrides?: object) => {
describe('Get thresholds formatted', () => {
it('should return first thresholds color for min and max', () => {
const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
const { instance } = setup({
thresholds: { mode: ThresholdsMode.Absolute, steps: [{ value: -Infinity, color: '#7EB26D' }] },
});
expect(instance.getFormattedThresholds()).toEqual([
{ value: 0, color: '#7EB26D' },
@ -47,11 +56,14 @@ describe('Get thresholds formatted', () => {
it('should get the correct formatted values when thresholds are added', () => {
const { instance } = setup({
thresholds: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
},
});
expect(instance.getFormattedThresholds()).toEqual([

View File

@ -1,14 +1,20 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { Threshold, DisplayValue, getColorFromHexRgbOrName, formattedValueToString } from '@grafana/data';
import {
DisplayValue,
getColorFromHexRgbOrName,
formattedValueToString,
FieldConfig,
ThresholdsMode,
getActiveThreshold,
Threshold,
} from '@grafana/data';
import { Themeable } from '../../types';
import { selectThemeVariant } from '../../themes';
export interface Props extends Themeable {
height: number;
maxValue: number;
minValue: number;
thresholds: Threshold[];
field: FieldConfig;
showThresholdMarkers: boolean;
showThresholdLabels: boolean;
width: number;
@ -23,11 +29,19 @@ export class Gauge extends PureComponent<Props> {
canvasElement: any;
static defaultProps: Partial<Props> = {
maxValue: 100,
minValue: 0,
showThresholdMarkers: true,
showThresholdLabels: false,
thresholds: [],
field: {
min: 0,
max: 100,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' },
],
},
},
};
componentDidMount() {
@ -38,22 +52,38 @@ export class Gauge extends PureComponent<Props> {
this.draw();
}
getFormattedThresholds() {
const { maxValue, minValue, thresholds, theme } = this.props;
getFormattedThresholds(): Threshold[] {
const { field, theme } = this.props;
const isPercent = field.thresholds?.mode === ThresholdsMode.Percentage;
const steps = field.thresholds!.steps;
let min = field.min!;
let max = field.max!;
if (isPercent) {
min = 0;
max = 100;
}
const lastThreshold = thresholds[thresholds.length - 1];
return [
...thresholds.map((threshold, index) => {
if (index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
const first = getActiveThreshold(min, steps);
const last = getActiveThreshold(max, steps);
const formatted: Threshold[] = [];
formatted.push({ value: min, color: getColorFromHexRgbOrName(first.color, theme.type) });
let skip = true;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (skip) {
if (first === step) {
skip = false;
}
const previousThreshold = thresholds[index - 1];
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
];
continue;
}
const prev = steps[i - 1];
formatted.push({ value: step.value, color: getColorFromHexRgbOrName(prev!.color, theme.type) });
if (step === last) {
break;
}
}
formatted.push({ value: max, color: getColorFromHexRgbOrName(last.color, theme.type) });
return formatted;
}
getFontScale(length: number): number {
@ -64,7 +94,7 @@ export class Gauge extends PureComponent<Props> {
}
draw() {
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
const { field, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
const autoProps = calculateGaugeAutoProps(width, height, value.title);
const dimension = Math.min(width, autoProps.gaugeHeight);
@ -85,12 +115,25 @@ export class Gauge extends PureComponent<Props> {
const thresholdLabelFontSize = fontSize / 2.5;
let min = field.min!;
let max = field.max!;
let numeric = value.numeric;
if (field.thresholds?.mode === ThresholdsMode.Percentage) {
min = 0;
max = 100;
if (value.percent === undefined) {
numeric = ((numeric - min) / (max - min)) * 100;
} else {
numeric = value.percent! * 100;
}
}
const options: any = {
series: {
gauges: {
gauge: {
min: minValue,
max: maxValue,
min,
max,
background: { color: backgroundColor },
border: { color: null },
shadow: { show: false },
@ -123,7 +166,7 @@ export class Gauge extends PureComponent<Props> {
};
const plotSeries = {
data: [[0, value.numeric]],
data: [[0, numeric]],
label: value.title,
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Graph } from './Graph';
import Chart from '../Chart';
import { dateTime, ArrayVector, FieldType, GraphSeriesXY } from '@grafana/data';
import { dateTime, ArrayVector, FieldType, GraphSeriesXY, FieldColorMode } from '@grafana/data';
import { select } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { TooltipContentProps } from '../Chart/Tooltip';
@ -47,7 +47,12 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'a-series',
values: new ArrayVector([10, 20, 10]),
config: { color: 'red' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'red',
},
},
},
timeStep: 3600000,
yAxis: {
@ -76,7 +81,12 @@ const series: GraphSeriesXY[] = [
name:
"B-series with an ultra wide label that is probably going go make the tooltip overflow window. This situation happens, so let's better make sure it behaves nicely :)",
values: new ArrayVector([20, 30, 40]),
config: { color: 'blue' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'blue',
},
},
},
timeStep: 3600000,
yAxis: {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { mount } from 'enzyme';
import Graph from './Graph';
import Chart from '../Chart';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime } from '@grafana/data';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorMode } from '@grafana/data';
const series: GraphSeriesXY[] = [
{
@ -25,7 +25,7 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'a-series',
values: new ArrayVector([10, 20, 10]),
config: { color: 'red' },
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'red' } },
},
timeStep: 3600000,
yAxis: {
@ -52,7 +52,7 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'b-series',
values: new ArrayVector([20, 30, 40]),
config: { color: 'blue' },
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'blue' } },
},
timeStep: 3600000,
yAxis: {

View File

@ -1,5 +1,10 @@
import React from 'react';
import { getValueFromDimension, getColumnFromDimension, formattedValueToString } from '@grafana/data';
import {
getValueFromDimension,
getColumnFromDimension,
formattedValueToString,
getDisplayProcessor,
} from '@grafana/data';
import { SeriesTable } from './SeriesTable';
import { GraphTooltipContentProps } from './types';
@ -19,11 +24,18 @@ export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({ dim
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]);
const processedValue = valueField.display ? formattedValueToString(valueField.display(value)) : value;
const display = valueField.display ?? getDisplayProcessor({ field: valueField });
const disp = display(value);
return (
<SeriesTable
series={[{ color: valueField.config.color, label: valueField.name, value: processedValue }]}
series={[
{
color: disp.color,
label: valueField.name,
value: formattedValueToString(disp),
},
]}
timestamp={processedTime}
/>
);

View File

@ -6,7 +6,7 @@ import { withHorizontallyCenteredStory } from '../../utils/storybook/withCentere
import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend';
import { LegendPlacement, LegendDisplayMode } from '../Legend/Legend';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime } from '@grafana/data';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorMode } from '@grafana/data';
const GraphWithLegendStories = storiesOf('Visualizations/Graph/GraphWithLegend', module);
GraphWithLegendStories.addDecorator(withHorizontallyCenteredStory);
@ -31,7 +31,12 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'a-series',
values: new ArrayVector([10, 20, 10]),
config: { color: 'red' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'red',
},
},
},
timeStep: 3600000,
yAxis: {
@ -58,7 +63,12 @@ const series: GraphSeriesXY[] = [
type: FieldType.number,
name: 'b-series',
values: new ArrayVector([20, 30, 40]),
config: { color: 'blue' },
config: {
color: {
mode: FieldColorMode.Fixed,
fixedColor: 'blue',
},
},
},
timeStep: 3600000,
yAxis: {

View File

@ -1,8 +1,17 @@
import { GraphSeriesValue, toDataFrame, FieldType, FieldCache } from '@grafana/data';
import {
GraphSeriesValue,
toDataFrame,
FieldType,
FieldCache,
FieldColorMode,
getColorFromHexRgbOrName,
GrafanaThemeType,
Field,
} from '@grafana/data';
import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData } from './utils';
const mockResult = (
value: GraphSeriesValue,
value: string,
datapointIndex: number,
seriesIndex: number,
color?: string,
@ -21,23 +30,42 @@ const mockResult = (
const aSeries = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'value', type: FieldType.number, values: [10, 20, 10], config: { color: 'red' } },
{
name: 'value',
type: FieldType.number,
values: [10, 20, 10],
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'red' } },
},
],
});
const bSeries = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
{ name: 'value', type: FieldType.number, values: [30, 60, 30], config: { color: 'blue' } },
{
name: 'value',
type: FieldType.number,
values: [30, 60, 30],
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'blue' } },
},
],
});
// C-series has the same x-axis range as A and B but is missing the middle point
const cSeries = toDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [100, 300] },
{ name: 'value', type: FieldType.number, values: [30, 30], config: { color: 'yellow' } },
{
name: 'value',
type: FieldType.number,
values: [30, 30],
config: { color: { mode: FieldColorMode.Fixed, fixedColor: 'yellow' } },
},
],
});
function getFixedThemedColor(field: Field): string {
return getColorFromHexRgbOrName(field.config.color!.fixedColor!, GrafanaThemeType.Dark);
}
describe('Graph utils', () => {
describe('getMultiSeriesGraphHoverInfo', () => {
describe('when series datapoints are x-axis aligned', () => {
@ -51,8 +79,12 @@ describe('Graph utils', () => {
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 0);
expect(result.time).toBe(100);
expect(result.results[0]).toEqual(mockResult(10, 0, 0, aValueField!.config.color, aValueField!.name, 100));
expect(result.results[1]).toEqual(mockResult(30, 0, 1, bValueField!.config.color, bValueField!.name, 100));
expect(result.results[0]).toEqual(
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, 100)
);
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, 100)
);
});
describe('returns the closest datapoints before the hover position', () => {
@ -67,8 +99,12 @@ describe('Graph utils', () => {
// hovering right before middle point
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 199);
expect(result.time).toBe(100);
expect(result.results[0]).toEqual(mockResult(10, 0, 0, aValueField!.config.color, aValueField!.name, 100));
expect(result.results[1]).toEqual(mockResult(30, 0, 1, bValueField!.config.color, bValueField!.name, 100));
expect(result.results[0]).toEqual(
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, 100)
);
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, 100)
);
});
it('when hovering right after a datapoint', () => {
@ -82,8 +118,12 @@ describe('Graph utils', () => {
// hovering right after middle point
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 201);
expect(result.time).toBe(200);
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
expect(result.results[1]).toEqual(mockResult(60, 1, 1, bValueField!.config.color, bValueField!.name, 200));
expect(result.results[0]).toEqual(
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
);
expect(result.results[1]).toEqual(
mockResult('60', 1, 1, getFixedThemedColor(bValueField!), bValueField!.name, 200)
);
});
});
});
@ -106,9 +146,13 @@ describe('Graph utils', () => {
// we expect a time of the hovered point
expect(result.time).toBe(200);
// we expect middle point from aSeries (the one we are hovering over)
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
expect(result.results[0]).toEqual(
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
);
// we expect closest point before hovered point from cSeries (1st point)
expect(result.results[1]).toEqual(mockResult(30, 0, 1, cValueField!.config.color, cValueField!.name, 100));
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, 100)
);
});
it('hovering right after over the middle point', () => {
@ -125,9 +169,13 @@ describe('Graph utils', () => {
// we expect the time of the closest point before hover
expect(result.time).toBe(200);
// we expect the closest datapoint before hover from aSeries
expect(result.results[0]).toEqual(mockResult(20, 1, 0, aValueField!.config.color, aValueField!.name, 200));
expect(result.results[0]).toEqual(
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, 200)
);
// we expect the closest datapoint before hover from cSeries (1st point)
expect(result.results[1]).toEqual(mockResult(30, 0, 1, cValueField!.config.color, cValueField!.name, 100));
expect(result.results[1]).toEqual(
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, 100)
);
});
});
});

View File

@ -1,4 +1,4 @@
import { GraphSeriesValue, Field, formattedValueToString } from '@grafana/data';
import { GraphSeriesValue, Field, formattedValueToString, getDisplayProcessor } from '@grafana/data';
/**
* Returns index of the closest datapoint BEFORE hover position
@ -53,14 +53,14 @@ export const getMultiSeriesGraphHoverInfo = (
results: MultiSeriesHoverInfo[];
time?: GraphSeriesValue;
} => {
let value, i, series, hoverIndex, hoverDistance, pointTime;
let i, field, hoverIndex, hoverDistance, pointTime;
const results: MultiSeriesHoverInfo[] = [];
let minDistance, minTime;
for (i = 0; i < yAxisDimensions.length; i++) {
series = yAxisDimensions[i];
field = yAxisDimensions[i];
const time = xAxisDimensions[i];
hoverIndex = findHoverIndexFromData(time, xAxisPosition);
hoverDistance = xAxisPosition - time.values.get(hoverIndex);
@ -75,14 +75,15 @@ export const getMultiSeriesGraphHoverInfo = (
minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime;
}
value = series.values.get(hoverIndex);
const display = field.display ?? getDisplayProcessor({ field });
const disp = display(field.values.get(hoverIndex));
results.push({
value: series.display ? formattedValueToString(series.display(value)) : value,
value: formattedValueToString(disp),
datapointIndex: hoverIndex,
seriesIndex: i,
color: series.config.color,
label: series.name,
color: disp.color,
label: field.name,
time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime,
});
}

View File

@ -37,6 +37,36 @@ describe('sharedSingleStatMigrationHandler', () => {
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
});
it('move thresholds to scale', () => {
const panel = {
options: {
fieldOptions: {
defaults: {
thresholds: [
{
color: 'green',
index: 0,
value: null,
},
{
color: 'orange',
index: 1,
value: 40,
},
{
color: 'red',
index: 2,
value: 80,
},
],
},
},
},
};
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
});
it('Remove unused `overrides` option', () => {
const panel = {
options: {

View File

@ -13,6 +13,10 @@ import {
PanelModel,
FieldDisplayOptions,
ConfigOverrideRule,
ThresholdsMode,
ThresholdsConfig,
validateFieldConfig,
FieldColorMode,
} from '@grafana/data';
export interface SingleStatBaseOptions {
@ -70,7 +74,10 @@ export function sharedSingleStatPanelChangedHandler(
thresholds.push({ value: -Infinity, color });
}
}
defaults.thresholds = thresholds;
defaults.thresholds = {
mode: ThresholdsMode.Absolute,
steps: thresholds,
};
}
// Convert value mappings
@ -112,8 +119,10 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
}
if (previousVersion < 6.6) {
const { fieldOptions } = options;
// discard the old `override` options and enter an empty array
if (options.fieldOptions && options.fieldOptions.override) {
if (fieldOptions && fieldOptions.override) {
const { override, ...rest } = options.fieldOptions;
options = {
...options,
@ -123,6 +132,34 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
},
};
}
// Move thresholds to steps
let thresholds = fieldOptions?.defaults?.thresholds;
if (thresholds) {
delete fieldOptions.defaults.thresholds;
} else {
thresholds = fieldOptions?.thresholds;
delete fieldOptions.thresholds;
}
if (thresholds) {
fieldOptions.defaults.thresholds = {
mode: ThresholdsMode.Absolute,
steps: thresholds,
};
}
// Migrate color from simple string to a mode
const { defaults } = fieldOptions;
if (defaults.color) {
const old = defaults.color;
defaults.color = {
mode: FieldColorMode.Fixed,
fixedColor: old,
};
}
validateFieldConfig(defaults);
}
return options as SingleStatBaseOptions;
@ -135,7 +172,15 @@ export function moveThresholdsAndMappingsToField(old: any) {
return old;
}
const { mappings, thresholds, ...rest } = old.fieldOptions;
const { mappings, ...rest } = old.fieldOptions;
let thresholds: ThresholdsConfig | undefined = undefined;
if (old.thresholds) {
thresholds = {
mode: ThresholdsMode.Absolute,
steps: migrateOldThresholds(old.thresholds)!,
};
}
return {
...old,
@ -144,7 +189,7 @@ export function moveThresholdsAndMappingsToField(old: any) {
defaults: {
...fieldOptions.defaults,
mappings,
thresholds: migrateOldThresholds(thresholds),
thresholds,
},
},
};

View File

@ -24,6 +24,9 @@ Object {
"last",
],
"defaults": Object {
"color": Object {
"mode": "thresholds",
},
"decimals": 5,
"mappings": Array [
Object {
@ -34,22 +37,39 @@ Object {
],
"max": 100,
"min": 10,
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "orange",
"value": 40,
},
Object {
"color": "red",
"value": 80,
},
],
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"index": 0,
"value": -Infinity,
},
Object {
"color": "orange",
"index": 1,
"value": 40,
},
Object {
"color": "red",
"index": 2,
"value": 80,
},
],
},
"unit": "watt",
},
},
}
`;
exports[`sharedSingleStatMigrationHandler move thresholds to scale 1`] = `
Object {
"fieldOptions": Object {
"defaults": Object {
"mappings": undefined,
"thresholds": undefined,
},
},
}
`;

View File

@ -1,18 +1,21 @@
import React, { FC } from 'react';
import { ReactTableCellProps, TableCellDisplayMode } from './types';
import { BarGauge, BarGaugeDisplayMode } from '../BarGauge/BarGauge';
import { VizOrientation } from '@grafana/data';
import { ThresholdsConfig, ThresholdsMode, VizOrientation } from '@grafana/data';
const defaultThresholds = [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
];
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
steps: [
{
color: 'blue',
value: -Infinity,
},
{
color: 'green',
value: 20,
},
],
};
export const BarGaugeCell: FC<ReactTableCellProps> = props => {
const { column, tableStyles, cell } = props;
@ -22,6 +25,14 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
return null;
}
let { config } = field;
if (!config.thresholds) {
config = {
...config,
thresholds: defaultScale,
};
}
const displayValue = field.display(cell.value);
let barGaugeMode = BarGaugeDisplayMode.Gradient;
@ -34,10 +45,8 @@ export const BarGaugeCell: FC<ReactTableCellProps> = props => {
<BarGauge
width={column.width - tableStyles.cellPadding * 2}
height={tableStyles.cellHeightInner}
thresholds={field.config.thresholds || defaultThresholds}
field={config}
value={displayValue}
maxValue={field.config.max || 100}
minValue={field.config.min || 0}
orientation={VizOrientation.Horizontal}
theme={tableStyles.theme}
itemSpacing={1}

View File

@ -3,17 +3,24 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ThresholdsEditor } from './ThresholdsEditor';
import { getTheme } from '../../themes';
import { ThresholdsMode, ThresholdsConfig } from '@grafana/data';
const ThresholdsEditorStories = storiesOf('UI/ThresholdsEditor', module);
const thresholds = [
{ index: 0, value: -Infinity, color: 'green' },
{ index: 1, value: 50, color: 'red' },
];
const thresholds: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 50, color: 'red' },
],
};
ThresholdsEditorStories.add('default', () => {
return <ThresholdsEditor thresholds={[]} onChange={action('Thresholds changed')} />;
return (
<ThresholdsEditor theme={getTheme()} thresholds={{} as ThresholdsConfig} onChange={action('Thresholds changed')} />
);
});
ThresholdsEditorStories.add('with thresholds', () => {
return <ThresholdsEditor thresholds={thresholds} onChange={action('Thresholds changed')} />;
return <ThresholdsEditor theme={getTheme()} thresholds={thresholds} onChange={action('Thresholds changed')} />;
});

View File

@ -1,14 +1,15 @@
import React, { ChangeEvent } from 'react';
import { mount } from 'enzyme';
import { GrafanaThemeType } from '@grafana/data';
import { GrafanaThemeType, GrafanaTheme, ThresholdsMode } from '@grafana/data';
import { ThresholdsEditor, Props, thresholdsWithoutKey } from './ThresholdsEditor';
import { colors } from '../../utils';
import { mockThemeContext } from '../../themes/ThemeContext';
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
theme: { type: GrafanaThemeType.Dark, isDark: true, isLight: false } as GrafanaTheme,
onChange: jest.fn(),
thresholds: [],
thresholds: { mode: ThresholdsMode.Absolute, steps: [] },
};
Object.assign(props, propOverrides);
@ -23,7 +24,7 @@ const setup = (propOverrides?: Partial<Props>) => {
};
function getCurrentThresholds(editor: ThresholdsEditor) {
return thresholdsWithoutKey(editor.state.thresholds);
return thresholdsWithoutKey(editor.props.thresholds, editor.state.steps);
}
describe('Render', () => {
@ -38,14 +39,14 @@ describe('Render', () => {
it('should render with base threshold', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.thresholds')).toMatchSnapshot();
});
});
describe('Initialization', () => {
it('should add a base threshold if missing', () => {
const { instance } = setup();
expect(getCurrentThresholds(instance)).toEqual([{ value: -Infinity, color: 'green' }]);
expect(getCurrentThresholds(instance).steps).toEqual([{ value: -Infinity, color: 'green' }]);
});
});
@ -53,9 +54,9 @@ describe('Add threshold', () => {
it('should add threshold', () => {
const { instance } = setup();
instance.onAddThresholdAfter(instance.state.thresholds[0]);
instance.onAddThresholdAfter(instance.state.steps[0]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: 'green' }, // 0
{ value: 50, color: colors[1] }, // 1
]);
@ -63,15 +64,18 @@ describe('Add threshold', () => {
it('should add another threshold above a first', () => {
const { instance } = setup({
thresholds: [
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
],
},
});
instance.onAddThresholdAfter(instance.state.thresholds[1]);
instance.onAddThresholdAfter(instance.state.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: colors[0] }, // 0
{ value: 50, color: colors[2] }, // 1
{ value: 75, color: colors[3] }, // 2
@ -80,16 +84,19 @@ describe('Add threshold', () => {
it('should add another threshold between first and second index', () => {
const { instance } = setup({
thresholds: [
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 75, color: colors[3] },
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 75, color: colors[3] },
],
},
});
instance.onAddThresholdAfter(instance.state.thresholds[1]);
instance.onAddThresholdAfter(instance.state.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: colors[0] },
{ value: 50, color: colors[2] },
{ value: 62.5, color: colors[4] },
@ -100,29 +107,35 @@ describe('Add threshold', () => {
describe('Remove threshold', () => {
it('should not remove threshold at index 0', () => {
const thresholds = [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
};
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(instance.state.thresholds[0]);
instance.onRemoveThreshold(instance.state.steps[0]);
expect(getCurrentThresholds(instance)).toEqual(thresholds);
});
it('should remove threshold', () => {
const thresholds = [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
};
const { instance } = setup({ thresholds });
instance.onRemoveThreshold(instance.state.thresholds[1]);
instance.onRemoveThreshold(instance.state.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 75, color: '#6ED0E0' },
]);
@ -131,37 +144,43 @@ describe('Remove threshold', () => {
describe('change threshold value', () => {
it('should not change threshold at index 0', () => {
const thresholds = [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D' },
{ value: 50, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
],
};
const { instance } = setup({ thresholds });
const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, instance.state.thresholds[0]);
instance.onChangeThresholdValue(mockEvent, instance.state.steps[0]);
expect(getCurrentThresholds(instance)).toEqual(thresholds);
});
it('should update value', () => {
const { instance } = setup();
const thresholds = [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 50, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 50, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
],
};
instance.state = {
thresholds,
steps: thresholds.steps,
};
const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent<HTMLInputElement>;
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
instance.onChangeThresholdValue(mockEvent, thresholds.steps[1]);
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 78, color: '#EAB839' },
{ value: 75, color: '#6ED0E0' },
@ -172,19 +191,22 @@ describe('change threshold value', () => {
describe('on blur threshold value', () => {
it('should resort rows and update indexes', () => {
const { instance } = setup();
const thresholds = [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 78, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
];
const thresholds = {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: '#7EB26D', key: 1 },
{ value: 78, color: '#EAB839', key: 2 },
{ value: 75, color: '#6ED0E0', key: 3 },
],
};
instance.setState({
thresholds,
steps: thresholds.steps,
});
instance.onBlur();
expect(getCurrentThresholds(instance)).toEqual([
expect(getCurrentThresholds(instance).steps).toEqual([
{ value: -Infinity, color: '#7EB26D' },
{ value: 75, color: '#6ED0E0' },
{ value: 78, color: '#EAB839' },

View File

@ -1,19 +1,31 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { Threshold, sortThresholds } from '@grafana/data';
import { Threshold, sortThresholds, ThresholdsConfig, ThresholdsMode, SelectableValue } from '@grafana/data';
import { colors } from '../../utils';
import { ThemeContext } from '../../themes';
import { getColorFromHexRgbOrName } from '@grafana/data';
import { Input } from '../Input/Input';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { Themeable } from '../../types';
import { css } from 'emotion';
import Select from '../Select/Select';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
export interface Props {
thresholds?: Threshold[];
onChange: (thresholds: Threshold[]) => void;
const modes: Array<SelectableValue<ThresholdsMode>> = [
{ value: ThresholdsMode.Absolute, label: 'Absolute', description: 'Pick thresholds based on the absolute values' },
{
value: ThresholdsMode.Percentage,
label: 'Percentage',
description: 'Pick threshold based on the percent between min/max',
},
];
export interface Props extends Themeable {
showAlphaUI?: boolean;
thresholds: ThresholdsConfig;
onChange: (thresholds: ThresholdsConfig) => void;
}
interface State {
thresholds: ThresholdWithKey[];
steps: ThresholdWithKey[];
}
interface ThresholdWithKey extends Threshold {
@ -22,12 +34,12 @@ interface ThresholdWithKey extends Threshold {
let counter = 100;
function toThresholdsWithKey(thresholds?: Threshold[]): ThresholdWithKey[] {
if (!thresholds || thresholds.length === 0) {
thresholds = [{ value: -Infinity, color: 'green' }];
function toThresholdsWithKey(steps?: Threshold[]): ThresholdWithKey[] {
if (!steps || steps.length === 0) {
steps = [{ value: -Infinity, color: 'green' }];
}
return thresholds.map(t => {
return steps.map(t => {
return {
color: t.color,
value: t.value === null ? -Infinity : t.value,
@ -40,21 +52,21 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const thresholds = toThresholdsWithKey(props.thresholds);
thresholds[0].value = -Infinity;
const steps = toThresholdsWithKey(props.thresholds!.steps);
steps[0].value = -Infinity;
this.state = { thresholds };
this.state = { steps };
}
onAddThresholdAfter = (threshold: ThresholdWithKey) => {
const { thresholds } = this.state;
const { steps } = this.state;
const maxValue = 100;
const minValue = 0;
let prev: ThresholdWithKey | undefined = undefined;
let next: ThresholdWithKey | undefined = undefined;
for (const t of thresholds) {
for (const t of steps) {
if (prev && prev.key === threshold.key) {
next = t;
break;
@ -65,35 +77,35 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const prevValue = prev && isFinite(prev.value) ? prev.value : minValue;
const nextValue = next && isFinite(next.value) ? next.value : maxValue;
const color = colors.filter(c => !thresholds.some(t => t.color === c))[1];
const color = colors.filter(c => !steps.some(t => t.color === c))[1];
const add = {
value: prevValue + (nextValue - prevValue) / 2.0,
color: color,
key: counter++,
};
const newThresholds = [...thresholds, add];
const newThresholds = [...steps, add];
sortThresholds(newThresholds);
this.setState(
{
thresholds: newThresholds,
steps: newThresholds,
},
() => this.onChange()
);
};
onRemoveThreshold = (threshold: ThresholdWithKey) => {
const { thresholds } = this.state;
if (!thresholds.length) {
const { steps } = this.state;
if (!steps.length) {
return;
}
// Don't remove index 0
if (threshold.key === thresholds[0].key) {
if (threshold.key === steps[0].key) {
return;
}
this.setState(
{
thresholds: thresholds.filter(t => t.key !== threshold.key),
steps: steps.filter(t => t.key !== threshold.key),
},
() => this.onChange()
);
@ -104,22 +116,22 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
const parsedValue = parseFloat(cleanValue);
const value = isNaN(parsedValue) ? '' : parsedValue;
const thresholds = this.state.thresholds.map(t => {
const steps = this.state.steps.map(t => {
if (t.key === threshold.key) {
t = { ...t, value: value as number };
}
return t;
});
if (thresholds.length) {
thresholds[0].value = -Infinity;
if (steps.length) {
steps[0].value = -Infinity;
}
this.setState({ thresholds });
this.setState({ steps });
};
onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => {
const { thresholds } = this.state;
const { steps } = this.state;
const newThresholds = thresholds.map(t => {
const newThresholds = steps.map(t => {
if (t.key === threshold.key) {
t = { ...t, color: color };
}
@ -129,29 +141,38 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
this.setState(
{
thresholds: newThresholds,
steps: newThresholds,
},
() => this.onChange()
);
};
onBlur = () => {
const thresholds = [...this.state.thresholds];
sortThresholds(thresholds);
const steps = [...this.state.steps];
sortThresholds(steps);
this.setState(
{
thresholds,
steps,
},
() => this.onChange()
);
};
onChange = () => {
const { thresholds } = this.state;
this.props.onChange(thresholdsWithoutKey(thresholds));
this.props.onChange(thresholdsWithoutKey(this.props.thresholds, this.state.steps));
};
onModeChanged = (item: SelectableValue<ThresholdsMode>) => {
if (item.value) {
this.props.onChange({
...this.props.thresholds,
mode: item.value,
});
}
};
renderInput = (threshold: ThresholdWithKey) => {
const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage;
return (
<div className="thresholds-row-input-inner">
<span className="thresholds-row-input-inner-arrow" />
@ -181,6 +202,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
onBlur={this.onBlur}
/>
</div>
{isPercent && (
<div className={css(`margin-left:-20px; margin-top:5px;`)}>
<i className="fa fa-percent" />
</div>
)}
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
<i className="fa fa-times" />
</div>
@ -191,42 +217,50 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
};
render() {
const { thresholds } = this.state;
const { steps } = this.state;
const { theme } = this.props;
const t = this.props.thresholds;
return (
<ThemeContext.Consumer>
{theme => {
return (
<PanelOptionsGroup title="Thresholds">
<div className="thresholds">
{thresholds
.slice(0)
.reverse()
.map(threshold => {
return (
<div className="thresholds-row" key={`${threshold.key}`}>
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
<i className="fa fa-plus" />
</div>
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
</div>
</PanelOptionsGroup>
);
}}
</ThemeContext.Consumer>
<PanelOptionsGroup title="Thresholds">
<>
<div className="thresholds">
{steps
.slice(0)
.reverse()
.map(threshold => {
return (
<div className="thresholds-row" key={`${threshold.key}`}>
<div className="thresholds-row-add-button" onClick={() => this.onAddThresholdAfter(threshold)}>
<i className="fa fa-plus" />
</div>
<div
className="thresholds-row-color-indicator"
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
/>
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
</div>
);
})}
</div>
{this.props.showAlphaUI && (
<div>
<Select options={modes} value={modes.filter(m => m.value === t.mode)} onChange={this.onModeChanged} />
</div>
)}
</>
</PanelOptionsGroup>
);
}
}
export function thresholdsWithoutKey(thresholds: ThresholdWithKey[]): Threshold[] {
return thresholds.map(t => {
const { key, ...rest } = t;
return rest; // everything except key
});
export function thresholdsWithoutKey(thresholds: ThresholdsConfig, steps: ThresholdWithKey[]): ThresholdsConfig {
const mode = thresholds.mode ?? ThresholdsMode.Absolute;
return {
mode,
steps: steps.map(t => {
const { key, ...rest } = t;
return rest; // everything except key
}),
};
}

View File

@ -1,182 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render with base threshold 1`] = `
<ThresholdsEditor
onChange={[MockFunction]}
thresholds={Array []}
<div
className="thresholds"
>
<Component
title="Thresholds"
<div
className="thresholds-row"
key="100"
>
<div
className="panel-options-group"
className="thresholds-row-add-button"
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
</div>
<div
className="thresholds-row-color-indicator"
style={
Object {
"backgroundColor": "#73BF69",
}
}
/>
<div
className="thresholds-row-input"
>
<div
className="panel-options-group__header"
className="thresholds-row-input-inner"
>
<span
className="panel-options-group__title"
>
Thresholds
</span>
</div>
<div
className="panel-options-group__body"
>
className="thresholds-row-input-inner-arrow"
/>
<div
className="thresholds"
className="thresholds-row-input-inner-color"
>
<div
className="thresholds-row"
key="100"
className="thresholds-row-input-inner-color-colorpicker"
>
<div
className="thresholds-row-add-button"
onClick={[Function]}
<WithTheme(ColorPicker)
color="green"
enableNamedColors={true}
onChange={[Function]}
>
<i
className="fa fa-plus"
/>
</div>
<div
className="thresholds-row-color-indicator"
style={
Object {
"backgroundColor": "#73BF69",
<ColorPicker
color="green"
enableNamedColors={true}
onChange={[Function]}
theme={
Object {
"type": "dark",
}
}
}
/>
<div
className="thresholds-row-input"
>
<div
className="thresholds-row-input-inner"
>
<span
className="thresholds-row-input-inner-arrow"
/>
<div
className="thresholds-row-input-inner-color"
>
<div
className="thresholds-row-input-inner-color-colorpicker"
>
<WithTheme(ColorPicker)
<PopoverController
content={
<ColorPickerPopover
color="green"
enableNamedColors={true}
onChange={[Function]}
>
<ColorPicker
color="green"
enableNamedColors={true}
onChange={[Function]}
theme={
Object {
"type": "dark",
}
theme={
Object {
"type": "dark",
}
>
<PopoverController
content={
<ColorPickerPopover
color="green"
enableNamedColors={true}
onChange={[Function]}
theme={
Object {
"type": "dark",
}
}
/>
}
hideAfter={300}
>
<ForwardRef(ColorPickerTrigger)
color="#73BF69"
onClick={[Function]}
onMouseLeave={[Function]}
>
<div
onClick={[Function]}
onMouseLeave={[Function]}
style={
Object {
"background": "inherit",
"border": "none",
"borderRadius": 10,
"color": "inherit",
"cursor": "pointer",
"overflow": "hidden",
"padding": 0,
}
}
>
<div
style={
Object {
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
"border": "none",
"float": "left",
"height": 15,
"margin": 0,
"position": "relative",
"width": 15,
"zIndex": 0,
}
}
>
<div
style={
Object {
"backgroundColor": "#73BF69",
"bottom": 0,
"display": "block",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</div>
</div>
</ForwardRef(ColorPickerTrigger)>
</PopoverController>
</ColorPicker>
</WithTheme(ColorPicker)>
</div>
</div>
<div
className="thresholds-row-input-inner-value"
}
/>
}
hideAfter={300}
>
<Input
className=""
readOnly={true}
type="text"
value="Base"
<ForwardRef(ColorPickerTrigger)
color="#73BF69"
onClick={[Function]}
onMouseLeave={[Function]}
>
<div
onClick={[Function]}
onMouseLeave={[Function]}
style={
Object {
"flexGrow": 1,
"background": "inherit",
"border": "none",
"borderRadius": 10,
"color": "inherit",
"cursor": "pointer",
"overflow": "hidden",
"padding": 0,
}
}
>
<input
className="gf-form-input"
readOnly={true}
type="text"
value="Base"
/>
<div
style={
Object {
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
"border": "none",
"float": "left",
"height": 15,
"margin": 0,
"position": "relative",
"width": 15,
"zIndex": 0,
}
}
>
<div
style={
Object {
"backgroundColor": "#73BF69",
"bottom": 0,
"display": "block",
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</div>
</div>
</Input>
</div>
</div>
</div>
</ForwardRef(ColorPickerTrigger)>
</PopoverController>
</ColorPicker>
</WithTheme(ColorPicker)>
</div>
</div>
<div
className="thresholds-row-input-inner-value"
>
<Input
className=""
readOnly={true}
type="text"
value="Base"
>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input"
readOnly={true}
type="text"
value="Base"
/>
</div>
</Input>
</div>
</div>
</div>
</Component>
</ThresholdsEditor>
</div>
</div>
`;

View File

@ -150,8 +150,7 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number, timeZ
const timeField = data.fields[1];
timeField.display = getDisplayProcessor({
config: timeField.config,
type: timeField.type,
field: timeField,
isUtc: timeZone === 'utc',
});

View File

@ -89,7 +89,7 @@ export class ResultProcessor {
// set display processor
for (const field of data.fields) {
field.display = getDisplayProcessor({
config: field.config,
field,
theme: config.theme,
});
}

View File

@ -23,7 +23,8 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
alignmentFactors: DisplayValueAlignmentFactors
): JSX.Element => {
const { options } = this.props;
const { field, display } = value;
const { field, display, view, colIndex } = value;
const f = view.dataFrame.fields[colIndex];
return (
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
@ -34,12 +35,11 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
width={width}
height={height}
orientation={options.orientation}
thresholds={field.thresholds}
field={field}
display={f.display!}
theme={config.theme}
itemSpacing={this.getItemSpacing()}
displayMode={options.displayMode}
minValue={field.min}
maxValue={field.max}
onClick={openMenu}
className={targetClassName}
alignmentFactors={alignmentFactors}

View File

@ -13,18 +13,24 @@ import {
DataLinksEditor,
Switch,
} from '@grafana/ui';
import { FieldDisplayOptions, FieldConfig, DataLink, PanelEditorProps } from '@grafana/data';
import { Threshold, ValueMapping } from '@grafana/data';
import {
ThresholdsConfig,
ValueMapping,
FieldDisplayOptions,
FieldConfig,
DataLink,
PanelEditorProps,
} from '@grafana/data';
import { BarGaugeOptions, displayModes } from './types';
import { orientationOptions } from '../gauge/types';
import {
getDataLinksVariableSuggestions,
getCalculationValueDataLinksVariableSuggestions,
} from 'app/features/panel/panellinks/link_srv';
import { config } from 'app/core/config';
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) => {
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
@ -118,7 +124,12 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
/>
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
<ThresholdsEditor
onChange={this.onThresholdsChanged}
thresholds={defaults.thresholds}
theme={config.theme}
showAlphaUI={config.featureToggles.newEdit}
/>
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />

View File

@ -8,27 +8,53 @@ Object {
"mean",
],
"defaults": Object {
"color": Object {
"mode": "thresholds",
},
"decimals": null,
"mappings": Array [],
"max": -22,
"min": 33,
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "orange",
"value": 40,
},
Object {
"color": "red",
"value": 80,
},
],
"max": 33,
"min": -22,
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"index": 0,
"value": -Infinity,
},
Object {
"color": "orange",
"index": 1,
"value": 40,
},
Object {
"color": "red",
"index": 2,
"value": 80,
},
],
},
"unit": "watt",
},
"overrides": Array [],
"thresholds": Array [
Object {
"color": "green",
"index": 0,
"value": -Infinity,
},
Object {
"color": "orange",
"index": 1,
"value": 40,
},
Object {
"color": "red",
"index": 2,
"value": 80,
},
],
"values": false,
},
"orientation": "vertical",

View File

@ -26,11 +26,9 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
value={display}
width={width}
height={height}
thresholds={field.thresholds}
field={field}
showThresholdLabels={options.showThresholdLabels}
showThresholdMarkers={options.showThresholdMarkers}
minValue={field.min}
maxValue={field.max}
theme={config.theme}
onClick={openMenu}
className={targetClassName}

View File

@ -10,13 +10,21 @@ import {
PanelOptionsGroup,
DataLinksEditor,
} from '@grafana/ui';
import { PanelEditorProps, FieldDisplayOptions, Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
import {
PanelEditorProps,
FieldDisplayOptions,
ThresholdsConfig,
ValueMapping,
FieldConfig,
DataLink,
} from '@grafana/data';
import { GaugeOptions } from './types';
import {
getCalculationValueDataLinksVariableSuggestions,
getDataLinksVariableSuggestions,
} from 'app/features/panel/panellinks/link_srv';
import { config } from 'app/core/config';
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
labelWidth = 6;
@ -30,7 +38,7 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
showThresholdMarkers: !this.props.options.showThresholdMarkers,
});
onThresholdsChanged = (thresholds: Threshold[]) => {
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
@ -122,7 +130,12 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
/>
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
<ThresholdsEditor
onChange={this.onThresholdsChanged}
thresholds={defaults.thresholds}
theme={config.theme}
showAlphaUI={config.featureToggles.newEdit}
/>
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />

View File

@ -7,6 +7,9 @@ Object {
"last",
],
"defaults": Object {
"color": Object {
"mode": "thresholds",
},
"decimals": 3,
"mappings": Array [
Object {
@ -21,24 +24,31 @@ Object {
],
"max": "50",
"min": "-50",
"thresholds": Array [
Object {
"color": "green",
"value": -Infinity,
},
Object {
"color": "#EAB839",
"value": -25,
},
Object {
"color": "#6ED0E0",
"value": 0,
},
Object {
"color": "red",
"value": 25,
},
],
"thresholds": Object {
"mode": "absolute",
"steps": Array [
Object {
"color": "green",
"index": 0,
"value": -Infinity,
},
Object {
"color": "#EAB839",
"index": 1,
"value": -25,
},
Object {
"color": "#6ED0E0",
"index": 2,
"value": 0,
},
Object {
"color": "red",
"index": 3,
"value": 25,
},
],
},
"unit": "accMS2",
},
},

View File

@ -36,6 +36,7 @@ import {
getFlotPairsConstant,
PanelEvents,
formattedValueToString,
FieldType,
} from '@grafana/data';
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -264,7 +265,7 @@ class GraphElement {
links,
};
const fieldDisplay = getDisplayProcessor({
config: fieldConfig,
field: { config: fieldConfig, type: FieldType.number },
theme: getCurrentTheme(),
})(field.values.get(item.dataIndex));
linksSupplier = links.length

View File

@ -1,7 +1,6 @@
import { colors } from '@grafana/ui';
import {
getFlotPairs,
getColorFromHexRgbOrName,
getDisplayProcessor,
NullValueMode,
reduceField,
@ -16,6 +15,8 @@ import {
hasMsResolution,
MS_DATE_TIME_FORMAT,
DEFAULT_DATE_TIME_FORMAT,
FieldColor,
FieldColorMode,
} from '@grafana/data';
import { SeriesOptions, GraphOptions } from './types';
@ -32,9 +33,11 @@ export const getGraphSeriesModel = (
const graphs: GraphSeriesXY[] = [];
const displayProcessor = getDisplayProcessor({
config: {
unit: fieldOptions?.defaults?.unit,
decimals: legendOptions.decimals,
field: {
config: {
unit: fieldOptions?.defaults?.unit,
decimals: legendOptions.decimals,
},
},
});
@ -74,15 +77,21 @@ export const getGraphSeriesModel = (
});
}
let seriesColor;
let color: FieldColor;
if (seriesOptions[field.name] && seriesOptions[field.name].color) {
// Case when panel has settings provided via SeriesOptions, i.e. graph panel
seriesColor = getColorFromHexRgbOrName(seriesOptions[field.name].color);
color = {
mode: FieldColorMode.Fixed,
fixedColor: seriesOptions[field.name].color,
};
} else if (field.config && field.config.color) {
// Case when color settings are set on field, i.e. Explore logs histogram (see makeSeriesForLogs)
seriesColor = field.config.color;
color = field.config.color;
} else {
seriesColor = colors[graphs.length % colors.length];
color = {
mode: FieldColorMode.Fixed,
fixedColor: colors[graphs.length % colors.length],
};
}
field.config = fieldOptions
@ -90,28 +99,31 @@ export const getGraphSeriesModel = (
...field.config,
unit: fieldOptions.defaults.unit,
decimals: fieldOptions.defaults.decimals,
color: seriesColor,
color,
}
: { ...field.config, color: seriesColor };
: { ...field.config, color };
field.display = getDisplayProcessor({ config: { ...field.config }, type: field.type });
field.display = getDisplayProcessor({ field });
// Time step is used to determine bars width when graph is rendered as bar chart
const timeStep = getSeriesTimeStep(timeField);
const useMsDateFormat = hasMsResolution(timeField);
timeField.display = getDisplayProcessor({
type: timeField.type,
isUtc: timeZone === 'utc',
config: {
unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
field: {
...timeField,
type: timeField.type,
config: {
unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
},
},
});
graphs.push({
label: field.name,
data: points,
color: seriesColor,
color: field.config.color?.fixedColor,
info: statsDisplayValues,
isVisible: true,
yAxis: {

View File

@ -183,9 +183,11 @@ class SingleStatCtrl extends MetricsPanelCtrl {
if (!fieldInfo) {
const processor = getDisplayProcessor({
config: {
mappings: convertOldAngularValueMapping(this.panel),
noValue: 'No Data',
field: {
config: {
mappings: convertOldAngularValueMapping(this.panel),
noValue: 'No Data',
},
},
theme: config.theme,
});
@ -242,11 +244,14 @@ class SingleStatCtrl extends MetricsPanelCtrl {
}
const processor = getDisplayProcessor({
config: {
...fieldInfo.field.config,
unit: panel.format,
decimals: panel.decimals,
mappings: convertOldAngularValueMapping(panel),
field: {
...fieldInfo.field,
config: {
...fieldInfo.field.config,
unit: panel.format,
decimals: panel.decimals,
mappings: convertOldAngularValueMapping(panel),
},
},
theme: config.theme,
isUtc: dashboard.isTimezoneUtc && dashboard.isTimezoneUtc(),

View File

@ -13,7 +13,14 @@ import {
Select,
} from '@grafana/ui';
import { Threshold, ValueMapping, FieldConfig, DataLink, PanelEditorProps, FieldDisplayOptions } from '@grafana/data';
import {
ThresholdsConfig,
ValueMapping,
FieldConfig,
DataLink,
PanelEditorProps,
FieldDisplayOptions,
} from '@grafana/data';
import { StatPanelOptions, colorModes, graphModes, justifyModes } from './types';
import { orientationOptions } from '../gauge/types';
@ -22,9 +29,10 @@ import {
getDataLinksVariableSuggestions,
getCalculationValueDataLinksVariableSuggestions,
} from 'app/features/panel/panellinks/link_srv';
import { config } from 'app/core/config';
export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) => {
onThresholdsChanged = (thresholds: ThresholdsConfig) => {
const current = this.props.options.fieldOptions.defaults;
this.onDefaultsChange({
...current,
@ -129,7 +137,12 @@ export class StatPanelEditor extends PureComponent<PanelEditorProps<StatPanelOpt
/>
</PanelOptionsGroup>
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={defaults.thresholds} />
<ThresholdsEditor
onChange={this.onThresholdsChanged}
thresholds={defaults.thresholds}
theme={config.theme}
showAlphaUI={config.featureToggles.newEdit}
/>
</PanelOptionsGrid>
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />

View File

@ -1,5 +1,5 @@
import { SingleStatBaseOptions, BigValueColorMode, BigValueGraphMode, BigValueJustifyMode } from '@grafana/ui';
import { VizOrientation, ReducerID, FieldDisplayOptions, SelectableValue } from '@grafana/data';
import { VizOrientation, ReducerID, FieldDisplayOptions, SelectableValue, ThresholdsMode } from '@grafana/data';
// Structure copied from angular
export interface StatPanelOptions extends SingleStatBaseOptions {
@ -27,10 +27,13 @@ export const standardFieldDisplayOptions: FieldDisplayOptions = {
values: false,
calcs: [ReducerID.mean],
defaults: {
thresholds: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' }, // 80%
],
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{ value: -Infinity, color: 'green' },
{ value: 80, color: 'red' }, // 80%
],
},
mappings: [],
},
overrides: [],

View File

@ -18163,7 +18163,7 @@ react-syntax-highlighter@^8.0.1:
prismjs "^1.8.4"
refractor "^2.4.1"
react-table@latest:
react-table@7.0.0-rc.15:
version "7.0.0-rc.15"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.15.tgz#bb855e4e2abbb4aaf0ed2334404a41f3ada8e13a"
integrity sha512-ofMOlgrioHhhvHjvjsQkxvfQzU98cqwy6BjPGNwhLN1vhgXeWi0mUGreaCPvRenEbTiXsQbMl4k3Xmx3Mut8Rw==