mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Add collapse fn to table and xy chart (#131748)
* add collapse fn to table and xy chart * adjust documentation * bug fixes * tests and fixes * fix bug * allow color picking if collapse fn is active Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
ca923bbdea
commit
81dce64f55
17 changed files with 480 additions and 57 deletions
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 type { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common';
|
||||
import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils';
|
||||
import { CollapseArgs, collapse } from '.';
|
||||
|
||||
describe('collapse_fn', () => {
|
||||
const fn = functionWrapper(collapse);
|
||||
const runFn = (input: Datatable, args: CollapseArgs) =>
|
||||
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||
|
||||
it('collapses all rows', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'val', name: 'val', meta: { type: 'number' } },
|
||||
{ id: 'split', name: 'split', meta: { type: 'string' } },
|
||||
],
|
||||
rows: [
|
||||
{ val: 1, split: 'A' },
|
||||
{ val: 2, split: 'B' },
|
||||
{ val: 3, split: 'B' },
|
||||
{ val: 4, split: 'A' },
|
||||
{ val: 5, split: 'A' },
|
||||
{ val: 6, split: 'A' },
|
||||
{ val: 7, split: 'B' },
|
||||
{ val: 8, split: 'B' },
|
||||
],
|
||||
},
|
||||
{ metric: ['val'], fn: 'sum' }
|
||||
);
|
||||
|
||||
expect(result.rows).toEqual([{ val: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 }]);
|
||||
});
|
||||
|
||||
const twoSplitTable: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'val', name: 'val', meta: { type: 'number' } },
|
||||
{ id: 'split', name: 'split', meta: { type: 'string' } },
|
||||
{ id: 'split2', name: 'split2', meta: { type: 'string' } },
|
||||
],
|
||||
rows: [
|
||||
{ val: 1, split: 'A', split2: 'C' },
|
||||
{ val: 2, split: 'B', split2: 'C' },
|
||||
{ val: 3, split2: 'C' },
|
||||
{ val: 4, split: 'A', split2: 'C' },
|
||||
{ val: 5 },
|
||||
{ val: 6, split: 'A', split2: 'D' },
|
||||
{ val: 7, split: 'B', split2: 'D' },
|
||||
{ val: 8, split: 'B', split2: 'D' },
|
||||
],
|
||||
};
|
||||
|
||||
it('splits by a column', async () => {
|
||||
const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'sum' });
|
||||
expect(result.rows).toEqual([
|
||||
{ val: 1 + 4 + 6, split: 'A' },
|
||||
{ val: 2 + 7 + 8, split: 'B' },
|
||||
{ val: 3 + 5, split: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies avg', async () => {
|
||||
const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'avg' });
|
||||
expect(result.rows).toEqual([
|
||||
{ val: (1 + 4 + 6) / 3, split: 'A' },
|
||||
{ val: (2 + 7 + 8) / 3, split: 'B' },
|
||||
{ val: (3 + 5) / 2, split: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies min', async () => {
|
||||
const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'min' });
|
||||
expect(result.rows).toEqual([
|
||||
{ val: 1, split: 'A' },
|
||||
{ val: 2, split: 'B' },
|
||||
{ val: 3, split: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies max', async () => {
|
||||
const result = await runFn(twoSplitTable, { metric: ['val'], by: ['split'], fn: 'max' });
|
||||
expect(result.rows).toEqual([
|
||||
{ val: 6, split: 'A' },
|
||||
{ val: 8, split: 'B' },
|
||||
{ val: 5, split: undefined },
|
||||
]);
|
||||
});
|
||||
});
|
101
x-pack/plugins/lens/common/expressions/collapse/collapse_fn.ts
Normal file
101
x-pack/plugins/lens/common/expressions/collapse/collapse_fn.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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, DatatableRow, getBucketIdentifier } from '@kbn/expressions-plugin/common';
|
||||
import type { CollapseExpressionFunction } from './types';
|
||||
|
||||
function getValueAsNumberArray(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((innerVal) => Number(innerVal));
|
||||
} else {
|
||||
return [Number(value)];
|
||||
}
|
||||
}
|
||||
|
||||
export const collapseFn: CollapseExpressionFunction['fn'] = (input, { by, metric, fn }) => {
|
||||
const accumulators: Record<string, Partial<Record<string, number>>> = {};
|
||||
const valueCounter: Record<string, Partial<Record<string, number>>> = {};
|
||||
metric?.forEach((m) => {
|
||||
accumulators[m] = {};
|
||||
valueCounter[m] = {};
|
||||
});
|
||||
const setMarker: Partial<Record<string, boolean>> = {};
|
||||
input.rows.forEach((row) => {
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
|
||||
metric?.forEach((m) => {
|
||||
const accumulatorValue = accumulators[m][bucketIdentifier];
|
||||
const currentValue = row[m];
|
||||
if (currentValue != null) {
|
||||
const currentNumberValues = getValueAsNumberArray(currentValue);
|
||||
switch (fn) {
|
||||
case 'avg':
|
||||
valueCounter[m][bucketIdentifier] =
|
||||
(valueCounter[m][bucketIdentifier] ?? 0) + currentNumberValues.length;
|
||||
case 'sum':
|
||||
accumulators[m][bucketIdentifier] = currentNumberValues.reduce(
|
||||
(a, b) => a + b,
|
||||
accumulatorValue || 0
|
||||
);
|
||||
break;
|
||||
case 'min':
|
||||
if (typeof accumulatorValue !== 'undefined') {
|
||||
accumulators[m][bucketIdentifier] = Math.min(
|
||||
accumulatorValue,
|
||||
...currentNumberValues
|
||||
);
|
||||
} else {
|
||||
accumulators[m][bucketIdentifier] = Math.min(...currentNumberValues);
|
||||
}
|
||||
break;
|
||||
case 'max':
|
||||
if (typeof accumulatorValue !== 'undefined') {
|
||||
accumulators[m][bucketIdentifier] = Math.max(
|
||||
accumulatorValue,
|
||||
...currentNumberValues
|
||||
);
|
||||
} else {
|
||||
accumulators[m][bucketIdentifier] = Math.max(...currentNumberValues);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (fn === 'avg') {
|
||||
metric?.forEach((m) => {
|
||||
Object.keys(accumulators[m]).forEach((bucketIdentifier) => {
|
||||
const accumulatorValue = accumulators[m][bucketIdentifier];
|
||||
const valueCount = valueCounter[m][bucketIdentifier];
|
||||
if (typeof accumulatorValue !== 'undefined' && typeof valueCount !== 'undefined') {
|
||||
accumulators[m][bucketIdentifier] = accumulatorValue / valueCount;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...input,
|
||||
columns: input.columns.filter((c) => by?.indexOf(c.id) !== -1 || metric?.indexOf(c.id) !== -1),
|
||||
rows: input.rows
|
||||
.map((row) => {
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
if (setMarker[bucketIdentifier]) return undefined;
|
||||
setMarker[bucketIdentifier] = true;
|
||||
const newRow: Datatable['rows'][number] = {};
|
||||
metric?.forEach((m) => {
|
||||
newRow[m] = accumulators[m][bucketIdentifier];
|
||||
});
|
||||
by?.forEach((b) => {
|
||||
newRow[b] = row[b];
|
||||
});
|
||||
|
||||
return newRow;
|
||||
})
|
||||
.filter(Boolean) as DatatableRow[],
|
||||
};
|
||||
};
|
68
x-pack/plugins/lens/common/expressions/collapse/index.ts
Normal file
68
x-pack/plugins/lens/common/expressions/collapse/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 type { CollapseExpressionFunction } from './types';
|
||||
|
||||
export interface CollapseArgs {
|
||||
by?: string[];
|
||||
metric?: string[];
|
||||
fn: 'sum' | 'avg' | 'min' | 'max';
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses multiple rows into a single row using the specified function.
|
||||
*
|
||||
* The `by` argument specifies the columns to group by - these columns are not collapsed.
|
||||
* The `metric` arguments specifies the collumns to apply the aggregate function to.
|
||||
*
|
||||
* All other columns are removed.
|
||||
*/
|
||||
export const collapse: CollapseExpressionFunction = {
|
||||
name: 'lens_collapse',
|
||||
type: 'datatable',
|
||||
|
||||
inputTypes: ['datatable'],
|
||||
|
||||
help: i18n.translate('xpack.lens.functions.collapse.help', {
|
||||
defaultMessage:
|
||||
'Collapses multiple rows into a single row using the specified aggregate function.',
|
||||
}),
|
||||
|
||||
args: {
|
||||
by: {
|
||||
help: i18n.translate('xpack.lens.functions.collapse.args.byHelpText', {
|
||||
defaultMessage: 'Columns to group by - these columns are kept as-is',
|
||||
}),
|
||||
multi: true,
|
||||
types: ['string'],
|
||||
required: false,
|
||||
},
|
||||
metric: {
|
||||
help: i18n.translate('xpack.lens.functions.collapse.args.metricHelpText', {
|
||||
defaultMessage: 'Column to calculate the specified aggregate function of',
|
||||
}),
|
||||
types: ['string'],
|
||||
multi: true,
|
||||
required: false,
|
||||
},
|
||||
fn: {
|
||||
help: i18n.translate('xpack.lens.functions.collapse.args.fnHelpText', {
|
||||
defaultMessage: 'The aggregate function to apply',
|
||||
}),
|
||||
types: ['string'],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(...args) {
|
||||
/** Build optimization: prevent adding extra code into initial bundle **/
|
||||
const { collapseFn } = await import('./collapse_fn');
|
||||
return collapseFn(...args);
|
||||
},
|
||||
};
|
16
x-pack/plugins/lens/common/expressions/collapse/types.ts
Normal file
16
x-pack/plugins/lens/common/expressions/collapse/types.ts
Normal file
|
@ -0,0 +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.
|
||||
*/
|
||||
|
||||
import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin';
|
||||
import { CollapseArgs } from '.';
|
||||
|
||||
export type CollapseExpressionFunction = ExpressionFunctionDefinition<
|
||||
'lens_collapse',
|
||||
Datatable,
|
||||
CollapseArgs,
|
||||
Datatable | Promise<Datatable>
|
||||
>;
|
|
@ -42,6 +42,7 @@ export interface ColumnState {
|
|||
colorMode?: 'none' | 'cell' | 'text';
|
||||
summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max';
|
||||
summaryLabel?: string;
|
||||
collapseFn?: string;
|
||||
}
|
||||
|
||||
export type DatatableColumnResult = ColumnState & { type: 'lens_datatable_column' };
|
||||
|
|
|
@ -47,6 +47,7 @@ export const datatableFn =
|
|||
let untransposedData: Datatable | undefined;
|
||||
// do the sorting at this level to propagate it also at CSV download
|
||||
const [layerId] = Object.keys(context.inspectorAdapters.tables || {});
|
||||
|
||||
const formatters: Record<string, ReturnType<FormatFactory>> = {};
|
||||
const formatFactory = await getFormatFactory(context);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export * from './counter_rate';
|
||||
export * from './collapse';
|
||||
export * from './format_column';
|
||||
export * from './rename_columns';
|
||||
export * from './time_scale';
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
} from '../../../common/expressions';
|
||||
|
||||
import './dimension_editor.scss';
|
||||
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||
|
||||
const idPrefix = htmlIdGenerator()();
|
||||
|
||||
|
@ -128,6 +129,17 @@ export function TableDimensionEditor(
|
|||
|
||||
return (
|
||||
<>
|
||||
{props.groupId === 'rows' && (
|
||||
<CollapseSetting
|
||||
value={column.collapseFn || ''}
|
||||
onChange={(collapseFn: string) => {
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { collapseFn }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
|
|
|
@ -490,7 +490,13 @@ describe('Datatable Visualization', () => {
|
|||
describe('#toExpression', () => {
|
||||
const getDatatableExpressionArgs = (state: DatatableVisualizationState) =>
|
||||
buildExpression(
|
||||
datatableVisualization.toExpression(state, frame.datasourceLayers) as Ast
|
||||
datatableVisualization.toExpression(
|
||||
state,
|
||||
frame.datasourceLayers,
|
||||
|
||||
{},
|
||||
{ '1': { type: 'expression', chain: [] } }
|
||||
) as Ast
|
||||
).findFunction('lens_datatable')[0].arguments;
|
||||
|
||||
const defaultExpressionTableState = {
|
||||
|
@ -524,7 +530,9 @@ describe('Datatable Visualization', () => {
|
|||
|
||||
const expression = datatableVisualization.toExpression(
|
||||
defaultExpressionTableState,
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
{},
|
||||
{ '1': { type: 'expression', chain: [] } }
|
||||
) as Ast;
|
||||
|
||||
const tableArgs = buildExpression(expression).findFunction('lens_datatable');
|
||||
|
@ -573,7 +581,9 @@ describe('Datatable Visualization', () => {
|
|||
|
||||
const expression = datatableVisualization.toExpression(
|
||||
defaultExpressionTableState,
|
||||
frame.datasourceLayers
|
||||
frame.datasourceLayers,
|
||||
{},
|
||||
{ '1': { type: 'expression', chain: [] } }
|
||||
);
|
||||
|
||||
expect(expression).toEqual(null);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { Ast, AstFunction } from '@kbn/interpreter';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PaletteRegistry, CUSTOM_PALETTE } from '@kbn/coloring';
|
||||
|
@ -203,7 +203,10 @@ export const getDatatableVisualization = ({
|
|||
)
|
||||
.map((accessor) => ({
|
||||
columnId: accessor,
|
||||
triggerIcon: columnMap[accessor].hidden ? 'invisible' : undefined,
|
||||
triggerIcon:
|
||||
columnMap[accessor].hidden || columnMap[accessor].collapseFn
|
||||
? 'invisible'
|
||||
: undefined,
|
||||
})),
|
||||
supportsMoreColumns: true,
|
||||
filterOperations: (op) => op.isBucketed,
|
||||
|
@ -342,6 +345,10 @@ export const getDatatableVisualization = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!datasourceExpressionsByLayers || Object.keys(datasourceExpressionsByLayers).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnMap: Record<string, ColumnState> = {};
|
||||
state.columns.forEach((column) => {
|
||||
columnMap[column.columnId] = column;
|
||||
|
@ -357,58 +364,84 @@ export const getDatatableVisualization = ({
|
|||
type: 'expression',
|
||||
chain: [
|
||||
...(datasourceExpression?.chain ?? []),
|
||||
...columns
|
||||
.filter((c) => c.collapseFn)
|
||||
.map((c) => {
|
||||
return {
|
||||
type: 'function',
|
||||
function: 'lens_collapse',
|
||||
arguments: {
|
||||
by: columns
|
||||
.filter(
|
||||
(col) =>
|
||||
col.columnId !== c.columnId &&
|
||||
datasource!.getOperationForColumnId(col.columnId)?.isBucketed
|
||||
)
|
||||
.map((col) => col.columnId),
|
||||
metric: columns
|
||||
.filter((col) => !datasource!.getOperationForColumnId(col.columnId)?.isBucketed)
|
||||
.map((col) => col.columnId),
|
||||
fn: [c.collapseFn!],
|
||||
},
|
||||
} as AstFunction;
|
||||
}),
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_datatable',
|
||||
arguments: {
|
||||
title: [title || ''],
|
||||
description: [description || ''],
|
||||
columns: columns.map((column) => {
|
||||
const paletteParams = {
|
||||
...column.palette?.params,
|
||||
// rewrite colors and stops as two distinct arguments
|
||||
colors: (column.palette?.params?.stops || []).map(({ color }) => color),
|
||||
stops:
|
||||
column.palette?.params?.name === 'custom'
|
||||
? (column.palette?.params?.stops || []).map(({ stop }) => stop)
|
||||
: [],
|
||||
reverse: false, // managed at UI level
|
||||
};
|
||||
const sortingHint = datasource!.getOperationForColumnId(column.columnId)!.sortingHint;
|
||||
columns: columns
|
||||
.filter((c) => !c.collapseFn)
|
||||
.map((column) => {
|
||||
const paletteParams = {
|
||||
...column.palette?.params,
|
||||
// rewrite colors and stops as two distinct arguments
|
||||
colors: (column.palette?.params?.stops || []).map(({ color }) => color),
|
||||
stops:
|
||||
column.palette?.params?.name === 'custom'
|
||||
? (column.palette?.params?.stops || []).map(({ stop }) => stop)
|
||||
: [],
|
||||
reverse: false, // managed at UI level
|
||||
};
|
||||
const sortingHint = datasource!.getOperationForColumnId(
|
||||
column.columnId
|
||||
)!.sortingHint;
|
||||
|
||||
const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none';
|
||||
const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none';
|
||||
|
||||
const canColor =
|
||||
datasource!.getOperationForColumnId(column.columnId)?.dataType === 'number';
|
||||
const canColor =
|
||||
datasource!.getOperationForColumnId(column.columnId)?.dataType === 'number';
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_datatable_column',
|
||||
arguments: {
|
||||
columnId: [column.columnId],
|
||||
hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden],
|
||||
width: typeof column.width === 'undefined' ? [] : [column.width],
|
||||
isTransposed:
|
||||
typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed],
|
||||
transposable: [
|
||||
!datasource!.getOperationForColumnId(column.columnId)?.isBucketed,
|
||||
],
|
||||
alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment],
|
||||
colorMode: [canColor && column.colorMode ? column.colorMode : 'none'],
|
||||
palette: [paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams)],
|
||||
summaryRow: hasNoSummaryRow ? [] : [column.summaryRow!],
|
||||
summaryLabel: hasNoSummaryRow
|
||||
? []
|
||||
: [column.summaryLabel ?? getDefaultSummaryLabel(column.summaryRow!)],
|
||||
sortingHint: sortingHint ? [sortingHint] : [],
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_datatable_column',
|
||||
arguments: {
|
||||
columnId: [column.columnId],
|
||||
hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden],
|
||||
width: typeof column.width === 'undefined' ? [] : [column.width],
|
||||
isTransposed:
|
||||
typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed],
|
||||
transposable: [
|
||||
!datasource!.getOperationForColumnId(column.columnId)?.isBucketed,
|
||||
],
|
||||
alignment:
|
||||
typeof column.alignment === 'undefined' ? [] : [column.alignment],
|
||||
colorMode: [canColor && column.colorMode ? column.colorMode : 'none'],
|
||||
palette: [paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams)],
|
||||
summaryRow: hasNoSummaryRow ? [] : [column.summaryRow!],
|
||||
summaryLabel: hasNoSummaryRow
|
||||
? []
|
||||
: [column.summaryLabel ?? getDefaultSummaryLabel(column.summaryRow!)],
|
||||
sortingHint: sortingHint ? [sortingHint] : [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
}),
|
||||
sortingColumnId: [state.sorting?.columnId || ''],
|
||||
sortingDirection: [state.sorting?.direction || 'none'],
|
||||
fitRowToContent: [state.rowHeight === 'auto'],
|
||||
|
|
|
@ -12,6 +12,7 @@ import { renameColumns } from '../common/expressions/rename_columns/rename_colum
|
|||
import { formatColumn } from '../common/expressions/format_column';
|
||||
import { counterRate } from '../common/expressions/counter_rate';
|
||||
import { getTimeScale } from '../common/expressions/time_scale/time_scale';
|
||||
import { collapse } from '../common/expressions';
|
||||
|
||||
export const setupExpressions = (
|
||||
expressions: ExpressionsSetup,
|
||||
|
@ -19,6 +20,7 @@ export const setupExpressions = (
|
|||
getTimeZone: Parameters<typeof getTimeScale>[0]
|
||||
) => {
|
||||
[
|
||||
collapse,
|
||||
counterRate,
|
||||
formatColumn,
|
||||
renameColumns,
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
const options = [
|
||||
{ text: i18n.translate('xpack.lens.collapse.none', { defaultMessage: 'None' }), value: '' },
|
||||
{ text: i18n.translate('xpack.lens.collapse.sum', { defaultMessage: 'Sum' }), value: 'sum' },
|
||||
{ text: i18n.translate('xpack.lens.collapse.min', { defaultMessage: 'Min' }), value: 'min' },
|
||||
{ text: i18n.translate('xpack.lens.collapse.max', { defaultMessage: 'Max' }), value: 'max' },
|
||||
{ text: i18n.translate('xpack.lens.collapse.avg', { defaultMessage: 'Average' }), value: 'avg' },
|
||||
];
|
||||
|
||||
export function CollapseSetting({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.collapse.label', { defaultMessage: 'Collapse by' })}
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
>
|
||||
<EuiSelect
|
||||
compressed
|
||||
data-test-subj="indexPattern-terms-orderBy"
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -48,7 +48,7 @@ export function getColorAssignments(
|
|||
|
||||
return mapValues(layersPerPalette, (paletteLayers) => {
|
||||
const seriesPerLayer = paletteLayers.map((layer, layerIndex) => {
|
||||
if (!layer.splitAccessor) {
|
||||
if (layer.collapseFn || !layer.splitAccessor) {
|
||||
return { numberOfSeries: layer.accessors.length, splits: [] };
|
||||
}
|
||||
const splitAccessor = layer.splitAccessor;
|
||||
|
@ -108,7 +108,7 @@ export function getAccessorColorConfig(
|
|||
if (isAnnotationsLayer(layer)) {
|
||||
return getAnnotationsAccessorColorConfig(layer);
|
||||
}
|
||||
const layerContainsSplits = Boolean(layer.splitAccessor);
|
||||
const layerContainsSplits = !layer.collapseFn && layer.splitAccessor;
|
||||
const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' };
|
||||
const totalSeriesCount = colorAssignments[currentPalette.name]?.totalSeriesCount;
|
||||
return layer.accessors.map((accessor) => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { Ast, AstFunction } from '@kbn/interpreter';
|
||||
import { ScaleType } from '@elastic/charts';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
|
||||
|
@ -426,14 +426,38 @@ const dataLayerToExpression = (
|
|||
],
|
||||
xScaleType: [getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear)],
|
||||
isHistogram: [isHistogramDimension],
|
||||
splitAccessor: layer.splitAccessor ? [layer.splitAccessor] : [],
|
||||
splitAccessor: layer.collapseFn || !layer.splitAccessor ? [] : [layer.splitAccessor],
|
||||
yConfig: layer.yConfig
|
||||
? layer.yConfig.map((yConfig) => yConfigToExpression(yConfig))
|
||||
: [],
|
||||
seriesType: [layer.seriesType],
|
||||
accessors: layer.accessors,
|
||||
columnToLabel: [JSON.stringify(columnToLabel)],
|
||||
...(datasourceExpression ? { table: [datasourceExpression] } : {}),
|
||||
...(datasourceExpression
|
||||
? {
|
||||
table: [
|
||||
{
|
||||
...datasourceExpression,
|
||||
chain: [
|
||||
...datasourceExpression.chain,
|
||||
...(layer.collapseFn
|
||||
? [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_collapse',
|
||||
arguments: {
|
||||
by: layer.xAccessor ? [layer.xAccessor] : [],
|
||||
metric: layer.accessors,
|
||||
fn: [layer.collapseFn!],
|
||||
},
|
||||
} as AstFunction,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
palette: [
|
||||
{
|
||||
type: 'expression',
|
||||
|
|
|
@ -46,6 +46,7 @@ export interface XYDataLayerConfig {
|
|||
yConfig?: YConfig[];
|
||||
splitAccessor?: string;
|
||||
palette?: PaletteOutput;
|
||||
collapseFn?: string;
|
||||
yScaleType?: YScaleType;
|
||||
xScaleType?: XScaleType;
|
||||
isHistogram?: boolean;
|
||||
|
|
|
@ -276,10 +276,12 @@ export const getXyVisualization = ({
|
|||
? [
|
||||
{
|
||||
columnId: dataLayer.splitAccessor,
|
||||
triggerIcon: 'colorBy' as const,
|
||||
palette: paletteService
|
||||
.get(dataLayer.palette?.name || 'default')
|
||||
.getCategoricalColors(10, dataLayer.palette?.params),
|
||||
triggerIcon: dataLayer.collapseFn ? ('invisible' as const) : ('colorBy' as const),
|
||||
palette: dataLayer.collapseFn
|
||||
? undefined
|
||||
: paletteService
|
||||
.get(dataLayer.palette?.name || 'default')
|
||||
.getCategoricalColors(10, dataLayer.palette?.params),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
|
|
|
@ -19,6 +19,7 @@ import { PalettePicker, useDebouncedValue } from '../../shared_components';
|
|||
import { isAnnotationsLayer, isReferenceLayer } from '../visualization_helpers';
|
||||
import { ReferenceLinePanel } from './reference_line_config_panel';
|
||||
import { AnnotationsPanel } from './annotations_config_panel';
|
||||
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||
|
||||
type UnwrapArray<T> = T extends Array<infer P> ? P : T;
|
||||
|
||||
|
@ -90,6 +91,12 @@ export function DimensionEditor(
|
|||
if (props.groupId === 'breakdown') {
|
||||
return (
|
||||
<>
|
||||
<CollapseSetting
|
||||
value={layer.collapseFn || ''}
|
||||
onChange={(collapseFn) => {
|
||||
setLocalState(updateLayer(localState, { ...layer, collapseFn }, index));
|
||||
}}
|
||||
/>
|
||||
<PalettePicker
|
||||
palettes={props.paletteService}
|
||||
activePalette={localLayer?.palette}
|
||||
|
@ -105,7 +112,11 @@ export function DimensionEditor(
|
|||
|
||||
return (
|
||||
<>
|
||||
<ColorPicker {...props} disabled={Boolean(localLayer.splitAccessor)} setConfig={setConfig} />
|
||||
<ColorPicker
|
||||
{...props}
|
||||
disabled={Boolean(!localLayer.collapseFn && localLayer.splitAccessor)}
|
||||
setConfig={setConfig}
|
||||
/>
|
||||
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue