Skip to content

Commit eb5c578

Browse files
committed
Add derivative function (elastic#81178)
1 parent 8adb439 commit eb5c578

12 files changed

+677
-38
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[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; [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md)
4+
5+
## ExpressionFunctionDefinitions.derivative property
6+
7+
<b>Signature:</b>
8+
9+
```typescript
10+
derivative: ExpressionFunctionDerivative;
11+
```

docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions
1818
| --- | --- | --- |
1919
| [clog](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.clog.md) | <code>ExpressionFunctionClog</code> | |
2020
| [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) | <code>ExpressionFunctionCumulativeSum</code> | |
21+
| [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | <code>ExpressionFunctionDerivative</code> | |
2122
| [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | <code>ExpressionFunctionFont</code> | |
2223
| [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) | <code>ExpressionFunctionKibanaContext</code> | |
2324
| [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) | <code>ExpressionFunctionKibana</code> | |
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[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; [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md)
4+
5+
## ExpressionFunctionDefinitions.derivative property
6+
7+
<b>Signature:</b>
8+
9+
```typescript
10+
derivative: ExpressionFunctionDerivative;
11+
```

docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions
1818
| --- | --- | --- |
1919
| [clog](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.clog.md) | <code>ExpressionFunctionClog</code> | |
2020
| [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) | <code>ExpressionFunctionCumulativeSum</code> | |
21+
| [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | <code>ExpressionFunctionDerivative</code> | |
2122
| [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | <code>ExpressionFunctionFont</code> | |
2223
| [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) | <code>ExpressionFunctionKibanaContext</code> | |
2324
| [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) | <code>ExpressionFunctionKibana</code> | |

src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919

2020
import { i18n } from '@kbn/i18n';
2121
import { ExpressionFunctionDefinition } from '../types';
22-
import { Datatable, DatatableRow } from '../../expression_types';
22+
import { Datatable } from '../../expression_types';
23+
import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers';
2324

2425
export interface CumulativeSumArgs {
2526
by?: string[];
@@ -35,15 +36,6 @@ export type ExpressionFunctionCumulativeSum = ExpressionFunctionDefinition<
3536
Datatable
3637
>;
3738

38-
/**
39-
* Returns a string identifying the group of a row by a list of columns to group by
40-
*/
41-
function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) {
42-
return (groupColumns || [])
43-
.map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId])))
44-
.join('|');
45-
}
46-
4739
/**
4840
* Calculates the cumulative sum of a specified column in the data table.
4941
*
@@ -114,38 +106,17 @@ export const cumulativeSum: ExpressionFunctionCumulativeSum = {
114106
},
115107

116108
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
117-
if (input.columns.some((column) => column.id === outputColumnId)) {
118-
throw new Error(
119-
i18n.translate('expressions.functions.cumulativeSum.columnConflictMessage', {
120-
defaultMessage:
121-
'Specified outputColumnId {columnId} already exists. Please pick another column id.',
122-
values: {
123-
columnId: outputColumnId,
124-
},
125-
})
126-
);
127-
}
128-
129-
const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId);
109+
const resultColumns = buildResultColumns(
110+
input,
111+
outputColumnId,
112+
inputColumnId,
113+
outputColumnName
114+
);
130115

131-
if (!inputColumnDefinition) {
116+
if (!resultColumns) {
132117
return input;
133118
}
134119

135-
const outputColumnDefinition = {
136-
...inputColumnDefinition,
137-
id: outputColumnId,
138-
name: outputColumnName || outputColumnId,
139-
};
140-
141-
const resultColumns = [...input.columns];
142-
// add output column after input column in the table
143-
resultColumns.splice(
144-
resultColumns.indexOf(inputColumnDefinition) + 1,
145-
0,
146-
outputColumnDefinition
147-
);
148-
149120
const accumulators: Partial<Record<string, number>> = {};
150121
return {
151122
...input,
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { i18n } from '@kbn/i18n';
21+
import { ExpressionFunctionDefinition } from '../types';
22+
import { Datatable } from '../../expression_types';
23+
import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers';
24+
25+
export interface DerivativeArgs {
26+
by?: string[];
27+
inputColumnId: string;
28+
outputColumnId: string;
29+
outputColumnName?: string;
30+
}
31+
32+
export type ExpressionFunctionDerivative = ExpressionFunctionDefinition<
33+
'derivative',
34+
Datatable,
35+
DerivativeArgs,
36+
Datatable
37+
>;
38+
39+
/**
40+
* Calculates the derivative of a specified column in the data table.
41+
*
42+
* Also supports multiple series in a single data table - use the `by` argument
43+
* to specify the columns to split the calculation by.
44+
* For each unique combination of all `by` columns a separate derivative will be calculated.
45+
* The order of rows won't be changed - this function is not modifying any existing columns, it's only
46+
* adding the specified `outputColumnId` column to every row of the table without adding or removing rows.
47+
*
48+
* Behavior:
49+
* * Will write the derivative of `inputColumnId` into `outputColumnId`
50+
* * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
51+
* * Derivative always start with an undefined value for the first row of a series, a cell will contain its own value minus the
52+
* value of the previous cell of the same series.
53+
*
54+
* Edge cases:
55+
* * Will return the input table if `inputColumnId` does not exist
56+
* * Will throw an error if `outputColumnId` exists already in provided data table
57+
* * If there is no previous row of the current series with a non `null` or `undefined` value, the output cell of the current row
58+
* will be set to `undefined`.
59+
* * If the row value contains `null` or `undefined`, it will be ignored and the output cell will be set to `undefined`
60+
* * If the value of the previous row of the same series contains `null` or `undefined`, the output cell of the current row will be set to `undefined` as well
61+
* * For all values besides `null` and `undefined`, the value will be cast to a number before it's used in the
62+
* calculation of the current series even if this results in `NaN` (like in case of objects).
63+
* * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings
64+
* before comparison. If the values are objects, the return value of their `toString` method will be used for comparison.
65+
* Missing values (`null` and `undefined`) will be treated as empty strings.
66+
*/
67+
export const derivative: ExpressionFunctionDerivative = {
68+
name: 'derivative',
69+
type: 'datatable',
70+
71+
inputTypes: ['datatable'],
72+
73+
help: i18n.translate('expressions.functions.derivative.help', {
74+
defaultMessage: 'Calculates the derivative of a column in a data table',
75+
}),
76+
77+
args: {
78+
by: {
79+
help: i18n.translate('expressions.functions.derivative.args.byHelpText', {
80+
defaultMessage: 'Column to split the derivative calculation by',
81+
}),
82+
multi: true,
83+
types: ['string'],
84+
required: false,
85+
},
86+
inputColumnId: {
87+
help: i18n.translate('expressions.functions.derivative.args.inputColumnIdHelpText', {
88+
defaultMessage: 'Column to calculate the derivative of',
89+
}),
90+
types: ['string'],
91+
required: true,
92+
},
93+
outputColumnId: {
94+
help: i18n.translate('expressions.functions.derivative.args.outputColumnIdHelpText', {
95+
defaultMessage: 'Column to store the resulting derivative in',
96+
}),
97+
types: ['string'],
98+
required: true,
99+
},
100+
outputColumnName: {
101+
help: i18n.translate('expressions.functions.derivative.args.outputColumnNameHelpText', {
102+
defaultMessage: 'Name of the column to store the resulting derivative in',
103+
}),
104+
types: ['string'],
105+
required: false,
106+
},
107+
},
108+
109+
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
110+
const resultColumns = buildResultColumns(
111+
input,
112+
outputColumnId,
113+
inputColumnId,
114+
outputColumnName
115+
);
116+
117+
if (!resultColumns) {
118+
return input;
119+
}
120+
121+
const previousValues: Partial<Record<string, number>> = {};
122+
return {
123+
...input,
124+
columns: resultColumns,
125+
rows: input.rows.map((row) => {
126+
const newRow = { ...row };
127+
128+
const bucketIdentifier = getBucketIdentifier(row, by);
129+
const previousValue = previousValues[bucketIdentifier];
130+
const currentValue = newRow[inputColumnId];
131+
132+
if (currentValue != null && previousValue != null) {
133+
newRow[outputColumnId] = Number(currentValue) - previousValue;
134+
} else {
135+
newRow[outputColumnId] = undefined;
136+
}
137+
138+
if (currentValue != null) {
139+
previousValues[bucketIdentifier] = Number(currentValue);
140+
} else {
141+
previousValues[bucketIdentifier] = undefined;
142+
}
143+
144+
return newRow;
145+
}),
146+
};
147+
},
148+
};

src/plugins/expressions/common/expression_functions/specs/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { variable } from './var';
2626
import { AnyExpressionFunctionDefinition } from '../types';
2727
import { theme } from './theme';
2828
import { cumulativeSum } from './cumulative_sum';
29+
import { derivative } from './derivative';
2930

3031
export const functionSpecs: AnyExpressionFunctionDefinition[] = [
3132
clog,
@@ -36,6 +37,7 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [
3637
variable,
3738
theme,
3839
cumulativeSum,
40+
derivative,
3941
];
4042

4143
export * from './clog';
@@ -46,3 +48,4 @@ export * from './var_set';
4648
export * from './var';
4749
export * from './theme';
4850
export * from './cumulative_sum';
51+
export * from './derivative';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { i18n } from '@kbn/i18n';
21+
import { Datatable, DatatableRow } from '../../expression_types';
22+
23+
/**
24+
* Returns a string identifying the group of a row by a list of columns to group by
25+
*/
26+
export function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) {
27+
return (groupColumns || [])
28+
.map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId])))
29+
.join('|');
30+
}
31+
32+
/**
33+
* Checks whether input and output columns are defined properly
34+
* and builds column array of the output table if that's the case.
35+
*
36+
* * Throws an error if the output column exists already.
37+
* * Returns undefined if the input column doesn't exist.
38+
* @param input Input datatable
39+
* @param outputColumnId Id of the output column
40+
* @param inputColumnId Id of the input column
41+
* @param outputColumnName Optional name of the output column
42+
*/
43+
export function buildResultColumns(
44+
input: Datatable,
45+
outputColumnId: string,
46+
inputColumnId: string,
47+
outputColumnName: string | undefined
48+
) {
49+
if (input.columns.some((column) => column.id === outputColumnId)) {
50+
throw new Error(
51+
i18n.translate('expressions.functions.seriesCalculations.columnConflictMessage', {
52+
defaultMessage:
53+
'Specified outputColumnId {columnId} already exists. Please pick another column id.',
54+
values: {
55+
columnId: outputColumnId,
56+
},
57+
})
58+
);
59+
}
60+
61+
const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId);
62+
63+
if (!inputColumnDefinition) {
64+
return;
65+
}
66+
67+
const outputColumnDefinition = {
68+
...inputColumnDefinition,
69+
id: outputColumnId,
70+
name: outputColumnName || outputColumnId,
71+
};
72+
73+
const resultColumns = [...input.columns];
74+
// add output column after input column in the table
75+
resultColumns.splice(resultColumns.indexOf(inputColumnDefinition) + 1, 0, outputColumnDefinition);
76+
return resultColumns;
77+
}

0 commit comments

Comments
 (0)