Skip to content

Commit

Permalink
Add derivative function (#81178) (#82245)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Nov 2, 2020
1 parent aae4273 commit 9d3583e
Show file tree
Hide file tree
Showing 12 changed files with 677 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -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; [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md)

## ExpressionFunctionDefinitions.derivative property

<b>Signature:</b>

```typescript
derivative: ExpressionFunctionDerivative;
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions
| --- | --- | --- |
| [clog](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.clog.md) | <code>ExpressionFunctionClog</code> | |
| [cumulative\_sum](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.cumulative_sum.md) | <code>ExpressionFunctionCumulativeSum</code> | |
| [derivative](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.derivative.md) | <code>ExpressionFunctionDerivative</code> | |
| [font](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.font.md) | <code>ExpressionFunctionFont</code> | |
| [kibana\_context](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana_context.md) | <code>ExpressionFunctionKibanaContext</code> | |
| [kibana](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinitions.kibana.md) | <code>ExpressionFunctionKibana</code> | |
Expand Down
Original file line number Diff line number Diff line change
@@ -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; [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md)

## ExpressionFunctionDefinitions.derivative property

<b>Signature:</b>

```typescript
derivative: ExpressionFunctionDerivative;
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ExpressionFunctionDefinitions
| --- | --- | --- |
| [clog](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.clog.md) | <code>ExpressionFunctionClog</code> | |
| [cumulative\_sum](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.cumulative_sum.md) | <code>ExpressionFunctionCumulativeSum</code> | |
| [derivative](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.derivative.md) | <code>ExpressionFunctionDerivative</code> | |
| [font](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.font.md) | <code>ExpressionFunctionFont</code> | |
| [kibana\_context](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana_context.md) | <code>ExpressionFunctionKibanaContext</code> | |
| [kibana](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinitions.kibana.md) | <code>ExpressionFunctionKibana</code> | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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.
*
Expand Down Expand Up @@ -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 inputColumnDefinition = input.columns.find((column) => column.id === inputColumnId);
const resultColumns = buildResultColumns(
input,
outputColumnId,
inputColumnId,
outputColumnName
);

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<Record<string, number>> = {};
return {
...input,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Record<string, number>> = {};
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;
}),
};
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,6 +37,7 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [
variable,
theme,
cumulativeSum,
derivative,
];

export * from './clog';
Expand All @@ -46,3 +48,4 @@ export * from './var_set';
export * from './var';
export * from './theme';
export * from './cumulative_sum';
export * from './derivative';
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 9d3583e

Please sign in to comment.