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:
@@ -5089,7 +5089,12 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx:5381": [
|
"public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "5"]
|
||||||
],
|
],
|
||||||
"public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [
|
"public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ You can perform the following transformations on your data.
|
|||||||
Use this transformation to add a new field calculated from two other fields. Each transformation allows you to add one new field.
|
Use this transformation to add a new field calculated from two other fields. Each transformation allows you to add one new field.
|
||||||
|
|
||||||
- **Mode -** Select a mode:
|
- **Mode -** Select a mode:
|
||||||
|
|
||||||
- **Reduce row -** Apply selected calculation on each row of selected fields independently.
|
- **Reduce row -** Apply selected calculation on each row of selected fields independently.
|
||||||
- **Binary operation -** Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields.
|
- **Binary operation -** Apply basic binary operations (for example, sum or multiply) on values in a single row from two selected fields.
|
||||||
- **Unary operation -** Apply basic unary operations on values in a single row from a selected field. The available operations are:
|
- **Unary operation -** Apply basic unary operations on values in a single row from a selected field. The available operations are:
|
||||||
@@ -135,7 +136,32 @@ Use this transformation to add a new field calculated from two other fields. Eac
|
|||||||
- **Natural logarithm (ln)** - Returns the natural logarithm of a given expression.
|
- **Natural logarithm (ln)** - Returns the natural logarithm of a given expression.
|
||||||
- **Floor (floor)** - Returns the largest integer less than or equal to a given expression.
|
- **Floor (floor)** - Returns the largest integer less than or equal to a given expression.
|
||||||
- **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression.
|
- **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression.
|
||||||
|
- **Cumulative functions** - Apply functions on the current row and all preceding rows.
|
||||||
|
|
||||||
|
**Note:** This mode is an experimental feature. Engineering and on-call support is not available.
|
||||||
|
Documentation is either limited or not provided outside of code comments. No SLA is provided.
|
||||||
|
Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature.
|
||||||
|
Contact Grafana Support to enable this feature in Grafana Cloud.
|
||||||
|
|
||||||
|
- **Total** - Calculates the cumulative total up to and including the current row.
|
||||||
|
- **Mean** - Calculates the mean up to and including the current row.
|
||||||
|
|
||||||
|
- **Window functions** - Apply window functions. The window can either be **trailing** or **centered**.
|
||||||
|
With a trailing window the current row will be the last row in the window.
|
||||||
|
With a centered window the window will be centered on the current row.
|
||||||
|
For even window sizes, the window will be centered between the current row, and the previous row.
|
||||||
|
|
||||||
|
**Note:** This mode is an experimental feature. Engineering and on-call support is not available.
|
||||||
|
Documentation is either limited or not provided outside of code comments. No SLA is provided.
|
||||||
|
Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature.
|
||||||
|
Contact Grafana Support to enable this feature in Grafana Cloud.
|
||||||
|
|
||||||
|
- **Mean** - Calculates the moving mean or running average.
|
||||||
|
- **Stddev** - Calculates the moving standard deviation.
|
||||||
|
- **Variance** - Calculates the moving variance.
|
||||||
|
|
||||||
- **Row index -** Insert a field with the row index.
|
- **Row index -** Insert a field with the row index.
|
||||||
|
|
||||||
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
|
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
|
||||||
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][].
|
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][].
|
||||||
- **Operation -** If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations.
|
- **Operation -** If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations.
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `costManagementUi` | Toggles the display of the cost management ui plugin |
|
| `costManagementUi` | Toggles the display of the cost management ui plugin |
|
||||||
| `managedPluginsInstall` | Install managed plugins directly from plugins catalog |
|
| `managedPluginsInstall` | Install managed plugins directly from plugins catalog |
|
||||||
| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query |
|
| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query |
|
||||||
|
| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation |
|
||||||
| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. |
|
| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. |
|
||||||
| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. |
|
| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. |
|
||||||
| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. |
|
| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. |
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import { mockTransformationsRegistry } from '../../utils/tests/mockTransformatio
|
|||||||
import { ReducerID } from '../fieldReducer';
|
import { ReducerID } from '../fieldReducer';
|
||||||
import { transformDataFrame } from '../transformDataFrame';
|
import { transformDataFrame } from '../transformDataFrame';
|
||||||
|
|
||||||
import { CalculateFieldMode, calculateFieldTransformer, ReduceOptions } from './calculateField';
|
import {
|
||||||
|
CalculateFieldMode,
|
||||||
|
calculateFieldTransformer,
|
||||||
|
ReduceOptions,
|
||||||
|
WindowSizeMode,
|
||||||
|
WindowAlignment,
|
||||||
|
} from './calculateField';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
|
|
||||||
const seriesA = toDataFrame({
|
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 {
|
export enum CalculateFieldMode {
|
||||||
ReduceRow = 'reduceRow',
|
ReduceRow = 'reduceRow',
|
||||||
|
CumulativeFunctions = 'cumulativeFunctions',
|
||||||
|
WindowFunctions = 'windowFunctions',
|
||||||
BinaryOperation = 'binary',
|
BinaryOperation = 'binary',
|
||||||
UnaryOperation = 'unary',
|
UnaryOperation = 'unary',
|
||||||
Index = 'index',
|
Index = 'index',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WindowSizeMode {
|
||||||
|
Percentage = 'percentage',
|
||||||
|
Fixed = 'fixed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WindowAlignment {
|
||||||
|
Trailing = 'trailing',
|
||||||
|
Centered = 'centered',
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReduceOptions {
|
export interface ReduceOptions {
|
||||||
include?: string[]; // Assume all fields
|
include?: string[]; // Assume all fields
|
||||||
reducer: ReducerID;
|
reducer: ReducerID;
|
||||||
nullValueMode?: NullValueMode;
|
nullValueMode?: NullValueMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CumulativeOptions {
|
||||||
|
field?: string;
|
||||||
|
reducer: ReducerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WindowOptions extends CumulativeOptions {
|
||||||
|
windowSize?: number;
|
||||||
|
windowSizeMode?: WindowSizeMode;
|
||||||
|
windowAlignment?: WindowAlignment;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UnaryOptions {
|
export interface UnaryOptions {
|
||||||
operator: UnaryOperationID;
|
operator: UnaryOperationID;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@@ -46,6 +69,13 @@ const defaultReduceOptions: ReduceOptions = {
|
|||||||
reducer: ReducerID.sum,
|
reducer: ReducerID.sum,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultWindowOptions: WindowOptions = {
|
||||||
|
reducer: ReducerID.mean,
|
||||||
|
windowAlignment: WindowAlignment.Trailing,
|
||||||
|
windowSizeMode: WindowSizeMode.Percentage,
|
||||||
|
windowSize: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
const defaultBinaryOptions: BinaryOptions = {
|
const defaultBinaryOptions: BinaryOptions = {
|
||||||
left: '',
|
left: '',
|
||||||
operator: BinaryOperationID.Add,
|
operator: BinaryOperationID.Add,
|
||||||
@@ -64,6 +94,8 @@ export interface CalculateFieldTransformerOptions {
|
|||||||
|
|
||||||
// Only one should be filled
|
// Only one should be filled
|
||||||
reduce?: ReduceOptions;
|
reduce?: ReduceOptions;
|
||||||
|
window?: WindowOptions;
|
||||||
|
cumulative?: CumulativeOptions;
|
||||||
binary?: BinaryOptions;
|
binary?: BinaryOptions;
|
||||||
unary?: UnaryOptions;
|
unary?: UnaryOptions;
|
||||||
index?: IndexOptions;
|
index?: IndexOptions;
|
||||||
@@ -104,39 +136,49 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
|||||||
const mode = options.mode ?? CalculateFieldMode.ReduceRow;
|
const mode = options.mode ?? CalculateFieldMode.ReduceRow;
|
||||||
let creator: ValuesCreator | undefined = undefined;
|
let creator: ValuesCreator | undefined = undefined;
|
||||||
|
|
||||||
if (mode === CalculateFieldMode.ReduceRow) {
|
switch (mode) {
|
||||||
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
case CalculateFieldMode.ReduceRow:
|
||||||
} else if (mode === CalculateFieldMode.UnaryOperation) {
|
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
||||||
creator = getUnaryCreator(defaults(options.unary, defaultUnaryOptions), data);
|
break;
|
||||||
} else if (mode === CalculateFieldMode.BinaryOperation) {
|
case CalculateFieldMode.CumulativeFunctions:
|
||||||
const binaryOptions = {
|
creator = getCumulativeCreator(defaults(options.cumulative, defaultReduceOptions), data);
|
||||||
...options.binary,
|
break;
|
||||||
left: ctx.interpolate(options.binary?.left!),
|
case CalculateFieldMode.WindowFunctions:
|
||||||
right: ctx.interpolate(options.binary?.right!),
|
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);
|
creator = getBinaryCreator(defaults(binaryOptions, defaultBinaryOptions), data);
|
||||||
} else if (mode === CalculateFieldMode.Index) {
|
break;
|
||||||
return data.map((frame) => {
|
case CalculateFieldMode.Index:
|
||||||
const indexArr = [...Array(frame.length).keys()];
|
return data.map((frame) => {
|
||||||
|
const indexArr = [...Array(frame.length).keys()];
|
||||||
|
|
||||||
if (options.index?.asPercentile) {
|
if (options.index?.asPercentile) {
|
||||||
for (let i = 0; i < indexArr.length; i++) {
|
for (let i = 0; i < indexArr.length; i++) {
|
||||||
indexArr[i] = indexArr[i] / indexArr.length;
|
indexArr[i] = indexArr[i] / indexArr.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const f = {
|
const f = {
|
||||||
name: options.alias ?? 'Row',
|
name: options.alias ?? 'Row',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: indexArr,
|
values: indexArr,
|
||||||
config: options.index?.asPercentile ? { unit: 'percentunit' } : {},
|
config: options.index?.asPercentile ? { unit: 'percentunit' } : {},
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...frame,
|
...frame,
|
||||||
fields: options.replaceFields ? [f] : [...frame.fields, f],
|
fields: options.replaceFields ? [f] : [...frame.fields, f],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing configured
|
// 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 {
|
function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): ValuesCreator {
|
||||||
let matcher = getFieldMatcher({
|
let matcher = getFieldMatcher({
|
||||||
id: FieldMatcherID.numeric,
|
id: FieldMatcherID.numeric,
|
||||||
@@ -227,6 +476,7 @@ function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): Va
|
|||||||
for (let j = 0; j < size; j++) {
|
for (let j = 0; j < size; j++) {
|
||||||
row.values[j] = columns[j][i];
|
row.values[j] = columns[j][i];
|
||||||
}
|
}
|
||||||
|
|
||||||
vals.push(reducer(row, ignoreNulls, nullAsZero)[options.reducer]);
|
vals.push(reducer(row, ignoreNulls, nullAsZero)[options.reducer]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +559,16 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (options.mode) {
|
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: {
|
case CalculateFieldMode.UnaryOperation: {
|
||||||
const { unary } = options;
|
const { unary } = options;
|
||||||
return `${unary?.operator ?? ''}${unary?.fieldName ? `(${unary.fieldName})` : ''}`;
|
return `${unary?.operator ?? ''}${unary?.fieldName ? `(${unary.fieldName})` : ''}`;
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export interface FeatureToggles {
|
|||||||
costManagementUi?: boolean;
|
costManagementUi?: boolean;
|
||||||
managedPluginsInstall?: boolean;
|
managedPluginsInstall?: boolean;
|
||||||
prometheusPromQAIL?: boolean;
|
prometheusPromQAIL?: boolean;
|
||||||
|
addFieldFromCalculationStatFunctions?: boolean;
|
||||||
alertmanagerRemoteSecondary?: boolean;
|
alertmanagerRemoteSecondary?: boolean;
|
||||||
alertmanagerRemotePrimary?: boolean;
|
alertmanagerRemotePrimary?: boolean;
|
||||||
alertmanagerRemoteOnly?: boolean;
|
alertmanagerRemoteOnly?: boolean;
|
||||||
|
|||||||
@@ -935,6 +935,13 @@ var (
|
|||||||
FrontendOnly: true,
|
FrontendOnly: true,
|
||||||
Owner: grafanaObservabilityMetricsSquad,
|
Owner: grafanaObservabilityMetricsSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "addFieldFromCalculationStatFunctions",
|
||||||
|
Description: "Add cumulative and window functions to the add field from calculation transformation",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaBiSquad,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "alertmanagerRemoteSecondary",
|
Name: "alertmanagerRemoteSecondary",
|
||||||
Description: "Enable Grafana to sync configuration and state with a remote Alertmanager.",
|
Description: "Enable Grafana to sync configuration and state with a remote Alertmanager.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ pluginsInstrumentationStatusSource,experimental,@grafana/plugins-platform-backen
|
|||||||
costManagementUi,experimental,@grafana/databases-frontend,false,false,false,false
|
costManagementUi,experimental,@grafana/databases-frontend,false,false,false,false
|
||||||
managedPluginsInstall,experimental,@grafana/plugins-platform-backend,false,false,false,false
|
managedPluginsInstall,experimental,@grafana/plugins-platform-backend,false,false,false,false
|
||||||
prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,false,true
|
prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,false,true
|
||||||
|
addFieldFromCalculationStatFunctions,experimental,@grafana/grafana-bi-squad,false,false,false,true
|
||||||
alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false,false
|
alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false,false
|
||||||
alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false,false
|
alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false,false
|
||||||
alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,false
|
alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,false
|
||||||
|
|||||||
|
@@ -539,6 +539,10 @@ const (
|
|||||||
// Prometheus and AI/ML to assist users in creating a query
|
// Prometheus and AI/ML to assist users in creating a query
|
||||||
FlagPrometheusPromQAIL = "prometheusPromQAIL"
|
FlagPrometheusPromQAIL = "prometheusPromQAIL"
|
||||||
|
|
||||||
|
// FlagAddFieldFromCalculationStatFunctions
|
||||||
|
// Add cumulative and window functions to the add field from calculation transformation
|
||||||
|
FlagAddFieldFromCalculationStatFunctions = "addFieldFromCalculationStatFunctions"
|
||||||
|
|
||||||
// FlagAlertmanagerRemoteSecondary
|
// FlagAlertmanagerRemoteSecondary
|
||||||
// Enable Grafana to sync configuration and state with a remote Alertmanager.
|
// Enable Grafana to sync configuration and state with a remote Alertmanager.
|
||||||
FlagAlertmanagerRemoteSecondary = "alertmanagerRemoteSecondary"
|
FlagAlertmanagerRemoteSecondary = "alertmanagerRemoteSecondary"
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ export const transformationDocsContent: TransformationDocsContentType = {
|
|||||||
- **Natural logarithm (ln)** - Returns the natural logarithm of a given expression.
|
- **Natural logarithm (ln)** - Returns the natural logarithm of a given expression.
|
||||||
- **Floor (floor)** - Returns the largest integer less than or equal to a given expression.
|
- **Floor (floor)** - Returns the largest integer less than or equal to a given expression.
|
||||||
- **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression.
|
- **Ceiling (ceil)** - Returns the smallest integer greater than or equal to a given expression.
|
||||||
|
- **Cumulative functions** - Apply functions on the current row and all preceding rows.
|
||||||
|
|
||||||
|
**Note:** This mode is an experimental feature. Engineering and on-call support is not available.
|
||||||
|
Documentation is either limited or not provided outside of code comments. No SLA is provided.
|
||||||
|
Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature.
|
||||||
|
Contact Grafana Support to enable this feature in Grafana Cloud.
|
||||||
|
- **Total** - Calculates the cumulative total up to and including the current row.
|
||||||
|
- **Mean** - Calculates the mean up to and including the current row.
|
||||||
|
- **Window functions** - Apply window functions. The window can either be **trailing** or **centered**.
|
||||||
|
With a trailing window the current row will be the last row in the window.
|
||||||
|
With a centered window the window will be centered on the current row.
|
||||||
|
For even window sizes, the window will be centered between the current row, and the previous row.
|
||||||
|
|
||||||
|
**Note:** This mode is an experimental feature. Engineering and on-call support is not available.
|
||||||
|
Documentation is either limited or not provided outside of code comments. No SLA is provided.
|
||||||
|
Enable the 'addFieldFromCalculationStatFunctions' in Grafana to use this feature.
|
||||||
|
Contact Grafana Support to enable this feature in Grafana Cloud.
|
||||||
|
- **Mean** - Calculates the moving mean or running average.
|
||||||
|
- **Stddev** - Calculates the moving standard deviation.
|
||||||
|
- **Variance** - Calculates the moving variance.
|
||||||
- **Row index -** Insert a field with the row index.
|
- **Row index -** Insert a field with the row index.
|
||||||
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
|
- **Field name -** Select the names of fields you want to use in the calculation for the new field.
|
||||||
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][].
|
- **Calculation -** If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][].
|
||||||
|
|||||||
@@ -24,10 +24,15 @@ import {
|
|||||||
BinaryOptions,
|
BinaryOptions,
|
||||||
UnaryOptions,
|
UnaryOptions,
|
||||||
CalculateFieldMode,
|
CalculateFieldMode,
|
||||||
|
WindowAlignment,
|
||||||
CalculateFieldTransformerOptions,
|
CalculateFieldTransformerOptions,
|
||||||
getNameFromOptions,
|
getNameFromOptions,
|
||||||
IndexOptions,
|
IndexOptions,
|
||||||
ReduceOptions,
|
ReduceOptions,
|
||||||
|
CumulativeOptions,
|
||||||
|
WindowOptions,
|
||||||
|
WindowSizeMode,
|
||||||
|
defaultWindowOptions,
|
||||||
} from '@grafana/data/src/transformations/transformers/calculateField';
|
} from '@grafana/data/src/transformations/transformers/calculateField';
|
||||||
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
|
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
@@ -38,9 +43,11 @@ import {
|
|||||||
InlineLabel,
|
InlineLabel,
|
||||||
InlineSwitch,
|
InlineSwitch,
|
||||||
Input,
|
Input,
|
||||||
|
RadioButtonGroup,
|
||||||
Select,
|
Select,
|
||||||
StatsPicker,
|
StatsPicker,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||||
|
|
||||||
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
|
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
|
||||||
|
|
||||||
@@ -57,6 +64,13 @@ const calculationModes = [
|
|||||||
{ value: CalculateFieldMode.Index, label: 'Row index' },
|
{ value: CalculateFieldMode.Index, label: 'Row index' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (cfg.featureToggles.addFieldFromCalculationStatFunctions) {
|
||||||
|
calculationModes.push(
|
||||||
|
{ value: CalculateFieldMode.CumulativeFunctions, label: 'Cumulative functions' },
|
||||||
|
{ value: CalculateFieldMode.WindowFunctions, label: 'Window functions' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const okTypes = new Set<FieldType>([FieldType.time, FieldType.number, FieldType.string]);
|
const okTypes = new Set<FieldType>([FieldType.time, FieldType.number, FieldType.string]);
|
||||||
|
|
||||||
const labelWidth = 16;
|
const labelWidth = 16;
|
||||||
@@ -188,6 +202,9 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
|||||||
onModeChanged = (value: SelectableValue<CalculateFieldMode>) => {
|
onModeChanged = (value: SelectableValue<CalculateFieldMode>) => {
|
||||||
const { options, onChange } = this.props;
|
const { options, onChange } = this.props;
|
||||||
const mode = value.value ?? CalculateFieldMode.BinaryOperation;
|
const mode = value.value ?? CalculateFieldMode.BinaryOperation;
|
||||||
|
if (mode === CalculateFieldMode.WindowFunctions) {
|
||||||
|
options.window = options.window ?? defaultWindowOptions;
|
||||||
|
}
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
mode,
|
mode,
|
||||||
@@ -203,14 +220,13 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
|||||||
};
|
};
|
||||||
|
|
||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
// Reduce by Row
|
// Cumulative functions
|
||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
|
|
||||||
updateReduceOptions = (v: ReduceOptions) => {
|
updateReduceOptions = (v: ReduceOptions) => {
|
||||||
const { options, onChange } = this.props;
|
const { options, onChange } = this.props;
|
||||||
onChange({
|
onChange({
|
||||||
...options,
|
...options,
|
||||||
mode: CalculateFieldMode.ReduceRow,
|
|
||||||
reduce: v,
|
reduce: v,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -250,6 +266,149 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------
|
||||||
|
// Window functions
|
||||||
|
//---------------------------------------------------------
|
||||||
|
|
||||||
|
updateWindowOptions = (v: WindowOptions) => {
|
||||||
|
const { options, onChange } = this.props;
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
mode: CalculateFieldMode.WindowFunctions,
|
||||||
|
window: v,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onWindowFieldChange = (v: SelectableValue<string>) => {
|
||||||
|
const { window } = this.props.options;
|
||||||
|
this.updateWindowOptions({
|
||||||
|
...window!,
|
||||||
|
field: v.value!,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onWindowSizeChange = (v?: number) => {
|
||||||
|
const { window } = this.props.options;
|
||||||
|
this.updateWindowOptions({
|
||||||
|
...window!,
|
||||||
|
windowSize: v && window?.windowSizeMode === WindowSizeMode.Percentage ? v / 100 : v,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onWindowSizeModeChange = (val: string) => {
|
||||||
|
const { window } = this.props.options;
|
||||||
|
const mode = val as WindowSizeMode;
|
||||||
|
this.updateWindowOptions({
|
||||||
|
...window!,
|
||||||
|
windowSize: window?.windowSize
|
||||||
|
? mode === WindowSizeMode.Percentage
|
||||||
|
? window!.windowSize! / 100
|
||||||
|
: window!.windowSize! * 100
|
||||||
|
: undefined,
|
||||||
|
windowSizeMode: mode,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onWindowStatsChange = (stats: string[]) => {
|
||||||
|
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
|
||||||
|
|
||||||
|
const { window } = this.props.options;
|
||||||
|
this.updateWindowOptions({ ...window, reducer });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTypeChange = (val: string) => {
|
||||||
|
const { window } = this.props.options;
|
||||||
|
this.updateWindowOptions({
|
||||||
|
...window!,
|
||||||
|
windowAlignment: val as WindowAlignment,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWindowFunctions(options?: WindowOptions) {
|
||||||
|
const { names } = this.state;
|
||||||
|
options = defaults(options, { reducer: ReducerID.sum });
|
||||||
|
const selectOptions = names.map((v) => ({ label: v, value: v }));
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: 'Trailing', value: WindowAlignment.Trailing },
|
||||||
|
{ label: 'Centered', value: WindowAlignment.Centered },
|
||||||
|
];
|
||||||
|
const windowSizeModeOptions = [
|
||||||
|
{ label: 'Percentage', value: WindowSizeMode.Percentage },
|
||||||
|
{ label: 'Fixed', value: WindowSizeMode.Fixed },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InlineField label="Field" labelWidth={labelWidth}>
|
||||||
|
<Select
|
||||||
|
placeholder="Field"
|
||||||
|
options={selectOptions}
|
||||||
|
className="min-width-18"
|
||||||
|
value={options?.field}
|
||||||
|
onChange={this.onWindowFieldChange}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
<InlineField label="Calculation" labelWidth={labelWidth}>
|
||||||
|
<StatsPicker
|
||||||
|
allowMultiple={false}
|
||||||
|
className="width-18"
|
||||||
|
stats={[options.reducer]}
|
||||||
|
onChange={this.onWindowStatsChange}
|
||||||
|
defaultStat={ReducerID.mean}
|
||||||
|
filterOptions={(ext) =>
|
||||||
|
ext.id === ReducerID.mean || ext.id === ReducerID.variance || ext.id === ReducerID.stdDev
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
<InlineField label="Type" labelWidth={labelWidth}>
|
||||||
|
<RadioButtonGroup
|
||||||
|
value={options.windowAlignment ?? WindowAlignment.Trailing}
|
||||||
|
options={typeOptions}
|
||||||
|
onChange={this.onTypeChange}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
<InlineField label="Window size mode">
|
||||||
|
<RadioButtonGroup
|
||||||
|
value={options.windowSizeMode ?? WindowSizeMode.Percentage}
|
||||||
|
options={windowSizeModeOptions}
|
||||||
|
onChange={this.onWindowSizeModeChange}
|
||||||
|
></RadioButtonGroup>
|
||||||
|
</InlineField>
|
||||||
|
<InlineField
|
||||||
|
label={options.windowSizeMode === WindowSizeMode.Percentage ? 'Window size %' : 'Window size'}
|
||||||
|
labelWidth={labelWidth}
|
||||||
|
tooltip={
|
||||||
|
options.windowSizeMode === WindowSizeMode.Percentage
|
||||||
|
? 'Set the window size as a percentage of the total data'
|
||||||
|
: 'Window size'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NumberInput
|
||||||
|
placeholder="Auto"
|
||||||
|
min={0.1}
|
||||||
|
value={
|
||||||
|
options.windowSize && options.windowSizeMode === WindowSizeMode.Percentage
|
||||||
|
? options.windowSize * 100
|
||||||
|
: options.windowSize
|
||||||
|
}
|
||||||
|
onChange={this.onWindowSizeChange}
|
||||||
|
></NumberInput>
|
||||||
|
</InlineField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------
|
||||||
|
// Reduce by Row
|
||||||
|
//---------------------------------------------------------
|
||||||
|
|
||||||
|
onReducerStatsChange = (stats: string[]) => {
|
||||||
|
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
|
||||||
|
|
||||||
|
const { reduce } = this.props.options;
|
||||||
|
this.updateReduceOptions({ ...reduce, reducer });
|
||||||
|
};
|
||||||
|
|
||||||
renderReduceRow(options?: ReduceOptions) {
|
renderReduceRow(options?: ReduceOptions) {
|
||||||
const { names, selected } = this.state;
|
const { names, selected } = this.state;
|
||||||
options = defaults(options, { reducer: ReducerID.sum });
|
options = defaults(options, { reducer: ReducerID.sum });
|
||||||
@@ -285,6 +444,64 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------
|
||||||
|
// Cumulative Operator
|
||||||
|
//---------------------------------------------------------
|
||||||
|
|
||||||
|
onCumulativeStatsChange = (stats: string[]) => {
|
||||||
|
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
|
||||||
|
|
||||||
|
const { reduce } = this.props.options;
|
||||||
|
this.updateCumulativeOptions({ ...reduce, reducer });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCumulativeOptions = (v: CumulativeOptions) => {
|
||||||
|
const { options, onChange } = this.props;
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
mode: CalculateFieldMode.CumulativeFunctions,
|
||||||
|
cumulative: v,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onCumulativeFieldChange = (v: SelectableValue<string>) => {
|
||||||
|
const { cumulative } = this.props.options;
|
||||||
|
this.updateCumulativeOptions({
|
||||||
|
...cumulative!,
|
||||||
|
field: v.value!,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderCumulativeFunctions(options?: CumulativeOptions) {
|
||||||
|
const { names } = this.state;
|
||||||
|
options = defaults(options, { reducer: ReducerID.sum });
|
||||||
|
const selectOptions = names.map((v) => ({ label: v, value: v }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InlineField label="Field" labelWidth={labelWidth}>
|
||||||
|
<Select
|
||||||
|
placeholder="Field"
|
||||||
|
options={selectOptions}
|
||||||
|
className="min-width-18"
|
||||||
|
value={options?.field}
|
||||||
|
onChange={this.onCumulativeFieldChange}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
<InlineField label="Calculation" labelWidth={labelWidth}>
|
||||||
|
<StatsPicker
|
||||||
|
allowMultiple={false}
|
||||||
|
className="width-18"
|
||||||
|
stats={[options.reducer]}
|
||||||
|
onChange={this.onCumulativeStatsChange}
|
||||||
|
defaultStat={ReducerID.sum}
|
||||||
|
filterOptions={(ext) => ext.id === ReducerID.sum || ext.id === ReducerID.mean}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
// Binary Operator
|
// Binary Operator
|
||||||
//---------------------------------------------------------
|
//---------------------------------------------------------
|
||||||
@@ -468,6 +685,8 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
|||||||
{mode === CalculateFieldMode.BinaryOperation && this.renderBinaryOperation(options.binary)}
|
{mode === CalculateFieldMode.BinaryOperation && this.renderBinaryOperation(options.binary)}
|
||||||
{mode === CalculateFieldMode.UnaryOperation && this.renderUnaryOperation(options.unary)}
|
{mode === CalculateFieldMode.UnaryOperation && this.renderUnaryOperation(options.unary)}
|
||||||
{mode === CalculateFieldMode.ReduceRow && this.renderReduceRow(options.reduce)}
|
{mode === CalculateFieldMode.ReduceRow && this.renderReduceRow(options.reduce)}
|
||||||
|
{mode === CalculateFieldMode.CumulativeFunctions && this.renderCumulativeFunctions(options.cumulative)}
|
||||||
|
{mode === CalculateFieldMode.WindowFunctions && this.renderWindowFunctions(options.window)}
|
||||||
{mode === CalculateFieldMode.Index && this.renderRowIndex(options.index)}
|
{mode === CalculateFieldMode.Index && this.renderRowIndex(options.index)}
|
||||||
<InlineField labelWidth={labelWidth} label="Alias">
|
<InlineField labelWidth={labelWidth} label="Alias">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
Reference in New Issue
Block a user