mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Expressions] [Lens] Add id and copyMetaFrom arg to mapColumn fn + add configurable onError argument to math fn (#90481)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
27f6a3b3e7
commit
019473948f
17 changed files with 706 additions and 365 deletions
|
@ -1697,6 +1697,16 @@ Aliases: `column`, `name`
|
|||
Aliases: `exp`, `fn`, `function`
|
||||
|`boolean`, `number`, `string`, `null`
|
||||
|A Canvas expression that is passed to each row as a single row `datatable`.
|
||||
|
||||
|`id`
|
||||
|
||||
|`string`, `null`
|
||||
|An optional id of the resulting column. When not specified or `null` the name argument is used as id.
|
||||
|
||||
|`copyMetaFrom`
|
||||
|
||||
|`string`, `null`
|
||||
|If set, the meta object from the specified column id is copied over to the specified target column. Throws an exception if the column doesn't exist
|
||||
|===
|
||||
|
||||
*Returns:* `datatable`
|
||||
|
@ -1755,9 +1765,16 @@ Interprets a `TinyMath` math expression using a `number` or `datatable` as _cont
|
|||
Alias: `expression`
|
||||
|`string`
|
||||
|An evaluated `TinyMath` expression. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html.
|
||||
|
||||
|`onError`
|
||||
|
||||
|`string`
|
||||
|In case the `TinyMath` evaluation fails or returns NaN, the return value is specified by onError. For example, `"null"`, `"zero"`, `"false"`, `"throw"`. When `"throw"`, it will throw an exception, terminating expression execution.
|
||||
|
||||
Default: `"throw"`
|
||||
|===
|
||||
|
||||
*Returns:* `number`
|
||||
*Returns:* `number` | `boolean` | `null`
|
||||
|
||||
|
||||
[float]
|
||||
|
|
|
@ -15,6 +15,8 @@ import { theme } from './theme';
|
|||
import { cumulativeSum } from './cumulative_sum';
|
||||
import { derivative } from './derivative';
|
||||
import { movingAverage } from './moving_average';
|
||||
import { mapColumn } from './map_column';
|
||||
import { math } from './math';
|
||||
|
||||
export const functionSpecs: AnyExpressionFunctionDefinition[] = [
|
||||
clog,
|
||||
|
@ -25,6 +27,8 @@ export const functionSpecs: AnyExpressionFunctionDefinition[] = [
|
|||
cumulativeSum,
|
||||
derivative,
|
||||
movingAverage,
|
||||
mapColumn,
|
||||
math,
|
||||
];
|
||||
|
||||
export * from './clog';
|
||||
|
@ -35,3 +39,5 @@ export * from './theme';
|
|||
export * from './cumulative_sum';
|
||||
export * from './derivative';
|
||||
export * from './moving_average';
|
||||
export { mapColumn, MapColumnArguments } from './map_column';
|
||||
export { math, MathArguments, MathInput } from './math';
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable, getType } from '../../expression_types';
|
||||
|
||||
export interface MapColumnArguments {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
expression?: (datatable: Datatable) => Promise<boolean | number | string | null>;
|
||||
copyMetaFrom?: string | null;
|
||||
}
|
||||
|
||||
export const mapColumn: ExpressionFunctionDefinition<
|
||||
'mapColumn',
|
||||
Datatable,
|
||||
MapColumnArguments,
|
||||
Promise<Datatable>
|
||||
> = {
|
||||
name: 'mapColumn',
|
||||
aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file.
|
||||
type: 'datatable',
|
||||
inputTypes: ['datatable'],
|
||||
help: i18n.translate('expressions.functions.mapColumnHelpText', {
|
||||
defaultMessage:
|
||||
'Adds a column calculated as the result of other columns. ' +
|
||||
'Changes are made only when you provide arguments.' +
|
||||
'See also {alterColumnFn} and {staticColumnFn}.',
|
||||
values: {
|
||||
alterColumnFn: '`alterColumn`',
|
||||
staticColumnFn: '`staticColumn`',
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
id: {
|
||||
types: ['string', 'null'],
|
||||
help: i18n.translate('expressions.functions.mapColumn.args.idHelpText', {
|
||||
defaultMessage:
|
||||
'An optional id of the resulting column. When `null` the name/column argument is used as id.',
|
||||
}),
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
types: ['string'],
|
||||
aliases: ['_', 'column'],
|
||||
help: i18n.translate('expressions.functions.mapColumn.args.nameHelpText', {
|
||||
defaultMessage: 'The name of the resulting column.',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
expression: {
|
||||
types: ['boolean', 'number', 'string', 'null'],
|
||||
resolve: false,
|
||||
aliases: ['exp', 'fn', 'function'],
|
||||
help: i18n.translate('expressions.functions.mapColumn.args.expressionHelpText', {
|
||||
defaultMessage:
|
||||
'An expression that is executed on every row, provided with a single-row {DATATABLE} context and returning the cell value.',
|
||||
values: {
|
||||
DATATABLE: '`datatable`',
|
||||
},
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
copyMetaFrom: {
|
||||
types: ['string', 'null'],
|
||||
help: i18n.translate('expressions.functions.mapColumn.args.copyMetaFromHelpText', {
|
||||
defaultMessage:
|
||||
"If set, the meta object from the specified column id is copied over to the specified target column. If the column doesn't exist it silently fails.",
|
||||
}),
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
const expression = args.expression || (() => Promise.resolve(null));
|
||||
const columnId = args.id != null ? args.id : args.name;
|
||||
|
||||
const columns = [...input.columns];
|
||||
const rowPromises = input.rows.map((row) => {
|
||||
return expression({
|
||||
type: 'datatable',
|
||||
columns,
|
||||
rows: [row],
|
||||
}).then((val) => ({
|
||||
...row,
|
||||
[columnId]: val,
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(rowPromises).then((rows) => {
|
||||
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
|
||||
const type = rows.length ? getType(rows[0][columnId]) : 'null';
|
||||
const newColumn = {
|
||||
id: columnId,
|
||||
name: args.name,
|
||||
meta: { type },
|
||||
};
|
||||
if (args.copyMetaFrom) {
|
||||
const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom);
|
||||
newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta || {}) };
|
||||
}
|
||||
|
||||
if (existingColumnIndex === -1) {
|
||||
columns.push(newColumn);
|
||||
} else {
|
||||
columns[existingColumnIndex] = newColumn;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'datatable',
|
||||
columns,
|
||||
rows,
|
||||
} as Datatable;
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { evaluate } from '@kbn/tinymath';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable, isDatatable } from '../../expression_types';
|
||||
|
||||
export type MathArguments = {
|
||||
expression: string;
|
||||
onError?: 'null' | 'zero' | 'false' | 'throw';
|
||||
};
|
||||
|
||||
export type MathInput = number | Datatable;
|
||||
|
||||
const TINYMATH = '`TinyMath`';
|
||||
const TINYMATH_URL =
|
||||
'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html';
|
||||
|
||||
const isString = (val: any): boolean => typeof val === 'string';
|
||||
|
||||
function pivotObjectArray<
|
||||
RowType extends { [key: string]: any },
|
||||
ReturnColumns extends string | number | symbol = keyof RowType
|
||||
>(rows: RowType[], columns?: string[]): Record<string, 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);
|
||||
}
|
||||
|
||||
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<
|
||||
'math',
|
||||
MathInput,
|
||||
MathArguments,
|
||||
boolean | number | null
|
||||
> = {
|
||||
name: 'math',
|
||||
type: undefined,
|
||||
inputTypes: ['number', 'datatable'],
|
||||
help: i18n.translate('expressions.functions.mathHelpText', {
|
||||
defaultMessage:
|
||||
'Interprets a {TINYMATH} math expression using a {TYPE_NUMBER} or {DATATABLE} as {CONTEXT}. ' +
|
||||
'The {DATATABLE} columns are available by their column name. ' +
|
||||
'If the {CONTEXT} is a number it is available as {value}.',
|
||||
values: {
|
||||
TINYMATH,
|
||||
CONTEXT: '_context_',
|
||||
DATATABLE: '`datatable`',
|
||||
value: '`value`',
|
||||
TYPE_NUMBER: '`number`',
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
expression: {
|
||||
aliases: ['_'],
|
||||
types: ['string'],
|
||||
help: i18n.translate('expressions.functions.math.args.expressionHelpText', {
|
||||
defaultMessage: 'An evaluated {TINYMATH} expression. See {TINYMATH_URL}.',
|
||||
values: {
|
||||
TINYMATH,
|
||||
TINYMATH_URL,
|
||||
},
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
types: ['string'],
|
||||
options: ['throw', 'false', 'zero', 'null'],
|
||||
help: i18n.translate('expressions.functions.math.args.onErrorHelpText', {
|
||||
defaultMessage:
|
||||
"In case the {TINYMATH} evaluation fails or returns NaN, the return value is specified by onError. When `'throw'`, it will throw an exception, terminating expression execution (default).",
|
||||
values: {
|
||||
TINYMATH,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
const { expression, onError } = args;
|
||||
const onErrorValue = onError ?? 'throw';
|
||||
|
||||
if (!expression || expression.trim() === '') {
|
||||
throw errors.emptyExpression();
|
||||
}
|
||||
|
||||
const mathContext = isDatatable(input)
|
||||
? pivotObjectArray(
|
||||
input.rows,
|
||||
input.columns.map((col) => col.name)
|
||||
)
|
||||
: { 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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 { mapColumn, MapColumnArguments } from '../map_column';
|
||||
import { emptyTable, functionWrapper, testTable } from './utils';
|
||||
|
||||
const pricePlusTwo = (datatable: Datatable) => Promise.resolve(datatable.rows[0].price + 2);
|
||||
|
||||
describe('mapColumn', () => {
|
||||
const fn = functionWrapper(mapColumn);
|
||||
const runFn = (input: Datatable, args: MapColumnArguments) =>
|
||||
fn(input, args) as Promise<Datatable>;
|
||||
|
||||
it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => {
|
||||
return runFn(testTable, {
|
||||
id: 'pricePlusTwo',
|
||||
name: 'pricePlusTwo',
|
||||
expression: pricePlusTwo,
|
||||
}).then((result) => {
|
||||
const arbitraryRowIndex = 2;
|
||||
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toEqual([
|
||||
...testTable.columns,
|
||||
{ id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } },
|
||||
]);
|
||||
expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo');
|
||||
expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo');
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites existing column with the new column if an existing column name is provided', () => {
|
||||
return runFn(testTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
|
||||
const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
|
||||
const arbitraryRowIndex = 4;
|
||||
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(testTable.columns.length);
|
||||
expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name');
|
||||
expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number');
|
||||
expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a column to empty tables', () => {
|
||||
return runFn(emptyTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(1);
|
||||
expect(result.columns[0]).toHaveProperty('name', 'name');
|
||||
expect(result.columns[0].meta).toHaveProperty('type', 'null');
|
||||
});
|
||||
});
|
||||
|
||||
it('should assign specific id, different from name, when id arg is passed for new columns', () => {
|
||||
return runFn(emptyTable, { name: 'name', id: 'myid', expression: pricePlusTwo }).then(
|
||||
(result) => {
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(1);
|
||||
expect(result.columns[0]).toHaveProperty('name', 'name');
|
||||
expect(result.columns[0]).toHaveProperty('id', 'myid');
|
||||
expect(result.columns[0].meta).toHaveProperty('type', 'null');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should assign specific id, different from name, when id arg is passed for copied column', () => {
|
||||
return runFn(testTable, { name: 'name', id: 'myid', expression: pricePlusTwo }).then(
|
||||
(result) => {
|
||||
const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns[nameColumnIndex]).toEqual({
|
||||
id: 'myid',
|
||||
name: 'name',
|
||||
meta: { type: 'number' },
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should copy over the meta information from the specified column', () => {
|
||||
return runFn(
|
||||
{
|
||||
...testTable,
|
||||
columns: [
|
||||
...testTable.columns,
|
||||
// add a new entry
|
||||
{
|
||||
id: 'myId',
|
||||
name: 'myName',
|
||||
meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } },
|
||||
},
|
||||
],
|
||||
rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })),
|
||||
},
|
||||
{ name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo }
|
||||
).then((result) => {
|
||||
const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns[nameColumnIndex]).toEqual({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be resilient if the references column for meta information does not exists', () => {
|
||||
return runFn(emptyTable, { name: 'name', copyMetaFrom: 'time', expression: pricePlusTwo }).then(
|
||||
(result) => {
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(1);
|
||||
expect(result.columns[0]).toHaveProperty('name', 'name');
|
||||
expect(result.columns[0]).toHaveProperty('id', 'name');
|
||||
expect(result.columns[0].meta).toHaveProperty('type', 'null');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly infer the type fromt he first row if the references column for meta information does not exists', () => {
|
||||
return runFn(
|
||||
{ ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] },
|
||||
{ name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo }
|
||||
).then((result) => {
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(1);
|
||||
expect(result.columns[0]).toHaveProperty('name', 'value');
|
||||
expect(result.columns[0]).toHaveProperty('id', 'value');
|
||||
expect(result.columns[0].meta).toHaveProperty('type', 'number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('expression', () => {
|
||||
it('maps null values to the new column', () => {
|
||||
return runFn(testTable, { name: 'empty' }).then((result) => {
|
||||
const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty');
|
||||
const arbitraryRowIndex = 8;
|
||||
|
||||
expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty');
|
||||
expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null');
|
||||
expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,19 +1,16 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 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 { functionWrapper } from '../../../test_helpers/function_wrapper';
|
||||
import { getFunctionErrors } from '../../../i18n';
|
||||
import { emptyTable, testTable } from './__fixtures__/test_tables';
|
||||
import { math } from './math';
|
||||
|
||||
const errors = getFunctionErrors().math;
|
||||
import { errors, math } from '../math';
|
||||
import { emptyTable, functionWrapper, testTable } from './utils';
|
||||
|
||||
describe('math', () => {
|
||||
const fn = functionWrapper(math);
|
||||
const fn = functionWrapper<unknown>(math);
|
||||
|
||||
it('evaluates math expressions without reference to context', () => {
|
||||
expect(fn(null, { expression: '10.5345' })).toBe(10.5345);
|
||||
|
@ -48,6 +45,19 @@ describe('math', () => {
|
|||
expect(fn(testTable, { expression: 'count(name)' })).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onError', () => {
|
||||
it('should return the desired fallback value, for invalid expressions', () => {
|
||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'zero' })).toBe(0);
|
||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'null' })).toBe(null);
|
||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'false' })).toBe(false);
|
||||
});
|
||||
it('should return the desired fallback value, for division by zero', () => {
|
||||
expect(fn(testTable, { expression: '1/0', onError: 'zero' })).toBe(0);
|
||||
expect(fn(testTable, { expression: '1/0', onError: 'null' })).toBe(null);
|
||||
expect(fn(testTable, { expression: '1/0', onError: 'false' })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid expressions', () => {
|
||||
|
@ -88,5 +98,23 @@ describe('math', () => {
|
|||
new RegExp(errors.emptyDatatable().message)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw when requesting fallback values for invalid expression', () => {
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'zero' })).not.toThrow();
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'false' })).not.toThrow();
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'null' })).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw when declared in the onError argument', () => {
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'throw' })).toThrow(
|
||||
new RegExp(errors.executionFailed().message)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when dividing by zero', () => {
|
||||
expect(() => fn(testTable, { expression: '1/0', onError: 'throw' })).toThrow(
|
||||
new RegExp('Cannot divide by 0')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,16 +9,219 @@
|
|||
import { mapValues } from 'lodash';
|
||||
import { AnyExpressionFunctionDefinition } from '../../types';
|
||||
import { ExecutionContext } from '../../../execution/types';
|
||||
import { Datatable } from '../../../expression_types';
|
||||
|
||||
/**
|
||||
* Takes a function spec and passes in default args,
|
||||
* overriding with any provided args.
|
||||
*/
|
||||
export const functionWrapper = (spec: AnyExpressionFunctionDefinition) => {
|
||||
export const functionWrapper = <ContextType = object | null>(
|
||||
spec: AnyExpressionFunctionDefinition
|
||||
) => {
|
||||
const defaultArgs = mapValues(spec.args, (argSpec) => argSpec.default);
|
||||
return (
|
||||
context: object | null,
|
||||
context: ContextType,
|
||||
args: Record<string, any> = {},
|
||||
handlers: ExecutionContext = {} as ExecutionContext
|
||||
) => spec.fn(context, { ...defaultArgs, ...args }, handlers);
|
||||
};
|
||||
|
||||
const emptyTable: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [],
|
||||
rows: [],
|
||||
};
|
||||
|
||||
const testTable: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
meta: { type: 'string' },
|
||||
},
|
||||
{
|
||||
id: 'time',
|
||||
name: 'time',
|
||||
meta: { type: 'date' },
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
name: 'price',
|
||||
meta: { type: 'number' },
|
||||
},
|
||||
{
|
||||
id: 'quantity',
|
||||
name: 'quantity',
|
||||
meta: { type: 'number' },
|
||||
},
|
||||
{
|
||||
id: 'in_stock',
|
||||
name: 'in_stock',
|
||||
meta: { type: 'boolean' },
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: 'product1',
|
||||
time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
|
||||
price: 605,
|
||||
quantity: 100,
|
||||
in_stock: true,
|
||||
},
|
||||
{
|
||||
name: 'product1',
|
||||
time: 1517929200950, // 06 Feb 2018 15:00:00 GMT
|
||||
price: 583,
|
||||
quantity: 200,
|
||||
in_stock: true,
|
||||
},
|
||||
{
|
||||
name: 'product1',
|
||||
time: 1518015600950, // 07 Feb 2018 15:00:00 GMT
|
||||
price: 420,
|
||||
quantity: 300,
|
||||
in_stock: true,
|
||||
},
|
||||
{
|
||||
name: 'product2',
|
||||
time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
|
||||
price: 216,
|
||||
quantity: 350,
|
||||
in_stock: false,
|
||||
},
|
||||
{
|
||||
name: 'product2',
|
||||
time: 1517929200950, // 06 Feb 2018 15:00:00 GMT
|
||||
price: 200,
|
||||
quantity: 256,
|
||||
in_stock: false,
|
||||
},
|
||||
{
|
||||
name: 'product2',
|
||||
time: 1518015600950, // 07 Feb 2018 15:00:00 GMT
|
||||
price: 190,
|
||||
quantity: 231,
|
||||
in_stock: false,
|
||||
},
|
||||
{
|
||||
name: 'product3',
|
||||
time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
|
||||
price: 67,
|
||||
quantity: 240,
|
||||
in_stock: true,
|
||||
},
|
||||
{
|
||||
name: 'product4',
|
||||
time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
|
||||
price: 311,
|
||||
quantity: 447,
|
||||
in_stock: false,
|
||||
},
|
||||
{
|
||||
name: 'product5',
|
||||
time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
|
||||
price: 288,
|
||||
quantity: 384,
|
||||
in_stock: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const stringTable: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
meta: { type: 'string' },
|
||||
},
|
||||
{
|
||||
id: 'time',
|
||||
name: 'time',
|
||||
meta: { type: 'string' },
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
name: 'price',
|
||||
meta: { type: 'string' },
|
||||
},
|
||||
{
|
||||
id: 'quantity',
|
||||
name: 'quantity',
|
||||
meta: { type: 'string' },
|
||||
},
|
||||
{
|
||||
id: 'in_stock',
|
||||
name: 'in_stock',
|
||||
meta: { type: 'string' },
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: 'product1',
|
||||
time: '2018-02-05T15:00:00.950Z',
|
||||
price: '605',
|
||||
quantity: '100',
|
||||
in_stock: 'true',
|
||||
},
|
||||
{
|
||||
name: 'product1',
|
||||
time: '2018-02-06T15:00:00.950Z',
|
||||
price: '583',
|
||||
quantity: '200',
|
||||
in_stock: 'true',
|
||||
},
|
||||
{
|
||||
name: 'product1',
|
||||
time: '2018-02-07T15:00:00.950Z',
|
||||
price: '420',
|
||||
quantity: '300',
|
||||
in_stock: 'true',
|
||||
},
|
||||
{
|
||||
name: 'product2',
|
||||
time: '2018-02-05T15:00:00.950Z',
|
||||
price: '216',
|
||||
quantity: '350',
|
||||
in_stock: 'false',
|
||||
},
|
||||
{
|
||||
name: 'product2',
|
||||
time: '2018-02-06T15:00:00.950Z',
|
||||
price: '200',
|
||||
quantity: '256',
|
||||
in_stock: 'false',
|
||||
},
|
||||
{
|
||||
name: 'product2',
|
||||
time: '2018-02-07T15:00:00.950Z',
|
||||
price: '190',
|
||||
quantity: '231',
|
||||
in_stock: 'false',
|
||||
},
|
||||
{
|
||||
name: 'product3',
|
||||
time: '2018-02-05T15:00:00.950Z',
|
||||
price: '67',
|
||||
quantity: '240',
|
||||
in_stock: 'true',
|
||||
},
|
||||
{
|
||||
name: 'product4',
|
||||
time: '2018-02-05T15:00:00.950Z',
|
||||
price: '311',
|
||||
quantity: '447',
|
||||
in_stock: 'false',
|
||||
},
|
||||
{
|
||||
name: 'product5',
|
||||
time: '2018-02-05T15:00:00.950Z',
|
||||
price: '288',
|
||||
quantity: '384',
|
||||
in_stock: 'true',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export { emptyTable, testTable, stringTable };
|
||||
|
|
|
@ -34,8 +34,6 @@ import { joinRows } from './join_rows';
|
|||
import { lt } from './lt';
|
||||
import { lte } from './lte';
|
||||
import { mapCenter } from './map_center';
|
||||
import { mapColumn } from './mapColumn';
|
||||
import { math } from './math';
|
||||
import { metric } from './metric';
|
||||
import { neq } from './neq';
|
||||
import { ply } from './ply';
|
||||
|
@ -89,8 +87,6 @@ export const functions = [
|
|||
lte,
|
||||
joinRows,
|
||||
mapCenter,
|
||||
mapColumn,
|
||||
math,
|
||||
metric,
|
||||
neq,
|
||||
ply,
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { functionWrapper } from '../../../test_helpers/function_wrapper';
|
||||
import { testTable, emptyTable } from './__fixtures__/test_tables';
|
||||
import { mapColumn } from './mapColumn';
|
||||
|
||||
const pricePlusTwo = (datatable) => Promise.resolve(datatable.rows[0].price + 2);
|
||||
|
||||
describe('mapColumn', () => {
|
||||
const fn = functionWrapper(mapColumn);
|
||||
|
||||
it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => {
|
||||
return fn(testTable, {
|
||||
id: 'pricePlusTwo',
|
||||
name: 'pricePlusTwo',
|
||||
expression: pricePlusTwo,
|
||||
}).then((result) => {
|
||||
const arbitraryRowIndex = 2;
|
||||
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toEqual([
|
||||
...testTable.columns,
|
||||
{ id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } },
|
||||
]);
|
||||
expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo');
|
||||
expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo');
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites existing column with the new column if an existing column name is provided', () => {
|
||||
return fn(testTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
|
||||
const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name');
|
||||
const arbitraryRowIndex = 4;
|
||||
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(testTable.columns.length);
|
||||
expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name');
|
||||
expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number');
|
||||
expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a column to empty tables', () => {
|
||||
return fn(emptyTable, { name: 'name', expression: pricePlusTwo }).then((result) => {
|
||||
expect(result.type).toBe('datatable');
|
||||
expect(result.columns).toHaveLength(1);
|
||||
expect(result.columns[0]).toHaveProperty('name', 'name');
|
||||
expect(result.columns[0].meta).toHaveProperty('type', 'null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('expression', () => {
|
||||
it('maps null values to the new column', () => {
|
||||
return fn(testTable, { name: 'empty' }).then((result) => {
|
||||
const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty');
|
||||
const arbitraryRowIndex = 8;
|
||||
|
||||
expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty');
|
||||
expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null');
|
||||
expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Datatable, ExpressionFunctionDefinition, getType } from '../../../types';
|
||||
import { getFunctionHelp } from '../../../i18n';
|
||||
|
||||
interface Arguments {
|
||||
name: string;
|
||||
expression: (datatable: Datatable) => Promise<boolean | number | string | null>;
|
||||
}
|
||||
|
||||
export function mapColumn(): ExpressionFunctionDefinition<
|
||||
'mapColumn',
|
||||
Datatable,
|
||||
Arguments,
|
||||
Promise<Datatable>
|
||||
> {
|
||||
const { help, args: argHelp } = getFunctionHelp().mapColumn;
|
||||
|
||||
return {
|
||||
name: 'mapColumn',
|
||||
aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file.
|
||||
type: 'datatable',
|
||||
inputTypes: ['datatable'],
|
||||
help,
|
||||
args: {
|
||||
name: {
|
||||
types: ['string'],
|
||||
aliases: ['_', 'column'],
|
||||
help: argHelp.name,
|
||||
required: true,
|
||||
},
|
||||
expression: {
|
||||
types: ['boolean', 'number', 'string', 'null'],
|
||||
resolve: false,
|
||||
aliases: ['exp', 'fn', 'function'],
|
||||
help: argHelp.expression,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
const expression = args.expression || (() => Promise.resolve(null));
|
||||
|
||||
const columns = [...input.columns];
|
||||
const rowPromises = input.rows.map((row) => {
|
||||
return expression({
|
||||
type: 'datatable',
|
||||
columns,
|
||||
rows: [row],
|
||||
}).then((val) => ({
|
||||
...row,
|
||||
[args.name]: val,
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(rowPromises).then((rows) => {
|
||||
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name);
|
||||
const type = rows.length ? getType(rows[0][args.name]) : 'null';
|
||||
const newColumn = { id: args.name, name: args.name, meta: { type } };
|
||||
|
||||
if (existingColumnIndex === -1) {
|
||||
columns.push(newColumn);
|
||||
} else {
|
||||
columns[existingColumnIndex] = newColumn;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'datatable',
|
||||
columns,
|
||||
rows,
|
||||
} as Datatable;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { evaluate } from '@kbn/tinymath';
|
||||
import { pivotObjectArray } from '../../../common/lib/pivot_object_array';
|
||||
import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types';
|
||||
import { getFunctionHelp, getFunctionErrors } from '../../../i18n';
|
||||
|
||||
interface Arguments {
|
||||
expression: string;
|
||||
}
|
||||
|
||||
type Input = number | Datatable;
|
||||
|
||||
export function math(): ExpressionFunctionDefinition<'math', Input, Arguments, number> {
|
||||
const { help, args: argHelp } = getFunctionHelp().math;
|
||||
const errors = getFunctionErrors().math;
|
||||
|
||||
return {
|
||||
name: 'math',
|
||||
type: 'number',
|
||||
inputTypes: ['number', 'datatable'],
|
||||
help,
|
||||
args: {
|
||||
expression: {
|
||||
aliases: ['_'],
|
||||
types: ['string'],
|
||||
help: argHelp.expression,
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
const { expression } = args;
|
||||
|
||||
if (!expression || expression.trim() === '') {
|
||||
throw errors.emptyExpression();
|
||||
}
|
||||
|
||||
const mathContext = isDatatable(input)
|
||||
? pivotObjectArray(
|
||||
input.rows,
|
||||
input.columns.map((col) => col.name)
|
||||
)
|
||||
: { 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)) {
|
||||
throw errors.executionFailed();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (isDatatable(input) && input.rows.length === 0) {
|
||||
throw errors.emptyDatatable();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { mapColumn } from '../../../canvas_plugin_src/functions/common/mapColumn';
|
||||
import { FunctionHelp } from '../function_help';
|
||||
import { FunctionFactory } from '../../../types';
|
||||
import { CANVAS, DATATABLE } from '../../constants';
|
||||
|
||||
export const help: FunctionHelp<FunctionFactory<typeof mapColumn>> = {
|
||||
help: i18n.translate('xpack.canvas.functions.mapColumnHelpText', {
|
||||
defaultMessage:
|
||||
'Adds a column calculated as the result of other columns. ' +
|
||||
'Changes are made only when you provide arguments.' +
|
||||
'See also {alterColumnFn} and {staticColumnFn}.',
|
||||
values: {
|
||||
alterColumnFn: '`alterColumn`',
|
||||
staticColumnFn: '`staticColumn`',
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
name: i18n.translate('xpack.canvas.functions.mapColumn.args.nameHelpText', {
|
||||
defaultMessage: 'The name of the resulting column.',
|
||||
}),
|
||||
expression: i18n.translate('xpack.canvas.functions.mapColumn.args.expressionHelpText', {
|
||||
defaultMessage:
|
||||
'A {CANVAS} expression that is passed to each row as a single row {DATATABLE}.',
|
||||
values: {
|
||||
CANVAS,
|
||||
DATATABLE,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { math } from '../../../canvas_plugin_src/functions/common/math';
|
||||
import { FunctionHelp } from '../function_help';
|
||||
import { FunctionFactory } from '../../../types';
|
||||
import { DATATABLE, CONTEXT, TINYMATH, TINYMATH_URL, TYPE_NUMBER } from '../../constants';
|
||||
|
||||
export const help: FunctionHelp<FunctionFactory<typeof math>> = {
|
||||
help: i18n.translate('xpack.canvas.functions.mathHelpText', {
|
||||
defaultMessage:
|
||||
'Interprets a {TINYMATH} math expression using a {TYPE_NUMBER} or {DATATABLE} as {CONTEXT}. ' +
|
||||
'The {DATATABLE} columns are available by their column name. ' +
|
||||
'If the {CONTEXT} is a number it is available as {value}.',
|
||||
values: {
|
||||
TINYMATH,
|
||||
CONTEXT,
|
||||
DATATABLE,
|
||||
value: '`value`',
|
||||
TYPE_NUMBER,
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
expression: i18n.translate('xpack.canvas.functions.math.args.expressionHelpText', {
|
||||
defaultMessage: 'An evaluated {TINYMATH} expression. See {TINYMATH_URL}.',
|
||||
values: {
|
||||
TINYMATH,
|
||||
TINYMATH_URL,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const errors = {
|
||||
emptyExpression: () =>
|
||||
new Error(
|
||||
i18n.translate('xpack.canvas.functions.math.emptyExpressionErrorMessage', {
|
||||
defaultMessage: 'Empty expression',
|
||||
})
|
||||
),
|
||||
tooManyResults: () =>
|
||||
new Error(
|
||||
i18n.translate('xpack.canvas.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('xpack.canvas.functions.math.executionFailedErrorMessage', {
|
||||
defaultMessage: 'Failed to execute math expression. Check your column names',
|
||||
})
|
||||
),
|
||||
emptyDatatable: () =>
|
||||
new Error(
|
||||
i18n.translate('xpack.canvas.functions.math.emptyDatatableErrorMessage', {
|
||||
defaultMessage: 'Empty datatable',
|
||||
})
|
||||
),
|
||||
};
|
|
@ -16,7 +16,6 @@ import { errors as demodata } from './dict/demodata';
|
|||
import { errors as getCell } from './dict/get_cell';
|
||||
import { errors as image } from './dict/image';
|
||||
import { errors as joinRows } from './dict/join_rows';
|
||||
import { errors as math } from './dict/math';
|
||||
import { errors as ply } from './dict/ply';
|
||||
import { errors as pointseries } from './dict/pointseries';
|
||||
import { errors as progress } from './dict/progress';
|
||||
|
@ -36,7 +35,6 @@ export const getFunctionErrors = () => ({
|
|||
getCell,
|
||||
image,
|
||||
joinRows,
|
||||
math,
|
||||
ply,
|
||||
pointseries,
|
||||
progress,
|
||||
|
|
|
@ -46,9 +46,7 @@ import { help as location } from './dict/location';
|
|||
import { help as lt } from './dict/lt';
|
||||
import { help as lte } from './dict/lte';
|
||||
import { help as mapCenter } from './dict/map_center';
|
||||
import { help as mapColumn } from './dict/map_column';
|
||||
import { help as markdown } from './dict/markdown';
|
||||
import { help as math } from './dict/math';
|
||||
import { help as metric } from './dict/metric';
|
||||
import { help as neq } from './dict/neq';
|
||||
import { help as pie } from './dict/pie';
|
||||
|
@ -209,9 +207,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
|
|||
lt,
|
||||
lte,
|
||||
mapCenter,
|
||||
mapColumn,
|
||||
markdown,
|
||||
math,
|
||||
metric,
|
||||
neq,
|
||||
pie,
|
||||
|
|
|
@ -5944,19 +5944,10 @@
|
|||
"xpack.canvas.functions.ltHelpText": "{CONTEXT} が引数よりも小さいかを戻します。",
|
||||
"xpack.canvas.functions.mapCenter.args.latHelpText": "マップの中央の緯度",
|
||||
"xpack.canvas.functions.mapCenterHelpText": "マップの中央座標とズームレベルのオブジェクトに戻ります。",
|
||||
"xpack.canvas.functions.mapColumn.args.expressionHelpText": "単一行 {DATATABLE} として各行に渡される {CANVAS} 表現です。",
|
||||
"xpack.canvas.functions.mapColumn.args.nameHelpText": "結果の列の名前です。",
|
||||
"xpack.canvas.functions.mapColumnHelpText": "他の列の結果として計算された列を追加します。引数が指定された場合のみ変更が加えられます。{alterColumnFn}と{staticColumnFn}もご参照ください。",
|
||||
"xpack.canvas.functions.markdown.args.contentHelpText": "{MARKDOWN} を含むテキストの文字列です。連結させるには、{stringFn} 関数を複数回渡します。",
|
||||
"xpack.canvas.functions.markdown.args.fontHelpText": "コンテンツの {CSS} フォントプロパティです。たとえば、{fontFamily} または {fontWeight} です。",
|
||||
"xpack.canvas.functions.markdown.args.openLinkHelpText": "新しいタブでリンクを開くためのtrue/false値。デフォルト値は「false」です。「true」に設定するとすべてのリンクが新しいタブで開くようになります。",
|
||||
"xpack.canvas.functions.markdownHelpText": "{MARKDOWN} テキストをレンダリングするエレメントを追加します。ヒント:単一の数字、メトリック、テキストの段落には {markdownFn} 関数を使います。",
|
||||
"xpack.canvas.functions.math.args.expressionHelpText": "評価された {TINYMATH} 表現です。{TINYMATH_URL} をご覧ください。",
|
||||
"xpack.canvas.functions.math.emptyDatatableErrorMessage": "空のデータベース",
|
||||
"xpack.canvas.functions.math.emptyExpressionErrorMessage": "空の表現",
|
||||
"xpack.canvas.functions.math.executionFailedErrorMessage": "数式の実行に失敗しました。列名を確認してください",
|
||||
"xpack.canvas.functions.math.tooManyResultsErrorMessage": "式は 1 つの数字を返す必要があります。表現を {mean} または {sum} で囲んでみてください",
|
||||
"xpack.canvas.functions.mathHelpText": "{TYPE_NUMBER}または{DATATABLE}を{CONTEXT}として使用して、{TINYMATH}数式を解釈します。{DATATABLE}列は列名で表示されます。{CONTEXT}が数字の場合は、{value}と表示されます。",
|
||||
"xpack.canvas.functions.metric.args.labelFontHelpText": "ラベルの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。",
|
||||
"xpack.canvas.functions.metric.args.labelHelpText": "メトリックを説明するテキストです。",
|
||||
"xpack.canvas.functions.metric.args.metricFontHelpText": "メトリックの {CSS} フォントプロパティです。例: {FONT_FAMILY} または {FONT_WEIGHT}。",
|
||||
|
|
|
@ -5955,19 +5955,10 @@
|
|||
"xpack.canvas.functions.ltHelpText": "返回 {CONTEXT} 是否小于参数。",
|
||||
"xpack.canvas.functions.mapCenter.args.latHelpText": "地图中心的纬度",
|
||||
"xpack.canvas.functions.mapCenterHelpText": "返回包含地图中心坐标和缩放级别的对象。",
|
||||
"xpack.canvas.functions.mapColumn.args.expressionHelpText": "作为单行 {DATATABLE} 传递到每一行的 {CANVAS} 表达式。",
|
||||
"xpack.canvas.functions.mapColumn.args.nameHelpText": "结果列的名称。",
|
||||
"xpack.canvas.functions.mapColumnHelpText": "添加计算为其他列的结果的列。只有提供参数时,才会执行更改。另请参见 {alterColumnFn} 和 {staticColumnFn}。",
|
||||
"xpack.canvas.functions.markdown.args.contentHelpText": "包含 {MARKDOWN} 的文本字符串。要进行串联,请多次传递 {stringFn} 函数。",
|
||||
"xpack.canvas.functions.markdown.args.fontHelpText": "内容的 {CSS} 字体属性。例如 {fontFamily} 或 {fontWeight}。",
|
||||
"xpack.canvas.functions.markdown.args.openLinkHelpText": "用于在新标签页中打开链接的 true 或 false 值。默认值为 `false`。设置为 `true` 时将在新标签页中打开所有链接。",
|
||||
"xpack.canvas.functions.markdownHelpText": "添加呈现 {MARKDOWN} 文本的元素。提示:将 {markdownFn} 函数用于单个数字、指标和文本段落。",
|
||||
"xpack.canvas.functions.math.args.expressionHelpText": "已计算的 {TINYMATH} 表达式。请参阅 {TINYMATH_URL}。",
|
||||
"xpack.canvas.functions.math.emptyDatatableErrorMessage": "空数据表",
|
||||
"xpack.canvas.functions.math.emptyExpressionErrorMessage": "空表达式",
|
||||
"xpack.canvas.functions.math.executionFailedErrorMessage": "无法执行数学表达式。检查您的列名称",
|
||||
"xpack.canvas.functions.math.tooManyResultsErrorMessage": "表达式必须返回单个数字。尝试将您的表达式包装在 {mean} 或 {sum} 中",
|
||||
"xpack.canvas.functions.mathHelpText": "使用 {TYPE_NUMBER} 或 {DATATABLE} 作为 {CONTEXT} 来解释 {TINYMATH} 数学表达式。{DATATABLE} 列按列名使用。如果 {CONTEXT} 是数字,则作为 {value} 使用。",
|
||||
"xpack.canvas.functions.metric.args.labelFontHelpText": "标签的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。",
|
||||
"xpack.canvas.functions.metric.args.labelHelpText": "描述指标的文本。",
|
||||
"xpack.canvas.functions.metric.args.metricFontHelpText": "指标的 {CSS} 字体属性。例如 {FONT_FAMILY} 或 {FONT_WEIGHT}。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue