mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[expressions] decrease bundle size (#114229)
This commit is contained in:
parent
109e966a7a
commit
73a0fc0948
39 changed files with 877 additions and 675 deletions
|
@ -19,11 +19,7 @@ import {
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import {
|
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||||
ExpressionsStart,
|
|
||||||
ReactExpressionRenderer,
|
|
||||||
ExpressionsInspectorAdapter,
|
|
||||||
} from '../../../src/plugins/expressions/public';
|
|
||||||
import { ExpressionEditor } from './editor/expression_editor';
|
import { ExpressionEditor } from './editor/expression_editor';
|
||||||
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
||||||
import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger';
|
import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger';
|
||||||
|
@ -42,10 +38,6 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) {
|
||||||
updateExpression(value);
|
updateExpression(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspectorAdapters = {
|
|
||||||
expression: new ExpressionsInspectorAdapter(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEvents = (event: any) => {
|
const handleEvents = (event: any) => {
|
||||||
if (event.name !== 'NAVIGATE') return;
|
if (event.name !== 'NAVIGATE') return;
|
||||||
// enrich event context with some extra data
|
// enrich event context with some extra data
|
||||||
|
@ -83,10 +75,9 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) {
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel paddingSize="none" role="figure">
|
<EuiPanel paddingSize="none" role="figure">
|
||||||
<ReactExpressionRenderer
|
<expressions.ReactExpressionRenderer
|
||||||
expression={expression}
|
expression={expression}
|
||||||
debug={true}
|
debug={true}
|
||||||
inspectorAdapters={inspectorAdapters}
|
|
||||||
onEvent={handleEvents}
|
onEvent={handleEvents}
|
||||||
renderError={(message: any) => {
|
renderError={(message: any) => {
|
||||||
return <div>{message}</div>;
|
return <div>{message}</div>;
|
||||||
|
|
|
@ -19,11 +19,7 @@ import {
|
||||||
EuiText,
|
EuiText,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import {
|
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||||
ExpressionsStart,
|
|
||||||
ReactExpressionRenderer,
|
|
||||||
ExpressionsInspectorAdapter,
|
|
||||||
} from '../../../src/plugins/expressions/public';
|
|
||||||
import { ExpressionEditor } from './editor/expression_editor';
|
import { ExpressionEditor } from './editor/expression_editor';
|
||||||
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
||||||
|
|
||||||
|
@ -45,10 +41,6 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) {
|
||||||
updateExpression(value);
|
updateExpression(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspectorAdapters = {
|
|
||||||
expression: new ExpressionsInspectorAdapter(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEvents = (event: any) => {
|
const handleEvents = (event: any) => {
|
||||||
updateVariables({ color: event.data.href === 'http://www.google.com' ? 'red' : 'blue' });
|
updateVariables({ color: event.data.href === 'http://www.google.com' ? 'red' : 'blue' });
|
||||||
};
|
};
|
||||||
|
@ -81,11 +73,10 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) {
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel paddingSize="none" role="figure">
|
<EuiPanel paddingSize="none" role="figure">
|
||||||
<ReactExpressionRenderer
|
<expressions.ReactExpressionRenderer
|
||||||
data-test-subj="expressionsVariablesTestRenderer"
|
data-test-subj="expressionsVariablesTestRenderer"
|
||||||
expression={expression}
|
expression={expression}
|
||||||
debug={true}
|
debug={true}
|
||||||
inspectorAdapters={inspectorAdapters}
|
|
||||||
variables={variables}
|
variables={variables}
|
||||||
onEvent={handleEvents}
|
onEvent={handleEvents}
|
||||||
renderError={(message: any) => {
|
renderError={(message: any) => {
|
||||||
|
|
|
@ -20,11 +20,7 @@ import {
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
EuiButton,
|
EuiButton,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import {
|
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||||
ExpressionsStart,
|
|
||||||
ReactExpressionRenderer,
|
|
||||||
ExpressionsInspectorAdapter,
|
|
||||||
} from '../../../src/plugins/expressions/public';
|
|
||||||
import { ExpressionEditor } from './editor/expression_editor';
|
import { ExpressionEditor } from './editor/expression_editor';
|
||||||
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
||||||
|
|
||||||
|
@ -42,9 +38,7 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) {
|
||||||
updateExpression(value);
|
updateExpression(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspectorAdapters = {
|
const inspectorAdapters = {};
|
||||||
expression: new ExpressionsInspectorAdapter(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPageBody>
|
<EuiPageBody>
|
||||||
|
@ -83,10 +77,12 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) {
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPanel data-test-subj="expressionRender" paddingSize="none" role="figure">
|
<EuiPanel data-test-subj="expressionRender" paddingSize="none" role="figure">
|
||||||
<ReactExpressionRenderer
|
<expressions.ReactExpressionRenderer
|
||||||
expression={expression}
|
expression={expression}
|
||||||
debug={true}
|
debug={true}
|
||||||
inspectorAdapters={inspectorAdapters}
|
onData$={(result, panels) => {
|
||||||
|
Object.assign(inspectorAdapters, panels);
|
||||||
|
}}
|
||||||
renderError={(message: any) => {
|
renderError={(message: any) => {
|
||||||
return <div>{message}</div>;
|
return <div>{message}</div>;
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { pluck } from 'rxjs/operators';
|
import { pluck } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
EuiCodeBlock,
|
EuiCodeBlock,
|
||||||
|
@ -22,12 +22,9 @@ import {
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
EuiButton,
|
EuiButton,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import {
|
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||||
ExpressionsStart,
|
|
||||||
ExpressionsInspectorAdapter,
|
|
||||||
} from '../../../src/plugins/expressions/public';
|
|
||||||
import { ExpressionEditor } from './editor/expression_editor';
|
import { ExpressionEditor } from './editor/expression_editor';
|
||||||
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
import { Adapters, Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
expressions: ExpressionsStart;
|
expressions: ExpressionsStart;
|
||||||
|
@ -37,25 +34,24 @@ interface Props {
|
||||||
export function RunExpressionsExample({ expressions, inspector }: Props) {
|
export function RunExpressionsExample({ expressions, inspector }: Props) {
|
||||||
const [expression, updateExpression] = useState('markdownVis "## expressions explorer"');
|
const [expression, updateExpression] = useState('markdownVis "## expressions explorer"');
|
||||||
const [result, updateResult] = useState<unknown>({});
|
const [result, updateResult] = useState<unknown>({});
|
||||||
|
const [inspectorAdapters, updateInspectorAdapters] = useState<Adapters>({});
|
||||||
|
|
||||||
const expressionChanged = (value: string) => {
|
const expressionChanged = (value: string) => {
|
||||||
updateExpression(value);
|
updateExpression(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspectorAdapters = useMemo(
|
|
||||||
() => ({
|
|
||||||
expression: new ExpressionsInspectorAdapter(),
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const execution = expressions.execute(expression, null, {
|
const execution = expressions.execute(expression, null, {
|
||||||
debug: true,
|
debug: true,
|
||||||
inspectorAdapters,
|
|
||||||
});
|
});
|
||||||
const subscription = execution.getData().pipe(pluck('result')).subscribe(updateResult);
|
const subscription = execution
|
||||||
|
.getData()
|
||||||
|
.pipe(pluck('result'))
|
||||||
|
.subscribe((data) => {
|
||||||
|
updateResult(data);
|
||||||
|
updateInspectorAdapters(execution.inspect());
|
||||||
|
});
|
||||||
|
execution.inspect();
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [expression, expressions, inspectorAdapters]);
|
}, [expression, expressions, inspectorAdapters]);
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
EsaggsExpressionFunctionDefinition,
|
EsaggsExpressionFunctionDefinition,
|
||||||
EsaggsStartDependencies,
|
EsaggsStartDependencies,
|
||||||
getEsaggsMeta,
|
getEsaggsMeta,
|
||||||
handleEsaggsRequest,
|
|
||||||
} from '../../../common/search/expressions';
|
} from '../../../common/search/expressions';
|
||||||
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
|
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
|
||||||
|
|
||||||
|
@ -48,10 +47,12 @@ export function getFunctionDefinition({
|
||||||
);
|
);
|
||||||
aggConfigs.hierarchical = args.metricsAtAllLevels;
|
aggConfigs.hierarchical = args.metricsAtAllLevels;
|
||||||
|
|
||||||
return { aggConfigs, indexPattern, searchSource, getNow };
|
const { handleEsaggsRequest } = await import('../../../common/search/expressions');
|
||||||
|
|
||||||
|
return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest };
|
||||||
}).pipe(
|
}).pipe(
|
||||||
switchMap(({ aggConfigs, indexPattern, searchSource, getNow }) =>
|
switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => {
|
||||||
handleEsaggsRequest({
|
return handleEsaggsRequest({
|
||||||
abortSignal,
|
abortSignal,
|
||||||
aggs: aggConfigs,
|
aggs: aggConfigs,
|
||||||
filters: get(input, 'filters', undefined),
|
filters: get(input, 'filters', undefined),
|
||||||
|
@ -65,8 +66,8 @@ export function getFunctionDefinition({
|
||||||
timeRange: get(input, 'timeRange', undefined),
|
timeRange: get(input, 'timeRange', undefined),
|
||||||
getNow,
|
getNow,
|
||||||
executionContext: getExecutionContext(),
|
executionContext: getExecutionContext(),
|
||||||
|
});
|
||||||
})
|
})
|
||||||
)
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { catchError } from 'rxjs/operators';
|
||||||
import { Execution, ExecutionResult } from './execution';
|
import { Execution, ExecutionResult } from './execution';
|
||||||
import { ExpressionValueError } from '../expression_types/specs';
|
import { ExpressionValueError } from '../expression_types/specs';
|
||||||
import { ExpressionAstExpression } from '../ast';
|
import { ExpressionAstExpression } from '../ast';
|
||||||
|
import { Adapters } from '../../../inspector/common/adapters';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `ExecutionContract` is a wrapper around `Execution` class. It provides the
|
* `ExecutionContract` is a wrapper around `Execution` class. It provides the
|
||||||
|
@ -75,5 +76,5 @@ export class ExecutionContract<Input = unknown, Output = unknown, InspectorAdapt
|
||||||
* Get Inspector adapters provided to all functions of expression through
|
* Get Inspector adapters provided to all functions of expression through
|
||||||
* execution context.
|
* execution context.
|
||||||
*/
|
*/
|
||||||
inspect = () => this.execution.inspectorAdapters;
|
inspect = (): Adapters => this.execution.inspectorAdapters;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type { KibanaExecutionContext } from 'src/core/public';
|
||||||
import { Datatable, ExpressionType } from '../expression_types';
|
import { Datatable, ExpressionType } from '../expression_types';
|
||||||
import { Adapters, RequestAdapter } from '../../../inspector/common';
|
import { Adapters, RequestAdapter } from '../../../inspector/common';
|
||||||
import { TablesAdapter } from '../util/tables_adapter';
|
import { TablesAdapter } from '../util/tables_adapter';
|
||||||
|
import { ExpressionsInspectorAdapter } from '../util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `ExecutionContext` is an object available to all functions during a single execution;
|
* `ExecutionContext` is an object available to all functions during a single execution;
|
||||||
|
@ -79,7 +80,8 @@ export interface ExecutionContext<
|
||||||
/**
|
/**
|
||||||
* Default inspector adapters created if inspector adapters are not set explicitly.
|
* Default inspector adapters created if inspector adapters are not set explicitly.
|
||||||
*/
|
*/
|
||||||
export interface DefaultInspectorAdapters extends Adapters {
|
export interface DefaultInspectorAdapters {
|
||||||
requests: RequestAdapter;
|
requests: RequestAdapter;
|
||||||
tables: TablesAdapter;
|
tables: TablesAdapter;
|
||||||
|
expression: ExpressionsInspectorAdapter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ExpressionFunctionDefinition } from '../types';
|
import { ExpressionFunctionDefinition } from '../types';
|
||||||
import { Datatable } from '../../expression_types';
|
import { Datatable } from '../../expression_types';
|
||||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
|
||||||
|
|
||||||
export interface CumulativeSumArgs {
|
export interface CumulativeSumArgs {
|
||||||
by?: string[];
|
by?: string[];
|
||||||
|
@ -22,7 +21,7 @@ export type ExpressionFunctionCumulativeSum = ExpressionFunctionDefinition<
|
||||||
'cumulative_sum',
|
'cumulative_sum',
|
||||||
Datatable,
|
Datatable,
|
||||||
CumulativeSumArgs,
|
CumulativeSumArgs,
|
||||||
Datatable
|
Promise<Datatable>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,37 +93,8 @@ export const cumulativeSum: ExpressionFunctionCumulativeSum = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
|
async fn(input, args) {
|
||||||
const resultColumns = buildResultColumns(
|
const { cumulativeSumFn } = await import('./cumulative_sum_fn');
|
||||||
input,
|
return cumulativeSumFn(input, args);
|
||||||
outputColumnId,
|
|
||||||
inputColumnId,
|
|
||||||
outputColumnName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!resultColumns) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accumulators: Partial<Record<string, number>> = {};
|
|
||||||
return {
|
|
||||||
...input,
|
|
||||||
columns: resultColumns,
|
|
||||||
rows: input.rows.map((row) => {
|
|
||||||
const newRow = { ...row };
|
|
||||||
|
|
||||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
|
||||||
const accumulatorValue = accumulators[bucketIdentifier] ?? 0;
|
|
||||||
const currentValue = newRow[inputColumnId];
|
|
||||||
if (currentValue != null) {
|
|
||||||
newRow[outputColumnId] = Number(currentValue) + accumulatorValue;
|
|
||||||
accumulators[bucketIdentifier] = newRow[outputColumnId];
|
|
||||||
} else {
|
|
||||||
newRow[outputColumnId] = accumulatorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRow;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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 { Datatable } from '../../expression_types';
|
||||||
|
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||||
|
import { CumulativeSumArgs } from './cumulative_sum';
|
||||||
|
|
||||||
|
export const cumulativeSumFn = (
|
||||||
|
input: Datatable,
|
||||||
|
{ by, inputColumnId, outputColumnId, outputColumnName }: CumulativeSumArgs
|
||||||
|
): Datatable => {
|
||||||
|
const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName);
|
||||||
|
|
||||||
|
if (!resultColumns) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accumulators: Partial<Record<string, number>> = {};
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
columns: resultColumns,
|
||||||
|
rows: input.rows.map((row) => {
|
||||||
|
const newRow = { ...row };
|
||||||
|
|
||||||
|
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||||
|
const accumulatorValue = accumulators[bucketIdentifier] ?? 0;
|
||||||
|
const currentValue = newRow[inputColumnId];
|
||||||
|
if (currentValue != null) {
|
||||||
|
newRow[outputColumnId] = Number(currentValue) + accumulatorValue;
|
||||||
|
accumulators[bucketIdentifier] = newRow[outputColumnId];
|
||||||
|
} else {
|
||||||
|
newRow[outputColumnId] = accumulatorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRow;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
|
@ -9,7 +9,6 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ExpressionFunctionDefinition } from '../types';
|
import { ExpressionFunctionDefinition } from '../types';
|
||||||
import { Datatable } from '../../expression_types';
|
import { Datatable } from '../../expression_types';
|
||||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
|
||||||
|
|
||||||
export interface DerivativeArgs {
|
export interface DerivativeArgs {
|
||||||
by?: string[];
|
by?: string[];
|
||||||
|
@ -22,7 +21,7 @@ export type ExpressionFunctionDerivative = ExpressionFunctionDefinition<
|
||||||
'derivative',
|
'derivative',
|
||||||
Datatable,
|
Datatable,
|
||||||
DerivativeArgs,
|
DerivativeArgs,
|
||||||
Datatable
|
Promise<Datatable>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -95,43 +94,8 @@ export const derivative: ExpressionFunctionDerivative = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
|
async fn(input, args) {
|
||||||
const resultColumns = buildResultColumns(
|
const { derivativeFn } = await import('./derivative_fn');
|
||||||
input,
|
return derivativeFn(input, args);
|
||||||
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;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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 { Datatable } from '../../expression_types';
|
||||||
|
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||||
|
import { DerivativeArgs } from './derivative';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 derivativeFn = (
|
||||||
|
input: Datatable,
|
||||||
|
{ by, inputColumnId, outputColumnId, outputColumnName }: DerivativeArgs
|
||||||
|
): Datatable => {
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
|
@ -6,11 +6,9 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { map, zipObject, isString } from 'lodash';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { evaluate } from '@kbn/tinymath';
|
|
||||||
import { ExpressionFunctionDefinition } from '../types';
|
import { ExpressionFunctionDefinition } from '../types';
|
||||||
import { Datatable, isDatatable } from '../../expression_types';
|
import { Datatable } from '../../expression_types';
|
||||||
|
|
||||||
export type MathArguments = {
|
export type MathArguments = {
|
||||||
expression: string;
|
expression: string;
|
||||||
|
@ -23,63 +21,11 @@ const TINYMATH = '`TinyMath`';
|
||||||
const TINYMATH_URL =
|
const TINYMATH_URL =
|
||||||
'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html';
|
'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html';
|
||||||
|
|
||||||
function pivotObjectArray<
|
|
||||||
RowType extends { [key: string]: unknown },
|
|
||||||
ReturnColumns extends keyof RowType & string
|
|
||||||
>(rows: RowType[], columns?: ReturnColumns[]) {
|
|
||||||
const columnNames = columns || Object.keys(rows[0]);
|
|
||||||
if (!columnNames.every(isString)) {
|
|
||||||
throw new Error('Columns should be an array of strings');
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnValues = map(columnNames, (name) => map(rows, name));
|
|
||||||
|
|
||||||
return zipObject(columnNames, columnValues) as { [K in ReturnColumns]: Array<RowType[K]> };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const errors = {
|
|
||||||
emptyExpression: () =>
|
|
||||||
new Error(
|
|
||||||
i18n.translate('expressions.functions.math.emptyExpressionErrorMessage', {
|
|
||||||
defaultMessage: 'Empty expression',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
tooManyResults: () =>
|
|
||||||
new Error(
|
|
||||||
i18n.translate('expressions.functions.math.tooManyResultsErrorMessage', {
|
|
||||||
defaultMessage:
|
|
||||||
'Expressions must return a single number. Try wrapping your expression in {mean} or {sum}',
|
|
||||||
values: {
|
|
||||||
mean: 'mean()',
|
|
||||||
sum: 'sum()',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
),
|
|
||||||
executionFailed: () =>
|
|
||||||
new Error(
|
|
||||||
i18n.translate('expressions.functions.math.executionFailedErrorMessage', {
|
|
||||||
defaultMessage: 'Failed to execute math expression. Check your column names',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
emptyDatatable: () =>
|
|
||||||
new Error(
|
|
||||||
i18n.translate('expressions.functions.math.emptyDatatableErrorMessage', {
|
|
||||||
defaultMessage: 'Empty datatable',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const fallbackValue = {
|
|
||||||
null: null,
|
|
||||||
zero: 0,
|
|
||||||
false: false,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const math: ExpressionFunctionDefinition<
|
export const math: ExpressionFunctionDefinition<
|
||||||
'math',
|
'math',
|
||||||
MathInput,
|
MathInput,
|
||||||
MathArguments,
|
MathArguments,
|
||||||
boolean | number | null
|
Promise<boolean | number | null>
|
||||||
> = {
|
> = {
|
||||||
name: 'math',
|
name: 'math',
|
||||||
type: undefined,
|
type: undefined,
|
||||||
|
@ -121,47 +67,8 @@ export const math: ExpressionFunctionDefinition<
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fn: (input, args) => {
|
fn: async (input, args) => {
|
||||||
const { expression, onError } = args;
|
const { mathFn } = await import('./math_fn');
|
||||||
const onErrorValue = onError ?? 'throw';
|
return mathFn(input, args);
|
||||||
|
|
||||||
if (!expression || expression.trim() === '') {
|
|
||||||
throw errors.emptyExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use unique ID if available, otherwise fall back to names
|
|
||||||
const mathContext = isDatatable(input)
|
|
||||||
? pivotObjectArray(
|
|
||||||
input.rows,
|
|
||||||
input.columns.map((col) => col.id)
|
|
||||||
)
|
|
||||||
: { value: input };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = evaluate(expression, mathContext);
|
|
||||||
if (Array.isArray(result)) {
|
|
||||||
if (result.length === 1) {
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
throw errors.tooManyResults();
|
|
||||||
}
|
|
||||||
if (isNaN(result)) {
|
|
||||||
// make TS happy
|
|
||||||
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
|
||||||
return fallbackValue[onErrorValue];
|
|
||||||
}
|
|
||||||
throw errors.executionFailed();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
|
||||||
return fallbackValue[onErrorValue];
|
|
||||||
}
|
|
||||||
if (isDatatable(input) && input.rows.length === 0) {
|
|
||||||
throw errors.emptyDatatable();
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const mathColumn: ExpressionFunctionDefinition<
|
||||||
'mathColumn',
|
'mathColumn',
|
||||||
Datatable,
|
Datatable,
|
||||||
MathColumnArguments,
|
MathColumnArguments,
|
||||||
Datatable
|
Promise<Datatable>
|
||||||
> = {
|
> = {
|
||||||
name: 'mathColumn',
|
name: 'mathColumn',
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
|
@ -63,7 +63,7 @@ export const mathColumn: ExpressionFunctionDefinition<
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fn: (input, args, context) => {
|
fn: async (input, args, context) => {
|
||||||
const columns = [...input.columns];
|
const columns = [...input.columns];
|
||||||
const existingColumnIndex = columns.findIndex(({ id }) => {
|
const existingColumnIndex = columns.findIndex(({ id }) => {
|
||||||
return id === args.id;
|
return id === args.id;
|
||||||
|
@ -76,8 +76,9 @@ export const mathColumn: ExpressionFunctionDefinition<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRows = input.rows.map((row) => {
|
const newRows = await Promise.all(
|
||||||
const result = math.fn(
|
input.rows.map(async (row) => {
|
||||||
|
const result = await math.fn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: input.columns,
|
columns: input.columns,
|
||||||
|
@ -103,7 +104,8 @@ export const mathColumn: ExpressionFunctionDefinition<
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...row, [args.id]: result };
|
return { ...row, [args.id]: result };
|
||||||
});
|
})
|
||||||
|
);
|
||||||
let type: DatatableColumnType = 'null';
|
let type: DatatableColumnType = 'null';
|
||||||
if (newRows.length) {
|
if (newRows.length) {
|
||||||
for (const row of newRows) {
|
for (const row of newRows) {
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* 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 { map, zipObject, isString } from 'lodash';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { evaluate } from '@kbn/tinymath';
|
||||||
|
import { isDatatable } from '../../expression_types';
|
||||||
|
import { MathArguments, MathInput } from './math';
|
||||||
|
|
||||||
|
function pivotObjectArray<
|
||||||
|
RowType extends { [key: string]: unknown },
|
||||||
|
ReturnColumns extends keyof RowType & string
|
||||||
|
>(rows: RowType[], columns?: ReturnColumns[]) {
|
||||||
|
const columnNames = columns || Object.keys(rows[0]);
|
||||||
|
if (!columnNames.every(isString)) {
|
||||||
|
throw new Error('Columns should be an array of strings');
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnValues = map(columnNames, (name) => map(rows, name));
|
||||||
|
|
||||||
|
return zipObject(columnNames, columnValues) as { [K in ReturnColumns]: Array<RowType[K]> };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errors = {
|
||||||
|
emptyExpression: () =>
|
||||||
|
new Error(
|
||||||
|
i18n.translate('expressions.functions.math.emptyExpressionErrorMessage', {
|
||||||
|
defaultMessage: 'Empty expression',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
tooManyResults: () =>
|
||||||
|
new Error(
|
||||||
|
i18n.translate('expressions.functions.math.tooManyResultsErrorMessage', {
|
||||||
|
defaultMessage:
|
||||||
|
'Expressions must return a single number. Try wrapping your expression in {mean} or {sum}',
|
||||||
|
values: {
|
||||||
|
mean: 'mean()',
|
||||||
|
sum: 'sum()',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
executionFailed: () =>
|
||||||
|
new Error(
|
||||||
|
i18n.translate('expressions.functions.math.executionFailedErrorMessage', {
|
||||||
|
defaultMessage: 'Failed to execute math expression. Check your column names',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
emptyDatatable: () =>
|
||||||
|
new Error(
|
||||||
|
i18n.translate('expressions.functions.math.emptyDatatableErrorMessage', {
|
||||||
|
defaultMessage: 'Empty datatable',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackValue = {
|
||||||
|
null: null,
|
||||||
|
zero: 0,
|
||||||
|
false: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const mathFn = (input: MathInput, args: MathArguments): boolean | number | null => {
|
||||||
|
const { expression, onError } = args;
|
||||||
|
const onErrorValue = onError ?? 'throw';
|
||||||
|
|
||||||
|
if (!expression || expression.trim() === '') {
|
||||||
|
throw errors.emptyExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use unique ID if available, otherwise fall back to names
|
||||||
|
const mathContext = isDatatable(input)
|
||||||
|
? pivotObjectArray(
|
||||||
|
input.rows,
|
||||||
|
input.columns.map((col) => col.id)
|
||||||
|
)
|
||||||
|
: { value: input };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = evaluate(expression, mathContext);
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
if (result.length === 1) {
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
throw errors.tooManyResults();
|
||||||
|
}
|
||||||
|
if (isNaN(result)) {
|
||||||
|
// make TS happy
|
||||||
|
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
||||||
|
return fallbackValue[onErrorValue];
|
||||||
|
}
|
||||||
|
throw errors.executionFailed();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
||||||
|
return fallbackValue[onErrorValue];
|
||||||
|
}
|
||||||
|
if (isDatatable(input) && input.rows.length === 0) {
|
||||||
|
throw errors.emptyDatatable();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -9,7 +9,6 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ExpressionFunctionDefinition } from '../types';
|
import { ExpressionFunctionDefinition } from '../types';
|
||||||
import { Datatable } from '../../expression_types';
|
import { Datatable } from '../../expression_types';
|
||||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
|
||||||
|
|
||||||
export interface MovingAverageArgs {
|
export interface MovingAverageArgs {
|
||||||
by?: string[];
|
by?: string[];
|
||||||
|
@ -23,7 +22,7 @@ export type ExpressionFunctionMovingAverage = ExpressionFunctionDefinition<
|
||||||
'moving_average',
|
'moving_average',
|
||||||
Datatable,
|
Datatable,
|
||||||
MovingAverageArgs,
|
MovingAverageArgs,
|
||||||
Datatable
|
Promise<Datatable>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,43 +99,8 @@ export const movingAverage: ExpressionFunctionMovingAverage = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName, window }) {
|
async fn(input, args) {
|
||||||
const resultColumns = buildResultColumns(
|
const { movingAverageFn } = await import('./moving_average_fn');
|
||||||
input,
|
return movingAverageFn(input, args);
|
||||||
outputColumnId,
|
|
||||||
inputColumnId,
|
|
||||||
outputColumnName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!resultColumns) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastNValuesByBucket: Partial<Record<string, number[]>> = {};
|
|
||||||
return {
|
|
||||||
...input,
|
|
||||||
columns: resultColumns,
|
|
||||||
rows: input.rows.map((row) => {
|
|
||||||
const newRow = { ...row };
|
|
||||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
|
||||||
const lastNValues = lastNValuesByBucket[bucketIdentifier];
|
|
||||||
const currentValue = newRow[inputColumnId];
|
|
||||||
if (lastNValues != null && currentValue != null) {
|
|
||||||
const sum = lastNValues.reduce((acc, current) => acc + current, 0);
|
|
||||||
newRow[outputColumnId] = sum / lastNValues.length;
|
|
||||||
} else {
|
|
||||||
newRow[outputColumnId] = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue != null) {
|
|
||||||
lastNValuesByBucket[bucketIdentifier] = [
|
|
||||||
...(lastNValues || []),
|
|
||||||
Number(currentValue),
|
|
||||||
].slice(-window);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRow;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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 { Datatable } from '../../expression_types';
|
||||||
|
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||||
|
import { MovingAverageArgs } from './moving_average';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the moving average 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 moving average 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 moving average of `inputColumnId` into `outputColumnId`
|
||||||
|
* * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
|
||||||
|
* * Moving average always starts with an undefined value for the first row of a series. Each next cell will contain sum of the last
|
||||||
|
* * [window] of values divided by [window] excluding the current bucket.
|
||||||
|
* If either of window edges moves outside the borders of data series, the window shrinks to include available values only.
|
||||||
|
*
|
||||||
|
* 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 null or undefined value is encountered, skip the current row and do not change the window
|
||||||
|
* * 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.
|
||||||
|
*/
|
||||||
|
export const movingAverageFn = (
|
||||||
|
input: Datatable,
|
||||||
|
{ by, inputColumnId, outputColumnId, outputColumnName, window }: MovingAverageArgs
|
||||||
|
): Datatable => {
|
||||||
|
const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName);
|
||||||
|
|
||||||
|
if (!resultColumns) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastNValuesByBucket: Partial<Record<string, number[]>> = {};
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
columns: resultColumns,
|
||||||
|
rows: input.rows.map((row) => {
|
||||||
|
const newRow = { ...row };
|
||||||
|
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||||
|
const lastNValues = lastNValuesByBucket[bucketIdentifier];
|
||||||
|
const currentValue = newRow[inputColumnId];
|
||||||
|
if (lastNValues != null && currentValue != null) {
|
||||||
|
const sum = lastNValues.reduce((acc, current) => acc + current, 0);
|
||||||
|
newRow[outputColumnId] = sum / lastNValues.length;
|
||||||
|
} else {
|
||||||
|
newRow[outputColumnId] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue != null) {
|
||||||
|
lastNValuesByBucket[bucketIdentifier] = [
|
||||||
|
...(lastNValues || []),
|
||||||
|
Number(currentValue),
|
||||||
|
].slice(-window);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRow;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
|
@ -9,7 +9,6 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ExpressionFunctionDefinition } from '../types';
|
import { ExpressionFunctionDefinition } from '../types';
|
||||||
import { Datatable } from '../../expression_types';
|
import { Datatable } from '../../expression_types';
|
||||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
|
||||||
|
|
||||||
export interface OverallMetricArgs {
|
export interface OverallMetricArgs {
|
||||||
by?: string[];
|
by?: string[];
|
||||||
|
@ -23,17 +22,9 @@ export type ExpressionFunctionOverallMetric = ExpressionFunctionDefinition<
|
||||||
'overall_metric',
|
'overall_metric',
|
||||||
Datatable,
|
Datatable,
|
||||||
OverallMetricArgs,
|
OverallMetricArgs,
|
||||||
Datatable
|
Promise<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.
|
* Calculates the overall metric of a specified column in the data table.
|
||||||
*
|
*
|
||||||
|
@ -109,73 +100,8 @@ export const overallMetric: ExpressionFunctionOverallMetric = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName, metric }) {
|
async fn(input, args) {
|
||||||
const resultColumns = buildResultColumns(
|
const { overallMetricFn } = await import('./overall_metric_fn');
|
||||||
input,
|
return overallMetricFn(input, args);
|
||||||
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];
|
|
||||||
|
|
||||||
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] = currentNumberValues.reduce(
|
|
||||||
(a, b) => a + b,
|
|
||||||
accumulatorValue || 0
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'min':
|
|
||||||
if (typeof accumulatorValue !== 'undefined') {
|
|
||||||
accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues);
|
|
||||||
} else {
|
|
||||||
accumulators[bucketIdentifier] = Math.min(...currentNumberValues);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'max':
|
|
||||||
if (typeof accumulatorValue !== 'undefined') {
|
|
||||||
accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues);
|
|
||||||
} else {
|
|
||||||
accumulators[bucketIdentifier] = Math.max(...currentNumberValues);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (metric === 'average') {
|
|
||||||
Object.keys(accumulators).forEach((bucketIdentifier) => {
|
|
||||||
const accumulatorValue = accumulators[bucketIdentifier];
|
|
||||||
const valueCount = valueCounter[bucketIdentifier];
|
|
||||||
if (typeof accumulatorValue !== 'undefined' && typeof valueCount !== 'undefined') {
|
|
||||||
accumulators[bucketIdentifier] = accumulatorValue / valueCount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...input,
|
|
||||||
columns: resultColumns,
|
|
||||||
rows: input.rows.map((row) => {
|
|
||||||
const newRow = { ...row };
|
|
||||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
|
||||||
newRow[outputColumnId] = accumulators[bucketIdentifier];
|
|
||||||
|
|
||||||
return newRow;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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 { Datatable } from '../../expression_types';
|
||||||
|
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||||
|
import { OverallMetricArgs } from './overall_metric';
|
||||||
|
|
||||||
|
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 overallMetricFn = (
|
||||||
|
input: Datatable,
|
||||||
|
{ by, inputColumnId, outputColumnId, outputColumnName, metric }: OverallMetricArgs
|
||||||
|
): Datatable => {
|
||||||
|
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];
|
||||||
|
|
||||||
|
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] = currentNumberValues.reduce(
|
||||||
|
(a, b) => a + b,
|
||||||
|
accumulatorValue || 0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'min':
|
||||||
|
if (typeof accumulatorValue !== 'undefined') {
|
||||||
|
accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues);
|
||||||
|
} else {
|
||||||
|
accumulators[bucketIdentifier] = Math.min(...currentNumberValues);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'max':
|
||||||
|
if (typeof accumulatorValue !== 'undefined') {
|
||||||
|
accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues);
|
||||||
|
} else {
|
||||||
|
accumulators[bucketIdentifier] = Math.max(...currentNumberValues);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (metric === 'average') {
|
||||||
|
Object.keys(accumulators).forEach((bucketIdentifier) => {
|
||||||
|
const accumulatorValue = accumulators[bucketIdentifier];
|
||||||
|
const valueCount = valueCounter[bucketIdentifier];
|
||||||
|
if (typeof accumulatorValue !== 'undefined' && typeof valueCount !== 'undefined') {
|
||||||
|
accumulators[bucketIdentifier] = accumulatorValue / valueCount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...input,
|
||||||
|
columns: resultColumns,
|
||||||
|
rows: input.rows.map((row) => {
|
||||||
|
const newRow = { ...row };
|
||||||
|
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||||
|
newRow[outputColumnId] = accumulators[bucketIdentifier];
|
||||||
|
|
||||||
|
return newRow;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
|
@ -14,10 +14,10 @@ import { Datatable } from '../../../expression_types/specs/datatable';
|
||||||
describe('interpreter/functions#cumulative_sum', () => {
|
describe('interpreter/functions#cumulative_sum', () => {
|
||||||
const fn = functionWrapper(cumulativeSum);
|
const fn = functionWrapper(cumulativeSum);
|
||||||
const runFn = (input: Datatable, args: CumulativeSumArgs) =>
|
const runFn = (input: Datatable, args: CumulativeSumArgs) =>
|
||||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||||
|
|
||||||
it('calculates cumulative sum', () => {
|
it('calculates cumulative sum', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -33,8 +33,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]);
|
expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces null or undefined data with zeroes until there is real data', () => {
|
it('replaces null or undefined data with zeroes until there is real data', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -50,8 +50,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([0, 0, 0, 1]);
|
expect(result.rows.map((row) => row.output)).toEqual([0, 0, 0, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates cumulative sum for multiple series', () => {
|
it('calculates cumulative sum for multiple series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -84,8 +84,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats missing split column as separate series', () => {
|
it('treats missing split column as separate series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -117,8 +117,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats null like undefined and empty string for split columns', () => {
|
it('treats null like undefined and empty string for split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -152,8 +152,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates cumulative sum for multiple series by multiple split columns', () => {
|
it('calculates cumulative sum for multiple series by multiple split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -177,8 +177,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1 + 4, 5, 6, 7, 7 + 8]);
|
expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1 + 4, 5, 6, 7, 7 + 8]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('splits separate series by the string representation of the cell values', () => {
|
it('splits separate series by the string representation of the cell values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -198,8 +198,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([1, 1 + 2, 10, 21]);
|
expect(result.rows.map((row) => row.output)).toEqual([1, 1 + 2, 10, 21]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('casts values to number before calculating cumulative sum', () => {
|
it('casts values to number before calculating cumulative sum', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -210,8 +210,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]);
|
expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('casts values to number before calculating cumulative sum for NaN like values', () => {
|
it('casts values to number before calculating cumulative sum for NaN like values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -222,8 +222,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([5, 12, NaN, NaN]);
|
expect(result.rows.map((row) => row.output)).toEqual([5, 12, NaN, NaN]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips undefined and null values', () => {
|
it('skips undefined and null values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -244,8 +244,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([0, 7, 7, 7, 7, 7, 10, 12, 12]);
|
expect(result.rows.map((row) => row.output)).toEqual([0, 7, 7, 7, 7, 7, 10, 12, 12]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('copies over meta information from the source column', () => {
|
it('copies over meta information from the source column', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -286,8 +286,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets output name on output column if specified', () => {
|
it('sets output name on output column if specified', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -310,7 +310,7 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns source table if input column does not exist', () => {
|
it('returns source table if input column does not exist', async () => {
|
||||||
const input: Datatable = {
|
const input: Datatable = {
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -324,11 +324,13 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
],
|
],
|
||||||
rows: [{ val: 5 }],
|
rows: [{ val: 5 }],
|
||||||
};
|
};
|
||||||
expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input);
|
expect(await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(
|
||||||
|
input
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if output column exists already', () => {
|
it('throws an error if output column exists already', async () => {
|
||||||
expect(() =>
|
await expect(
|
||||||
runFn(
|
runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
|
@ -345,6 +347,6 @@ describe('interpreter/functions#cumulative_sum', () => {
|
||||||
},
|
},
|
||||||
{ inputColumnId: 'val', outputColumnId: 'val' }
|
{ inputColumnId: 'val', outputColumnId: 'val' }
|
||||||
)
|
)
|
||||||
).toThrow();
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { Datatable } from '../../../expression_types/specs/datatable';
|
||||||
describe('interpreter/functions#derivative', () => {
|
describe('interpreter/functions#derivative', () => {
|
||||||
const fn = functionWrapper(derivative);
|
const fn = functionWrapper(derivative);
|
||||||
const runFn = (input: Datatable, args: DerivativeArgs) =>
|
const runFn = (input: Datatable, args: DerivativeArgs) =>
|
||||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||||
|
|
||||||
it('calculates derivative', () => {
|
it('calculates derivative', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -33,8 +33,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 2, -4, -1]);
|
expect(result.rows.map((row) => row.output)).toEqual([undefined, 2, -4, -1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips null or undefined values until there is real data', () => {
|
it('skips null or undefined values until there is real data', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -70,8 +70,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats 0 as real data', () => {
|
it('treats 0 as real data', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -108,8 +108,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates derivative for multiple series', () => {
|
it('calculates derivative for multiple series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -142,8 +142,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats missing split column as separate series', () => {
|
it('treats missing split column as separate series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -175,8 +175,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats null like undefined and empty string for split columns', () => {
|
it('treats null like undefined and empty string for split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -210,8 +210,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates derivative for multiple series by multiple split columns', () => {
|
it('calculates derivative for multiple series by multiple split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -244,8 +244,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('splits separate series by the string representation of the cell values', () => {
|
it('splits separate series by the string representation of the cell values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -265,8 +265,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 2 - 1, undefined, 11 - 10]);
|
expect(result.rows.map((row) => row.output)).toEqual([undefined, 2 - 1, undefined, 11 - 10]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('casts values to number before calculating derivative', () => {
|
it('casts values to number before calculating derivative', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -277,8 +277,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, 3 - 7, 2 - 3]);
|
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', () => {
|
it('casts values to number before calculating derivative for NaN like values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -289,8 +289,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, NaN, NaN, 5 - 2]);
|
expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, NaN, NaN, 5 - 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('copies over meta information from the source column', () => {
|
it('copies over meta information from the source column', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -331,8 +331,8 @@ describe('interpreter/functions#derivative', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets output name on output column if specified', () => {
|
it('sets output name on output column if specified', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -355,7 +355,7 @@ describe('interpreter/functions#derivative', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns source table if input column does not exist', () => {
|
it('returns source table if input column does not exist', async () => {
|
||||||
const input: Datatable = {
|
const input: Datatable = {
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -369,11 +369,13 @@ describe('interpreter/functions#derivative', () => {
|
||||||
],
|
],
|
||||||
rows: [{ val: 5 }],
|
rows: [{ val: 5 }],
|
||||||
};
|
};
|
||||||
expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input);
|
expect(await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(
|
||||||
|
input
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if output column exists already', () => {
|
it('throws an error if output column exists already', async () => {
|
||||||
expect(() =>
|
await expect(
|
||||||
runFn(
|
runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
|
@ -390,6 +392,6 @@ describe('interpreter/functions#derivative', () => {
|
||||||
},
|
},
|
||||||
{ inputColumnId: 'val', outputColumnId: 'val' }
|
{ inputColumnId: 'val', outputColumnId: 'val' }
|
||||||
)
|
)
|
||||||
).toThrow();
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,129 +6,163 @@
|
||||||
* Side Public License, v 1.
|
* Side Public License, v 1.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { errors, math, MathArguments, MathInput } from '../math';
|
import { math, MathArguments, MathInput } from '../math';
|
||||||
|
import { errors } from '../math_fn';
|
||||||
import { emptyTable, functionWrapper, testTable } from './utils';
|
import { emptyTable, functionWrapper, testTable } from './utils';
|
||||||
|
|
||||||
describe('math', () => {
|
describe('math', () => {
|
||||||
const fn = functionWrapper(math);
|
const fn = functionWrapper(math);
|
||||||
|
|
||||||
it('evaluates math expressions without reference to context', () => {
|
it('evaluates math expressions without reference to context', async () => {
|
||||||
expect(fn(null as unknown as MathInput, { expression: '10.5345' })).toBe(10.5345);
|
expect(await fn(null as unknown as MathInput, { expression: '10.5345' })).toBe(10.5345);
|
||||||
expect(fn(null as unknown as MathInput, { expression: '123 + 456' })).toBe(579);
|
expect(await fn(null as unknown as MathInput, { expression: '123 + 456' })).toBe(579);
|
||||||
expect(fn(null as unknown as MathInput, { expression: '100 - 46' })).toBe(54);
|
expect(await fn(null as unknown as MathInput, { expression: '100 - 46' })).toBe(54);
|
||||||
expect(fn(1, { expression: '100 / 5' })).toBe(20);
|
expect(await fn(1, { expression: '100 / 5' })).toBe(20);
|
||||||
expect(fn('foo' as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
expect(await fn('foo' as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
||||||
expect(fn(true as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
expect(await fn(true as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
||||||
expect(fn(testTable, { expression: '100 * 5' })).toBe(500);
|
expect(await fn(testTable, { expression: '100 * 5' })).toBe(500);
|
||||||
expect(fn(emptyTable, { expression: '100 * 5' })).toBe(500);
|
expect(await fn(emptyTable, { expression: '100 * 5' })).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('evaluates math expressions with reference to the value of the context, must be a number', () => {
|
it('evaluates math expressions with reference to the value of the context, must be a number', async () => {
|
||||||
expect(fn(-103, { expression: 'abs(value)' })).toBe(103);
|
expect(await fn(-103, { expression: 'abs(value)' })).toBe(103);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('evaluates math expressions with references to columns by id in a datatable', () => {
|
it('evaluates math expressions with references to columns by id in a datatable', async () => {
|
||||||
expect(fn(testTable, { expression: 'unique(in_stock)' })).toBe(2);
|
expect(await fn(testTable, { expression: 'unique(in_stock)' })).toBe(2);
|
||||||
expect(fn(testTable, { expression: 'sum(quantity)' })).toBe(2508);
|
expect(await fn(testTable, { expression: 'sum(quantity)' })).toBe(2508);
|
||||||
expect(fn(testTable, { expression: 'mean(price)' })).toBe(320);
|
expect(await fn(testTable, { expression: 'mean(price)' })).toBe(320);
|
||||||
expect(fn(testTable, { expression: 'min(price)' })).toBe(67);
|
expect(await fn(testTable, { expression: 'min(price)' })).toBe(67);
|
||||||
expect(fn(testTable, { expression: 'median(quantity)' })).toBe(256);
|
expect(await fn(testTable, { expression: 'median(quantity)' })).toBe(256);
|
||||||
expect(fn(testTable, { expression: 'max(price)' })).toBe(605);
|
expect(await fn(testTable, { expression: 'max(price)' })).toBe(605);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not use the name for math', () => {
|
it('does not use the name for math', async () => {
|
||||||
expect(() => fn(testTable, { expression: 'unique("in_stock label")' })).toThrow(
|
await expect(fn(testTable, { expression: 'unique("in_stock label")' })).rejects.toHaveProperty(
|
||||||
'Unknown variable'
|
'message',
|
||||||
|
'Unknown variable: in_stock label'
|
||||||
);
|
);
|
||||||
expect(() => fn(testTable, { expression: 'sum("quantity label")' })).toThrow(
|
|
||||||
'Unknown variable'
|
await expect(fn(testTable, { expression: 'sum("quantity label")' })).rejects.toHaveProperty(
|
||||||
|
'message',
|
||||||
|
'Unknown variable: quantity label'
|
||||||
);
|
);
|
||||||
expect(() => fn(testTable, { expression: 'mean("price label")' })).toThrow('Unknown variable');
|
|
||||||
expect(() => fn(testTable, { expression: 'min("price label")' })).toThrow('Unknown variable');
|
await expect(fn(testTable, { expression: 'mean("price label")' })).rejects.toHaveProperty(
|
||||||
expect(() => fn(testTable, { expression: 'median("quantity label")' })).toThrow(
|
'message',
|
||||||
'Unknown variable'
|
'Unknown variable: price label'
|
||||||
|
);
|
||||||
|
await expect(fn(testTable, { expression: 'min("price label")' })).rejects.toHaveProperty(
|
||||||
|
'message',
|
||||||
|
'Unknown variable: price label'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fn(testTable, { expression: 'median("quantity label")' })).rejects.toHaveProperty(
|
||||||
|
'message',
|
||||||
|
'Unknown variable: quantity label'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fn(testTable, { expression: 'max("price label")' })).rejects.toHaveProperty(
|
||||||
|
'message',
|
||||||
|
'Unknown variable: price label'
|
||||||
);
|
);
|
||||||
expect(() => fn(testTable, { expression: 'max("price label")' })).toThrow('Unknown variable');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('args', () => {
|
describe('args', () => {
|
||||||
describe('expression', () => {
|
describe('expression', () => {
|
||||||
it('sets the math expression to be evaluted', () => {
|
it('sets the math expression to be evaluted', async () => {
|
||||||
expect(fn(null as unknown as MathInput, { expression: '10' })).toBe(10);
|
expect(await fn(null as unknown as MathInput, { expression: '10' })).toBe(10);
|
||||||
expect(fn(23.23, { expression: 'floor(value)' })).toBe(23);
|
expect(await fn(23.23, { expression: 'floor(value)' })).toBe(23);
|
||||||
expect(fn(testTable, { expression: 'count(price)' })).toBe(9);
|
expect(await fn(testTable, { expression: 'count(price)' })).toBe(9);
|
||||||
expect(fn(testTable, { expression: 'count(name)' })).toBe(9);
|
expect(await fn(testTable, { expression: 'count(name)' })).toBe(9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onError', () => {
|
describe('onError', () => {
|
||||||
it('should return the desired fallback value, for invalid expressions', () => {
|
it('should return the desired fallback value, for invalid expressions', async () => {
|
||||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'zero' })).toBe(0);
|
expect(await fn(testTable, { expression: 'mean(name)', onError: 'zero' })).toBe(0);
|
||||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'null' })).toBe(null);
|
expect(await fn(testTable, { expression: 'mean(name)', onError: 'null' })).toBe(null);
|
||||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'false' })).toBe(false);
|
expect(await fn(testTable, { expression: 'mean(name)', onError: 'false' })).toBe(false);
|
||||||
});
|
});
|
||||||
it('should return the desired fallback value, for division by zero', () => {
|
it('should return the desired fallback value, for division by zero', async () => {
|
||||||
expect(fn(testTable, { expression: '1/0', onError: 'zero' })).toBe(0);
|
expect(await fn(testTable, { expression: '1/0', onError: 'zero' })).toBe(0);
|
||||||
expect(fn(testTable, { expression: '1/0', onError: 'null' })).toBe(null);
|
expect(await fn(testTable, { expression: '1/0', onError: 'null' })).toBe(null);
|
||||||
expect(fn(testTable, { expression: '1/0', onError: 'false' })).toBe(false);
|
expect(await fn(testTable, { expression: '1/0', onError: 'false' })).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid expressions', () => {
|
describe('invalid expressions', () => {
|
||||||
it('throws when expression evaluates to an array', () => {
|
it('throws when expression evaluates to an array', async () => {
|
||||||
expect(() => fn(testTable, { expression: 'multiply(price, 2)' })).toThrow(
|
await expect(fn(testTable, { expression: 'multiply(price, 2)' })).rejects.toHaveProperty(
|
||||||
new RegExp(errors.tooManyResults().message.replace(/[()]/g, '\\$&'))
|
'message',
|
||||||
|
errors.tooManyResults().message
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when using an unknown context variable', () => {
|
it('throws when using an unknown context variable', async () => {
|
||||||
expect(() => fn(testTable, { expression: 'sum(foo)' })).toThrow('Unknown variable: foo');
|
await expect(fn(testTable, { expression: 'sum(foo)' })).rejects.toHaveProperty(
|
||||||
});
|
'message',
|
||||||
|
'Unknown variable: foo'
|
||||||
it('throws when using non-numeric data', () => {
|
|
||||||
expect(() => fn(testTable, { expression: 'mean(name)' })).toThrow(
|
|
||||||
new RegExp(errors.executionFailed().message)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(() => fn(testTable, { expression: 'mean(in_stock)' })).toThrow(
|
|
||||||
new RegExp(errors.executionFailed().message)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when missing expression', () => {
|
it('throws when using non-numeric data', async () => {
|
||||||
expect(() => fn(testTable)).toThrow(new RegExp(errors.emptyExpression().message));
|
await expect(fn(testTable, { expression: 'mean(name)' })).rejects.toHaveProperty(
|
||||||
|
'message',
|
||||||
expect(() => fn(testTable, { expession: '' } as unknown as MathArguments)).toThrow(
|
errors.executionFailed().message
|
||||||
new RegExp(errors.emptyExpression().message)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(() => fn(testTable, { expession: ' ' } as unknown as MathArguments)).toThrow(
|
await expect(fn(testTable, { expression: 'mean(in_stock)' })).rejects.toHaveProperty(
|
||||||
new RegExp(errors.emptyExpression().message)
|
'message',
|
||||||
|
errors.executionFailed().message
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when passing a context variable from an empty datatable', () => {
|
it('throws when missing expression', async () => {
|
||||||
expect(() => fn(emptyTable, { expression: 'mean(foo)' })).toThrow(
|
await expect(fn(testTable)).rejects.toHaveProperty(
|
||||||
new RegExp(errors.emptyDatatable().message)
|
'message',
|
||||||
|
errors.emptyExpression().message
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fn(testTable, { expession: '' } as unknown as MathArguments)
|
||||||
|
).rejects.toHaveProperty('message', errors.emptyExpression().message);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fn(testTable, { expession: ' ' } as unknown as MathArguments)
|
||||||
|
).rejects.toHaveProperty('message', errors.emptyExpression().message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when passing a context variable from an empty datatable', async () => {
|
||||||
|
await expect(() => fn(emptyTable, { expression: 'mean(foo)' })).rejects.toHaveProperty(
|
||||||
|
'message',
|
||||||
|
errors.emptyDatatable().message
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw when requesting fallback values for invalid expression', () => {
|
it('should not throw when requesting fallback values for invalid expression', async () => {
|
||||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'zero' })).not.toThrow();
|
await expect(
|
||||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'false' })).not.toThrow();
|
fn(testTable, { expression: 'mean(name)', onError: 'zero' })
|
||||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'null' })).not.toThrow();
|
).resolves.toBeDefined();
|
||||||
|
await expect(
|
||||||
|
fn(testTable, { expression: 'mean(name)', onError: 'false' })
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
await expect(
|
||||||
|
fn(testTable, { expression: 'mean(name)', onError: 'null' })
|
||||||
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when declared in the onError argument', () => {
|
it('should throw when declared in the onError argument', async () => {
|
||||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'throw' })).toThrow(
|
await expect(
|
||||||
new RegExp(errors.executionFailed().message)
|
fn(testTable, { expression: 'mean(name)', onError: 'throw' })
|
||||||
);
|
).rejects.toHaveProperty('message', errors.executionFailed().message);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when dividing by zero', () => {
|
it('should throw when dividing by zero', async () => {
|
||||||
expect(() => fn(testTable, { expression: '1/0', onError: 'throw' })).toThrow(
|
await expect(fn(testTable, { expression: '1/0', onError: 'throw' })).rejects.toHaveProperty(
|
||||||
new RegExp('Cannot divide by 0')
|
'message',
|
||||||
|
'Cannot divide by 0'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,14 +12,18 @@ import { functionWrapper, testTable, tableWithNulls } from './utils';
|
||||||
describe('mathColumn', () => {
|
describe('mathColumn', () => {
|
||||||
const fn = functionWrapper(mathColumn);
|
const fn = functionWrapper(mathColumn);
|
||||||
|
|
||||||
it('throws if the id is used', () => {
|
it('throws if the id is used', async () => {
|
||||||
expect(() => fn(testTable, { id: 'price', name: 'price', expression: 'price * 2' })).toThrow(
|
await expect(
|
||||||
`ID must be unique`
|
fn(testTable, { id: 'price', name: 'price', expression: 'price * 2' })
|
||||||
);
|
).rejects.toHaveProperty('message', `ID must be unique`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies math to each row by id', () => {
|
it('applies math to each row by id', async () => {
|
||||||
const result = fn(testTable, { id: 'output', name: 'output', expression: 'quantity * price' });
|
const result = await fn(testTable, {
|
||||||
|
id: 'output',
|
||||||
|
name: 'output',
|
||||||
|
expression: 'quantity * price',
|
||||||
|
});
|
||||||
expect(result.columns).toEqual([
|
expect(result.columns).toEqual([
|
||||||
...testTable.columns,
|
...testTable.columns,
|
||||||
{ id: 'output', name: 'output', meta: { params: { id: 'number' }, type: 'number' } },
|
{ id: 'output', name: 'output', meta: { params: { id: 'number' }, type: 'number' } },
|
||||||
|
@ -34,7 +38,7 @@ describe('mathColumn', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('extracts a single array value, but not a multi-value array', () => {
|
it('extracts a single array value, but not a multi-value array', async () => {
|
||||||
const arrayTable = {
|
const arrayTable = {
|
||||||
...testTable,
|
...testTable,
|
||||||
rows: [
|
rows: [
|
||||||
|
@ -52,23 +56,24 @@ describe('mathColumn', () => {
|
||||||
name: 'output',
|
name: 'output',
|
||||||
expression: 'quantity',
|
expression: 'quantity',
|
||||||
};
|
};
|
||||||
expect(fn(arrayTable, args).rows[0].output).toEqual(100);
|
expect((await fn(arrayTable, args)).rows[0].output).toEqual(100);
|
||||||
expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError(
|
await expect(fn(arrayTable, { ...args, expression: 'price' })).rejects.toHaveProperty(
|
||||||
`Cannot perform math on array values`
|
'message',
|
||||||
|
`Cannot perform math on array values at output`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles onError', () => {
|
it('handles onError', async () => {
|
||||||
const args = {
|
const args = {
|
||||||
id: 'output',
|
id: 'output',
|
||||||
name: 'output',
|
name: 'output',
|
||||||
expression: 'quantity / 0',
|
expression: 'quantity / 0',
|
||||||
};
|
};
|
||||||
expect(() => fn(testTable, args)).toThrowError(`Cannot divide by 0`);
|
await expect(fn(testTable, args)).rejects.toHaveProperty('message', `Cannot divide by 0`);
|
||||||
expect(() => fn(testTable, { ...args, onError: 'throw' })).toThrow();
|
await expect(fn(testTable, { ...args, onError: 'throw' })).rejects.toBeDefined();
|
||||||
expect(fn(testTable, { ...args, onError: 'zero' }).rows[0].output).toEqual(0);
|
expect((await fn(testTable, { ...args, onError: 'zero' })).rows[0].output).toEqual(0);
|
||||||
expect(fn(testTable, { ...args, onError: 'false' }).rows[0].output).toEqual(false);
|
expect((await fn(testTable, { ...args, onError: 'false' })).rows[0].output).toEqual(false);
|
||||||
expect(fn(testTable, { ...args, onError: 'null' }).rows[0].output).toEqual(null);
|
expect((await fn(testTable, { ...args, onError: 'null' })).rows[0].output).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should copy over the meta information from the specified column', async () => {
|
it('should copy over the meta information from the specified column', async () => {
|
||||||
|
@ -96,9 +101,14 @@ describe('mathColumn', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly infer the type from the first non-null row', () => {
|
it('should correctly infer the type from the first non-null row', async () => {
|
||||||
expect(
|
expect(
|
||||||
fn(tableWithNulls, { id: 'value', name: 'value', expression: 'price + 2', onError: 'null' })
|
await fn(tableWithNulls, {
|
||||||
|
id: 'value',
|
||||||
|
name: 'value',
|
||||||
|
expression: 'price + 2',
|
||||||
|
onError: 'null',
|
||||||
|
})
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
|
|
|
@ -16,10 +16,10 @@ const defaultArgs = { window: 5, inputColumnId: 'val', outputColumnId: 'output'
|
||||||
describe('interpreter/functions#movingAverage', () => {
|
describe('interpreter/functions#movingAverage', () => {
|
||||||
const fn = functionWrapper(movingAverage);
|
const fn = functionWrapper(movingAverage);
|
||||||
const runFn = (input: Datatable, args: MovingAverageArgs) =>
|
const runFn = (input: Datatable, args: MovingAverageArgs) =>
|
||||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||||
|
|
||||||
it('calculates movingAverage', () => {
|
it('calculates movingAverage', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -40,8 +40,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips null or undefined values until there is real data', () => {
|
it('skips null or undefined values until there is real data', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -77,8 +77,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats 0 as real data', () => {
|
it('treats 0 as real data', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -115,8 +115,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates movingAverage for multiple series', () => {
|
it('calculates movingAverage for multiple series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -149,8 +149,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats missing split column as separate series', () => {
|
it('treats missing split column as separate series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -182,8 +182,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats null like undefined and empty string for split columns', () => {
|
it('treats null like undefined and empty string for split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -217,8 +217,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates movingAverage for multiple series by multiple split columns', () => {
|
it('calculates movingAverage for multiple series by multiple split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -251,8 +251,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('splits separate series by the string representation of the cell values', () => {
|
it('splits separate series by the string representation of the cell values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -272,8 +272,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 1, undefined, 10]);
|
expect(result.rows.map((row) => row.output)).toEqual([undefined, 1, undefined, 10]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('casts values to number before calculating movingAverage', () => {
|
it('casts values to number before calculating movingAverage', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -289,8 +289,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips NaN like values', () => {
|
it('skips NaN like values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -301,8 +301,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 5, (5 + 7) / 2, NaN, NaN]);
|
expect(result.rows.map((row) => row.output)).toEqual([undefined, 5, (5 + 7) / 2, NaN, NaN]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('copies over meta information from the source column', () => {
|
it('copies over meta information from the source column', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -343,8 +343,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets output name on output column if specified', () => {
|
it('sets output name on output column if specified', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -367,7 +367,7 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns source table if input column does not exist', () => {
|
it('returns source table if input column does not exist', async () => {
|
||||||
const input: Datatable = {
|
const input: Datatable = {
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -382,12 +382,12 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
rows: [{ val: 5 }],
|
rows: [{ val: 5 }],
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
runFn(input, { ...defaultArgs, inputColumnId: 'nonexisting', outputColumnId: 'output' })
|
await runFn(input, { ...defaultArgs, inputColumnId: 'nonexisting', outputColumnId: 'output' })
|
||||||
).toBe(input);
|
).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if output column exists already', () => {
|
it('throws an error if output column exists already', async () => {
|
||||||
expect(() =>
|
await expect(
|
||||||
runFn(
|
runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
|
@ -404,11 +404,11 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
},
|
},
|
||||||
{ ...defaultArgs, inputColumnId: 'val', outputColumnId: 'val' }
|
{ ...defaultArgs, inputColumnId: 'val', outputColumnId: 'val' }
|
||||||
)
|
)
|
||||||
).toThrow();
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates moving average for window equal to 1', () => {
|
it('calculates moving average for window equal to 1', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -441,8 +441,8 @@ describe('interpreter/functions#movingAverage', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates moving average for window bigger than array', () => {
|
it('calculates moving average for window bigger than array', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { overallMetric, OverallMetricArgs } from '../overall_metric';
|
||||||
describe('interpreter/functions#overall_metric', () => {
|
describe('interpreter/functions#overall_metric', () => {
|
||||||
const fn = functionWrapper(overallMetric);
|
const fn = functionWrapper(overallMetric);
|
||||||
const runFn = (input: Datatable, args: OverallMetricArgs) =>
|
const runFn = (input: Datatable, args: OverallMetricArgs) =>
|
||||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||||
|
|
||||||
it('ignores null or undefined with sum', () => {
|
it('ignores null or undefined with sum', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -33,8 +33,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([12, 12, 12, 12]);
|
expect(result.rows.map((row) => row.output)).toEqual([12, 12, 12, 12]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores null or undefined with average', () => {
|
it('ignores null or undefined with average', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -50,8 +50,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([3, 3, 3, 3, 3]);
|
expect(result.rows.map((row) => row.output)).toEqual([3, 3, 3, 3, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores null or undefined with min', () => {
|
it('ignores null or undefined with min', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -67,8 +67,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([1, 1, 1, 1, 1]);
|
expect(result.rows.map((row) => row.output)).toEqual([1, 1, 1, 1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores null or undefined with max', () => {
|
it('ignores null or undefined with max', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -84,8 +84,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([-1, -1, -1, -1, -1]);
|
expect(result.rows.map((row) => row.output)).toEqual([-1, -1, -1, -1, -1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates overall sum for multiple series', () => {
|
it('calculates overall sum for multiple series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -118,8 +118,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats missing split column as separate series', () => {
|
it('treats missing split column as separate series', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -142,7 +142,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1, 3, 1, 2, 2]);
|
expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1, 3, 1, 2, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats null like undefined and empty string for split columns', () => {
|
it('treats null like undefined and empty string for split columns', async () => {
|
||||||
const table: Datatable = {
|
const table: Datatable = {
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -162,7 +162,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = runFn(table, {
|
const result = await runFn(table, {
|
||||||
inputColumnId: 'val',
|
inputColumnId: 'val',
|
||||||
outputColumnId: 'output',
|
outputColumnId: 'output',
|
||||||
by: ['split'],
|
by: ['split'],
|
||||||
|
@ -180,7 +180,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
3 + 5 + 7 + 9,
|
3 + 5 + 7 + 9,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result2 = runFn(table, {
|
const result2 = await runFn(table, {
|
||||||
inputColumnId: 'val',
|
inputColumnId: 'val',
|
||||||
outputColumnId: 'output',
|
outputColumnId: 'output',
|
||||||
by: ['split'],
|
by: ['split'],
|
||||||
|
@ -188,7 +188,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
});
|
});
|
||||||
expect(result2.rows.map((row) => row.output)).toEqual([6, 8, 9, 6, 9, 6, 9, 8, 9]);
|
expect(result2.rows.map((row) => row.output)).toEqual([6, 8, 9, 6, 9, 6, 9, 8, 9]);
|
||||||
|
|
||||||
const result3 = runFn(table, {
|
const result3 = await runFn(table, {
|
||||||
inputColumnId: 'val',
|
inputColumnId: 'val',
|
||||||
outputColumnId: 'output',
|
outputColumnId: 'output',
|
||||||
by: ['split'],
|
by: ['split'],
|
||||||
|
@ -207,8 +207,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles array values', () => {
|
it('handles array values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -224,8 +224,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([28, 28, 28, 28]);
|
expect(result.rows.map((row) => row.output)).toEqual([28, 28, 28, 28]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('takes array values into account for average calculation', () => {
|
it('takes array values into account for average calculation', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -241,7 +241,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([3, 3]);
|
expect(result.rows.map((row) => row.output)).toEqual([3, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles array values for split columns', () => {
|
it('handles array values for split columns', async () => {
|
||||||
const table: Datatable = {
|
const table: Datatable = {
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -261,7 +261,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = runFn(table, {
|
const result = await runFn(table, {
|
||||||
inputColumnId: 'val',
|
inputColumnId: 'val',
|
||||||
outputColumnId: 'output',
|
outputColumnId: 'output',
|
||||||
by: ['split'],
|
by: ['split'],
|
||||||
|
@ -279,7 +279,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
3 + 5 + 7 + 9 + 99,
|
3 + 5 + 7 + 9 + 99,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result2 = runFn(table, {
|
const result2 = await runFn(table, {
|
||||||
inputColumnId: 'val',
|
inputColumnId: 'val',
|
||||||
outputColumnId: 'output',
|
outputColumnId: 'output',
|
||||||
by: ['split'],
|
by: ['split'],
|
||||||
|
@ -288,8 +288,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result2.rows.map((row) => row.output)).toEqual([6, 11, 99, 6, 99, 6, 99, 11, 99]);
|
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', () => {
|
it('calculates cumulative sum for multiple series by multiple split columns', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -313,8 +313,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([1 + 4, 2, 3, 1 + 4, 5, 6, 7 + 8, 7 + 8]);
|
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', () => {
|
it('splits separate series by the string representation of the cell values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -334,8 +334,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([1 + 2, 1 + 2, 10 + 11, 10 + 11]);
|
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', () => {
|
it('casts values to number before calculating cumulative sum', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -346,8 +346,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([7, 7, 7, 7]);
|
expect(result.rows.map((row) => row.output)).toEqual([7, 7, 7, 7]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('casts values to number before calculating metric for NaN like values', () => {
|
it('casts values to number before calculating metric for NaN like values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -358,8 +358,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([NaN, NaN, NaN, NaN]);
|
expect(result.rows.map((row) => row.output)).toEqual([NaN, NaN, NaN, NaN]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips undefined and null values', () => {
|
it('skips undefined and null values', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||||
|
@ -380,8 +380,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
expect(result.rows.map((row) => row.output)).toEqual([4, 4, 4, 4, 4, 4, 4, 4, 4]);
|
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', () => {
|
it('copies over meta information from the source column', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -422,8 +422,8 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets output name on output column if specified', () => {
|
it('sets output name on output column if specified', async () => {
|
||||||
const result = runFn(
|
const result = await runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -451,7 +451,7 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns source table if input column does not exist', () => {
|
it('returns source table if input column does not exist', async () => {
|
||||||
const input: Datatable = {
|
const input: Datatable = {
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -466,12 +466,12 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
rows: [{ val: 5 }],
|
rows: [{ val: 5 }],
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' })
|
await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' })
|
||||||
).toBe(input);
|
).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if output column exists already', () => {
|
it('throws an error if output column exists already', async () => {
|
||||||
expect(() =>
|
await expect(
|
||||||
runFn(
|
runFn(
|
||||||
{
|
{
|
||||||
type: 'datatable',
|
type: 'datatable',
|
||||||
|
@ -488,6 +488,6 @@ describe('interpreter/functions#overall_metric', () => {
|
||||||
},
|
},
|
||||||
{ inputColumnId: 'val', outputColumnId: 'val', metric: 'max' }
|
{ inputColumnId: 'val', outputColumnId: 'val', metric: 'max' }
|
||||||
)
|
)
|
||||||
).toThrow();
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,20 +22,23 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static exports.
|
// Static exports.
|
||||||
export { ExpressionExecutor, IExpressionLoaderParams, ExpressionRenderError } from './types';
|
export type {
|
||||||
export {
|
ExpressionExecutor,
|
||||||
|
IExpressionLoaderParams,
|
||||||
|
ExpressionRenderError,
|
||||||
|
ExpressionRendererEvent,
|
||||||
|
} from './types';
|
||||||
|
export type { ExpressionLoader } from './loader';
|
||||||
|
export type { ExpressionRenderHandler } from './render';
|
||||||
|
export type {
|
||||||
ExpressionRendererComponent,
|
ExpressionRendererComponent,
|
||||||
ReactExpressionRenderer,
|
|
||||||
ReactExpressionRendererProps,
|
ReactExpressionRendererProps,
|
||||||
ReactExpressionRendererType,
|
ReactExpressionRendererType,
|
||||||
} from './react_expression_renderer';
|
} from './react_expression_renderer';
|
||||||
export { ExpressionRenderHandler, ExpressionRendererEvent } from './render';
|
export type {
|
||||||
export {
|
|
||||||
AnyExpressionFunctionDefinition,
|
AnyExpressionFunctionDefinition,
|
||||||
AnyExpressionTypeDefinition,
|
AnyExpressionTypeDefinition,
|
||||||
ArgumentType,
|
ArgumentType,
|
||||||
buildExpression,
|
|
||||||
buildExpressionFunction,
|
|
||||||
Datatable,
|
Datatable,
|
||||||
DatatableColumn,
|
DatatableColumn,
|
||||||
DatatableColumnType,
|
DatatableColumnType,
|
||||||
|
@ -79,17 +82,12 @@ export {
|
||||||
FontStyle,
|
FontStyle,
|
||||||
FontValue,
|
FontValue,
|
||||||
FontWeight,
|
FontWeight,
|
||||||
format,
|
|
||||||
formatExpression,
|
|
||||||
FunctionsRegistry,
|
FunctionsRegistry,
|
||||||
IInterpreterRenderHandlers,
|
IInterpreterRenderHandlers,
|
||||||
InterpreterErrorType,
|
InterpreterErrorType,
|
||||||
IRegistry,
|
IRegistry,
|
||||||
isExpressionAstBuilder,
|
|
||||||
KnownTypeToString,
|
KnownTypeToString,
|
||||||
Overflow,
|
Overflow,
|
||||||
parse,
|
|
||||||
parseExpression,
|
|
||||||
PointSeries,
|
PointSeries,
|
||||||
PointSeriesColumn,
|
PointSeriesColumn,
|
||||||
PointSeriesColumnName,
|
PointSeriesColumnName,
|
||||||
|
@ -109,6 +107,13 @@ export {
|
||||||
ExpressionsServiceSetup,
|
ExpressionsServiceSetup,
|
||||||
ExpressionsServiceStart,
|
ExpressionsServiceStart,
|
||||||
TablesAdapter,
|
TablesAdapter,
|
||||||
ExpressionsInspectorAdapter,
|
} from '../common';
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildExpression,
|
||||||
|
buildExpressionFunction,
|
||||||
|
formatExpression,
|
||||||
|
isExpressionAstBuilder,
|
||||||
|
parseExpression,
|
||||||
createDefaultInspectorAdapters,
|
createDefaultInspectorAdapters,
|
||||||
} from '../common';
|
} from '../common';
|
||||||
|
|
|
@ -88,8 +88,8 @@ jest.mock('./services', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute helper function', () => {
|
describe('execute helper function', () => {
|
||||||
it('returns ExpressionLoader instance', () => {
|
it('returns ExpressionLoader instance', async () => {
|
||||||
const response = loader(element, '', {});
|
const response = await loader(element, '', {});
|
||||||
expect(response).toBeInstanceOf(ExpressionLoader);
|
expect(response).toBeInstanceOf(ExpressionLoader);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -194,10 +194,10 @@ export class ExpressionLoader {
|
||||||
|
|
||||||
export type IExpressionLoader = (
|
export type IExpressionLoader = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
expression: string | ExpressionAstExpression,
|
expression?: string | ExpressionAstExpression,
|
||||||
params: IExpressionLoaderParams
|
params?: IExpressionLoaderParams
|
||||||
) => ExpressionLoader;
|
) => Promise<ExpressionLoader>;
|
||||||
|
|
||||||
export const loader: IExpressionLoader = (element, expression, params) => {
|
export const loader: IExpressionLoader = async (element, expression?, params?) => {
|
||||||
return new ExpressionLoader(element, expression, params);
|
return new ExpressionLoader(element, expression, params);
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,8 +30,6 @@ const createSetupContract = (): Setup => {
|
||||||
const createStartContract = (): Start => {
|
const createStartContract = (): Start => {
|
||||||
return {
|
return {
|
||||||
execute: jest.fn(),
|
execute: jest.fn(),
|
||||||
ExpressionLoader: jest.fn(),
|
|
||||||
ExpressionRenderHandler: jest.fn(),
|
|
||||||
getFunction: jest.fn(),
|
getFunction: jest.fn(),
|
||||||
getFunctions: jest.fn(),
|
getFunctions: jest.fn(),
|
||||||
getRenderer: jest.fn(),
|
getRenderer: jest.fn(),
|
||||||
|
@ -39,8 +37,8 @@ const createStartContract = (): Start => {
|
||||||
getType: jest.fn(),
|
getType: jest.fn(),
|
||||||
getTypes: jest.fn(),
|
getTypes: jest.fn(),
|
||||||
loader: jest.fn(),
|
loader: jest.fn(),
|
||||||
ReactExpressionRenderer: jest.fn((props) => <></>),
|
|
||||||
render: jest.fn(),
|
render: jest.fn(),
|
||||||
|
ReactExpressionRenderer: jest.fn((props) => <></>),
|
||||||
run: jest.fn(),
|
run: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,16 +8,17 @@
|
||||||
|
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||||
import { ExpressionsServiceSetup, ExpressionsServiceStart } from '../common';
|
import { SerializableRecord } from '@kbn/utility-types';
|
||||||
|
import type { ExpressionsServiceSetup, ExpressionsServiceStart } from '../common';
|
||||||
import {
|
import {
|
||||||
ExpressionsService,
|
ExpressionsService,
|
||||||
setRenderersRegistry,
|
setRenderersRegistry,
|
||||||
setNotifications,
|
setNotifications,
|
||||||
setExpressionsService,
|
setExpressionsService,
|
||||||
} from './services';
|
} from './services';
|
||||||
import { ReactExpressionRenderer } from './react_expression_renderer';
|
import { ReactExpressionRenderer } from './react_expression_renderer_wrapper';
|
||||||
import { ExpressionLoader, IExpressionLoader, loader } from './loader';
|
import type { IExpressionLoader } from './loader';
|
||||||
import { render, ExpressionRenderHandler } from './render';
|
import type { IExpressionRenderer } from './render';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expressions public setup contract, extends {@link ExpressionsServiceSetup}
|
* Expressions public setup contract, extends {@link ExpressionsServiceSetup}
|
||||||
|
@ -28,11 +29,9 @@ export type ExpressionsSetup = ExpressionsServiceSetup;
|
||||||
* Expressions public start contrect, extends {@link ExpressionServiceStart}
|
* Expressions public start contrect, extends {@link ExpressionServiceStart}
|
||||||
*/
|
*/
|
||||||
export interface ExpressionsStart extends ExpressionsServiceStart {
|
export interface ExpressionsStart extends ExpressionsServiceStart {
|
||||||
ExpressionLoader: typeof ExpressionLoader;
|
|
||||||
ExpressionRenderHandler: typeof ExpressionRenderHandler;
|
|
||||||
loader: IExpressionLoader;
|
loader: IExpressionLoader;
|
||||||
|
render: IExpressionRenderer;
|
||||||
ReactExpressionRenderer: typeof ReactExpressionRenderer;
|
ReactExpressionRenderer: typeof ReactExpressionRenderer;
|
||||||
render: typeof render;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpressionsPublicPlugin implements Plugin<ExpressionsSetup, ExpressionsStart> {
|
export class ExpressionsPublicPlugin implements Plugin<ExpressionsSetup, ExpressionsStart> {
|
||||||
|
@ -66,13 +65,24 @@ export class ExpressionsPublicPlugin implements Plugin<ExpressionsSetup, Express
|
||||||
setNotifications(core.notifications);
|
setNotifications(core.notifications);
|
||||||
|
|
||||||
const { expressions } = this;
|
const { expressions } = this;
|
||||||
|
|
||||||
|
const loader: IExpressionLoader = async (element, expression, params) => {
|
||||||
|
const { ExpressionLoader } = await import('./loader');
|
||||||
|
return new ExpressionLoader(element, expression, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const render: IExpressionRenderer = async (element, data, options) => {
|
||||||
|
const { ExpressionRenderHandler } = await import('./render');
|
||||||
|
const handler = new ExpressionRenderHandler(element, options);
|
||||||
|
handler.render(data as SerializableRecord);
|
||||||
|
return handler;
|
||||||
|
};
|
||||||
|
|
||||||
const start = {
|
const start = {
|
||||||
...expressions.start(),
|
...expressions.start(),
|
||||||
ExpressionLoader,
|
|
||||||
ExpressionRenderHandler,
|
|
||||||
loader,
|
loader,
|
||||||
ReactExpressionRenderer,
|
|
||||||
render,
|
render,
|
||||||
|
ReactExpressionRenderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Object.freeze(start);
|
return Object.freeze(start);
|
||||||
|
|
|
@ -10,13 +10,12 @@ import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { share } from 'rxjs/operators';
|
import { share } from 'rxjs/operators';
|
||||||
import { ReactExpressionRenderer } from './react_expression_renderer';
|
import { default as ReactExpressionRenderer } from './react_expression_renderer';
|
||||||
import { ExpressionLoader } from './loader';
|
import { ExpressionLoader } from './loader';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { EuiProgress } from '@elastic/eui';
|
import { EuiProgress } from '@elastic/eui';
|
||||||
import { IInterpreterRenderHandlers } from '../common';
|
import { IInterpreterRenderHandlers } from '../common';
|
||||||
import { RenderErrorHandlerFnType } from './types';
|
import { RenderErrorHandlerFnType, ExpressionRendererEvent } from './types';
|
||||||
import { ExpressionRendererEvent } from './render';
|
|
||||||
|
|
||||||
jest.mock('./loader', () => {
|
jest.mock('./loader', () => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -13,10 +13,9 @@ import { filter } from 'rxjs/operators';
|
||||||
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
||||||
import { EuiLoadingChart, EuiProgress } from '@elastic/eui';
|
import { EuiLoadingChart, EuiProgress } from '@elastic/eui';
|
||||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||||
import { IExpressionLoaderParams, ExpressionRenderError } from './types';
|
import { IExpressionLoaderParams, ExpressionRenderError, ExpressionRendererEvent } from './types';
|
||||||
import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common';
|
import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common';
|
||||||
import { ExpressionLoader } from './loader';
|
import { ExpressionLoader } from './loader';
|
||||||
import { ExpressionRendererEvent } from './render';
|
|
||||||
|
|
||||||
// Accept all options of the runner as props except for the
|
// Accept all options of the runner as props except for the
|
||||||
// dom element which is provided by the component itself
|
// dom element which is provided by the component itself
|
||||||
|
@ -58,7 +57,8 @@ const defaultState: State = {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReactExpressionRenderer = ({
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default function ReactExpressionRenderer({
|
||||||
className,
|
className,
|
||||||
dataAttrs,
|
dataAttrs,
|
||||||
padding,
|
padding,
|
||||||
|
@ -69,7 +69,7 @@ export const ReactExpressionRenderer = ({
|
||||||
reload$,
|
reload$,
|
||||||
debounce,
|
debounce,
|
||||||
...expressionLoaderOptions
|
...expressionLoaderOptions
|
||||||
}: ReactExpressionRendererProps) => {
|
}: ReactExpressionRendererProps) {
|
||||||
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
|
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
|
||||||
const [state, setState] = useState<State>({ ...defaultState });
|
const [state, setState] = useState<State>({ ...defaultState });
|
||||||
const hasCustomRenderErrorHandler = !!renderError;
|
const hasCustomRenderErrorHandler = !!renderError;
|
||||||
|
@ -237,4 +237,4 @@ export const ReactExpressionRenderer = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { lazy, Suspense } from 'react';
|
||||||
|
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||||
|
import type { ReactExpressionRendererProps } from './react_expression_renderer';
|
||||||
|
|
||||||
|
const ReactExpressionRendererComponent = lazy(() => import('./react_expression_renderer'));
|
||||||
|
|
||||||
|
export const ReactExpressionRenderer = (props: ReactExpressionRendererProps) => (
|
||||||
|
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||||
|
<ReactExpressionRendererComponent {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
|
@ -53,8 +53,8 @@ const getHandledError = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('render helper function', () => {
|
describe('render helper function', () => {
|
||||||
it('returns ExpressionRenderHandler instance', () => {
|
it('returns ExpressionRenderHandler instance', async () => {
|
||||||
const response = render(element, {});
|
const response = await render(element, {});
|
||||||
expect(response).toBeInstanceOf(ExpressionRenderHandler);
|
expect(response).toBeInstanceOf(ExpressionRenderHandler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,14 +11,14 @@ import { Observable } from 'rxjs';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { SerializableRecord } from '@kbn/utility-types';
|
import { SerializableRecord } from '@kbn/utility-types';
|
||||||
import { ExpressionRenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types';
|
|
||||||
import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler';
|
|
||||||
import {
|
import {
|
||||||
IInterpreterRenderHandlers,
|
ExpressionRenderError,
|
||||||
IInterpreterRenderEvent,
|
RenderErrorHandlerFnType,
|
||||||
IInterpreterRenderUpdateParams,
|
IExpressionLoaderParams,
|
||||||
RenderMode,
|
ExpressionRendererEvent,
|
||||||
} from '../common';
|
} from './types';
|
||||||
|
import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler';
|
||||||
|
import { IInterpreterRenderHandlers, IInterpreterRenderUpdateParams, RenderMode } from '../common';
|
||||||
|
|
||||||
import { getRenderersRegistry } from './services';
|
import { getRenderersRegistry } from './services';
|
||||||
|
|
||||||
|
@ -32,9 +32,6 @@ export interface ExpressionRenderHandlerParams {
|
||||||
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
|
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export type ExpressionRendererEvent = IInterpreterRenderEvent<any>;
|
|
||||||
|
|
||||||
type UpdateValue = IInterpreterRenderUpdateParams<IExpressionLoaderParams>;
|
type UpdateValue = IInterpreterRenderUpdateParams<IExpressionLoaderParams>;
|
||||||
|
|
||||||
export class ExpressionRenderHandler {
|
export class ExpressionRenderHandler {
|
||||||
|
@ -154,12 +151,14 @@ export class ExpressionRenderHandler {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render(
|
export type IExpressionRenderer = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
data: unknown,
|
data: unknown,
|
||||||
options?: ExpressionRenderHandlerParams
|
options?: ExpressionRenderHandlerParams
|
||||||
): ExpressionRenderHandler {
|
) => Promise<ExpressionRenderHandler>;
|
||||||
|
|
||||||
|
export const render: IExpressionRenderer = async (element, data, options) => {
|
||||||
const handler = new ExpressionRenderHandler(element, options);
|
const handler = new ExpressionRenderHandler(element, options);
|
||||||
handler.render(data as SerializableRecord);
|
handler.render(data as SerializableRecord);
|
||||||
return handler;
|
return handler;
|
||||||
}
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
ExpressionValue,
|
ExpressionValue,
|
||||||
ExpressionsService,
|
ExpressionsService,
|
||||||
RenderMode,
|
RenderMode,
|
||||||
|
IInterpreterRenderEvent,
|
||||||
} from '../../common';
|
} from '../../common';
|
||||||
import { ExpressionRenderHandlerParams } from '../render';
|
import { ExpressionRenderHandlerParams } from '../render';
|
||||||
|
|
||||||
|
@ -75,3 +76,6 @@ export type RenderErrorHandlerFnType = (
|
||||||
error: ExpressionRenderError,
|
error: ExpressionRenderError,
|
||||||
handlers: IInterpreterRenderHandlers
|
handlers: IInterpreterRenderHandlers
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type ExpressionRendererEvent = IInterpreterRenderEvent<any>;
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
} from '../../../../plugins/embeddable/public';
|
} from '../../../../plugins/embeddable/public';
|
||||||
import {
|
import {
|
||||||
IExpressionLoaderParams,
|
IExpressionLoaderParams,
|
||||||
ExpressionsStart,
|
ExpressionLoader,
|
||||||
ExpressionRenderError,
|
ExpressionRenderError,
|
||||||
ExpressionAstExpression,
|
ExpressionAstExpression,
|
||||||
} from '../../../../plugins/expressions/public';
|
} from '../../../../plugins/expressions/public';
|
||||||
|
@ -81,8 +81,6 @@ export type VisualizeSavedObjectAttributes = SavedObjectAttributes & {
|
||||||
export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput;
|
export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput;
|
||||||
export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput;
|
export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput;
|
||||||
|
|
||||||
type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>;
|
|
||||||
|
|
||||||
export class VisualizeEmbeddable
|
export class VisualizeEmbeddable
|
||||||
extends Embeddable<VisualizeInput, VisualizeOutput>
|
extends Embeddable<VisualizeInput, VisualizeOutput>
|
||||||
implements ReferenceOrValueEmbeddable<VisualizeByValueInput, VisualizeByReferenceInput>
|
implements ReferenceOrValueEmbeddable<VisualizeByValueInput, VisualizeByReferenceInput>
|
||||||
|
@ -302,7 +300,7 @@ export class VisualizeEmbeddable
|
||||||
super.render(this.domNode);
|
super.render(this.domNode);
|
||||||
|
|
||||||
const expressions = getExpressions();
|
const expressions = getExpressions();
|
||||||
this.handler = new expressions.ExpressionLoader(this.domNode, undefined, {
|
this.handler = await expressions.loader(this.domNode, undefined, {
|
||||||
onRenderError: (element: HTMLElement, error: ExpressionRenderError) => {
|
onRenderError: (element: HTMLElement, error: ExpressionRenderError) => {
|
||||||
this.onContainerError(error);
|
this.onContainerError(error);
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,5 +9,5 @@
|
||||||
"requiredPlugins": ["data", "savedObjects", "kibanaUtils", "expressions"],
|
"requiredPlugins": ["data", "savedObjects", "kibanaUtils", "expressions"],
|
||||||
"server": true,
|
"server": true,
|
||||||
"ui": true,
|
"ui": true,
|
||||||
"requiredBundles": ["inspector"]
|
"requiredBundles": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,8 @@ import { first, pluck } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
IInterpreterRenderHandlers,
|
IInterpreterRenderHandlers,
|
||||||
ExpressionValue,
|
ExpressionValue,
|
||||||
TablesAdapter,
|
|
||||||
} from '../../../../../../../src/plugins/expressions/public';
|
} from '../../../../../../../src/plugins/expressions/public';
|
||||||
import { RequestAdapter } from '../../../../../../../src/plugins/inspector/public';
|
import { ExpressionRenderHandler } from '../../types';
|
||||||
import { Adapters, ExpressionRenderHandler } from '../../types';
|
|
||||||
import { getExpressions } from '../../services';
|
import { getExpressions } from '../../services';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -50,13 +48,9 @@ class Main extends React.Component<{}, State> {
|
||||||
initialContext: ExpressionValue = {}
|
initialContext: ExpressionValue = {}
|
||||||
) => {
|
) => {
|
||||||
this.setState({ expression });
|
this.setState({ expression });
|
||||||
const adapters: Adapters = {
|
|
||||||
requests: new RequestAdapter(),
|
|
||||||
tables: new TablesAdapter(),
|
|
||||||
};
|
|
||||||
return getExpressions()
|
return getExpressions()
|
||||||
.execute(expression, context || { type: 'null' }, {
|
.execute(expression, context || { type: 'null' }, {
|
||||||
inspectorAdapters: adapters,
|
|
||||||
searchContext: initialContext as any,
|
searchContext: initialContext as any,
|
||||||
})
|
})
|
||||||
.getData()
|
.getData()
|
||||||
|
@ -70,7 +64,7 @@ class Main extends React.Component<{}, State> {
|
||||||
lastRenderHandler.destroy();
|
lastRenderHandler.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRenderHandler = getExpressions().render(this.chartRef.current!, context, {
|
lastRenderHandler = await getExpressions().render(this.chartRef.current!, context, {
|
||||||
onRenderError: (el: HTMLElement, error: unknown, handler: IInterpreterRenderHandlers) => {
|
onRenderError: (el: HTMLElement, error: unknown, handler: IInterpreterRenderHandlers) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
expression: 'Render error!\n\n' + JSON.stringify(error),
|
expression: 'Render error!\n\n' + JSON.stringify(error),
|
||||||
|
|
|
@ -58,6 +58,7 @@ describe('lens_merge_tables', () => {
|
||||||
const adapters: DefaultInspectorAdapters = {
|
const adapters: DefaultInspectorAdapters = {
|
||||||
tables: new TablesAdapter(),
|
tables: new TablesAdapter(),
|
||||||
requests: {} as never,
|
requests: {} as never,
|
||||||
|
expression: {} as never,
|
||||||
};
|
};
|
||||||
mergeTables.fn(null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, {
|
mergeTables.fn(null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, {
|
||||||
inspectorAdapters: adapters,
|
inspectorAdapters: adapters,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue