From 2dcb6b46875f4764b45589e058803c1b90c3f59e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 2 Nov 2020 10:30:44 +0100 Subject: [PATCH] Add derivative function (#81178) --- ...xpressionfunctiondefinitions.derivative.md | 11 + ...ns-public.expressionfunctiondefinitions.md | 1 + ...xpressionfunctiondefinitions.derivative.md | 11 + ...ns-server.expressionfunctiondefinitions.md | 1 + .../specs/cumulative_sum.ts | 47 +- .../expression_functions/specs/derivative.ts | 148 +++++++ .../expression_functions/specs/index.ts | 3 + .../specs/series_calculation_helpers.ts | 77 ++++ .../specs/tests/derivative.test.ts | 406 ++++++++++++++++++ .../common/expression_functions/types.ts | 2 + src/plugins/expressions/public/public.api.md | 4 + src/plugins/expressions/server/server.api.md | 4 + 12 files changed, 677 insertions(+), 38 deletions(-) create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md create mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md create mode 100644 src/plugins/expressions/common/expression_functions/specs/derivative.ts create mode 100644 src/plugins/expressions/common/expression_functions/specs/series_calculation_helpers.ts create mode 100644 src/plugins/expressions/common/expression_functions/specs/tests/derivative.test.ts 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)