[Lens] Formula overall functions (#99461) (#102303)

Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
Kibana Machine 2021-06-16 06:57:24 -04:00 committed by GitHub
parent 9647af5476
commit 658d6378c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 970 additions and 0 deletions

View file

@ -21,6 +21,7 @@ export interface ExpressionFunctionDefinitions
| [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | <code>ExpressionFunctionDerivative</code> | |
| [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | <code>ExpressionFunctionFont</code> | |
| [moving\_average](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.moving_average.md) | <code>ExpressionFunctionMovingAverage</code> | |
| [overall\_metric](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md) | <code>ExpressionFunctionOverallMetric</code> | |
| [theme](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.theme.md) | <code>ExpressionFunctionTheme</code> | |
| [var\_set](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var_set.md) | <code>ExpressionFunctionVarSet</code> | |
| [var](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.var.md) | <code>ExpressionFunctionVar</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) &gt; [overall\_metric](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.overall_metric.md)
## ExpressionFunctionDefinitions.overall\_metric property
<b>Signature:</b>
```typescript
overall_metric: ExpressionFunctionOverallMetric;
```

View file

@ -21,6 +21,7 @@ export interface ExpressionFunctionDefinitions
| [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | <code>ExpressionFunctionDerivative</code> | |
| [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | <code>ExpressionFunctionFont</code> | |
| [moving\_average](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.moving_average.md) | <code>ExpressionFunctionMovingAverage</code> | |
| [overall\_metric](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md) | <code>ExpressionFunctionOverallMetric</code> | |
| [theme](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.theme.md) | <code>ExpressionFunctionTheme</code> | |
| [var\_set](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var_set.md) | <code>ExpressionFunctionVarSet</code> | |
| [var](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.var.md) | <code>ExpressionFunctionVar</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) &gt; [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) &gt; [overall\_metric](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.overall_metric.md)
## ExpressionFunctionDefinitions.overall\_metric property
<b>Signature:</b>
```typescript
overall_metric: ExpressionFunctionOverallMetric;
```

View file

@ -12,6 +12,7 @@ export * from './var_set';
export * from './var';
export * from './theme';
export * from './cumulative_sum';
export * from './overall_metric';
export * from './derivative';
export * from './moving_average';
export * from './ui_setting';

View file

@ -0,0 +1,168 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../types';
import { Datatable } from '../../expression_types';
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
export interface OverallMetricArgs {
by?: string[];
inputColumnId: string;
outputColumnId: string;
outputColumnName?: string;
metric: 'sum' | 'min' | 'max' | 'average';
}
export type ExpressionFunctionOverallMetric = ExpressionFunctionDefinition<
'overall_metric',
Datatable,
OverallMetricArgs,
Datatable
>;
function getValueAsNumberArray(value: unknown) {
if (Array.isArray(value)) {
return value.map((innerVal) => Number(innerVal));
} else {
return [Number(value)];
}
}
/**
* Calculates the overall metric of a specified column in the data table.
*
* Also supports multiple series in a single data table - use the `by` argument
* to specify the columns to split the calculation by.
* For each unique combination of all `by` columns a separate overall metric will be calculated.
* The order of rows won't be changed - this function is not modifying any existing columns, it's only
* adding the specified `outputColumnId` column to every row of the table without adding or removing rows.
*
* Behavior:
* * Will write the overall metric of `inputColumnId` into `outputColumnId`
* * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
* * Each cell will contain the calculated metric based on the values of all cells belonging to the current series.
*
* Edge cases:
* * Will return the input table if `inputColumnId` does not exist
* * Will throw an error if `outputColumnId` exists already in provided data table
* * If the row value contains `null` or `undefined`, it will be ignored and overwritten with the overall metric of
* all cells of the same series.
* * For all values besides `null` and `undefined`, the value will be cast to a number before it's added to the
* overall metric of the current series - if this results in `NaN` (like in case of objects), all cells of the
* current series will be set to `NaN`.
* * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings
* before comparison. If the values are objects, the return value of their `toString` method will be used for comparison.
* Missing values (`null` and `undefined`) will be treated as empty strings.
*/
export const overallMetric: ExpressionFunctionOverallMetric = {
name: 'overall_metric',
type: 'datatable',
inputTypes: ['datatable'],
help: i18n.translate('expressions.functions.overallMetric.help', {
defaultMessage: 'Calculates the overall sum, min, max or average of a column in a data table',
}),
args: {
by: {
help: i18n.translate('expressions.functions.overallMetric.args.byHelpText', {
defaultMessage: 'Column to split the overall calculation by',
}),
multi: true,
types: ['string'],
required: false,
},
metric: {
help: i18n.translate('expressions.functions.overallMetric.metricHelpText', {
defaultMessage: 'Metric to calculate',
}),
types: ['string'],
options: ['sum', 'min', 'max', 'average'],
},
inputColumnId: {
help: i18n.translate('expressions.functions.overallMetric.args.inputColumnIdHelpText', {
defaultMessage: 'Column to calculate the overall metric of',
}),
types: ['string'],
required: true,
},
outputColumnId: {
help: i18n.translate('expressions.functions.overallMetric.args.outputColumnIdHelpText', {
defaultMessage: 'Column to store the resulting overall metric in',
}),
types: ['string'],
required: true,
},
outputColumnName: {
help: i18n.translate('expressions.functions.overallMetric.args.outputColumnNameHelpText', {
defaultMessage: 'Name of the column to store the resulting overall metric in',
}),
types: ['string'],
required: false,
},
},
fn(input, { by, inputColumnId, outputColumnId, outputColumnName, metric }) {
const resultColumns = buildResultColumns(
input,
outputColumnId,
inputColumnId,
outputColumnName
);
if (!resultColumns) {
return input;
}
const accumulators: Partial<Record<string, number>> = {};
const valueCounter: Partial<Record<string, number>> = {};
input.rows.forEach((row) => {
const bucketIdentifier = getBucketIdentifier(row, by);
const accumulatorValue = accumulators[bucketIdentifier] ?? 0;
const currentValue = row[inputColumnId];
if (currentValue != null) {
const currentNumberValues = getValueAsNumberArray(currentValue);
switch (metric) {
case 'average':
valueCounter[bucketIdentifier] =
(valueCounter[bucketIdentifier] ?? 0) + currentNumberValues.length;
case 'sum':
accumulators[bucketIdentifier] =
accumulatorValue + currentNumberValues.reduce((a, b) => a + b, 0);
break;
case 'min':
accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues);
break;
case 'max':
accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues);
break;
}
}
});
if (metric === 'average') {
Object.keys(accumulators).forEach((bucketIdentifier) => {
accumulators[bucketIdentifier] =
accumulators[bucketIdentifier]! / valueCounter[bucketIdentifier]!;
});
}
return {
...input,
columns: resultColumns,
rows: input.rows.map((row) => {
const newRow = { ...row };
const bucketIdentifier = getBucketIdentifier(row, by);
newRow[outputColumnId] = accumulators[bucketIdentifier];
return newRow;
}),
};
},
};

View file

@ -0,0 +1,450 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { functionWrapper } from './utils';
import { ExecutionContext } from '../../../execution/types';
import { Datatable } from '../../../expression_types/specs/datatable';
import { overallMetric, OverallMetricArgs } from '../overall_metric';
describe('interpreter/functions#overall_metric', () => {
const fn = functionWrapper(overallMetric);
const runFn = (input: Datatable, args: OverallMetricArgs) =>
fn(input, args, {} as ExecutionContext) as Datatable;
it('calculates overall sum', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [{ val: 5 }, { val: 7 }, { val: 3 }, { val: 2 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' }
);
expect(result.columns).toContainEqual({
id: 'output',
name: 'output',
meta: { type: 'number' },
});
expect(result.rows.map((row) => row.output)).toEqual([17, 17, 17, 17]);
});
it('ignores null or undefined', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [{}, { val: null }, { val: undefined }, { val: 1 }, { val: 5 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'average' }
);
expect(result.columns).toContainEqual({
id: 'output',
name: 'output',
meta: { type: 'number' },
});
expect(result.rows.map((row) => row.output)).toEqual([3, 3, 3, 3, 3]);
});
it('calculates overall sum for multiple series', () => {
const result = runFn(
{
type: 'datatable',
columns: [
{ id: 'val', name: 'val', meta: { type: 'number' } },
{ id: 'split', name: 'split', meta: { type: 'string' } },
],
rows: [
{ val: 1, split: 'A' },
{ val: 2, split: 'B' },
{ val: 3, split: 'B' },
{ val: 4, split: 'A' },
{ val: 5, split: 'A' },
{ val: 6, split: 'A' },
{ val: 7, split: 'B' },
{ val: 8, split: 'B' },
],
},
{ inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' }
);
expect(result.rows.map((row) => row.output)).toEqual([
1 + 4 + 5 + 6,
2 + 3 + 7 + 8,
2 + 3 + 7 + 8,
1 + 4 + 5 + 6,
1 + 4 + 5 + 6,
1 + 4 + 5 + 6,
2 + 3 + 7 + 8,
2 + 3 + 7 + 8,
]);
});
it('treats missing split column as separate series', () => {
const result = runFn(
{
type: 'datatable',
columns: [
{ id: 'val', name: 'val', meta: { type: 'number' } },
{ id: 'split', name: 'split', meta: { type: 'string' } },
],
rows: [
{ val: 1, split: 'A' },
{ val: 2, split: 'B' },
{ val: 3 },
{ val: 4, split: 'A' },
{ val: 5 },
{ val: 6, split: 'A' },
{ val: 7, split: 'B' },
{ val: 8, split: 'B' },
],
},
{ inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' }
);
expect(result.rows.map((row) => row.output)).toEqual([
1 + 4 + 6,
2 + 7 + 8,
3 + 5,
1 + 4 + 6,
3 + 5,
1 + 4 + 6,
2 + 7 + 8,
2 + 7 + 8,
]);
});
it('treats null like undefined and empty string for split columns', () => {
const table: Datatable = {
type: 'datatable',
columns: [
{ id: 'val', name: 'val', meta: { type: 'number' } },
{ id: 'split', name: 'split', meta: { type: 'string' } },
],
rows: [
{ val: 1, split: 'A' },
{ val: 2, split: 'B' },
{ val: 3 },
{ val: 4, split: 'A' },
{ val: 5 },
{ val: 6, split: 'A' },
{ val: 7, split: null },
{ val: 8, split: 'B' },
{ val: 9, split: '' },
],
};
const result = runFn(table, {
inputColumnId: 'val',
outputColumnId: 'output',
by: ['split'],
metric: 'sum',
});
expect(result.rows.map((row) => row.output)).toEqual([
1 + 4 + 6,
2 + 8,
3 + 5 + 7 + 9,
1 + 4 + 6,
3 + 5 + 7 + 9,
1 + 4 + 6,
3 + 5 + 7 + 9,
2 + 8,
3 + 5 + 7 + 9,
]);
const result2 = runFn(table, {
inputColumnId: 'val',
outputColumnId: 'output',
by: ['split'],
metric: 'max',
});
expect(result2.rows.map((row) => row.output)).toEqual([6, 8, 9, 6, 9, 6, 9, 8, 9]);
});
it('handles array values', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [{ val: 5 }, { val: [7, 10] }, { val: [3, 1] }, { val: 2 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' }
);
expect(result.columns).toContainEqual({
id: 'output',
name: 'output',
meta: { type: 'number' },
});
expect(result.rows.map((row) => row.output)).toEqual([28, 28, 28, 28]);
});
it('takes array values into account for average calculation', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [{ val: [3, 4] }, { val: 2 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'average' }
);
expect(result.columns).toContainEqual({
id: 'output',
name: 'output',
meta: { type: 'number' },
});
expect(result.rows.map((row) => row.output)).toEqual([3, 3]);
});
it('handles array values for split columns', () => {
const table: Datatable = {
type: 'datatable',
columns: [
{ id: 'val', name: 'val', meta: { type: 'number' } },
{ id: 'split', name: 'split', meta: { type: 'string' } },
],
rows: [
{ val: 1, split: 'A' },
{ val: [2, 11], split: 'B' },
{ val: 3 },
{ val: 4, split: 'A' },
{ val: 5 },
{ val: 6, split: 'A' },
{ val: 7, split: null },
{ val: 8, split: 'B' },
{ val: [9, 99], split: '' },
],
};
const result = runFn(table, {
inputColumnId: 'val',
outputColumnId: 'output',
by: ['split'],
metric: 'sum',
});
expect(result.rows.map((row) => row.output)).toEqual([
1 + 4 + 6,
2 + 11 + 8,
3 + 5 + 7 + 9 + 99,
1 + 4 + 6,
3 + 5 + 7 + 9 + 99,
1 + 4 + 6,
3 + 5 + 7 + 9 + 99,
2 + 11 + 8,
3 + 5 + 7 + 9 + 99,
]);
const result2 = runFn(table, {
inputColumnId: 'val',
outputColumnId: 'output',
by: ['split'],
metric: 'max',
});
expect(result2.rows.map((row) => row.output)).toEqual([6, 11, 99, 6, 99, 6, 99, 11, 99]);
});
it('calculates cumulative sum for multiple series by multiple split columns', () => {
const result = runFn(
{
type: 'datatable',
columns: [
{ id: 'val', name: 'val', meta: { type: 'number' } },
{ id: 'split', name: 'split', meta: { type: 'string' } },
{ id: 'split2', name: 'split2', meta: { type: 'string' } },
],
rows: [
{ val: 1, split: 'A', split2: 'C' },
{ val: 2, split: 'B', split2: 'C' },
{ val: 3, split2: 'C' },
{ val: 4, split: 'A', split2: 'C' },
{ val: 5 },
{ val: 6, split: 'A', split2: 'D' },
{ val: 7, split: 'B', split2: 'D' },
{ val: 8, split: 'B', split2: 'D' },
],
},
{ inputColumnId: 'val', outputColumnId: 'output', by: ['split', 'split2'], metric: 'sum' }
);
expect(result.rows.map((row) => row.output)).toEqual([1 + 4, 2, 3, 1 + 4, 5, 6, 7 + 8, 7 + 8]);
});
it('splits separate series by the string representation of the cell values', () => {
const result = runFn(
{
type: 'datatable',
columns: [
{ id: 'val', name: 'val', meta: { type: 'number' } },
{ id: 'split', name: 'split', meta: { type: 'string' } },
],
rows: [
{ val: 1, split: { anObj: 3 } },
{ val: 2, split: { anotherObj: 5 } },
{ val: 10, split: 5 },
{ val: 11, split: '5' },
],
},
{ inputColumnId: 'val', outputColumnId: 'output', by: ['split'], metric: 'sum' }
);
expect(result.rows.map((row) => row.output)).toEqual([1 + 2, 1 + 2, 10 + 11, 10 + 11]);
});
it('casts values to number before calculating cumulative sum', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [{ val: 5 }, { val: '7' }, { val: '3' }, { val: 2 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'max' }
);
expect(result.rows.map((row) => row.output)).toEqual([7, 7, 7, 7]);
});
it('casts values to number before calculating metric for NaN like values', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [{ val: 5 }, { val: '7' }, { val: {} }, { val: 2 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'min' }
);
expect(result.rows.map((row) => row.output)).toEqual([NaN, NaN, NaN, NaN]);
});
it('skips undefined and null values', () => {
const result = runFn(
{
type: 'datatable',
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
rows: [
{ val: null },
{ val: 7 },
{ val: undefined },
{ val: undefined },
{ val: undefined },
{ val: undefined },
{ val: '3' },
{ val: 2 },
{ val: null },
],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'average' }
);
expect(result.rows.map((row) => row.output)).toEqual([4, 4, 4, 4, 4, 4, 4, 4, 4]);
});
it('copies over meta information from the source column', () => {
const result = runFn(
{
type: 'datatable',
columns: [
{
id: 'val',
name: 'val',
meta: {
type: 'number',
field: 'afield',
index: 'anindex',
params: { id: 'number', params: { pattern: '000' } },
source: 'synthetic',
sourceParams: {
some: 'params',
},
},
},
],
rows: [{ val: 5 }],
},
{ inputColumnId: 'val', outputColumnId: 'output', metric: 'sum' }
);
expect(result.columns).toContainEqual({
id: 'output',
name: 'output',
meta: {
type: 'number',
field: 'afield',
index: 'anindex',
params: { id: 'number', params: { pattern: '000' } },
source: 'synthetic',
sourceParams: {
some: 'params',
},
},
});
});
it('sets output name on output column if specified', () => {
const result = runFn(
{
type: 'datatable',
columns: [
{
id: 'val',
name: 'val',
meta: {
type: 'number',
},
},
],
rows: [{ val: 5 }],
},
{
inputColumnId: 'val',
outputColumnId: 'output',
outputColumnName: 'Output name',
metric: 'min',
}
);
expect(result.columns).toContainEqual({
id: 'output',
name: 'Output name',
meta: { type: 'number' },
});
});
it('returns source table if input column does not exist', () => {
const input: Datatable = {
type: 'datatable',
columns: [
{
id: 'val',
name: 'val',
meta: {
type: 'number',
},
},
],
rows: [{ val: 5 }],
};
expect(
runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' })
).toBe(input);
});
it('throws an error if output column exists already', () => {
expect(() =>
runFn(
{
type: 'datatable',
columns: [
{
id: 'val',
name: 'val',
meta: {
type: 'number',
},
},
],
rows: [{ val: 5 }],
},
{ inputColumnId: 'val', outputColumnId: 'val', metric: 'max' }
)
).toThrow();
});
});

