3
0
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. ()

* 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:
Oscar Kilhed 2023-10-11 10:50:11 +02:00 committed by GitHub
parent 1f8b08202e
commit 608f066ca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 24 deletions
.betterer.results
docs/sources/panels-visualizations/configure-standard-options
packages/grafana-data/src
public/app
core/components/OptionsUI
features/dashboard/components/PanelEditor

View File

@ -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"]

View File

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

View File

@ -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[] = [];

View File

@ -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)

View File

@ -101,6 +101,9 @@ export interface FieldConfig<TOptions = any> {
// Panel Specific Values
custom?: TOptions;
// Calculate min max per field
fieldMinMax?: boolean;
}
export interface FieldTypeConfig {

View File

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

View File

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

View File

@ -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) {