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:
Oscar Kilhed authored and GitHub committed 2023-10-11 10:50:11 +02:00
1 parent 1f8b08202e
commit 608f066ca5
8 files changed
+195 -24

No files matched your search

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