View file

@ -18,6 +18,7 @@ import {
ExpressionFunctionCumulativeSum,
ExpressionFunctionDerivative,
ExpressionFunctionMovingAverage,
ExpressionFunctionOverallMetric,
} from './specs';
import { ExpressionAstFunction } from '../ast';
import { PersistableStateDefinition } from '../../../kibana_utils/common';
@ -119,6 +120,7 @@ export interface ExpressionFunctionDefinitions {
var: ExpressionFunctionVar;
theme: ExpressionFunctionTheme;
cumulative_sum: ExpressionFunctionCumulativeSum;
overall_metric: ExpressionFunctionOverallMetric;
derivative: ExpressionFunctionDerivative;
moving_average: ExpressionFunctionMovingAverage;
}

View file

@ -29,6 +29,7 @@ import {
derivative,
movingAverage,
mapColumn,
overallMetric,
math,
} from '../expression_functions';
@ -340,6 +341,7 @@ export class ExpressionsService implements PersistableStateService<ExpressionAst
cumulativeSum,
derivative,
movingAverage,
overallMetric,
mapColumn,
math,
]) {

View file

@ -400,6 +400,10 @@ export interface ExpressionFunctionDefinitions {
//
// (undocumented)
moving_average: ExpressionFunctionMovingAverage;
// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionOverallMetric" needs to be exported by the entry point index.d.ts
//
// (undocumented)
overall_metric: ExpressionFunctionOverallMetric;
// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionTheme" needs to be exported by the entry point index.d.ts
//
// (undocumented)

View file

@ -372,6 +372,10 @@ export interface ExpressionFunctionDefinitions {
//
// (undocumented)
moving_average: ExpressionFunctionMovingAverage;
// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionOverallMetric" needs to be exported by the entry point index.d.ts
//
// (undocumented)
overall_metric: ExpressionFunctionOverallMetric;
// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionTheme" needs to be exported by the entry point index.d.ts
//
// (undocumented)

View file

@ -146,6 +146,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
const possibleOperations = useMemo(() => {
return Object.values(operationDefinitionMap)
.filter(({ hidden }) => !hidden)
.filter(
(operationDefinition) =>
!('selectionStyle' in operationDefinition) ||
operationDefinition.selectionStyle !== 'hidden'
)
.filter(({ type }) => fieldByOperation[type]?.size || operationWithoutField.has(type))
.sort((op1, op2) => {
return op1.displayName.localeCompare(op2.displayName);

View file

@ -9,3 +9,13 @@ export { counterRateOperation, CounterRateIndexPatternColumn } from './counter_r
export { cumulativeSumOperation, CumulativeSumIndexPatternColumn } from './cumulative_sum';
export { derivativeOperation, DerivativeIndexPatternColumn } from './differences';
export { movingAverageOperation, MovingAverageIndexPatternColumn } from './moving_average';
export {
overallSumOperation,
OverallSumIndexPatternColumn,
overallMinOperation,
OverallMinIndexPatternColumn,
overallMaxOperation,
OverallMaxIndexPatternColumn,
overallAverageOperation,
OverallAverageIndexPatternColumn,
} from './overall_metric';

View file

@ -0,0 +1,224 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { FormattedIndexPatternColumn, ReferenceBasedIndexPatternColumn } from '../column_types';
import { optionallHistogramBasedOperationToExpression } from './utils';
import { OperationDefinition } from '..';
import { getFormatFromPreviousColumn } from '../helpers';
type OverallMetricIndexPatternColumn<T extends string> = FormattedIndexPatternColumn &
ReferenceBasedIndexPatternColumn & {
operationType: T;
};
export type OverallSumIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_sum'>;
export type OverallMinIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_min'>;
export type OverallMaxIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_max'>;
export type OverallAverageIndexPatternColumn = OverallMetricIndexPatternColumn<'overall_average'>;
function buildOverallMetricOperation<T extends OverallMetricIndexPatternColumn<string>>({
type,
displayName,
ofName,
description,
metric,
}: {
type: T['operationType'];
displayName: string;
ofName: (name?: string) => string;
description: string;
metric: string;
}): OperationDefinition<T, 'fullReference'> {
return {
type,
priority: 1,
displayName,
input: 'fullReference',
selectionStyle: 'hidden',
requiredReferences: [
{
input: ['field', 'managedReference', 'fullReference'],
validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed,
},
],
getPossibleOperation: () => {
return {
dataType: 'number',
isBucketed: false,
scale: 'ratio',
};
},
getDefaultLabel: (column, indexPattern, columns) => {
const ref = columns[column.references[0]];
return ofName(
ref && 'sourceField' in ref
? indexPattern.getFieldByName(ref.sourceField)?.displayName
: undefined
);
},
toExpression: (layer, columnId) => {
return optionallHistogramBasedOperationToExpression(layer, columnId, 'overall_metric', {
metric: [metric],
});
},
buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }, columnParams) => {
const ref = layer.columns[referenceIds[0]];
return {
label: ofName(
ref && 'sourceField' in ref
? indexPattern.getFieldByName(ref.sourceField)?.displayName
: undefined
),
dataType: 'number',
operationType: 'overall_sum',
isBucketed: false,
scale: 'ratio',
references: referenceIds,
params: getFormatFromPreviousColumn(previousColumn),
} as T;
},
isTransferable: () => {
return true;
},
filterable: false,
shiftable: false,
documentation: {
section: 'calculation',
signature: i18n.translate('xpack.lens.indexPattern.overall_metric', {
defaultMessage: 'metric: number',
}),
description,
},
};
}
export const overallSumOperation = buildOverallMetricOperation<OverallSumIndexPatternColumn>({
type: 'overall_sum',
displayName: i18n.translate('xpack.lens.indexPattern.overallSum', {
defaultMessage: 'Overall sum',
}),
ofName: (name?: string) => {
return i18n.translate('xpack.lens.indexPattern.overallSumOf', {
defaultMessage: 'Overall sum of {name}',
values: {
name:
name ??
i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
defaultMessage: '(incomplete)',
}),
},
});
},
metric: 'sum',
description: i18n.translate('xpack.lens.indexPattern.overall_sum.documentation', {
defaultMessage: `
Calculates the sum of a metric of all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
Other dimensions breaking down the data like top values or filter are treated as separate series.
If no date histograms or interval functions are used in the current chart, \`overall_sum\` is calculating the sum over all dimensions no matter the used function.
Example: Percentage of total
\`sum(bytes) / overall_sum(sum(bytes))\`
`,
}),
});
export const overallMinOperation = buildOverallMetricOperation<OverallMinIndexPatternColumn>({
type: 'overall_min',
displayName: i18n.translate('xpack.lens.indexPattern.overallMin', {
defaultMessage: 'Overall min',
}),
ofName: (name?: string) => {
return i18n.translate('xpack.lens.indexPattern.overallMinOf', {
defaultMessage: 'Overall min of {name}',
values: {
name:
name ??
i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
defaultMessage: '(incomplete)',
}),
},
});
},
metric: 'min',
description: i18n.translate('xpack.lens.indexPattern.overall_min.documentation', {
defaultMessage: `
Calculates the minimum of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
Other dimensions breaking down the data like top values or filter are treated as separate series.
If no date histograms or interval functions are used in the current chart, \`overall_min\` is calculating the minimum over all dimensions no matter the used function
Example: Percentage of range
\`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(bytes) - overall_min(bytes))\`
`,
}),
});
export const overallMaxOperation = buildOverallMetricOperation<OverallMaxIndexPatternColumn>({
type: 'overall_max',
displayName: i18n.translate('xpack.lens.indexPattern.overallMax', {
defaultMessage: 'Overall max',
}),
ofName: (name?: string) => {
return i18n.translate('xpack.lens.indexPattern.overallMaxOf', {
defaultMessage: 'Overall max of {name}',
values: {
name:
name ??
i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
defaultMessage: '(incomplete)',
}),
},
});
},
metric: 'max',
description: i18n.translate('xpack.lens.indexPattern.overall_max.documentation', {
defaultMessage: `
Calculates the maximum of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
Other dimensions breaking down the data like top values or filter are treated as separate series.
If no date histograms or interval functions are used in the current chart, \`overall_max\` is calculating the maximum over all dimensions no matter the used function
Example: Percentage of range
\`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(bytes) - overall_min(bytes))\`
`,
}),
});
export const overallAverageOperation = buildOverallMetricOperation<OverallAverageIndexPatternColumn>(
{
type: 'overall_average',
displayName: i18n.translate('xpack.lens.indexPattern.overallMax', {
defaultMessage: 'Overall max',
}),
ofName: (name?: string) => {
return i18n.translate('xpack.lens.indexPattern.overallAverageOf', {
defaultMessage: 'Overall average of {name}',
values: {
name:
name ??
i18n.translate('xpack.lens.indexPattern.incompleteOperation', {
defaultMessage: '(incomplete)',
}),
},
});
},
metric: 'average',
description: i18n.translate('xpack.lens.indexPattern.overall_average.documentation', {
defaultMessage: `
Calculates the average of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function.
Other dimensions breaking down the data like top values or filter are treated as separate series.
If no date histograms or interval functions are used in the current chart, \`overall_average\` is calculating the average over all dimensions no matter the used function
Example: Divergence from the mean:
\`sum(bytes) - overall_average(sum(bytes))\`
`,
}),
}
);

