mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Transformations: Cumulative and window modes for Add field from calculation (#77029)
* cumulative sum * refactor and create new mode * refactor - use reduceOptions for new mode also * revert naming * Add window function, rename statistical to cumulative (#77066) * Add window function, rename statistical to cumulative * Fix merge errors * fix more merge errors * refactor + add window funcs Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com> * add ff + tests + centered moving avg Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com> * make sum and mean cumulative more efficient (#77173) * make sum and mean cumulative more efficient * remove cumulative variance, add window stddev * refactor window to not use reducer for mean. wip variance stdDev * fix tests after optimization --------- Co-authored-by: Victor Marin <victor.marin@grafana.com> * optimize window func (#77266) * make sum and mean cumulative more efficient * remove cumulative variance, add window stddev * refactor window to not use reducer for mean. wip variance stdDev * fix tests after optimization * fix test lint * optimize window * tests are passing * fix nulls * fix all nulls --------- Co-authored-by: Victor Marin <victor.marin@grafana.com> * change window size to be percentage * fix tests to use percentage * fixed/percentage window size (#77369) * Add docs for cumulative and window functions of the add field from calculation transform. (#77352) add docs * splling * change WindowType -> WindowAlignment * update betterer * refactor getWindowCreator * add docs to content.ts * add feature toggle message --------- Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
This commit is contained in:
@@ -7,7 +7,13 @@ import { mockTransformationsRegistry } from '../../utils/tests/mockTransformatio
|
||||
import { ReducerID } from '../fieldReducer';
|
||||
import { transformDataFrame } from '../transformDataFrame';
|
||||
|
||||
import { CalculateFieldMode, calculateFieldTransformer, ReduceOptions } from './calculateField';
|
||||
import {
|
||||
CalculateFieldMode,
|
||||
calculateFieldTransformer,
|
||||
ReduceOptions,
|
||||
WindowSizeMode,
|
||||
WindowAlignment,
|
||||
} from './calculateField';
|
||||
import { DataTransformerID } from './ids';
|
||||
|
||||
const seriesA = toDataFrame({
|
||||
@@ -398,4 +404,492 @@ describe('calculateField transformer w/ timeseries', () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving average on odd window size', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 1,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1.5);
|
||||
expect(data.fields[1].values[1]).toEqual(2);
|
||||
expect(data.fields[1].values[2]).toEqual(2.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving average on even window size', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 0.5,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(1.5);
|
||||
expect(data.fields[1].values[2]).toEqual(2.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving average when window size larger than dataset', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 5,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(2);
|
||||
expect(data.fields[1].values[1]).toEqual(2);
|
||||
expect(data.fields[1].values[2]).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates trailing moving average', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
field: 'x',
|
||||
windowSize: 1,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(1.5);
|
||||
expect(data.fields[1].values[2]).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when calculating moving average if window size < 1', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
field: 'x',
|
||||
windowSize: 0,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const err = new Error('Add field from calculation transformation - Window size must be larger than 0');
|
||||
expect(received[0]).toEqual(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates cumulative total', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.CumulativeFunctions,
|
||||
cumulative: {
|
||||
field: 'x',
|
||||
reducer: ReducerID.sum,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(3);
|
||||
expect(data.fields[1].values[2]).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates cumulative mean', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.CumulativeFunctions,
|
||||
cumulative: {
|
||||
field: 'x',
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(1.5);
|
||||
expect(data.fields[1].values[2]).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates cumulative total with nulls', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.CumulativeFunctions,
|
||||
cumulative: {
|
||||
field: 'x',
|
||||
reducer: ReducerID.sum,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(1);
|
||||
expect(data.fields[1].values[2]).toEqual(3);
|
||||
expect(data.fields[1].values[3]).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates trailing moving average with nulls', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 7] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(1);
|
||||
expect(data.fields[1].values[2]).toEqual(1.5);
|
||||
expect(data.fields[1].values[3]).toEqual(4.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates trailing moving variance', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
field: 'x',
|
||||
windowSize: 1,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.variance,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(0);
|
||||
expect(data.fields[1].values[1]).toEqual(0.25);
|
||||
expect(data.fields[1].values[2]).toBeCloseTo(0.6666666, 4);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving stddev', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 1,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.stdDev,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(0.5);
|
||||
expect(data.fields[1].values[1]).toBeCloseTo(0.8164, 2);
|
||||
expect(data.fields[1].values[2]).toEqual(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving stddev with null', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.stdDev,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(0);
|
||||
expect(data.fields[1].values[1]).toEqual(0.5);
|
||||
expect(data.fields[1].values[2]).toEqual(0.5);
|
||||
expect(data.fields[1].values[3]).toEqual(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving average with nulls', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 7] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1);
|
||||
expect(data.fields[1].values[1]).toEqual(1.5);
|
||||
expect(data.fields[1].values[2]).toEqual(4.5);
|
||||
expect(data.fields[1].values[3]).toEqual(4.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving average with only nulls', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [null, null, null, null] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(0);
|
||||
expect(data.fields[1].values[1]).toEqual(0);
|
||||
expect(data.fields[1].values[2]).toEqual(0);
|
||||
expect(data.fields[1].values[3]).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates centered moving average with 4 values', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Centered,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.mean,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, 2, 3, 4] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(1.5);
|
||||
expect(data.fields[1].values[1]).toEqual(2);
|
||||
expect(data.fields[1].values[2]).toEqual(3);
|
||||
expect(data.fields[1].values[3]).toEqual(3.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates trailing moving variance with null in the middle', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.variance,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [1, null, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(0);
|
||||
expect(data.fields[1].values[1]).toEqual(0);
|
||||
expect(data.fields[1].values[2]).toEqual(0.25);
|
||||
expect(data.fields[1].values[3]).toEqual(0.25);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates trailing moving variance with null in position 0', async () => {
|
||||
const cfg = {
|
||||
id: DataTransformerID.calculateField,
|
||||
options: {
|
||||
mode: CalculateFieldMode.WindowFunctions,
|
||||
window: {
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
field: 'x',
|
||||
windowSize: 0.75,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
reducer: ReducerID.variance,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const series = toDataFrame({
|
||||
fields: [{ name: 'x', type: FieldType.number, values: [null, 1, 2, 3] }],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => {
|
||||
const data = received[0][0];
|
||||
|
||||
expect(data.fields.length).toEqual(2);
|
||||
expect(data.fields[1].values[0]).toEqual(0);
|
||||
expect(data.fields[1].values[1]).toEqual(0);
|
||||
expect(data.fields[1].values[2]).toEqual(0.25);
|
||||
expect(data.fields[1].values[3]).toBeCloseTo(0.6666666, 4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,17 +16,40 @@ import { noopTransformer } from './noop';
|
||||
|
||||
export enum CalculateFieldMode {
|
||||
ReduceRow = 'reduceRow',
|
||||
CumulativeFunctions = 'cumulativeFunctions',
|
||||
WindowFunctions = 'windowFunctions',
|
||||
BinaryOperation = 'binary',
|
||||
UnaryOperation = 'unary',
|
||||
Index = 'index',
|
||||
}
|
||||
|
||||
export enum WindowSizeMode {
|
||||
Percentage = 'percentage',
|
||||
Fixed = 'fixed',
|
||||
}
|
||||
|
||||
export enum WindowAlignment {
|
||||
Trailing = 'trailing',
|
||||
Centered = 'centered',
|
||||
}
|
||||
|
||||
export interface ReduceOptions {
|
||||
include?: string[]; // Assume all fields
|
||||
reducer: ReducerID;
|
||||
nullValueMode?: NullValueMode;
|
||||
}
|
||||
|
||||
export interface CumulativeOptions {
|
||||
field?: string;
|
||||
reducer: ReducerID;
|
||||
}
|
||||
|
||||
export interface WindowOptions extends CumulativeOptions {
|
||||
windowSize?: number;
|
||||
windowSizeMode?: WindowSizeMode;
|
||||
windowAlignment?: WindowAlignment;
|
||||
}
|
||||
|
||||
export interface UnaryOptions {
|
||||
operator: UnaryOperationID;
|
||||
fieldName: string;
|
||||
@@ -46,6 +69,13 @@ const defaultReduceOptions: ReduceOptions = {
|
||||
reducer: ReducerID.sum,
|
||||
};
|
||||
|
||||
export const defaultWindowOptions: WindowOptions = {
|
||||
reducer: ReducerID.mean,
|
||||
windowAlignment: WindowAlignment.Trailing,
|
||||
windowSizeMode: WindowSizeMode.Percentage,
|
||||
windowSize: 0.1,
|
||||
};
|
||||
|
||||
const defaultBinaryOptions: BinaryOptions = {
|
||||
left: '',
|
||||
operator: BinaryOperationID.Add,
|
||||
@@ -64,6 +94,8 @@ export interface CalculateFieldTransformerOptions {
|
||||
|
||||
// Only one should be filled
|
||||
reduce?: ReduceOptions;
|
||||
window?: WindowOptions;
|
||||
cumulative?: CumulativeOptions;
|
||||
binary?: BinaryOptions;
|
||||
unary?: UnaryOptions;
|
||||
index?: IndexOptions;
|
||||
@@ -104,39 +136,49 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
||||
const mode = options.mode ?? CalculateFieldMode.ReduceRow;
|
||||
let creator: ValuesCreator | undefined = undefined;
|
||||
|
||||
if (mode === CalculateFieldMode.ReduceRow) {
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
||||
} else if (mode === CalculateFieldMode.UnaryOperation) {
|
||||
creator = getUnaryCreator(defaults(options.unary, defaultUnaryOptions), data);
|
||||
} else if (mode === CalculateFieldMode.BinaryOperation) {
|
||||
const binaryOptions = {
|
||||
...options.binary,
|
||||
left: ctx.interpolate(options.binary?.left!),
|
||||
right: ctx.interpolate(options.binary?.right!),
|
||||
};
|
||||
switch (mode) {
|
||||
case CalculateFieldMode.ReduceRow:
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.CumulativeFunctions:
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultReduceOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.WindowFunctions:
|
||||
creator = getWindowCreator(defaults(options.window, defaultWindowOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.UnaryOperation:
|
||||
creator = getUnaryCreator(defaults(options.unary, defaultUnaryOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.BinaryOperation:
|
||||
const binaryOptions = {
|
||||
...options.binary,
|
||||
left: ctx.interpolate(options.binary?.left!),
|
||||
right: ctx.interpolate(options.binary?.right!),
|
||||
};
|
||||
|
||||
creator = getBinaryCreator(defaults(binaryOptions, defaultBinaryOptions), data);
|
||||
} else if (mode === CalculateFieldMode.Index) {
|
||||
return data.map((frame) => {
|
||||
const indexArr = [...Array(frame.length).keys()];
|
||||
creator = getBinaryCreator(defaults(binaryOptions, defaultBinaryOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.Index:
|
||||
return data.map((frame) => {
|
||||
const indexArr = [...Array(frame.length).keys()];
|
||||
|
||||
if (options.index?.asPercentile) {
|
||||
for (let i = 0; i < indexArr.length; i++) {
|
||||
indexArr[i] = indexArr[i] / indexArr.length;
|
||||
if (options.index?.asPercentile) {
|
||||
for (let i = 0; i < indexArr.length; i++) {
|
||||
indexArr[i] = indexArr[i] / indexArr.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const f = {
|
||||
name: options.alias ?? 'Row',
|
||||
type: FieldType.number,
|
||||
values: indexArr,
|
||||
config: options.index?.asPercentile ? { unit: 'percentunit' } : {},
|
||||
};
|
||||
return {
|
||||
...frame,
|
||||
fields: options.replaceFields ? [f] : [...frame.fields, f],
|
||||
};
|
||||
});
|
||||
const f = {
|
||||
name: options.alias ?? 'Row',
|
||||
type: FieldType.number,
|
||||
values: indexArr,
|
||||
config: options.index?.asPercentile ? { unit: 'percentunit' } : {},
|
||||
};
|
||||
return {
|
||||
...frame,
|
||||
fields: options.replaceFields ? [f] : [...frame.fields, f],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Nothing configured
|
||||
@@ -180,6 +222,213 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
||||
},
|
||||
};
|
||||
|
||||
function getWindowCreator(options: WindowOptions, allFrames: DataFrame[]): ValuesCreator {
|
||||
if (options.windowSize! <= 0) {
|
||||
throw new Error('Add field from calculation transformation - Window size must be larger than 0');
|
||||
}
|
||||
|
||||
let matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.numeric,
|
||||
});
|
||||
|
||||
if (options.field) {
|
||||
matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.byNames,
|
||||
options: {
|
||||
names: [options.field],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (frame: DataFrame) => {
|
||||
const window = Math.ceil(
|
||||
options.windowSize! * (options.windowSizeMode === WindowSizeMode.Percentage ? frame.length : 1)
|
||||
);
|
||||
|
||||
// Find the columns that should be examined
|
||||
let selectedField: Field | null = null;
|
||||
for (const field of frame.fields) {
|
||||
if (matcher(field, frame, allFrames)) {
|
||||
selectedField = field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (![ReducerID.mean, ReducerID.stdDev, ReducerID.variance].includes(options.reducer)) {
|
||||
throw new Error(`Add field from calculation transformation - Unsupported reducer: ${options.reducer}`);
|
||||
}
|
||||
|
||||
if (options.windowAlignment === WindowAlignment.Centered) {
|
||||
return getCenteredWindowValues(frame, options.reducer, selectedField, window);
|
||||
} else {
|
||||
return getTrailingWindowValues(frame, options.reducer, selectedField, window);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getTrailingWindowValues(frame: DataFrame, reducer: ReducerID, selectedField: Field, window: number) {
|
||||
const vals: number[] = [];
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
if (reducer === ReducerID.mean) {
|
||||
const currentValue = selectedField.values[i];
|
||||
if (currentValue !== null) {
|
||||
count++;
|
||||
sum += currentValue;
|
||||
|
||||
if (i > window - 1) {
|
||||
sum -= selectedField.values[i - window];
|
||||
count--;
|
||||
}
|
||||
}
|
||||
vals.push(count === 0 ? 0 : sum / count);
|
||||
} else if (reducer === ReducerID.variance) {
|
||||
const start = Math.max(0, i - window + 1);
|
||||
const end = i + 1;
|
||||
vals.push(calculateVariance(selectedField.values.slice(start, end)));
|
||||
} else if (reducer === ReducerID.stdDev) {
|
||||
const start = Math.max(0, i - window + 1);
|
||||
const end = i + 1;
|
||||
vals.push(calculateStdDev(selectedField.values.slice(start, end)));
|
||||
}
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
|
||||
function getCenteredWindowValues(frame: DataFrame, reducer: ReducerID, selectedField: Field, window: number) {
|
||||
const vals: number[] = [];
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
// Current value (i) is included in the leading part of the window. Which means if the window size is odd,
|
||||
// the leading part of the window will be larger than the trailing part.
|
||||
const leadingPartOfWindow = Math.ceil(window / 2) - 1;
|
||||
const trailingPartOfWindow = Math.floor(window / 2);
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
const first = i - trailingPartOfWindow;
|
||||
const last = i + leadingPartOfWindow;
|
||||
if (reducer === ReducerID.mean) {
|
||||
if (i === 0) {
|
||||
// We're at the start and need to prime the leading part of the window
|
||||
for (let x = 0; x < leadingPartOfWindow + 1 && x < selectedField.values.length; x++) {
|
||||
if (selectedField.values[x] !== null) {
|
||||
sum += selectedField.values[x];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (last < selectedField.values.length) {
|
||||
// Last is inside the data and should be added.
|
||||
if (selectedField.values[last] !== null) {
|
||||
sum += selectedField.values[last];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (first > 0) {
|
||||
// Remove values that have fallen outside of the window, if the start of the window isn't outside of the data.
|
||||
if (selectedField.values[first - 1] !== null) {
|
||||
sum -= selectedField.values[first - 1];
|
||||
count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
vals.push(count === 0 ? 0 : sum / count);
|
||||
} else if (reducer === ReducerID.variance) {
|
||||
const windowVals = selectedField.values.slice(
|
||||
Math.max(0, first),
|
||||
Math.min(last + 1, selectedField.values.length)
|
||||
);
|
||||
vals.push(calculateVariance(windowVals));
|
||||
} else if (reducer === ReducerID.stdDev) {
|
||||
const windowVals = selectedField.values.slice(
|
||||
Math.max(0, first),
|
||||
Math.min(last + 1, selectedField.values.length)
|
||||
);
|
||||
vals.push(calculateStdDev(windowVals));
|
||||
}
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
|
||||
function calculateVariance(vals: number[]): number {
|
||||
if (vals.length < 1) {
|
||||
return 0;
|
||||
}
|
||||
let squareSum = 0;
|
||||
let runningMean = 0;
|
||||
let nonNullCount = 0;
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
const currentValue = vals[i];
|
||||
if (currentValue !== null) {
|
||||
nonNullCount++;
|
||||
let _oldMean = runningMean;
|
||||
runningMean += (currentValue - _oldMean) / nonNullCount;
|
||||
squareSum += (currentValue - _oldMean) * (currentValue - runningMean);
|
||||
}
|
||||
}
|
||||
if (nonNullCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
const variance = squareSum / nonNullCount;
|
||||
return variance;
|
||||
}
|
||||
|
||||
function calculateStdDev(vals: number[]): number {
|
||||
return Math.sqrt(calculateVariance(vals));
|
||||
}
|
||||
|
||||
function getCumulativeCreator(options: CumulativeOptions, allFrames: DataFrame[]): ValuesCreator {
|
||||
let matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.numeric,
|
||||
});
|
||||
|
||||
if (options.field) {
|
||||
matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.byNames,
|
||||
options: {
|
||||
names: [options.field],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (![ReducerID.mean, ReducerID.sum].includes(options.reducer)) {
|
||||
throw new Error(`Add field from calculation transformation - Unsupported reducer: ${options.reducer}`);
|
||||
}
|
||||
|
||||
return (frame: DataFrame) => {
|
||||
// Find the columns that should be examined
|
||||
let selectedField: Field | null = null;
|
||||
for (const field of frame.fields) {
|
||||
if (matcher(field, frame, allFrames)) {
|
||||
selectedField = field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vals: number[] = [];
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
total += selectedField.values[i];
|
||||
if (options.reducer === ReducerID.sum) {
|
||||
vals.push(total);
|
||||
} else if (options.reducer === ReducerID.mean) {
|
||||
vals.push(total / (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return vals;
|
||||
};
|
||||
}
|
||||
|
||||
function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): ValuesCreator {
|
||||
let matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.numeric,
|
||||
@@ -227,6 +476,7 @@ function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): Va
|
||||
for (let j = 0; j < size; j++) {
|
||||
row.values[j] = columns[j][i];
|
||||
}
|
||||
|
||||
vals.push(reducer(row, ignoreNulls, nullAsZero)[options.reducer]);
|
||||
}
|
||||
|
||||
@@ -309,6 +559,16 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) {
|
||||
}
|
||||
|
||||
switch (options.mode) {
|
||||
case CalculateFieldMode.CumulativeFunctions: {
|
||||
const { cumulative } = options;
|
||||
return `cumulative ${cumulative?.reducer ?? ''}${cumulative?.field ? `(${cumulative.field})` : ''}`;
|
||||
}
|
||||
case CalculateFieldMode.WindowFunctions: {
|
||||
const { window } = options;
|
||||
return `${window?.windowAlignment ?? ''} moving ${window?.reducer ?? ''}${
|
||||
window?.field ? `(${window.field})` : ''
|
||||
}`;
|
||||
}
|
||||
case CalculateFieldMode.UnaryOperation: {
|
||||
const { unary } = options;
|
||||
return `${unary?.operator ?? ''}${unary?.fieldName ? `(${unary.fieldName})` : ''}`;
|
||||
|
||||
@@ -151,6 +151,7 @@ export interface FeatureToggles {
|
||||
costManagementUi?: boolean;
|
||||
managedPluginsInstall?: boolean;
|
||||
prometheusPromQAIL?: boolean;
|
||||
addFieldFromCalculationStatFunctions?: boolean;
|
||||
alertmanagerRemoteSecondary?: boolean;
|
||||
alertmanagerRemotePrimary?: boolean;
|
||||
alertmanagerRemoteOnly?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user