diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md
new file mode 100644
index 000000000000..cce7a463d156
--- /dev/null
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md) > [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md)
+
+## ExpressionFunctionDefinitions.derivative property
+
+Signature:
+
+```typescript
+derivative: ExpressionFunctionDerivative;
+```
diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md
index d1703a1e019e..0e180d1fabe3 100644
--- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md
+++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.md
@@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions
| --- | --- | --- |
| [clog](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.clog.md) | ExpressionFunctionClog
| |
| [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum
| |
+| [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative
| |
| [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont
| |
| [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext
| |
| [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana
| |
diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md
new file mode 100644
index 000000000000..6c51f1eb9775
--- /dev/null
+++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunctionDefinitions](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md) > [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md)
+
+## ExpressionFunctionDefinitions.derivative property
+
+Signature:
+
+```typescript
+derivative: ExpressionFunctionDerivative;
+```
diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md
index 05b4ddce4ccd..d4b71a36e0de 100644
--- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md
+++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.md
@@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions
| --- | --- | --- |
| [clog](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.clog.md) | ExpressionFunctionClog
| |
| [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) | ExpressionFunctionCumulativeSum
| |
+| [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | ExpressionFunctionDerivative
| |
| [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | ExpressionFunctionFont
| |
| [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) | ExpressionFunctionKibanaContext
| |
| [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) | ExpressionFunctionKibana
| |
diff --git a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts
index 970015638794..672abadd3c01 100644
--- a/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts
@@ -19,7 +19,8 @@
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../types';
-import { Datatable, DatatableRow } from '../../expression_types';
+import { Datatable } from '../../expression_types';
+import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers';
export interface CumulativeSumArgs {
by?: string[];
@@ -35,15 +36,6 @@ export type ExpressionFunctionCumulativeSum = ExpressionFunctionDefinition<
Datatable
>;
-/**
- * Returns a string identifying the group of a row by a list of columns to group by
- */
-function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) {
- return (groupColumns || [])
- .map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId])))
- .join('|');
-}
-
/**
* Calculates the cumulative sum of a specified column in the data table.
*
@@ -114,38 +106,17 @@ export const cumulativeSum: ExpressionFunctionCumulativeSum = {
},
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
- if (input.columns.some((column) => column.id === outputColumnId)) {
- throw new Error(
- i18n.translate('expressions.functions.cumulativeSum.columnConflictMessage', {
- defaultMessage:
- 'Specified outputColumnId {columnId} already exists. Please pick another column id.',
- values: {
- columnId: outputColumnId,
- },
- })
- );
- }
+ const resultColumns = buildResultColumns(
+ input,
+ outputColumnId,
+ inputColumnId,
+ outputColumnName
+ );
- const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId);
-
- if (!inputColumnDefinition) {
+ if (!resultColumns) {
return input;
}
- const outputColumnDefinition = {
- ...inputColumnDefinition,
- id: outputColumnId,
- name: outputColumnName || outputColumnId,
- };
-
- const resultColumns = [...input.columns];
- // add output column after input column in the table
- resultColumns.splice(
- resultColumns.indexOf(inputColumnDefinition) + 1,
- 0,
- outputColumnDefinition
- );
-
const accumulators: Partial> = {};
return {
...input,
diff --git a/src/plugins/expressions/common/expression_functions/specs/derivative.ts b/src/plugins/expressions/common/expression_functions/specs/derivative.ts
new file mode 100644
index 000000000000..44ac198e2d17
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/derivative.ts
@@ -0,0 +1,148 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { ExpressionFunctionDefinition } from '../types';
+import { Datatable } from '../../expression_types';
+import { buildResultColumns, getBucketIdentifier } from './series_calculation_helpers';
+
+export interface DerivativeArgs {
+ by?: string[];
+ inputColumnId: string;
+ outputColumnId: string;
+ outputColumnName?: string;
+}
+
+export type ExpressionFunctionDerivative = ExpressionFunctionDefinition<
+ 'derivative',
+ Datatable,
+ DerivativeArgs,
+ Datatable
+>;
+
+/**
+ * Calculates the derivative 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 derivative 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 derivative of `inputColumnId` into `outputColumnId`
+ * * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
+ * * Derivative always start with an undefined value for the first row of a series, a cell will contain its own value minus the
+ * value of the previous cell of the same 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 there is no previous row of the current series with a non `null` or `undefined` value, the output cell of the current row
+ * will be set to `undefined`.
+ * * If the row value contains `null` or `undefined`, it will be ignored and the output cell will be set to `undefined`
+ * * 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
+ * * For all values besides `null` and `undefined`, the value will be cast to a number before it's used in the
+ * calculation of the current series even if this results in `NaN` (like in case of objects).
+ * * 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 derivative: ExpressionFunctionDerivative = {
+ name: 'derivative',
+ type: 'datatable',
+
+ inputTypes: ['datatable'],
+
+ help: i18n.translate('expressions.functions.derivative.help', {
+ defaultMessage: 'Calculates the derivative of a column in a data table',
+ }),
+
+ args: {
+ by: {
+ help: i18n.translate('expressions.functions.derivative.args.byHelpText', {
+ defaultMessage: 'Column to split the derivative calculation by',
+ }),
+ multi: true,
+ types: ['string'],
+ required: false,
+ },
+ inputColumnId: {
+ help: i18n.translate('expressions.functions.derivative.args.inputColumnIdHelpText', {
+ defaultMessage: 'Column to calculate the derivative of',
+ }),
+ types: ['string'],
+ required: true,
+ },
+ outputColumnId: {
+ help: i18n.translate('expressions.functions.derivative.args.outputColumnIdHelpText', {
+ defaultMessage: 'Column to store the resulting derivative in',
+ }),
+ types: ['string'],
+ required: true,
+ },
+ outputColumnName: {
+ help: i18n.translate('expressions.functions.derivative.args.outputColumnNameHelpText', {
+ defaultMessage: 'Name of the column to store the resulting derivative in',
+ }),
+ types: ['string'],
+ required: false,
+ },
+ },
+
+ fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
+ const resultColumns = buildResultColumns(
+ input,
+ outputColumnId,
+ inputColumnId,
+ outputColumnName
+ );
+
+ if (!resultColumns) {
+ return input;
+ }
+
+ const previousValues: Partial> = {};
+ return {
+ ...input,
+ columns: resultColumns,
+ rows: input.rows.map((row) => {
+ const newRow = { ...row };
+
+ const bucketIdentifier = getBucketIdentifier(row, by);
+ const previousValue = previousValues[bucketIdentifier];
+ const currentValue = newRow[inputColumnId];
+
+ if (currentValue != null && previousValue != null) {
+ newRow[outputColumnId] = Number(currentValue) - previousValue;
+ } else {
+ newRow[outputColumnId] = undefined;
+ }
+
+ if (currentValue != null) {
+ previousValues[bucketIdentifier] = Number(currentValue);
+ } else {
+ previousValues[bucketIdentifier] = undefined;
+ }
+
+ return newRow;
+ }),
+ };
+ },
+};
diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts
index aadea5882b9c..d414057598f1 100644
--- a/src/plugins/expressions/common/expression_functions/specs/index.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/index.ts
@@ -26,6 +26,7 @@ import { variable } from './var';
import { AnyExpressionFunctionDefinition } from '../types';
import { theme } from './theme';
import { cumulativeSum } from './cumulative_sum';
+import { derivative } from './derivative';
export const functionSpecs: AnyExpressionFunctionDefinition[] = [
clog,
@@ -36,6 +37,7 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [
variable,
theme,
cumulativeSum,
+ derivative,
];
export * from './clog';
@@ -46,3 +48,4 @@ export * from './var_set';
export * from './var';
export * from './theme';
export * from './cumulative_sum';
+export * from './derivative';
diff --git a/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts b/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts
new file mode 100644
index 000000000000..8ba9d527d4c5
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts
@@ -0,0 +1,77 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { Datatable, DatatableRow } from '../../expression_types';
+
+/**
+ * Returns a string identifying the group of a row by a list of columns to group by
+ */
+export function getBucketIdentifier(row: DatatableRow, groupColumns?: string[]) {
+ return (groupColumns || [])
+ .map((groupColumnId) => (row[groupColumnId] == null ? '' : String(row[groupColumnId])))
+ .join('|');
+}
+
+/**
+ * Checks whether input and output columns are defined properly
+ * and builds column array of the output table if that's the case.
+ *
+ * * Throws an error if the output column exists already.
+ * * Returns undefined if the input column doesn't exist.
+ * @param input Input datatable
+ * @param outputColumnId Id of the output column
+ * @param inputColumnId Id of the input column
+ * @param outputColumnName Optional name of the output column
+ */
+export function buildResultColumns(
+ input: Datatable,
+ outputColumnId: string,
+ inputColumnId: string,
+ outputColumnName: string | undefined
+) {
+ if (input.columns.some((column) => column.id === outputColumnId)) {
+ throw new Error(
+ i18n.translate('expressions.functions.seriesCalculations.columnConflictMessage', {
+ defaultMessage:
+ 'Specified outputColumnId {columnId} already exists. Please pick another column id.',
+ values: {
+ columnId: outputColumnId,
+ },
+ })
+ );
+ }
+
+ const inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId);
+
+ if (!inputColumnDefinition) {
+ return;
+ }
+
+ const outputColumnDefinition = {
+ ...inputColumnDefinition,
+ id: outputColumnId,
+ name: outputColumnName || outputColumnId,
+ };
+
+ const resultColumns = [...input.columns];
+ // add output column after input column in the table
+ resultColumns.splice(resultColumns.indexOf(inputColumnDefinition) + 1, 0, outputColumnDefinition);
+ return resultColumns;
+}
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts
new file mode 100644
index 000000000000..63b2df538255
--- /dev/null
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts
@@ -0,0 +1,406 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { functionWrapper } from './utils';
+import { derivative, DerivativeArgs } from '../derivative';
+import { ExecutionContext } from '../../../execution/types';
+import { Datatable } from '../../../expression_types/specs/datatable';
+
+describe('interpreter/functions#derivative', () => {
+ const fn = functionWrapper(derivative);
+ const runFn = (input: Datatable, args: DerivativeArgs) =>
+ fn(input, args, {} as ExecutionContext) as Datatable;
+
+ it('calculates derivative', () => {
+ 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' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: { type: 'number' },
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([undefined, 2, -4, -1]);
+ });
+
+ it('skips null or undefined values until there is real data', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [
+ {},
+ { val: null },
+ { val: undefined },
+ { val: 1 },
+ { val: 2 },
+ { val: undefined },
+ { val: undefined },
+ { val: 4 },
+ { val: 8 },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output' }
+ );
+ expect(result.columns).toContainEqual({
+ id: 'output',
+ name: 'output',
+ meta: { type: 'number' },
+ });
+ expect(result.rows.map((row) => row.output)).toEqual([
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ 2 - 1,
+ undefined,
+ undefined,
+ undefined,
+ 8 - 4,
+ ]);
+ });
+
+ it('treats 0 as real data', () => {
+ const result = runFn(
+ {
+ type: 'datatable',
+ columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
+ rows: [
+ {},
+ { val: null },
+ { val: undefined },
+ { val: 1 },
+ { val: 2 },
+ { val: 0 },
+ { val: undefined },
+ { val: 0 },
+ { val: undefined },
+ { val: 0 },
+ { val: 8 },
+ { val: 0 },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ 2 - 1,
+ 0 - 2,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ 8 - 0,
+ 0 - 8,
+ ]);
+ });
+
+ it('calculates derivative 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'] }
+ );
+
+ expect(result.rows.map((row) => row.output)).toEqual([
+ undefined,
+ undefined,
+ 3 - 2,
+ 4 - 1,
+ 5 - 4,
+ 6 - 5,
+ 7 - 3,
+ 8 - 7,
+ ]);
+ });
+
+ 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'] }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([
+ undefined,
+ undefined,
+ undefined,
+ 4 - 1,
+ 5 - 3,
+ 6 - 4,
+ 7 - 2,
+ 8 - 7,
+ ]);
+ });
+
+ it('treats null like undefined and empty string for split columns', () => {
+ 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: null },
+ { val: 8, split: 'B' },
+ { val: 9, split: '' },
+ ],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output', by: ['split'] }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([
+ undefined,
+ undefined,
+ undefined,
+ 4 - 1,
+ 5 - 3,
+ 6 - 4,
+ 7 - 5,
+ 8 - 2,
+ 9 - 7,
+ ]);
+ });
+
+ it('calculates derivative 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'] }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([
+ undefined,
+ undefined,
+ undefined,
+ 4 - 1,
+ undefined,
+ undefined,
+ undefined,
+ 8 - 7,
+ ]);
+ });
+
+ 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'] }
+ );
+
+ expect(result.rows.map((row) => row.output)).toEqual([undefined, 2 - 1, undefined, 11 - 10]);
+ });
+
+ it('casts values to number before calculating derivative', () => {
+ 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' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, 3 - 7, 2 - 3]);
+ });
+
+ it('casts values to number before calculating derivative 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 }, { val: 5 }],
+ },
+ { inputColumnId: 'val', outputColumnId: 'output' }
+ );
+ expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, NaN, NaN, 5 - 2]);
+ });
+
+ 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' }
+ );
+ 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' }
+ );
+ 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' })).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' }
+ )
+ ).toThrow();
+ });
+});
diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts
index fb1823e85b39..134e9e3a6350 100644
--- a/src/plugins/expressions/common/expression_functions/types.ts
+++ b/src/plugins/expressions/common/expression_functions/types.ts
@@ -30,6 +30,7 @@ import {
ExpressionFunctionVar,
ExpressionFunctionTheme,
ExpressionFunctionCumulativeSum,
+ ExpressionFunctionDerivative,
} from './specs';
import { ExpressionAstFunction } from '../ast';
import { PersistableStateDefinition } from '../../../kibana_utils/common';
@@ -133,4 +134,5 @@ export interface ExpressionFunctionDefinitions {
var: ExpressionFunctionVar;
theme: ExpressionFunctionTheme;
cumulative_sum: ExpressionFunctionCumulativeSum;
+ derivative: ExpressionFunctionDerivative;
}
diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md
index 5ca085ebb0d1..06b7bc214244 100644
--- a/src/plugins/expressions/public/public.api.md
+++ b/src/plugins/expressions/public/public.api.md
@@ -389,6 +389,10 @@ export interface ExpressionFunctionDefinitions {
//
// (undocumented)
cumulative_sum: ExpressionFunctionCumulativeSum;
+ // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDerivative" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ derivative: ExpressionFunctionDerivative;
// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionFont" needs to be exported by the entry point index.d.ts
//
// (undocumented)
diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md
index c8d546492903..cacd61a8638a 100644
--- a/src/plugins/expressions/server/server.api.md
+++ b/src/plugins/expressions/server/server.api.md
@@ -361,6 +361,10 @@ export interface ExpressionFunctionDefinitions {
//
// (undocumented)
cumulative_sum: ExpressionFunctionCumulativeSum;
+ // Warning: (ae-forgotten-export) The symbol "ExpressionFunctionDerivative" needs to be exported by the entry point index.d.ts
+ //
+ // (undocumented)
+ derivative: ExpressionFunctionDerivative;
// Warning: (ae-forgotten-export) The symbol "ExpressionFunctionFont" needs to be exported by the entry point index.d.ts
//
// (undocumented)