View file

@ -134,3 +134,35 @@ export function dateBasedOperationToExpression(
},
];
}
/**
* Creates an expression ast for a date based operation (cumulative sum, derivative, moving average, counter rate)
*/
export function optionallHistogramBasedOperationToExpression(
layer: IndexPatternLayer,
columnId: string,
functionName: string,
additionalArgs: Record<string, unknown[]> = {}
): ExpressionFunctionAST[] {
const currentColumn = (layer.columns[columnId] as unknown) as ReferenceBasedIndexPatternColumn;
const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed);
const nonHistogramColumns = buckets.filter(
(colId) =>
layer.columns[colId].operationType !== 'date_histogram' &&
layer.columns[colId].operationType !== 'range'
)!;
return [
{
type: 'function',
function: functionName,
arguments: {
by: nonHistogramColumns.length === buckets.length ? [] : nonHistogramColumns,
inputColumnId: [currentColumn.references[0]],
outputColumnId: [columnId],
outputColumnName: [currentColumn.label],
...additionalArgs,
},
},
];
}

View file

@ -33,6 +33,14 @@ import {
DerivativeIndexPatternColumn,
movingAverageOperation,
MovingAverageIndexPatternColumn,
OverallSumIndexPatternColumn,
overallSumOperation,
OverallMinIndexPatternColumn,
overallMinOperation,
OverallMaxIndexPatternColumn,
overallMaxOperation,
OverallAverageIndexPatternColumn,
overallAverageOperation,
} from './calculations';
import { countOperation, CountIndexPatternColumn } from './count';
import {
@ -71,6 +79,10 @@ export type IndexPatternColumn =
| CountIndexPatternColumn
| LastValueIndexPatternColumn
| CumulativeSumIndexPatternColumn
| OverallSumIndexPatternColumn
| OverallMinIndexPatternColumn
| OverallMaxIndexPatternColumn
| OverallAverageIndexPatternColumn
| CounterRateIndexPatternColumn
| DerivativeIndexPatternColumn
| MovingAverageIndexPatternColumn
@ -98,6 +110,10 @@ export {
CounterRateIndexPatternColumn,
DerivativeIndexPatternColumn,
MovingAverageIndexPatternColumn,
OverallSumIndexPatternColumn,
OverallMinIndexPatternColumn,
OverallMaxIndexPatternColumn,
OverallAverageIndexPatternColumn,
} from './calculations';
export { CountIndexPatternColumn } from './count';
export { LastValueIndexPatternColumn } from './last_value';
@ -126,6 +142,10 @@ const internalOperationDefinitions = [
movingAverageOperation,
mathOperation,
formulaOperation,
overallSumOperation,
overallMinOperation,
overallMaxOperation,
overallAverageOperation,
];
export { termsOperation } from './terms';
@ -141,6 +161,10 @@ export {
counterRateOperation,
derivativeOperation,
movingAverageOperation,
overallSumOperation,
overallAverageOperation,
overallMaxOperation,
overallMinOperation,
} from './calculations';
export { formulaOperation } from './formula/formula';

View file

@ -31,4 +31,8 @@ export {
CounterRateIndexPatternColumn,
DerivativeIndexPatternColumn,
MovingAverageIndexPatternColumn,
OverallSumIndexPatternColumn,
OverallMinIndexPatternColumn,
OverallMaxIndexPatternColumn,
OverallAverageIndexPatternColumn,
} from './definitions';

View file

@ -319,6 +319,22 @@ describe('getOperationTypesForField', () => {
"operationType": "moving_average",
"type": "fullReference",
},
Object {
"operationType": "overall_sum",
"type": "fullReference",
},
Object {
"operationType": "overall_min",
"type": "fullReference",
},
Object {
"operationType": "overall_max",
"type": "fullReference",
},
Object {
"operationType": "overall_average",
"type": "fullReference",
},
Object {
"field": "bytes",
"operationType": "min",