From 8a4d215914167c37161e8e4760574aea186cdc0f Mon Sep 17 00:00:00 2001 From: Kristina Date: Sat, 3 May 2025 09:03:27 -0500 Subject: [PATCH] Transformations: Add percentage to unary operations (#97358) Co-authored-by: Leon Sorokin --- .../transformers/calculateField.test.ts | 150 +++++++++++++----- .../transformers/calculateField.ts | 13 +- .../grafana-data/src/utils/unaryOperators.ts | 9 +- 3 files changed, 133 insertions(+), 39 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index ddae9577013..62e4dd174bf 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -457,48 +457,124 @@ describe('calculateField transformer w/ timeseries', () => { }); }); - it('unary math', async () => { - const unarySeries = toDataFrame({ - fields: [ - { name: 'TheTime', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, - { name: 'A', type: FieldType.number, values: [1, -10, -200, 300] }, - ], + describe('unary math', () => { + it('calculates absolute value accurately', async () => { + const unarySeries = toDataFrame({ + fields: [ + { name: 'TheTime', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, + { name: 'A', type: FieldType.number, values: [1, -10, -200, 300] }, + ], + }); + + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.UnaryOperation, + unary: { + fieldName: 'A', + operator: UnaryOperationID.Abs, + }, + replaceFields: true, + }, + }; + + await expect(transformDataFrame([cfg], [unarySeries])).toEmitValuesWith((received) => { + const data = received[0]; + const filtered = data[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toEqual([ + { + 'abs(A)': 1, + TheTime: 1000, + }, + { + 'abs(A)': 10, + TheTime: 2000, + }, + { + 'abs(A)': 200, + TheTime: 3000, + }, + { + 'abs(A)': 300, + TheTime: 4000, + }, + ]); + }); }); - const cfg = { - id: DataTransformerID.calculateField, - options: { - mode: CalculateFieldMode.UnaryOperation, - unary: { - fieldName: 'A', - operator: UnaryOperationID.Abs, + it('calculates percentage value accurately', async () => { + const testCases = [ + { + values: [1, 2, 3, 4], + result: [0.1, 0.2, 0.3, 0.4], }, - replaceFields: true, - }, - }; + { + values: [1, 0, 5, 4], + result: [0.1, 0, 0.5, 0.4], + }, + { + values: [1, Infinity, 5, 4], + result: [0.1, Infinity, 0.5, 0.4], + }, + { + values: [0, 0, 0, 0], + result: [0, 0, 0, 0], + }, + { + values: ['a', 's', 2, 4], + result: [NaN, NaN, 0.3333333333333333, 0.6666666666666666], + }, + { + values: [1e5, 1, 2, 4], + result: [0.999930004899657, 0.000009999300048996571, 0.000019998600097993142, 0.000039997200195986284], + }, + ]; - await expect(transformDataFrame([cfg], [unarySeries])).toEmitValuesWith((received) => { - const data = received[0]; - const filtered = data[0]; - const rows = new DataFrameView(filtered).toArray(); - expect(rows).toEqual([ - { - 'abs(A)': 1, - TheTime: 1000, + const cfg = { + id: DataTransformerID.calculateField, + options: { + mode: CalculateFieldMode.UnaryOperation, + unary: { + fieldName: 'A', + operator: UnaryOperationID.Percent, + }, + replaceFields: true, }, - { - 'abs(A)': 10, - TheTime: 2000, - }, - { - 'abs(A)': 200, - TheTime: 3000, - }, - { - 'abs(A)': 300, - TheTime: 4000, - }, - ]); + }; + + testCases.forEach(async (testCase) => { + const unarySeries = toDataFrame({ + fields: [ + { name: 'TheTime', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, + { name: 'A', type: FieldType.number, values: testCase.values }, + ], + }); + + await expect(transformDataFrame([cfg], [unarySeries])).toEmitValuesWith((received) => { + const data = received[0]; + const filtered = data[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toEqual([ + { + 'percent(A)': testCase.result[0], + TheTime: 1000, + }, + { + 'percent(A)': testCase.result[1], + TheTime: 2000, + }, + { + 'percent(A)': testCase.result[2], + TheTime: 3000, + }, + { + 'percent(A)': testCase.result[3], + TheTime: 4000, + }, + ]); + }); + }); }); }); diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 9df8a1e7b39..8b290f3cbc9 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -652,8 +652,19 @@ function getUnaryCreator(options: UnaryOptions, allFrames: DataFrame[]): ValuesC } const arr = new Array(value.length); + + let sum = 0; + + if (options.operator === UnaryOperationID.Percent) { + for (let i = 0; i < value.length; i++) { + if (Number.isFinite(value[i])) { + sum += value[i]; + } + } + } + for (let i = 0; i < arr.length; i++) { - arr[i] = operator.operation(value[i]); + arr[i] = operator.operation(value[i], sum); } return arr; diff --git a/packages/grafana-data/src/utils/unaryOperators.ts b/packages/grafana-data/src/utils/unaryOperators.ts index e440382f31e..7767bded5ad 100644 --- a/packages/grafana-data/src/utils/unaryOperators.ts +++ b/packages/grafana-data/src/utils/unaryOperators.ts @@ -7,9 +7,10 @@ export enum UnaryOperationID { Round = 'round', Floor = 'floor', Ceil = 'ceil', + Percent = 'percent', } -export type UnaryOperation = (value: number) => number; +export type UnaryOperation = (value: number, total?: number) => number; interface UnaryOperatorInfo extends RegistryItem { operation: UnaryOperation; @@ -54,5 +55,11 @@ export const unaryOperators = new Registry(() => { operation: (value: number) => Math.ceil(value), unaryOperationID: UnaryOperationID.Ceil, }, + { + id: UnaryOperationID.Percent, + name: 'Percentage', + operation: (value: number, total = 1) => value / (total || 1), + unaryOperationID: UnaryOperationID.Percent, + }, ]; });