mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PanelConfig: Add option to calculate min/max per field instead of using the global min/max in the data frame. (#75952)
* Add option to calculate min max per field * Fix eslint warnings * Add back hideFromDefaults that went missing * whitespace * Add tests * Refactor range calculation * Rename localMinMax -> fieldMinMax * Remove the lint exceptions Removing these as to not hide these once we get around to fixing the underlying typing issue. * Update docs
This commit is contained in:
parent
1f8b08202e
commit
608f066ca5
.betterer.results
docs/sources/panels-visualizations/configure-standard-options
packages/grafana-data/src
public/app
core/components/OptionsUI
features/dashboard/components/PanelEditor
@ -173,7 +173,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"packages/grafana-data/src/field/fieldOverrides.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/grafana-data/src/field/overrides/processors.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@ -1390,10 +1391,15 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "77"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "78"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "79"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "80"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "81"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "82"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "83"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "80"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "81"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "82"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "83"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "84"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "85"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "86"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "87"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "88"]
|
||||
],
|
||||
"public/app/core/components/OptionsUI/slider.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
|
@ -82,11 +82,15 @@ Grafana can sometimes be too aggressive in parsing strings and displaying them a
|
||||
|
||||
### Min
|
||||
|
||||
Lets you set the minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
|
||||
Lets you set the minimum value used in percentage threshold calculations. Leave blank to automatically calculate the minimum.
|
||||
|
||||
### Max
|
||||
|
||||
Lets you set the maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
|
||||
Lets you set the maximum value used in percentage threshold calculations. Leave blank to automatically calculate the maximum.
|
||||
|
||||
### Field min/max
|
||||
|
||||
By default the calculated min and max will be based on the minimum and maximum, in all series and fields. Turning field min/max on, will calculate the min or max on each field individually, based on the minimum or maximum value of the field.
|
||||
|
||||
### Decimals
|
||||
|
||||
|
@ -330,6 +330,106 @@ describe('applyFieldOverrides', () => {
|
||||
expect(range.min).toEqual(-20);
|
||||
});
|
||||
|
||||
it('should calculate min/max per field when fieldMinMax is set', () => {
|
||||
const df = toDataFrame([
|
||||
{ title: 'AAA', value: 100, value2: 1234 },
|
||||
{ title: 'BBB', value: -20, value2: null },
|
||||
{ title: 'CCC', value: 200, value2: 1000 },
|
||||
]);
|
||||
|
||||
const fieldCfgSource: FieldConfigSource = {
|
||||
defaults: {
|
||||
fieldMinMax: true,
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
const data = applyFieldOverrides({
|
||||
data: [df], // the frame
|
||||
fieldConfig: fieldCfgSource,
|
||||
replaceVariables: undefined as unknown as InterpolateFunction,
|
||||
theme: createTheme(),
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
})[0];
|
||||
|
||||
const valueColumn1 = data.fields[1];
|
||||
const range1 = valueColumn1.state!.range!;
|
||||
expect(range1.max).toEqual(200);
|
||||
expect(range1.min).toEqual(-20);
|
||||
|
||||
const valueColumn2 = data.fields[2];
|
||||
const range2 = valueColumn2.state!.range!;
|
||||
expect(range2.max).toEqual(1234);
|
||||
expect(range2.min).toEqual(1000);
|
||||
});
|
||||
|
||||
it('should calculate min/max locally for fields with fieldMinMax and globally for other fields', () => {
|
||||
const df = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'first', type: FieldType.number, values: [-1, -2] },
|
||||
{ name: 'second', type: FieldType.number, values: [1, 2] },
|
||||
{ name: 'third', type: FieldType.number, values: [1000, 2000] },
|
||||
],
|
||||
});
|
||||
|
||||
const fieldCfgSource: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [
|
||||
{
|
||||
matcher: { id: FieldMatcherID.byName, options: 'second' },
|
||||
properties: [{ id: 'fieldMinMax', value: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const data = applyFieldOverrides({
|
||||
data: [df], // the frame
|
||||
fieldConfig: fieldCfgSource,
|
||||
replaceVariables: undefined as unknown as InterpolateFunction,
|
||||
theme: createTheme(),
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
})[0];
|
||||
|
||||
const valueColumn0 = data.fields[0];
|
||||
const range0 = valueColumn0.state!.range!;
|
||||
expect(range0.max).toEqual(2000);
|
||||
expect(range0.min).toEqual(-2);
|
||||
|
||||
const valueColumn1 = data.fields[1];
|
||||
const range1 = valueColumn1.state!.range!;
|
||||
expect(range1.max).toEqual(2);
|
||||
expect(range1.min).toEqual(1);
|
||||
|
||||
const valueColumn2 = data.fields[2];
|
||||
const range2 = valueColumn2.state!.range!;
|
||||
expect(range2.max).toEqual(2000);
|
||||
expect(range2.min).toEqual(-2);
|
||||
});
|
||||
|
||||
it('should not calculate min if min is set', () => {
|
||||
const df = toDataFrame({
|
||||
fields: [{ name: 'first', type: FieldType.number, values: [-1, -2] }],
|
||||
});
|
||||
|
||||
const fieldCfgSource: FieldConfigSource = {
|
||||
defaults: {
|
||||
min: 1,
|
||||
fieldMinMax: true,
|
||||
},
|
||||
overrides: [],
|
||||
};
|
||||
const data = applyFieldOverrides({
|
||||
data: [df], // the frame
|
||||
fieldConfig: fieldCfgSource,
|
||||
replaceVariables: undefined as unknown as InterpolateFunction,
|
||||
theme: createTheme(),
|
||||
fieldConfigRegistry: customFieldRegistry,
|
||||
})[0];
|
||||
|
||||
const valueColumn0 = data.fields[0];
|
||||
const range0 = valueColumn0.state!.range!;
|
||||
expect(range0.max).toEqual(-1);
|
||||
expect(range0.min).toEqual(1);
|
||||
});
|
||||
|
||||
it('getLinks should use applied field config', () => {
|
||||
const replaceVariablesCalls: ScopedVars[] = [];
|
||||
|
||||
|
@ -40,6 +40,7 @@ import { mapInternalLinkToExplore } from '../utils/dataLinks';
|
||||
|
||||
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
||||
import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor';
|
||||
import { getMinMaxAndDelta } from './scale';
|
||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||
|
||||
interface OverrideProps {
|
||||
@ -166,15 +167,8 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
}
|
||||
|
||||
// Set the Min/Max value automatically
|
||||
let range: NumericRange | undefined = undefined;
|
||||
if (field.type === FieldType.number) {
|
||||
if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) {
|
||||
globalRange = findNumericFieldMinMax(options.data!);
|
||||
}
|
||||
const min = config.min ?? globalRange!.min;
|
||||
const max = config.max ?? globalRange!.max;
|
||||
range = { min, max, delta: max! - min! };
|
||||
}
|
||||
const { range, newGlobalRange } = calculateRange(config, field, globalRange, options.data!);
|
||||
globalRange = newGlobalRange;
|
||||
|
||||
field.state!.seriesIndex = seriesIndex;
|
||||
field.state!.range = range;
|
||||
@ -243,6 +237,32 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
});
|
||||
}
|
||||
|
||||
function calculateRange(
|
||||
config: FieldConfig<any>,
|
||||
field: Field,
|
||||
globalRange: NumericRange | undefined,
|
||||
data: DataFrame[]
|
||||
): { range?: { min?: number | null; max?: number | null; delta: number }; newGlobalRange: NumericRange | undefined } {
|
||||
// Only calculate ranges when the field is a number and one of min/max is set to auto.
|
||||
if (field.type !== FieldType.number || (isNumber(config.min) && isNumber(config.max))) {
|
||||
return { newGlobalRange: globalRange };
|
||||
}
|
||||
|
||||
// Calculate the min/max from the field.
|
||||
if (config.fieldMinMax) {
|
||||
const localRange = getMinMaxAndDelta(field);
|
||||
const min = config.min ?? localRange.min;
|
||||
const max = config.max ?? localRange.max;
|
||||
return { range: { min, max, delta: max! - min! }, newGlobalRange: globalRange };
|
||||
}
|
||||
|
||||
// We use the global range if supplied, otherwise we calculate it.
|
||||
const newGlobalRange = globalRange ?? findNumericFieldMinMax(data);
|
||||
const min = config.min ?? newGlobalRange!.min;
|
||||
const max = config.max ?? newGlobalRange!.max;
|
||||
return { range: { min, max, delta: max! - min! }, newGlobalRange };
|
||||
}
|
||||
|
||||
// this is a significant optimization for streaming, where we currently re-process all values in the buffer on ech update
|
||||
// via field.display(value). this can potentially be removed once we...
|
||||
// 1. process data packets incrementally and/if cache the results in the streaming datafame (maybe by buffer index)
|
||||
|
@ -101,6 +101,9 @@ export interface FieldConfig<TOptions = any> {
|
||||
|
||||
// Panel Specific Values
|
||||
custom?: TOptions;
|
||||
|
||||
// Calculate min max per field
|
||||
fieldMinMax?: boolean;
|
||||
}
|
||||
|
||||
export interface FieldTypeConfig {
|
||||
|
@ -67,6 +67,23 @@ export const mockStandardProperties = () => {
|
||||
shouldApply: () => true,
|
||||
};
|
||||
|
||||
const fieldMinMax = {
|
||||
id: 'fieldMinMax',
|
||||
path: 'fieldMinMax',
|
||||
name: 'localminmax',
|
||||
description: 'Calculate min/max per field ',
|
||||
|
||||
editor: () => null,
|
||||
override: () => null,
|
||||
process: identityOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
placeholder: 'auto',
|
||||
},
|
||||
|
||||
shouldApply: () => true,
|
||||
};
|
||||
|
||||
const decimals = {
|
||||
id: 'decimals',
|
||||
path: 'decimals',
|
||||
@ -166,5 +183,5 @@ export const mockStandardProperties = () => {
|
||||
shouldApply: () => true,
|
||||
};
|
||||
|
||||
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links, color];
|
||||
return [unit, min, max, fieldMinMax, decimals, title, noValue, thresholds, mappings, links, color];
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { BooleanFieldSettings } from '@react-awesome-query-builder/ui';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@ -27,6 +28,7 @@ import {
|
||||
FieldNamePickerConfigSettings,
|
||||
booleanOverrideProcessor,
|
||||
} from '@grafana/data';
|
||||
import { FieldConfig } from '@grafana/schema';
|
||||
import { RadioButtonGroup, TimeZonePicker, Switch } from '@grafana/ui';
|
||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||
import { ThresholdsValueEditor } from 'app/features/dimensions/editors/ThresholdsEditor/thresholds';
|
||||
@ -245,6 +247,23 @@ export const getAllStandardFieldConfigs = () => {
|
||||
category,
|
||||
};
|
||||
|
||||
const fieldMinMax: FieldConfigPropertyItem<any, boolean, BooleanFieldSettings> = {
|
||||
id: 'fieldMinMax',
|
||||
path: 'fieldMinMax',
|
||||
name: 'Field min/max',
|
||||
description: 'Calculate min max per field',
|
||||
|
||||
editor: standardEditorsRegistry.get('boolean').editor as any,
|
||||
override: standardEditorsRegistry.get('boolean').editor as any,
|
||||
process: booleanOverrideProcessor,
|
||||
|
||||
shouldApply: (field) => field.type === FieldType.number,
|
||||
showIf: (options: FieldConfig) => {
|
||||
return options.min === undefined || options.max === undefined;
|
||||
},
|
||||
category,
|
||||
};
|
||||
|
||||
const min: FieldConfigPropertyItem<any, number, NumberFieldConfigSettings> = {
|
||||
id: 'min',
|
||||
path: 'min',
|
||||
@ -397,5 +416,5 @@ export const getAllStandardFieldConfigs = () => {
|
||||
category,
|
||||
};
|
||||
|
||||
return [unit, min, max, decimals, displayName, color, noValue, links, mappings, thresholds, filterable];
|
||||
return [unit, min, max, fieldMinMax, decimals, displayName, color, noValue, links, mappings, thresholds, filterable];
|
||||
};
|
||||
|
@ -97,12 +97,14 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
* Field options
|
||||
*/
|
||||
for (const fieldOption of plugin.fieldConfigRegistry.list()) {
|
||||
if (
|
||||
fieldOption.isCustom &&
|
||||
fieldOption.showIf &&
|
||||
!fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series)
|
||||
) {
|
||||
continue;
|
||||
if (fieldOption.isCustom) {
|
||||
if (fieldOption.showIf && !fieldOption.showIf(currentFieldConfig.defaults.custom, data?.series)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (fieldOption.showIf && !fieldOption.showIf(currentFieldConfig.defaults, data?.series)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldOption.hideFromDefaults) {
|
||||
|
Loading…
Reference in New Issue
Block a user