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:
Victor Marin
2023-11-03 16:39:58 +02:00
committed by GitHub
parent 67b2972052
commit 61d63d3034
11 changed files with 1071 additions and 33 deletions

View File

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

View File

@@ -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})` : ''}`;

View File

@@ -151,6 +151,7 @@ export interface FeatureToggles {
costManagementUi?: boolean;
managedPluginsInstall?: boolean;
prometheusPromQAIL?: boolean;
addFieldFromCalculationStatFunctions?: boolean;
alertmanagerRemoteSecondary?: boolean;
alertmanagerRemotePrimary?: boolean;
alertmanagerRemoteOnly?: boolean;