diff --git a/devenv/dev-dashboards/datasource-testdata/bar-gauge-demo2.json b/devenv/dev-dashboards/datasource-testdata/bar-gauge-demo2.json index f038101d9e6..b3aeab709d7 100644 --- a/devenv/dev-dashboards/datasource-testdata/bar-gauge-demo2.json +++ b/devenv/dev-dashboards/datasource-testdata/bar-gauge-demo2.json @@ -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", diff --git a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json index 52bcfd8460e..f920c5d6fe3 100644 --- a/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json +++ b/devenv/dev-dashboards/panel-bargauge/panel_tests_bar_gauge2.json @@ -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", diff --git a/devenv/dev-dashboards/panel-stat/panel-stat-tests.json b/devenv/dev-dashboards/panel-stat/panel-stat-tests.json index e0c11b7b124..1a772c9124e 100644 --- a/devenv/dev-dashboards/panel-stat/panel-stat-tests.json +++ b/devenv/dev-dashboards/panel-stat/panel-stat-tests.json @@ -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", diff --git a/packages/grafana-data/src/field/displayProcessor.test.ts b/packages/grafana-data/src/field/displayProcessor.test.ts index 02aadcab7d4..f5ed8d8e151 100644 --- a/packages/grafana-data/src/field/displayProcessor.test.ts +++ b/packages/grafana-data/src/field/displayProcessor.test.ts @@ -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'); diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index 8d71a89dfed..001ce558878 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -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; // Context isUtc?: boolean; @@ -31,80 +27,81 @@ const timeFormats: KeyValue = { }; export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayProcessor { - if (options && !_.isEmpty(options)) { - const field = options.config ? options.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 (!options || _.isEmpty(options) || !options.field) { + return toStringProcessor; + } + const { field } = options; + const config = field.config ?? {}; + + 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(field.unit || 'none'); - - return (value: any) => { - const { theme } = options; - const { mappings, thresholds } = field; - let color; + const formatFunc = getValueFormat(config.unit || 'none'); + const scaleFunc = getScaleCalculator(field as Field, options.theme); - let text = _.toString(value); - let numeric = toNumber(value); - let prefix: string | undefined = undefined; - let suffix: string | undefined = undefined; + return (value: any) => { + const { mappings } = config; - let shouldFormat = true; - if (mappings && mappings.length > 0) { - const mappedValue = getMappedValue(mappings, value); + let text = _.toString(value); + let numeric = toNumber(value); + let prefix: string | undefined = undefined; + let suffix: string | undefined = undefined; - if (mappedValue) { - text = mappedValue.text; - const v = toNumber(text); + let shouldFormat = true; + if (mappings && mappings.length > 0) { + const mappedValue = getMappedValue(mappings, value); - if (!isNaN(v)) { - numeric = v; - } + if (mappedValue) { + text = mappedValue.text; + const v = toNumber(text); - shouldFormat = false; + 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; - } + 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; + + // 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 diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index b866fe562a2..ea73280a296 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -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); }); diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index 65ef117b973..64338fc9146 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -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); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 9203f077ce3..d7a37d15d32 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -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) diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index d9dcaeafc36..94530b36c5c 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -1,4 +1,5 @@ export * from './fieldDisplay'; export * from './displayProcessor'; +export * from './scale'; -export { applyFieldOverrides } from './fieldOverrides'; +export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides'; diff --git a/packages/grafana-data/src/field/scale.test.ts b/packages/grafana-data/src/field/scale.test.ts new file mode 100644 index 00000000000..3245b20f79e --- /dev/null +++ b/packages/grafana-data/src/field/scale.test.ts @@ -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 = { + 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, + }, + }, + ] + `); + }); +}); diff --git a/packages/grafana-data/src/field/scale.ts b/packages/grafana-data/src/field/scale.ts new file mode 100644 index 00000000000..735def3c887 --- /dev/null +++ b/packages/grafana-data/src/field/scale.ts @@ -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; + }); +} diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index cabf37841d0..1f628491361 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -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; } diff --git a/packages/grafana-data/src/types/displayValue.ts b/packages/grafana-data/src/types/displayValue.ts index c93575b7040..3c5c30d0bb0 100644 --- a/packages/grafana-data/src/types/displayValue.ts +++ b/packages/grafana-data/src/types/displayValue.ts @@ -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; } diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts new file mode 100644 index 00000000000..34314a0d72e --- /dev/null +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -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 +} diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index f51d76f75f1..f6b7e29503b 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -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'; diff --git a/packages/grafana-data/src/types/threshold.ts b/packages/grafana-data/src/types/threshold.ts deleted file mode 100644 index fac4352a0b0..00000000000 --- a/packages/grafana-data/src/types/threshold.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Threshold { - value: number; - color: string; -} diff --git a/packages/grafana-data/src/types/thresholds.ts b/packages/grafana-data/src/types/thresholds.ts new file mode 100644 index 00000000000..ecd4046c4bf --- /dev/null +++ b/packages/grafana-data/src/types/thresholds.ts @@ -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[]; +} diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index b812b2f823b..e6214739dd4 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -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'; diff --git a/packages/grafana-data/src/utils/thresholds.ts b/packages/grafana-data/src/utils/thresholds.ts deleted file mode 100644 index 20068893078..00000000000 --- a/packages/grafana-data/src/utils/thresholds.ts +++ /dev/null @@ -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; - }); -} diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx index fbc50c7838c..f595a1ac4da 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx @@ -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) { threshold2Value, } = getKnobs(); + const field: Partial = { + 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) { 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); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx index e5371b4b110..4ba1d9d1b9b 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -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 { + const field: Partial = { + 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); }); }); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index 2e98f2e3d6a..2b0fef2c20e 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -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 { static defaultProps: Partial = { - maxValue: 100, - minValue: 0, lcdCellWidth: 12, value: { text: '100', @@ -64,7 +64,14 @@ export class BarGauge extends PureComponent { }, 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 { } 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 { }; } - 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 { } 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 { 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); } diff --git a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap index b789648ea2f..0ecb489b449 100644 --- a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap +++ b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap @@ -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, + }, } } /> diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index bce40c3f246..300d113b1ac 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -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(); 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([ diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index d3b6122c76c..485709389af 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -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 { canvasElement: any; static defaultProps: Partial = { - 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 { this.draw(); } - getFormattedThresholds() { - const { maxValue, minValue, thresholds, theme } = this.props; - - const lastThreshold = thresholds[thresholds.length - 1]; + 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; + } - 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 { } 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 { 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 { }; const plotSeries = { - data: [[0, value.numeric]], + data: [[0, numeric]], label: value.title, }; diff --git a/packages/grafana-ui/src/components/Graph/Graph.story.tsx b/packages/grafana-ui/src/components/Graph/Graph.story.tsx index 64d623f6338..03cb5824ae5 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.story.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.story.tsx @@ -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: { diff --git a/packages/grafana-ui/src/components/Graph/Graph.test.tsx b/packages/grafana-ui/src/components/Graph/Graph.test.tsx index 8098ffdedf1..d8d5710b770 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.test.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.test.tsx @@ -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: { diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx index bdcc5cc7330..7f3450d49c9 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/SingleModeGraphTooltip.tsx @@ -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 = ({ 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 ( ); diff --git a/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx b/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx index 767ca5c8c9e..f9a0caa9ee3 100644 --- a/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx @@ -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: { diff --git a/packages/grafana-ui/src/components/Graph/utils.test.ts b/packages/grafana-ui/src/components/Graph/utils.test.ts index 32155422125..fc0b654c32d 100644 --- a/packages/grafana-ui/src/components/Graph/utils.test.ts +++ b/packages/grafana-ui/src/components/Graph/utils.test.ts @@ -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) + ); }); }); }); diff --git a/packages/grafana-ui/src/components/Graph/utils.ts b/packages/grafana-ui/src/components/Graph/utils.ts index 11ed33578a9..6b4c157a1b9 100644 --- a/packages/grafana-ui/src/components/Graph/utils.ts +++ b/packages/grafana-ui/src/components/Graph/utils.ts @@ -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, }); } diff --git a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts index 72927c241ec..6943f96c690 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.test.ts @@ -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: { diff --git a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts index ac6aa7de684..66ad445c48c 100644 --- a/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts +++ b/packages/grafana-ui/src/components/SingleStatShared/SingleStatBaseOptions.ts @@ -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 = props => { const { column, tableStyles, cell } = props; @@ -22,6 +25,14 @@ export const BarGaugeCell: FC = 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 = props => { { - return ; + return ( + + ); }); ThresholdsEditorStories.add('with thresholds', () => { - return ; + return ; }); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index a930c0fe072..b0479203a49 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -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) => { 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) => { }; 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; - 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; - 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' }, diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index f79f4e57455..0389942b75a 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -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> = [ + { 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 { 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 { 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 { 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 { 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) => { + if (item.value) { + this.props.onChange({ + ...this.props.thresholds, + mode: item.value, + }); + } }; renderInput = (threshold: ThresholdWithKey) => { + const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage; return (
@@ -181,6 +202,11 @@ export class ThresholdsEditor extends PureComponent { onBlur={this.onBlur} />
+ {isPercent && ( +
+ +
+ )}
this.onRemoveThreshold(threshold)}>
@@ -191,42 +217,50 @@ export class ThresholdsEditor extends PureComponent { }; render() { - const { thresholds } = this.state; + const { steps } = this.state; + const { theme } = this.props; + const t = this.props.thresholds; return ( - - {theme => { - return ( - -
- {thresholds - .slice(0) - .reverse() - .map(threshold => { - return ( -
-
this.onAddThresholdAfter(threshold)}> - -
-
-
{this.renderInput(threshold)}
-
- ); - })} -
- - ); - }} - + + <> +
+ {steps + .slice(0) + .reverse() + .map(threshold => { + return ( +
+
this.onAddThresholdAfter(threshold)}> + +
+
+
{this.renderInput(threshold)}
+
+ ); + })} +
+ + {this.props.showAlphaUI && ( +
+
- +
+
+
- -
-
-
+ + + +
+
+ +
+ +
+ +
- -
+ + `; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 87e1ff0d081..5115118ced3 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -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', }); diff --git a/public/app/features/explore/utils/ResultProcessor.ts b/public/app/features/explore/utils/ResultProcessor.ts index e25b907153e..8c1277babdd 100644 --- a/public/app/features/explore/utils/ResultProcessor.ts +++ b/public/app/features/explore/utils/ResultProcessor.ts @@ -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, }); } diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index f294a8fe5e1..ea2925c0af4 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -23,7 +23,8 @@ export class BarGaugePanel extends PureComponent> { 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 ( @@ -34,12 +35,11 @@ export class BarGaugePanel extends PureComponent> { 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} diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx index 9300d072362..9899a10a779 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx @@ -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> { - 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 - + diff --git a/public/app/plugins/panel/bargauge/__snapshots__/BarGaugeMigrations.test.ts.snap b/public/app/plugins/panel/bargauge/__snapshots__/BarGaugeMigrations.test.ts.snap index eb1bb7cab99..414c936b92e 100644 --- a/public/app/plugins/panel/bargauge/__snapshots__/BarGaugeMigrations.test.ts.snap +++ b/public/app/plugins/panel/bargauge/__snapshots__/BarGaugeMigrations.test.ts.snap @@ -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", diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 4eb6dd2538b..46620de7a97 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -26,11 +26,9 @@ export class GaugePanel extends PureComponent> { 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} diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx index b49440c6d35..4028d841806 100644 --- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -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> { labelWidth = 6; @@ -30,7 +38,7 @@ export class GaugePanelEditor extends PureComponent { + onThresholdsChanged = (thresholds: ThresholdsConfig) => { const current = this.props.options.fieldOptions.defaults; this.onDefaultsChange({ ...current, @@ -122,7 +130,12 @@ export class GaugePanelEditor extends PureComponent - + diff --git a/public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap b/public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap index 093914fc7a0..4d2252959d1 100644 --- a/public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap +++ b/public/app/plugins/panel/gauge/__snapshots__/GaugeMigrations.test.ts.snap @@ -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", }, }, diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 9b20497e3ca..fd53faccea5 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -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 diff --git a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts index c8a60d534bf..cc2fc3eb893 100644 --- a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts +++ b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts @@ -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: { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 0a1dcba4830..a0da13c2a18 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -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(), diff --git a/public/app/plugins/panel/stat/StatPanelEditor.tsx b/public/app/plugins/panel/stat/StatPanelEditor.tsx index 5b797ff1d9c..76655ec6726 100644 --- a/public/app/plugins/panel/stat/StatPanelEditor.tsx +++ b/public/app/plugins/panel/stat/StatPanelEditor.tsx @@ -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> { - 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 - + diff --git a/public/app/plugins/panel/stat/types.ts b/public/app/plugins/panel/stat/types.ts index 6686004e674..1caae1bda7f 100644 --- a/public/app/plugins/panel/stat/types.ts +++ b/public/app/plugins/panel/stat/types.ts @@ -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: [], diff --git a/yarn.lock b/yarn.lock index a11c83cf29f..588e58c34aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==