[XY] xyVis and layeredXyVis. (#128255)

* Added extended layers expressions.

* Added support of tables at layers.

* Added annotations to layeredXyVIs.

* Refactored the implementation to be reusable.

* Fixed undefined layers.

* Fixed empty arrays problems.

* Fixed input translations and removed not used arguments.

* Fixed missing required args error, and added required to arguments.

* Simplified expression configuration.

* Added strict to all the expressions.

* Moved dataLayer to the separate component.

* Refactored dataLayers helpers and xy_chart.

* fillOpacity usage validation is added.

* Fixed valueLabels argument options. Removed not used. Added validation for usage.

* Added validation to the layeredXyVis.

* Fixed extent validation.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marta Bondyra <marta.bondyra@gmail.com>
Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co>
This commit is contained in:
Yaroslav Kuznietsov 2022-05-03 17:29:01 +03:00 committed by GitHub
parent c1d44151e2
commit b29b468961
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
118 changed files with 7394 additions and 3800 deletions

View file

@ -124,7 +124,7 @@ pageLoadAssetSize:
visTypeGauge: 24113 visTypeGauge: 24113
unifiedSearch: 71059 unifiedSearch: 71059
data: 454087 data: 454087
expressionXY: 26500
eventAnnotation: 19334 eventAnnotation: 19334
screenshotting: 22870 screenshotting: 22870
synthetics: 40958 synthetics: 40958
expressionXY: 29000

View file

@ -10,7 +10,7 @@ import { Position } from '@elastic/charts';
import type { PaletteOutput } from '@kbn/coloring'; import type { PaletteOutput } from '@kbn/coloring';
import { Datatable, DatatableRow } from '@kbn/expressions-plugin'; import { Datatable, DatatableRow } from '@kbn/expressions-plugin';
import { LayerTypes } from '../constants'; import { LayerTypes } from '../constants';
import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../types'; import { DataLayerConfig, XYProps } from '../types';
export const mockPaletteOutput: PaletteOutput = { export const mockPaletteOutput: PaletteOutput = {
type: 'palette', type: 'palette',
@ -46,9 +46,9 @@ export const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable =
rows, rows,
}); });
export const sampleLayer: DataLayerConfigResult = { export const sampleLayer: DataLayerConfig = {
type: 'dataLayer',
layerId: 'first', layerId: 'first',
type: 'dataLayer',
layerType: LayerTypes.DATA, layerType: LayerTypes.DATA,
seriesType: 'line', seriesType: 'line',
xAccessor: 'c', xAccessor: 'c',
@ -59,9 +59,12 @@ export const sampleLayer: DataLayerConfigResult = {
yScaleType: 'linear', yScaleType: 'linear',
isHistogram: false, isHistogram: false,
palette: mockPaletteOutput, palette: mockPaletteOutput,
table: createSampleDatatableWithRows([]),
}; };
export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLayer]): XYArgs => ({ export const createArgsWithLayers = (
layers: DataLayerConfig | DataLayerConfig[] = sampleLayer
): XYProps => ({
xTitle: '', xTitle: '',
yTitle: '', yTitle: '',
yRightTitle: '', yRightTitle: '',
@ -104,25 +107,17 @@ export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLa
mode: 'full', mode: 'full',
type: 'axisExtentConfig', type: 'axisExtentConfig',
}, },
layers, layers: Array.isArray(layers) ? layers : [layers],
}); });
export function sampleArgs() { export function sampleArgs() {
const data: LensMultiTable = { const data = createSampleDatatableWithRows([
type: 'lens_multitable',
tables: {
first: createSampleDatatableWithRows([
{ a: 1, b: 2, c: 'I', d: 'Foo' }, { a: 1, b: 2, c: 'I', d: 'Foo' },
{ a: 1, b: 5, c: 'J', d: 'Bar' }, { a: 1, b: 5, c: 'J', d: 'Bar' },
]), ]);
},
dateRange: { return {
fromDate: new Date('2019-01-02T05:00:00.000Z'), data,
toDate: new Date('2019-01-03T05:00:00.000Z'), args: createArgsWithLayers({ ...sampleLayer, table: data }),
},
}; };
const args: XYArgs = createArgsWithLayers();
return { data, args };
} }

View file

@ -7,16 +7,21 @@
*/ */
export const XY_VIS = 'xyVis'; export const XY_VIS = 'xyVis';
export const LAYERED_XY_VIS = 'layeredXyVis';
export const Y_CONFIG = 'yConfig'; export const Y_CONFIG = 'yConfig';
export const EXTENDED_Y_CONFIG = 'extendedYConfig';
export const MULTITABLE = 'lens_multitable'; export const MULTITABLE = 'lens_multitable';
export const DATA_LAYER = 'dataLayer'; export const DATA_LAYER = 'dataLayer';
export const EXTENDED_DATA_LAYER = 'extendedDataLayer';
export const LEGEND_CONFIG = 'legendConfig'; export const LEGEND_CONFIG = 'legendConfig';
export const XY_VIS_RENDERER = 'xyVis'; export const XY_VIS_RENDERER = 'xyVis';
export const GRID_LINES_CONFIG = 'gridlinesConfig'; export const GRID_LINES_CONFIG = 'gridlinesConfig';
export const ANNOTATION_LAYER = 'annotationLayer'; export const ANNOTATION_LAYER = 'annotationLayer';
export const EXTENDED_ANNOTATION_LAYER = 'extendedAnnotationLayer';
export const TICK_LABELS_CONFIG = 'tickLabelsConfig'; export const TICK_LABELS_CONFIG = 'tickLabelsConfig';
export const AXIS_EXTENT_CONFIG = 'axisExtentConfig'; export const AXIS_EXTENT_CONFIG = 'axisExtentConfig';
export const REFERENCE_LINE_LAYER = 'referenceLineLayer'; export const REFERENCE_LINE_LAYER = 'referenceLineLayer';
export const EXTENDED_REFERENCE_LINE_LAYER = 'extendedReferenceLineLayer';
export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig'; export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig';
export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig'; export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig';
@ -106,6 +111,23 @@ export const XYCurveTypes = {
export const ValueLabelModes = { export const ValueLabelModes = {
HIDE: 'hide', HIDE: 'hide',
INSIDE: 'inside', SHOW: 'show',
OUTSIDE: 'outside', } as const;
export const AvailableReferenceLineIcons = {
EMPTY: 'empty',
ASTERISK: 'asterisk',
ALERT: 'alert',
BELL: 'bell',
BOLT: 'bolt',
BUG: 'bug',
CIRCLE: 'circle',
EDITOR_COMMENT: 'editorComment',
FLAG: 'flag',
HEART: 'heart',
MAP_MARKER: 'mapMarker',
PIN_FILLED: 'pinFilled',
STAR_EMPTY: 'starEmpty',
TAG: 'tag',
TRIANGLE: 'triangle',
} as const; } as const;

View file

@ -6,13 +6,14 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { LayerTypes, ANNOTATION_LAYER } from '../constants'; import { LayerTypes, ANNOTATION_LAYER } from '../constants';
import { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../types'; import { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../types';
import { strings } from '../i18n';
export function annotationLayerConfigFunction(): ExpressionFunctionDefinition< export function annotationLayerFunction(): ExpressionFunctionDefinition<
typeof ANNOTATION_LAYER, typeof ANNOTATION_LAYER,
null, Datatable,
AnnotationLayerArgs, AnnotationLayerArgs,
AnnotationLayerConfigResult AnnotationLayerConfigResult
> { > {
@ -20,21 +21,17 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition<
name: ANNOTATION_LAYER, name: ANNOTATION_LAYER,
aliases: [], aliases: [],
type: ANNOTATION_LAYER, type: ANNOTATION_LAYER,
inputTypes: ['null'], inputTypes: ['datatable'],
help: 'Annotation layer in lens', help: strings.getAnnotationLayerFnHelp(),
args: { args: {
layerId: {
types: ['string'],
help: '',
},
hide: { hide: {
types: ['boolean'], types: ['boolean'],
default: false, default: false,
help: 'Show details', help: strings.getAnnotationLayerHideHelp(),
}, },
annotations: { annotations: {
types: ['manual_point_event_annotation', 'manual_range_event_annotation'], types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
help: '', help: strings.getAnnotationLayerAnnotationsHelp(),
multi: true, multi: true,
}, },
}, },
@ -42,6 +39,7 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition<
return { return {
type: ANNOTATION_LAYER, type: ANNOTATION_LAYER,
...args, ...args,
annotations: args.annotations ?? [],
layerType: LayerTypes.ANNOTATIONS, layerType: LayerTypes.ANNOTATIONS,
}; };
}, },

View file

@ -11,6 +11,13 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/commo
import { AxisExtentConfig, AxisExtentConfigResult } from '../types'; import { AxisExtentConfig, AxisExtentConfigResult } from '../types';
import { AxisExtentModes, AXIS_EXTENT_CONFIG } from '../constants'; import { AxisExtentModes, AXIS_EXTENT_CONFIG } from '../constants';
const errors = {
upperBoundLowerOrEqualToLowerBoundError: () =>
i18n.translate('expressionXY.reusable.function.axisExtentConfig.errors.emptyUpperBound', {
defaultMessage: 'Upper bound should be greater than lower bound, if custom mode is enabled.',
}),
};
export const axisExtentConfigFunction: ExpressionFunctionDefinition< export const axisExtentConfigFunction: ExpressionFunctionDefinition<
typeof AXIS_EXTENT_CONFIG, typeof AXIS_EXTENT_CONFIG,
null, null,
@ -27,10 +34,12 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition<
args: { args: {
mode: { mode: {
types: ['string'], types: ['string'],
options: [...Object.values(AxisExtentModes)],
help: i18n.translate('expressionXY.axisExtentConfig.extentMode.help', { help: i18n.translate('expressionXY.axisExtentConfig.extentMode.help', {
defaultMessage: 'The extent mode', defaultMessage: 'The extent mode',
}), }),
options: [...Object.values(AxisExtentModes)],
strict: true,
default: AxisExtentModes.FULL,
}, },
lowerBound: { lowerBound: {
types: ['number'], types: ['number'],
@ -46,6 +55,16 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition<
}, },
}, },
fn(input, args) { fn(input, args) {
if (args.mode === AxisExtentModes.CUSTOM) {
if (
args.lowerBound !== undefined &&
args.upperBound !== undefined &&
args.lowerBound >= args.upperBound
) {
throw new Error(errors.upperBoundLowerOrEqualToLowerBoundError());
}
}
return { return {
type: AXIS_EXTENT_CONFIG, type: AXIS_EXTENT_CONFIG,
...args, ...args,

View file

@ -0,0 +1,72 @@
/*
* 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 { SeriesTypes, XScaleTypes, YScaleTypes, Y_CONFIG } from '../constants';
import { strings } from '../i18n';
import { DataLayerFn, ExtendedDataLayerFn } from '../types';
type CommonDataLayerFn = DataLayerFn | ExtendedDataLayerFn;
export const commonDataLayerArgs: CommonDataLayerFn['args'] = {
hide: {
types: ['boolean'],
default: false,
help: strings.getHideHelp(),
},
xAccessor: {
types: ['string'],
help: strings.getXAccessorHelp(),
},
seriesType: {
types: ['string'],
options: [...Object.values(SeriesTypes)],
help: strings.getSeriesTypeHelp(),
required: true,
strict: true,
},
xScaleType: {
options: [...Object.values(XScaleTypes)],
help: strings.getXScaleTypeHelp(),
default: XScaleTypes.ORDINAL,
strict: true,
},
isHistogram: {
types: ['boolean'],
default: false,
help: strings.getIsHistogramHelp(),
},
yScaleType: {
options: [...Object.values(YScaleTypes)],
help: strings.getYScaleTypeHelp(),
default: YScaleTypes.LINEAR,
strict: true,
},
splitAccessor: {
types: ['string'],
help: strings.getSplitAccessorHelp(),
},
accessors: {
types: ['string'],
help: strings.getAccessorsHelp(),
multi: true,
},
yConfig: {
types: [Y_CONFIG],
help: strings.getYConfigHelp(),
multi: true,
},
columnToLabel: {
types: ['string'],
help: strings.getColumnToLabelHelp(),
},
palette: {
types: ['palette', 'system_palette'],
help: strings.getPaletteHelp(),
default: '{palette}',
},
};

View file

@ -0,0 +1,30 @@
/*
* 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 { EXTENDED_Y_CONFIG } from '../constants';
import { strings } from '../i18n';
import { ReferenceLineLayerFn, ExtendedReferenceLineLayerFn } from '../types';
type CommonReferenceLineLayerFn = ReferenceLineLayerFn | ExtendedReferenceLineLayerFn;
export const commonReferenceLineLayerArgs: CommonReferenceLineLayerFn['args'] = {
accessors: {
types: ['string'],
help: strings.getRLAccessorsHelp(),
multi: true,
},
yConfig: {
types: [EXTENDED_Y_CONFIG],
help: strings.getRLYConfigHelp(),
multi: true,
},
columnToLabel: {
types: ['string'],
help: strings.getColumnToLabelHelp(),
},
};

View file

@ -0,0 +1,118 @@
/*
* 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 {
AXIS_EXTENT_CONFIG,
AXIS_TITLES_VISIBILITY_CONFIG,
EndValues,
FittingFunctions,
GRID_LINES_CONFIG,
LABELS_ORIENTATION_CONFIG,
LEGEND_CONFIG,
TICK_LABELS_CONFIG,
ValueLabelModes,
XYCurveTypes,
} from '../constants';
import { strings } from '../i18n';
import { LayeredXyVisFn, XyVisFn } from '../types';
type CommonXYFn = XyVisFn | LayeredXyVisFn;
export const commonXYArgs: CommonXYFn['args'] = {
xTitle: {
types: ['string'],
help: strings.getXTitleHelp(),
},
yTitle: {
types: ['string'],
help: strings.getYTitleHelp(),
},
yRightTitle: {
types: ['string'],
help: strings.getYRightTitleHelp(),
},
yLeftExtent: {
types: [AXIS_EXTENT_CONFIG],
help: strings.getYLeftExtentHelp(),
default: `{${AXIS_EXTENT_CONFIG}}`,
},
yRightExtent: {
types: [AXIS_EXTENT_CONFIG],
help: strings.getYRightExtentHelp(),
default: `{${AXIS_EXTENT_CONFIG}}`,
},
legend: {
types: [LEGEND_CONFIG],
help: strings.getLegendHelp(),
default: `{${LEGEND_CONFIG}}`,
},
fittingFunction: {
types: ['string'],
options: [...Object.values(FittingFunctions)],
help: strings.getFittingFunctionHelp(),
strict: true,
},
endValue: {
types: ['string'],
options: [...Object.values(EndValues)],
help: strings.getEndValueHelp(),
strict: true,
},
emphasizeFitting: {
types: ['boolean'],
default: false,
help: '',
},
valueLabels: {
types: ['string'],
options: [...Object.values(ValueLabelModes)],
help: strings.getValueLabelsHelp(),
strict: true,
default: ValueLabelModes.HIDE,
},
tickLabelsVisibilitySettings: {
types: [TICK_LABELS_CONFIG],
help: strings.getTickLabelsVisibilitySettingsHelp(),
},
labelsOrientation: {
types: [LABELS_ORIENTATION_CONFIG],
help: strings.getLabelsOrientationHelp(),
},
gridlinesVisibilitySettings: {
types: [GRID_LINES_CONFIG],
help: strings.getGridlinesVisibilitySettingsHelp(),
},
axisTitlesVisibilitySettings: {
types: [AXIS_TITLES_VISIBILITY_CONFIG],
help: strings.getAxisTitlesVisibilitySettingsHelp(),
},
curveType: {
types: ['string'],
options: [...Object.values(XYCurveTypes)],
help: strings.getCurveTypeHelp(),
strict: true,
},
fillOpacity: {
types: ['number'],
help: strings.getFillOpacityHelp(),
},
hideEndzones: {
types: ['boolean'],
default: false,
help: strings.getHideEndzonesHelp(),
},
valuesInLegend: {
types: ['boolean'],
default: false,
help: strings.getValuesInLegendHelp(),
},
ariaLabel: {
types: ['string'],
help: strings.getAriaLabelHelp(),
},
};

View file

@ -0,0 +1,30 @@
/*
* 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 { YAxisModes } from '../constants';
import { strings } from '../i18n';
import { YConfigFn, ExtendedYConfigFn } from '../types';
type CommonYConfigFn = YConfigFn | ExtendedYConfigFn;
export const commonYConfigArgs: CommonYConfigFn['args'] = {
forAccessor: {
types: ['string'],
help: strings.getForAccessorHelp(),
},
axisMode: {
types: ['string'],
options: [...Object.values(YAxisModes)],
help: strings.getAxisModeHelp(),
strict: true,
},
color: {
types: ['string'],
help: strings.getColorHelp(),
},
};

View file

@ -7,15 +7,15 @@
*/ */
import { DataLayerArgs } from '../types'; import { DataLayerArgs } from '../types';
import { dataLayerConfigFunction } from '.';
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
import { mockPaletteOutput } from '../__mocks__'; import { mockPaletteOutput, sampleArgs } from '../__mocks__';
import { LayerTypes } from '../constants'; import { LayerTypes } from '../constants';
import { dataLayerFunction } from './data_layer';
describe('dataLayerConfig', () => { describe('dataLayerConfig', () => {
test('produces the correct arguments', () => { test('produces the correct arguments', () => {
const { data } = sampleArgs();
const args: DataLayerArgs = { const args: DataLayerArgs = {
layerId: 'first',
seriesType: 'line', seriesType: 'line',
xAccessor: 'c', xAccessor: 'c',
accessors: ['a', 'b'], accessors: ['a', 'b'],
@ -26,8 +26,13 @@ describe('dataLayerConfig', () => {
palette: mockPaletteOutput, palette: mockPaletteOutput,
}; };
const result = dataLayerConfigFunction.fn(null, args, createMockExecutionContext()); const result = dataLayerFunction.fn(data, args, createMockExecutionContext());
expect(result).toEqual({ type: 'dataLayer', layerType: LayerTypes.DATA, ...args }); expect(result).toEqual({
type: 'dataLayer',
layerType: LayerTypes.DATA,
...args,
table: data,
});
}); });
}); });

View file

@ -0,0 +1,30 @@
/*
* 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 { DataLayerFn } from '../types';
import { DATA_LAYER, LayerTypes } from '../constants';
import { strings } from '../i18n';
import { commonDataLayerArgs } from './common_data_layer_args';
export const dataLayerFunction: DataLayerFn = {
name: DATA_LAYER,
aliases: [],
type: DATA_LAYER,
help: strings.getDataLayerFnHelp(),
inputTypes: ['datatable'],
args: { ...commonDataLayerArgs },
fn(table, args) {
return {
type: DATA_LAYER,
...args,
accessors: args.accessors ?? [],
layerType: LayerTypes.DATA,
table,
};
},
};

View file

@ -1,123 +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 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 type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { DataLayerArgs, DataLayerConfigResult } from '../types';
import {
DATA_LAYER,
LayerTypes,
SeriesTypes,
XScaleTypes,
YScaleTypes,
Y_CONFIG,
} from '../constants';
export const dataLayerConfigFunction: ExpressionFunctionDefinition<
typeof DATA_LAYER,
null,
DataLayerArgs,
DataLayerConfigResult
> = {
name: DATA_LAYER,
aliases: [],
type: DATA_LAYER,
help: i18n.translate('expressionXY.dataLayer.help', {
defaultMessage: `Configure a layer in the xy chart`,
}),
inputTypes: ['null'],
args: {
hide: {
types: ['boolean'],
default: false,
help: i18n.translate('expressionXY.dataLayer.hide.help', {
defaultMessage: 'Show / hide axis',
}),
},
layerId: {
types: ['string'],
help: i18n.translate('expressionXY.dataLayer.layerId.help', {
defaultMessage: 'Layer ID',
}),
},
xAccessor: {
types: ['string'],
help: i18n.translate('expressionXY.dataLayer.xAccessor.help', {
defaultMessage: 'X-axis',
}),
},
seriesType: {
types: ['string'],
options: [...Object.values(SeriesTypes)],
help: i18n.translate('expressionXY.dataLayer.seriesType.help', {
defaultMessage: 'The type of chart to display.',
}),
},
xScaleType: {
options: [...Object.values(XScaleTypes)],
help: i18n.translate('expressionXY.dataLayer.xScaleType.help', {
defaultMessage: 'The scale type of the x axis',
}),
default: XScaleTypes.ORDINAL,
},
isHistogram: {
types: ['boolean'],
default: false,
help: i18n.translate('expressionXY.dataLayer.isHistogram.help', {
defaultMessage: 'Whether to layout the chart as a histogram',
}),
},
yScaleType: {
options: [...Object.values(YScaleTypes)],
help: i18n.translate('expressionXY.dataLayer.yScaleType.help', {
defaultMessage: 'The scale type of the y axes',
}),
default: YScaleTypes.LINEAR,
},
splitAccessor: {
types: ['string'],
help: i18n.translate('expressionXY.dataLayer.splitAccessor.help', {
defaultMessage: 'The column to split by',
}),
},
accessors: {
types: ['string'],
help: i18n.translate('expressionXY.dataLayer.accessors.help', {
defaultMessage: 'The columns to display on the y axis.',
}),
multi: true,
},
yConfig: {
types: [Y_CONFIG],
help: i18n.translate('expressionXY.dataLayer.yConfig.help', {
defaultMessage: 'Additional configuration for y axes',
}),
multi: true,
},
columnToLabel: {
types: ['string'],
help: i18n.translate('expressionXY.dataLayer.columnToLabel.help', {
defaultMessage: 'JSON key-value pairs of column ID to label',
}),
},
palette: {
types: ['palette', 'system_palette'],
help: i18n.translate('expressionXY.dataLayer.palette.help', {
defaultMessage: 'Palette',
}),
default: '{palette}',
},
},
fn(input, args) {
return {
type: DATA_LAYER,
...args,
layerType: LayerTypes.DATA,
};
},
};

View file

@ -0,0 +1,51 @@
/*
* 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 type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { LayerTypes, EXTENDED_ANNOTATION_LAYER } from '../constants';
import { ExtendedAnnotationLayerConfigResult, ExtendedAnnotationLayerArgs } from '../types';
import { strings } from '../i18n';
export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition<
typeof EXTENDED_ANNOTATION_LAYER,
Datatable,
ExtendedAnnotationLayerArgs,
ExtendedAnnotationLayerConfigResult
> {
return {
name: EXTENDED_ANNOTATION_LAYER,
aliases: [],
type: EXTENDED_ANNOTATION_LAYER,
inputTypes: ['datatable'],
help: strings.getAnnotationLayerFnHelp(),
args: {
hide: {
types: ['boolean'],
default: false,
help: strings.getAnnotationLayerHideHelp(),
},
annotations: {
types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
help: strings.getAnnotationLayerAnnotationsHelp(),
multi: true,
},
layerId: {
types: ['string'],
help: strings.getLayerIdHelp(),
},
},
fn: (input, args) => {
return {
type: EXTENDED_ANNOTATION_LAYER,
...args,
annotations: args.annotations ?? [],
layerType: LayerTypes.ANNOTATIONS,
};
},
};
}

View file

@ -0,0 +1,40 @@
/*
* 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 { ExtendedDataLayerFn } from '../types';
import { EXTENDED_DATA_LAYER, LayerTypes } from '../constants';
import { strings } from '../i18n';
import { commonDataLayerArgs } from './common_data_layer_args';
export const extendedDataLayerFunction: ExtendedDataLayerFn = {
name: EXTENDED_DATA_LAYER,
aliases: [],
type: EXTENDED_DATA_LAYER,
help: strings.getDataLayerFnHelp(),
inputTypes: ['datatable'],
args: {
...commonDataLayerArgs,
table: {
types: ['datatable'],
help: strings.getTableHelp(),
},
layerId: {
types: ['string'],
help: strings.getLayerIdHelp(),
},
},
fn(input, args) {
return {
type: EXTENDED_DATA_LAYER,
...args,
accessors: args.accessors ?? [],
layerType: LayerTypes.DATA,
table: args.table ?? input,
};
},
};

View file

@ -0,0 +1,40 @@
/*
* 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 { LayerTypes, EXTENDED_REFERENCE_LINE_LAYER } from '../constants';
import { ExtendedReferenceLineLayerFn } from '../types';
import { strings } from '../i18n';
import { commonReferenceLineLayerArgs } from './common_reference_line_layer_args';
export const extendedReferenceLineLayerFunction: ExtendedReferenceLineLayerFn = {
name: EXTENDED_REFERENCE_LINE_LAYER,
aliases: [],
type: EXTENDED_REFERENCE_LINE_LAYER,
help: strings.getRLHelp(),
inputTypes: ['datatable'],
args: {
...commonReferenceLineLayerArgs,
table: {
types: ['datatable'],
help: strings.getTableHelp(),
},
layerId: {
types: ['string'],
help: strings.getLayerIdHelp(),
},
},
fn(input, args) {
return {
type: EXTENDED_REFERENCE_LINE_LAYER,
...args,
accessors: args.accessors ?? [],
layerType: LayerTypes.REFERENCELINE,
table: args.table ?? input,
};
},
};

View file

@ -0,0 +1,80 @@
/*
* 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 {
AvailableReferenceLineIcons,
EXTENDED_Y_CONFIG,
FillStyles,
IconPositions,
LineStyles,
} from '../constants';
import { strings } from '../i18n';
import { ExtendedYConfigFn } from '../types';
import { commonYConfigArgs } from './common_y_config_args';
export const extendedYAxisConfigFunction: ExtendedYConfigFn = {
name: EXTENDED_Y_CONFIG,
aliases: [],
type: EXTENDED_Y_CONFIG,
help: strings.getYConfigFnHelp(),
inputTypes: ['null'],
args: {
...commonYConfigArgs,
lineStyle: {
types: ['string'],
options: [...Object.values(LineStyles)],
help: i18n.translate('expressionXY.yConfig.lineStyle.help', {
defaultMessage: 'The style of the reference line',
}),
strict: true,
},
lineWidth: {
types: ['number'],
help: i18n.translate('expressionXY.yConfig.lineWidth.help', {
defaultMessage: 'The width of the reference line',
}),
},
icon: {
types: ['string'],
help: i18n.translate('expressionXY.yConfig.icon.help', {
defaultMessage: 'An optional icon used for reference lines',
}),
options: [...Object.values(AvailableReferenceLineIcons)],
strict: true,
},
iconPosition: {
types: ['string'],
options: [...Object.values(IconPositions)],
help: i18n.translate('expressionXY.yConfig.iconPosition.help', {
defaultMessage: 'The placement of the icon for the reference line',
}),
strict: true,
},
textVisibility: {
types: ['boolean'],
help: i18n.translate('expressionXY.yConfig.textVisibility.help', {
defaultMessage: 'Visibility of the label on the reference line',
}),
},
fill: {
types: ['string'],
options: [...Object.values(FillStyles)],
help: i18n.translate('expressionXY.yConfig.fill.help', {
defaultMessage: 'Fill',
}),
strict: true,
},
},
fn(input, args) {
return {
type: EXTENDED_Y_CONFIG,
...args,
};
},
};

View file

@ -7,13 +7,18 @@
*/ */
export * from './xy_vis'; export * from './xy_vis';
export * from './layered_xy_vis';
export * from './legend_config'; export * from './legend_config';
export * from './annotation_layer_config'; export * from './annotation_layer';
export * from './extended_annotation_layer';
export * from './y_axis_config'; export * from './y_axis_config';
export * from './data_layer_config'; export * from './extended_y_axis_config';
export * from './data_layer';
export * from './extended_data_layer';
export * from './grid_lines_config'; export * from './grid_lines_config';
export * from './axis_extent_config'; export * from './axis_extent_config';
export * from './tick_labels_config'; export * from './tick_labels_config';
export * from './labels_orientation_config'; export * from './labels_orientation_config';
export * from './reference_line_layer_config'; export * from './reference_line_layer';
export * from './extended_reference_line_layer';
export * from './axis_titles_visibility_config'; export * from './axis_titles_visibility_config';

View file

@ -0,0 +1,39 @@
/*
* 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 { LayeredXyVisFn } from '../types';
import {
EXTENDED_DATA_LAYER,
EXTENDED_REFERENCE_LINE_LAYER,
LAYERED_XY_VIS,
EXTENDED_ANNOTATION_LAYER,
} from '../constants';
import { commonXYArgs } from './common_xy_args';
import { strings } from '../i18n';
export const layeredXyVisFunction: LayeredXyVisFn = {
name: LAYERED_XY_VIS,
type: 'render',
inputTypes: ['datatable'],
help: strings.getXYHelp(),
args: {
...commonXYArgs,
layers: {
types: [EXTENDED_DATA_LAYER, EXTENDED_REFERENCE_LINE_LAYER, EXTENDED_ANNOTATION_LAYER],
help: i18n.translate('expressionXY.layeredXyVis.layers.help', {
defaultMessage: 'Layers of visual series',
}),
multi: true,
},
},
async fn(data, args, handlers) {
const { layeredXyVisFn } = await import('./layered_xy_vis_fn');
return await layeredXyVisFn(data, args, handlers);
},
};

View file

@ -0,0 +1,33 @@
/*
* 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 { XY_VIS_RENDERER } from '../constants';
import { appendLayerIds } from '../helpers';
import { LayeredXyVisFn } from '../types';
import { logDatatables } from '../utils';
export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) => {
const layers = appendLayerIds(args.layers ?? [], 'layers');
logDatatables(layers, handlers);
return {
type: 'render',
as: XY_VIS_RENDERER,
value: {
args: {
...args,
layers,
ariaLabel:
args.ariaLabel ??
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
},
};
};

View file

@ -12,9 +12,9 @@ import { LegendConfig } from '../types';
import { legendConfigFunction } from './legend_config'; import { legendConfigFunction } from './legend_config';
describe('legendConfigFunction', () => { describe('legendConfigFunction', () => {
test('produces the correct arguments', () => { test('produces the correct arguments', async () => {
const args: LegendConfig = { isVisible: true, position: Position.Left }; const args: LegendConfig = { isVisible: true, position: Position.Left };
const result = legendConfigFunction.fn(null, args, createMockExecutionContext()); const result = await legendConfigFunction.fn(null, args, createMockExecutionContext());
expect(result).toEqual({ type: 'legendConfig', ...args }); expect(result).toEqual({ type: 'legendConfig', ...args });
}); });

View file

@ -8,16 +8,10 @@
import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { LEGEND_CONFIG } from '../constants'; import { LEGEND_CONFIG } from '../constants';
import { LegendConfig, LegendConfigResult } from '../types'; import { LegendConfigFn } from '../types';
export const legendConfigFunction: ExpressionFunctionDefinition< export const legendConfigFunction: LegendConfigFn = {
typeof LEGEND_CONFIG,
null,
LegendConfig,
LegendConfigResult
> = {
name: LEGEND_CONFIG, name: LEGEND_CONFIG,
aliases: [], aliases: [],
type: LEGEND_CONFIG, type: LEGEND_CONFIG,
@ -31,6 +25,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
help: i18n.translate('expressionXY.legendConfig.isVisible.help', { help: i18n.translate('expressionXY.legendConfig.isVisible.help', {
defaultMessage: 'Specifies whether or not the legend is visible.', defaultMessage: 'Specifies whether or not the legend is visible.',
}), }),
default: true,
}, },
position: { position: {
types: ['string'], types: ['string'],
@ -38,6 +33,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
help: i18n.translate('expressionXY.legendConfig.position.help', { help: i18n.translate('expressionXY.legendConfig.position.help', {
defaultMessage: 'Specifies the legend position.', defaultMessage: 'Specifies the legend position.',
}), }),
strict: true,
}, },
showSingleSeries: { showSingleSeries: {
types: ['boolean'], types: ['boolean'],
@ -58,6 +54,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
defaultMessage: defaultMessage:
'Specifies the horizontal alignment of the legend when it is displayed inside chart.', 'Specifies the horizontal alignment of the legend when it is displayed inside chart.',
}), }),
strict: true,
}, },
verticalAlignment: { verticalAlignment: {
types: ['string'], types: ['string'],
@ -66,6 +63,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
defaultMessage: defaultMessage:
'Specifies the vertical alignment of the legend when it is displayed inside chart.', 'Specifies the vertical alignment of the legend when it is displayed inside chart.',
}), }),
strict: true,
}, },
floatingColumns: { floatingColumns: {
types: ['number'], types: ['number'],
@ -93,10 +91,8 @@ export const legendConfigFunction: ExpressionFunctionDefinition<
}), }),
}, },
}, },
fn(input, args) { async fn(input, args, handlers) {
return { const { legendConfigFn } = await import('./legend_config_fn');
type: LEGEND_CONFIG, return await legendConfigFn(input, args, handlers);
...args,
};
}, },
}; };

View 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 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 { LEGEND_CONFIG } from '../constants';
import { LegendConfigFn } from '../types';
const errors = {
positionUsageWithIsInsideError: () =>
i18n.translate(
'expressionXY.reusable.function.legendConfig.errors.positionUsageWithIsInsideError',
{
defaultMessage:
'`position` argument is not applied if `isInside = true`. Please, use `horizontalAlignment` and `verticalAlignment` arguments instead.',
}
),
alignmentUsageWithFalsyIsInsideError: () =>
i18n.translate(
'expressionXY.reusable.function.legendConfig.errors.alignmentUsageWithFalsyIsInsideError',
{
defaultMessage:
'`horizontalAlignment` and `verticalAlignment` arguments are not applied if `isInside = false`. Please, use the `position` argument instead.',
}
),
floatingColumnsWithFalsyIsInsideError: () =>
i18n.translate(
'expressionXY.reusable.function.legendConfig.errors.floatingColumnsWithFalsyIsInsideError',
{
defaultMessage: '`floatingColumns` arguments are not applied if `isInside = false`.',
}
),
legendSizeWithFalsyIsInsideError: () =>
i18n.translate(
'expressionXY.reusable.function.legendConfig.errors.legendSizeWithFalsyIsInsideError',
{
defaultMessage: '`legendSize` argument is not applied if `isInside = false`.',
}
),
};
export const legendConfigFn: LegendConfigFn['fn'] = async (data, args) => {
if (args.isInside) {
if (args.position) {
throw new Error(errors.positionUsageWithIsInsideError());
}
if (args.legendSize !== undefined) {
throw new Error(errors.legendSizeWithFalsyIsInsideError());
}
}
if (!args.isInside) {
if (args.verticalAlignment || args.horizontalAlignment) {
throw new Error(errors.alignmentUsageWithFalsyIsInsideError());
}
if (args.floatingColumns !== undefined) {
throw new Error(errors.floatingColumnsWithFalsyIsInsideError());
}
}
return { type: LEGEND_CONFIG, ...args };
};

View file

@ -0,0 +1,30 @@
/*
* 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 { LayerTypes, REFERENCE_LINE_LAYER } from '../constants';
import { ReferenceLineLayerFn } from '../types';
import { strings } from '../i18n';
import { commonReferenceLineLayerArgs } from './common_reference_line_layer_args';
export const referenceLineLayerFunction: ReferenceLineLayerFn = {
name: REFERENCE_LINE_LAYER,
aliases: [],
type: REFERENCE_LINE_LAYER,
help: strings.getRLHelp(),
inputTypes: ['datatable'],
args: { ...commonReferenceLineLayerArgs },
fn(table, args) {
return {
type: REFERENCE_LINE_LAYER,
...args,
accessors: args.accessors ?? [],
layerType: LayerTypes.REFERENCELINE,
table,
};
},
};

View file

@ -1,62 +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 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 type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { LayerTypes, REFERENCE_LINE_LAYER, Y_CONFIG } from '../constants';
import { ReferenceLineLayerArgs, ReferenceLineLayerConfigResult } from '../types';
export const referenceLineLayerConfigFunction: ExpressionFunctionDefinition<
typeof REFERENCE_LINE_LAYER,
null,
ReferenceLineLayerArgs,
ReferenceLineLayerConfigResult
> = {
name: REFERENCE_LINE_LAYER,
aliases: [],
type: REFERENCE_LINE_LAYER,
help: i18n.translate('expressionXY.referenceLineLayer.help', {
defaultMessage: `Configure a reference line in the xy chart`,
}),
inputTypes: ['null'],
args: {
layerId: {
types: ['string'],
help: i18n.translate('expressionXY.referenceLineLayer.layerId.help', {
defaultMessage: `Layer ID`,
}),
},
accessors: {
types: ['string'],
help: i18n.translate('expressionXY.referenceLineLayer.accessors.help', {
defaultMessage: 'The columns to display on the y axis.',
}),
multi: true,
},
yConfig: {
types: [Y_CONFIG],
help: i18n.translate('expressionXY.referenceLineLayer.yConfig.help', {
defaultMessage: 'Additional configuration for y axes',
}),
multi: true,
},
columnToLabel: {
types: ['string'],
help: i18n.translate('expressionXY.referenceLineLayer.columnToLabel.help', {
defaultMessage: 'JSON key-value pairs of column ID to label',
}),
},
},
fn(input, args) {
return {
type: REFERENCE_LINE_LAYER,
...args,
layerType: LayerTypes.REFERENCELINE,
};
},
};

View file

@ -0,0 +1,103 @@
/*
* 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 { AxisExtentModes, ValueLabelModes } from '../constants';
import {
AxisExtentConfigResult,
DataLayerConfigResult,
ValueLabelMode,
CommonXYDataLayerConfig,
} from '../types';
const errors = {
extendBoundsAreInvalidError: () =>
i18n.translate('expressionXY.reusable.function.xyVis.errors.extendBoundsAreInvalidError', {
defaultMessage:
'For area and bar modes, and custom extent mode, the lower bound should be less or greater than 0 and the upper bound - be greater or equal than 0',
}),
notUsedFillOpacityError: () =>
i18n.translate('expressionXY.reusable.function.xyVis.errors.notUsedFillOpacityError', {
defaultMessage: '`fillOpacity` argument is applicable only for area charts.',
}),
valueLabelsForNotBarsOrHistogramBarsChartsError: () =>
i18n.translate(
'expressionXY.reusable.function.xyVis.errors.valueLabelsForNotBarsOrHistogramBarsChartsError',
{
defaultMessage:
'`valueLabels` argument is applicable only for bar charts, which are not histograms.',
}
),
dataBoundsForNotLineChartError: () =>
i18n.translate('expressionXY.reusable.function.xyVis.errors.dataBoundsForNotLineChartError', {
defaultMessage: 'Only line charts can be fit to the data bounds',
}),
};
export const hasBarLayer = (layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>) =>
layers.filter(({ seriesType }) => seriesType.includes('bar')).length > 0;
export const hasAreaLayer = (layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>) =>
layers.filter(({ seriesType }) => seriesType.includes('area')).length > 0;
export const hasHistogramBarLayer = (
layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>
) =>
layers.filter(({ seriesType, isHistogram }) => seriesType.includes('bar') && isHistogram).length >
0;
export const isValidExtentWithCustomMode = (extent: AxisExtentConfigResult) => {
const isValidLowerBound =
extent.lowerBound === undefined || (extent.lowerBound !== undefined && extent.lowerBound <= 0);
const isValidUpperBound =
extent.upperBound === undefined || (extent.upperBound !== undefined && extent.upperBound >= 0);
return isValidLowerBound && isValidUpperBound;
};
export const validateExtentForDataBounds = (
extent: AxisExtentConfigResult,
layers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>
) => {
const lineSeries = layers.filter(({ seriesType }) => seriesType.includes('line'));
if (!lineSeries.length && extent.mode === AxisExtentModes.DATA_BOUNDS) {
throw new Error(errors.dataBoundsForNotLineChartError());
}
};
export const validateExtent = (
extent: AxisExtentConfigResult,
hasBarOrArea: boolean,
dataLayers: Array<DataLayerConfigResult | CommonXYDataLayerConfig>
) => {
if (
extent.mode === AxisExtentModes.CUSTOM &&
hasBarOrArea &&
!isValidExtentWithCustomMode(extent)
) {
throw new Error(errors.extendBoundsAreInvalidError());
}
validateExtentForDataBounds(extent, dataLayers);
};
export const validateFillOpacity = (fillOpacity: number | undefined, hasArea: boolean) => {
if (fillOpacity !== undefined && !hasArea) {
throw new Error(errors.notUsedFillOpacityError());
}
};
export const validateValueLabels = (
valueLabels: ValueLabelMode,
hasBar: boolean,
hasNotHistogramBars: boolean
) => {
if ((!hasBar || !hasNotHistogramBars) && valueLabels !== ValueLabelModes.HIDE) {
throw new Error(errors.valueLabelsForNotBarsOrHistogramBarsChartsError());
}
};

View file

@ -8,14 +8,23 @@
import { xyVisFunction } from '.'; import { xyVisFunction } from '.';
import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks';
import { sampleArgs } from '../__mocks__'; import { sampleArgs, sampleLayer } from '../__mocks__';
import { XY_VIS } from '../constants'; import { XY_VIS } from '../constants';
describe('xyVis', () => { describe('xyVis', () => {
test('it renders with the specified data and args', () => { test('it renders with the specified data and args', async () => {
const { data, args } = sampleArgs(); const { data, args } = sampleArgs();
const result = xyVisFunction.fn(data, args, createMockExecutionContext()); const { layers, ...rest } = args;
const result = await xyVisFunction.fn(
data,
{ ...rest, dataLayers: [sampleLayer], referenceLineLayers: [], annotationLayers: [] },
createMockExecutionContext()
);
expect(result).toEqual({ type: 'render', as: XY_VIS, value: { data, args } }); expect(result).toEqual({
type: 'render',
as: XY_VIS,
value: { args: { ...rest, layers: [sampleLayer] } },
});
}); });
}); });

View file

@ -6,243 +6,36 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { i18n } from '@kbn/i18n'; import { XyVisFn } from '../types';
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin'; import { XY_VIS, DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants';
import { prepareLogTable } from '@kbn/visualizations-plugin/common/utils'; import { strings } from '../i18n';
import { LensMultiTable, XYArgs, XYRender } from '../types'; import { commonXYArgs } from './common_xy_args';
import {
XY_VIS,
DATA_LAYER,
MULTITABLE,
XYCurveTypes,
LEGEND_CONFIG,
ValueLabelModes,
FittingFunctions,
GRID_LINES_CONFIG,
XY_VIS_RENDERER,
AXIS_EXTENT_CONFIG,
TICK_LABELS_CONFIG,
REFERENCE_LINE_LAYER,
LABELS_ORIENTATION_CONFIG,
AXIS_TITLES_VISIBILITY_CONFIG,
EndValues,
ANNOTATION_LAYER,
LayerTypes,
} from '../constants';
const strings = { export const xyVisFunction: XyVisFn = {
getMetricHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.metric', {
defaultMessage: 'Vertical axis',
}),
getXAxisHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.x', {
defaultMessage: 'Horizontal axis',
}),
getBreakdownHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
defaultMessage: 'Break down by',
}),
getReferenceLineHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
defaultMessage: 'Break down by',
}),
};
export const xyVisFunction: ExpressionFunctionDefinition<
typeof XY_VIS,
LensMultiTable,
XYArgs,
XYRender
> = {
name: XY_VIS, name: XY_VIS,
type: 'render', type: 'render',
inputTypes: [MULTITABLE], inputTypes: ['datatable'],
help: i18n.translate('expressionXY.xyVis.help', { help: strings.getXYHelp(),
defaultMessage: 'An X/Y chart',
}),
args: { args: {
title: { ...commonXYArgs,
types: ['string'], dataLayers: {
help: 'The chart title.', types: [DATA_LAYER],
}, help: strings.getDataLayerHelp(),
description: {
types: ['string'],
help: '',
},
xTitle: {
types: ['string'],
help: i18n.translate('expressionXY.xyVis.xTitle.help', {
defaultMessage: 'X axis title',
}),
},
yTitle: {
types: ['string'],
help: i18n.translate('expressionXY.xyVis.yLeftTitle.help', {
defaultMessage: 'Y left axis title',
}),
},
yRightTitle: {
types: ['string'],
help: i18n.translate('expressionXY.xyVis.yRightTitle.help', {
defaultMessage: 'Y right axis title',
}),
},
yLeftExtent: {
types: [AXIS_EXTENT_CONFIG],
help: i18n.translate('expressionXY.xyVis.yLeftExtent.help', {
defaultMessage: 'Y left axis extents',
}),
},
yRightExtent: {
types: [AXIS_EXTENT_CONFIG],
help: i18n.translate('expressionXY.xyVis.yRightExtent.help', {
defaultMessage: 'Y right axis extents',
}),
},
legend: {
types: [LEGEND_CONFIG],
help: i18n.translate('expressionXY.xyVis.legend.help', {
defaultMessage: 'Configure the chart legend.',
}),
},
fittingFunction: {
types: ['string'],
options: [...Object.values(FittingFunctions)],
help: i18n.translate('expressionXY.xyVis.fittingFunction.help', {
defaultMessage: 'Define how missing values are treated',
}),
},
endValue: {
types: ['string'],
options: [...Object.values(EndValues)],
help: i18n.translate('expressionXY.xyVis.endValue.help', {
defaultMessage: 'End value',
}),
},
emphasizeFitting: {
types: ['boolean'],
default: false,
help: '',
},
valueLabels: {
types: ['string'],
options: [...Object.values(ValueLabelModes)],
help: i18n.translate('expressionXY.xyVis.valueLabels.help', {
defaultMessage: 'Value labels mode',
}),
},
tickLabelsVisibilitySettings: {
types: [TICK_LABELS_CONFIG],
help: i18n.translate('expressionXY.xyVis.tickLabelsVisibilitySettings.help', {
defaultMessage: 'Show x and y axes tick labels',
}),
},
labelsOrientation: {
types: [LABELS_ORIENTATION_CONFIG],
help: i18n.translate('expressionXY.xyVis.labelsOrientation.help', {
defaultMessage: 'Defines the rotation of the axis labels',
}),
},
gridlinesVisibilitySettings: {
types: [GRID_LINES_CONFIG],
help: i18n.translate('expressionXY.xyVis.gridlinesVisibilitySettings.help', {
defaultMessage: 'Show x and y axes gridlines',
}),
},
axisTitlesVisibilitySettings: {
types: [AXIS_TITLES_VISIBILITY_CONFIG],
help: i18n.translate('expressionXY.xyVis.axisTitlesVisibilitySettings.help', {
defaultMessage: 'Show x and y axes titles',
}),
},
layers: {
types: [DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER],
help: i18n.translate('expressionXY.xyVis.layers.help', {
defaultMessage: 'Layers of visual series',
}),
multi: true, multi: true,
}, },
curveType: { referenceLineLayers: {
types: ['string'], types: [REFERENCE_LINE_LAYER],
options: [...Object.values(XYCurveTypes)], help: strings.getReferenceLineLayerHelp(),
help: i18n.translate('expressionXY.xyVis.curveType.help', { multi: true,
defaultMessage: 'Define how curve type is rendered for a line chart',
}),
}, },
fillOpacity: { annotationLayers: {
types: ['number'], types: [ANNOTATION_LAYER],
help: i18n.translate('expressionXY.xyVis.fillOpacity.help', { help: strings.getAnnotationLayerHelp(),
defaultMessage: 'Define the area chart fill opacity', multi: true,
}),
},
hideEndzones: {
types: ['boolean'],
default: false,
help: i18n.translate('expressionXY.xyVis.hideEndzones.help', {
defaultMessage: 'Hide endzone markers for partial data',
}),
},
valuesInLegend: {
types: ['boolean'],
default: false,
help: i18n.translate('expressionXY.xyVis.valuesInLegend.help', {
defaultMessage: 'Show values in legend',
}),
},
ariaLabel: {
types: ['string'],
help: i18n.translate('expressionXY.xyVis.ariaLabel.help', {
defaultMessage: 'Specifies the aria label of the xy chart',
}),
required: false,
}, },
}, },
fn(data, args, handlers) { async fn(data, args, handlers) {
if (handlers?.inspectorAdapters?.tables) { const { xyVisFn } = await import('./xy_vis_fn');
args.layers.forEach((layer) => { return await xyVisFn(data, args, handlers);
if (layer.layerType === LayerTypes.ANNOTATIONS) {
return;
}
let xAccessor;
let splitAccessor;
if (layer.layerType === LayerTypes.DATA) {
xAccessor = layer.xAccessor;
splitAccessor = layer.splitAccessor;
}
const { layerId, accessors, layerType } = layer;
const logTable = prepareLogTable(
data.tables[layerId],
[
[
accessors ? accessors : undefined,
layerType === 'data' ? strings.getMetricHelp() : strings.getReferenceLineHelp(),
],
[xAccessor ? [xAccessor] : undefined, strings.getXAxisHelp()],
[splitAccessor ? [splitAccessor] : undefined, strings.getBreakdownHelp()],
],
true
);
handlers.inspectorAdapters.tables.logDatatable(layerId, logTable);
});
}
return {
type: 'render',
as: XY_VIS_RENDERER,
value: {
data,
args: {
...args,
ariaLabel:
args.ariaLabel ??
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
},
};
}, },
}; };

View file

@ -0,0 +1,69 @@
/*
* 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 { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils';
import { LayerTypes, XY_VIS_RENDERER } from '../constants';
import { appendLayerIds } from '../helpers';
import { XYLayerConfig, XyVisFn } from '../types';
import { getLayerDimensions } from '../utils';
import {
hasAreaLayer,
hasBarLayer,
hasHistogramBarLayer,
validateExtent,
validateFillOpacity,
validateValueLabels,
} from './validate';
export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
const { dataLayers = [], referenceLineLayers = [], annotationLayers = [], ...restArgs } = args;
const layers: XYLayerConfig[] = [
...appendLayerIds(dataLayers, 'dataLayers'),
...appendLayerIds(referenceLineLayers, 'referenceLineLayers'),
...appendLayerIds(annotationLayers, 'annotationLayers'),
];
if (handlers.inspectorAdapters.tables) {
const layerDimensions = layers.reduce<Dimension[]>((dimensions, layer) => {
if (layer.layerType === LayerTypes.ANNOTATIONS) {
return dimensions;
}
return [...dimensions, ...getLayerDimensions(layer)];
}, []);
const logTable = prepareLogTable(data, layerDimensions, true);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
const hasBar = hasBarLayer(dataLayers);
const hasArea = hasAreaLayer(dataLayers);
validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers);
validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers);
validateFillOpacity(args.fillOpacity, hasArea);
const hasNotHistogramBars = !hasHistogramBarLayer(dataLayers);
validateValueLabels(args.valueLabels, hasBar, hasNotHistogramBars);
return {
type: 'render',
as: XY_VIS_RENDERER,
value: {
args: {
...restArgs,
layers,
ariaLabel:
args.ariaLabel ??
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
},
};
};

View file

@ -6,84 +6,18 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { i18n } from '@kbn/i18n'; import { Y_CONFIG } from '../constants';
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { YConfigFn } from '../types';
import { FillStyles, IconPositions, LineStyles, YAxisModes, Y_CONFIG } from '../constants'; import { strings } from '../i18n';
import { YConfig, YConfigResult } from '../types'; import { commonYConfigArgs } from './common_y_config_args';
export const yAxisConfigFunction: ExpressionFunctionDefinition< export const yAxisConfigFunction: YConfigFn = {
typeof Y_CONFIG,
null,
YConfig,
YConfigResult
> = {
name: Y_CONFIG, name: Y_CONFIG,
aliases: [], aliases: [],
type: Y_CONFIG, type: Y_CONFIG,
help: i18n.translate('expressionXY.yConfig.help', { help: strings.getYConfigFnHelp(),
defaultMessage: `Configure the behavior of a xy chart's y axis metric`,
}),
inputTypes: ['null'], inputTypes: ['null'],
args: { args: { ...commonYConfigArgs },
forAccessor: {
types: ['string'],
help: i18n.translate('expressionXY.yConfig.forAccessor.help', {
defaultMessage: 'The accessor this configuration is for',
}),
},
axisMode: {
types: ['string'],
options: [...Object.values(YAxisModes)],
help: i18n.translate('expressionXY.yConfig.axisMode.help', {
defaultMessage: 'The axis mode of the metric',
}),
},
color: {
types: ['string'],
help: i18n.translate('expressionXY.yConfig.color.help', {
defaultMessage: 'The color of the series',
}),
},
lineStyle: {
types: ['string'],
options: [...Object.values(LineStyles)],
help: i18n.translate('expressionXY.yConfig.lineStyle.help', {
defaultMessage: 'The style of the reference line',
}),
},
lineWidth: {
types: ['number'],
help: i18n.translate('expressionXY.yConfig.lineWidth.help', {
defaultMessage: 'The width of the reference line',
}),
},
icon: {
types: ['string'],
help: i18n.translate('expressionXY.yConfig.icon.help', {
defaultMessage: 'An optional icon used for reference lines',
}),
},
iconPosition: {
types: ['string'],
options: [...Object.values(IconPositions)],
help: i18n.translate('expressionXY.yConfig.iconPosition.help', {
defaultMessage: 'The placement of the icon for the reference line',
}),
},
textVisibility: {
types: ['boolean'],
help: i18n.translate('expressionXY.yConfig.textVisibility.help', {
defaultMessage: 'Visibility of the label on the reference line',
}),
},
fill: {
types: ['string'],
options: [...Object.values(FillStyles)],
help: i18n.translate('expressionXY.yConfig.fill.help', {
defaultMessage: 'Fill',
}),
},
},
fn(input, args) { fn(input, args) {
return { return {
type: Y_CONFIG, type: Y_CONFIG,

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { appendLayerIds } from './layers';

View file

@ -0,0 +1,49 @@
/*
* 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 { generateLayerId, appendLayerIds } from './layers';
describe('#generateLayerId', () => {
it('should return the combination of keyword and index', () => {
const key = 'some-key';
const index = 10;
const id = generateLayerId(key, index);
expect(id).toBe(`${key}-${index}`);
});
});
describe('#appendLayerIds', () => {
it('should add layerId to each layer', () => {
const layers = [{ name: 'someName' }, { name: 'someName2' }, { name: 'someName3' }];
const keyword = 'keyword';
const expectedLayerIds = [
{ ...layers[0], layerId: `${keyword}-0` },
{ ...layers[1], layerId: `${keyword}-1` },
{ ...layers[2], layerId: `${keyword}-2` },
];
const layersWithIds = appendLayerIds(layers, keyword);
expect(layersWithIds).toStrictEqual(expectedLayerIds);
});
it('should filter out undefined layers', () => {
const layers = [undefined, undefined, undefined];
const result = appendLayerIds(layers, 'some-key');
expect(result).toStrictEqual([]);
const layers2 = [{ name: 'someName' }, undefined, { name: 'someName3' }];
const keyword = 'keyword';
const expectedLayerIds = [
{ ...layers2[0], layerId: `${keyword}-0` },
{ ...layers2[2], layerId: `${keyword}-1` },
];
const layersWithIds = appendLayerIds(layers2, keyword);
expect(layersWithIds).toStrictEqual(expectedLayerIds);
});
});

View file

@ -0,0 +1,27 @@
/*
* 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 { WithLayerId } from '../types';
function isWithLayerId<T>(layer: T): layer is T & WithLayerId {
return (layer as T & WithLayerId).layerId ? true : false;
}
export const generateLayerId = (keyword: string, index: number) => `${keyword}-${index}`;
export function appendLayerIds<T>(
layers: Array<T | undefined>,
keyword: string
): Array<T & WithLayerId> {
return layers
.filter((l): l is T => l !== undefined)
.map((l, index) => ({
...l,
layerId: isWithLayerId(l) ? l.layerId : generateLayerId(keyword, index),
}));
}

View file

@ -0,0 +1,212 @@
/*
* 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';
export const strings = {
getXYHelp: () =>
i18n.translate('expressionXY.xyVis.help', {
defaultMessage: 'An X/Y chart',
}),
getMetricHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.metric', {
defaultMessage: 'Vertical axis',
}),
getXAxisHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.x', {
defaultMessage: 'Horizontal axis',
}),
getBreakdownHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
defaultMessage: 'Break down by',
}),
getReferenceLineHelp: () =>
i18n.translate('expressionXY.xyVis.logDatatable.breakDown', {
defaultMessage: 'Break down by',
}),
getXTitleHelp: () =>
i18n.translate('expressionXY.xyVis.xTitle.help', {
defaultMessage: 'X axis title',
}),
getYTitleHelp: () =>
i18n.translate('expressionXY.xyVis.yLeftTitle.help', {
defaultMessage: 'Y left axis title',
}),
getYRightTitleHelp: () =>
i18n.translate('expressionXY.xyVis.yRightTitle.help', {
defaultMessage: 'Y right axis title',
}),
getYLeftExtentHelp: () =>
i18n.translate('expressionXY.xyVis.yLeftExtent.help', {
defaultMessage: 'Y left axis extents',
}),
getYRightExtentHelp: () =>
i18n.translate('expressionXY.xyVis.yRightExtent.help', {
defaultMessage: 'Y right axis extents',
}),
getLegendHelp: () =>
i18n.translate('expressionXY.xyVis.legend.help', {
defaultMessage: 'Configure the chart legend.',
}),
getFittingFunctionHelp: () =>
i18n.translate('expressionXY.xyVis.fittingFunction.help', {
defaultMessage: 'Define how missing values are treated',
}),
getEndValueHelp: () =>
i18n.translate('expressionXY.xyVis.endValue.help', {
defaultMessage: 'End value',
}),
getValueLabelsHelp: () =>
i18n.translate('expressionXY.xyVis.valueLabels.help', {
defaultMessage: 'Value labels mode',
}),
getTickLabelsVisibilitySettingsHelp: () =>
i18n.translate('expressionXY.xyVis.tickLabelsVisibilitySettings.help', {
defaultMessage: 'Show x and y axes tick labels',
}),
getLabelsOrientationHelp: () =>
i18n.translate('expressionXY.xyVis.labelsOrientation.help', {
defaultMessage: 'Defines the rotation of the axis labels',
}),
getGridlinesVisibilitySettingsHelp: () =>
i18n.translate('expressionXY.xyVis.gridlinesVisibilitySettings.help', {
defaultMessage: 'Show x and y axes gridlines',
}),
getAxisTitlesVisibilitySettingsHelp: () =>
i18n.translate('expressionXY.xyVis.axisTitlesVisibilitySettings.help', {
defaultMessage: 'Show x and y axes titles',
}),
getDataLayerHelp: () =>
i18n.translate('expressionXY.xyVis.dataLayer.help', {
defaultMessage: 'Data layer of visual series',
}),
getReferenceLineLayerHelp: () =>
i18n.translate('expressionXY.xyVis.referenceLineLayer.help', {
defaultMessage: 'Reference line layer',
}),
getAnnotationLayerHelp: () =>
i18n.translate('expressionXY.xyVis.annotationLayer.help', {
defaultMessage: 'Annotation layer',
}),
getCurveTypeHelp: () =>
i18n.translate('expressionXY.xyVis.curveType.help', {
defaultMessage: 'Define how curve type is rendered for a line chart',
}),
getFillOpacityHelp: () =>
i18n.translate('expressionXY.xyVis.fillOpacity.help', {
defaultMessage: 'Define the area chart fill opacity',
}),
getHideEndzonesHelp: () =>
i18n.translate('expressionXY.xyVis.hideEndzones.help', {
defaultMessage: 'Hide endzone markers for partial data',
}),
getValuesInLegendHelp: () =>
i18n.translate('expressionXY.xyVis.valuesInLegend.help', {
defaultMessage: 'Show values in legend',
}),
getAriaLabelHelp: () =>
i18n.translate('expressionXY.xyVis.ariaLabel.help', {
defaultMessage: 'Specifies the aria label of the xy chart',
}),
getDataLayerFnHelp: () =>
i18n.translate('expressionXY.dataLayer.help', {
defaultMessage: `Configure a layer in the xy chart`,
}),
getHideHelp: () =>
i18n.translate('expressionXY.dataLayer.hide.help', {
defaultMessage: 'Show / hide axis',
}),
getXAccessorHelp: () =>
i18n.translate('expressionXY.dataLayer.xAccessor.help', {
defaultMessage: 'X-axis',
}),
getSeriesTypeHelp: () =>
i18n.translate('expressionXY.dataLayer.seriesType.help', {
defaultMessage: 'The type of chart to display.',
}),
getXScaleTypeHelp: () =>
i18n.translate('expressionXY.dataLayer.xScaleType.help', {
defaultMessage: 'The scale type of the x axis',
}),
getIsHistogramHelp: () =>
i18n.translate('expressionXY.dataLayer.isHistogram.help', {
defaultMessage: 'Whether to layout the chart as a histogram',
}),
getYScaleTypeHelp: () =>
i18n.translate('expressionXY.dataLayer.yScaleType.help', {
defaultMessage: 'The scale type of the y axes',
}),
getSplitAccessorHelp: () =>
i18n.translate('expressionXY.dataLayer.splitAccessor.help', {
defaultMessage: 'The column to split by',
}),
getAccessorsHelp: () =>
i18n.translate('expressionXY.dataLayer.accessors.help', {
defaultMessage: 'The columns to display on the y axis.',
}),
getYConfigHelp: () =>
i18n.translate('expressionXY.dataLayer.yConfig.help', {
defaultMessage: 'Additional configuration for y axes',
}),
getColumnToLabelHelp: () =>
i18n.translate('expressionXY.layer.columnToLabel.help', {
defaultMessage: 'JSON key-value pairs of column ID to label',
}),
getPaletteHelp: () =>
i18n.translate('expressionXY.dataLayer.palette.help', {
defaultMessage: 'Palette',
}),
getTableHelp: () =>
i18n.translate('expressionXY.layers.table.help', {
defaultMessage: 'Table',
}),
getLayerIdHelp: () =>
i18n.translate('expressionXY.layers.layerId.help', {
defaultMessage: 'Layer ID',
}),
getRLAccessorsHelp: () =>
i18n.translate('expressionXY.referenceLineLayer.accessors.help', {
defaultMessage: 'The columns to display on the y axis.',
}),
getRLYConfigHelp: () =>
i18n.translate('expressionXY.referenceLineLayer.yConfig.help', {
defaultMessage: 'Additional configuration for y axes',
}),
getRLHelp: () =>
i18n.translate('expressionXY.referenceLineLayer.help', {
defaultMessage: `Configure a reference line in the xy chart`,
}),
getYConfigFnHelp: () =>
i18n.translate('expressionXY.yConfig.help', {
defaultMessage: `Configure the behavior of a xy chart's y axis metric`,
}),
getForAccessorHelp: () =>
i18n.translate('expressionXY.yConfig.forAccessor.help', {
defaultMessage: 'The accessor this configuration is for',
}),
getAxisModeHelp: () =>
i18n.translate('expressionXY.yConfig.axisMode.help', {
defaultMessage: 'The axis mode of the metric',
}),
getColorHelp: () =>
i18n.translate('expressionXY.yConfig.color.help', {
defaultMessage: 'The color of the series',
}),
getAnnotationLayerFnHelp: () =>
i18n.translate('expressionXY.annotationLayer.help', {
defaultMessage: `Configure an annotation layer in the xy chart`,
}),
getAnnotationLayerHideHelp: () =>
i18n.translate('expressionXY.annotationLayer.hide.help', {
defaultMessage: 'Show / hide details',
}),
getAnnotationLayerAnnotationsHelp: () =>
i18n.translate('expressionXY.annotationLayer.annotations.help', {
defaultMessage: 'Annotations',
}),
};

View file

@ -9,20 +9,6 @@
export const PLUGIN_ID = 'expressionXy'; export const PLUGIN_ID = 'expressionXy';
export const PLUGIN_NAME = 'expressionXy'; export const PLUGIN_NAME = 'expressionXy';
export {
xyVisFunction,
yAxisConfigFunction,
legendConfigFunction,
gridlinesConfigFunction,
dataLayerConfigFunction,
axisExtentConfigFunction,
tickLabelsConfigFunction,
annotationLayerConfigFunction,
labelsOrientationConfigFunction,
referenceLineLayerConfigFunction,
axisTitlesVisibilityConfigFunction,
} from './expression_functions';
export type { export type {
XYArgs, XYArgs,
YConfig, YConfig,
@ -42,25 +28,37 @@ export type {
XYChartProps, XYChartProps,
LegendConfig, LegendConfig,
IconPosition, IconPosition,
YConfigResult,
DataLayerArgs, DataLayerArgs,
LensMultiTable, LensMultiTable,
ValueLabelMode, ValueLabelMode,
AxisExtentMode, AxisExtentMode,
DataLayerConfig,
FittingFunction, FittingFunction,
ExtendedYConfig,
AxisExtentConfig, AxisExtentConfig,
CollectiveConfig,
LegendConfigResult, LegendConfigResult,
AxesSettingsConfig, AxesSettingsConfig,
CommonXYLayerConfig,
AnnotationLayerArgs, AnnotationLayerArgs,
XYLayerConfigResult, ExtendedYConfigResult,
GridlinesConfigResult, GridlinesConfigResult,
DataLayerConfigResult, DataLayerConfigResult,
TickLabelsConfigResult, TickLabelsConfigResult,
AxisExtentConfigResult, AxisExtentConfigResult,
ReferenceLineLayerArgs, ReferenceLineLayerArgs,
CommonXYDataLayerConfig,
LabelsOrientationConfig, LabelsOrientationConfig,
AnnotationLayerConfigResult, ReferenceLineLayerConfig,
AvailableReferenceLineIcon,
XYExtendedLayerConfigResult,
CommonXYAnnotationLayerConfig,
ExtendedDataLayerConfigResult,
LabelsOrientationConfigResult, LabelsOrientationConfigResult,
CommonXYDataLayerConfigResult,
ReferenceLineLayerConfigResult, ReferenceLineLayerConfigResult,
CommonXYReferenceLineLayerConfig,
AxisTitlesVisibilityConfigResult, AxisTitlesVisibilityConfigResult,
ExtendedReferenceLineLayerConfigResult,
CommonXYReferenceLineLayerConfigResult,
} from './types'; } from './types';

View file

@ -9,7 +9,7 @@
import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
import { $Values } from '@kbn/utility-types'; import { $Values } from '@kbn/utility-types';
import type { PaletteOutput } from '@kbn/coloring'; import type { PaletteOutput } from '@kbn/coloring';
import { Datatable } from '@kbn/expressions-plugin'; import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin';
import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common';
import { import {
AxisExtentModes, AxisExtentModes,
@ -34,9 +34,17 @@ import {
LEGEND_CONFIG, LEGEND_CONFIG,
DATA_LAYER, DATA_LAYER,
AXIS_EXTENT_CONFIG, AXIS_EXTENT_CONFIG,
EXTENDED_DATA_LAYER,
EXTENDED_REFERENCE_LINE_LAYER,
ANNOTATION_LAYER, ANNOTATION_LAYER,
EndValues, EndValues,
EXTENDED_Y_CONFIG,
AvailableReferenceLineIcons,
XY_VIS,
LAYERED_XY_VIS,
EXTENDED_ANNOTATION_LAYER,
} from '../constants'; } from '../constants';
import { XYRender } from './expression_renderers';
export type EndValue = $Values<typeof EndValues>; export type EndValue = $Values<typeof EndValues>;
export type LayerType = $Values<typeof LayerTypes>; export type LayerType = $Values<typeof LayerTypes>;
@ -51,6 +59,7 @@ export type IconPosition = $Values<typeof IconPositions>;
export type ValueLabelMode = $Values<typeof ValueLabelModes>; export type ValueLabelMode = $Values<typeof ValueLabelModes>;
export type AxisExtentMode = $Values<typeof AxisExtentModes>; export type AxisExtentMode = $Values<typeof AxisExtentModes>;
export type FittingFunction = $Values<typeof FittingFunctions>; export type FittingFunction = $Values<typeof FittingFunctions>;
export type AvailableReferenceLineIcon = $Values<typeof AvailableReferenceLineIcons>;
export interface AxesSettingsConfig { export interface AxesSettingsConfig {
x: boolean; x: boolean;
@ -69,11 +78,8 @@ export interface AxisConfig {
hide?: boolean; hide?: boolean;
} }
export interface YConfig { export interface ExtendedYConfig extends YConfig {
forAccessor: string; icon?: AvailableReferenceLineIcon;
axisMode?: YAxisMode;
color?: string;
icon?: string;
lineWidth?: number; lineWidth?: number;
lineStyle?: LineStyle; lineStyle?: LineStyle;
fill?: FillStyle; fill?: FillStyle;
@ -81,12 +87,13 @@ export interface YConfig {
textVisibility?: boolean; textVisibility?: boolean;
} }
export interface ValidLayer extends DataLayerConfigResult { export interface YConfig {
xAccessor: NonNullable<DataLayerConfigResult['xAccessor']>; forAccessor: string;
axisMode?: YAxisMode;
color?: string;
} }
export interface DataLayerArgs { export interface DataLayerArgs {
layerId: string;
accessors: string[]; accessors: string[];
seriesType: SeriesType; seriesType: SeriesType;
xAccessor?: string; xAccessor?: string;
@ -96,11 +103,30 @@ export interface DataLayerArgs {
yScaleType: YScaleType; yScaleType: YScaleType;
xScaleType: XScaleType; xScaleType: XScaleType;
isHistogram: boolean; isHistogram: boolean;
// palette will always be set on the expression
palette: PaletteOutput; palette: PaletteOutput;
yConfig?: YConfigResult[]; yConfig?: YConfigResult[];
} }
export interface ValidLayer extends DataLayerConfigResult {
xAccessor: NonNullable<DataLayerConfigResult['xAccessor']>;
}
export interface ExtendedDataLayerArgs {
layerId?: string;
accessors: string[];
seriesType: SeriesType;
xAccessor?: string;
hide?: boolean;
splitAccessor?: string;
columnToLabel?: string; // Actually a JSON key-value pair
yScaleType: YScaleType;
xScaleType: XScaleType;
isHistogram: boolean;
palette: PaletteOutput;
yConfig?: YConfigResult[];
table?: Datatable;
}
export interface LegendConfig { export interface LegendConfig {
/** /**
* Flag whether the legend should be shown. If there is just a single series, it will be hidden * Flag whether the legend should be shown. If there is just a single series, it will be hidden
@ -121,11 +147,11 @@ export interface LegendConfig {
/** /**
* Horizontal Alignment of the legend when it is set inside chart * Horizontal Alignment of the legend when it is set inside chart
*/ */
horizontalAlignment?: HorizontalAlignment; horizontalAlignment?: typeof HorizontalAlignment.Right | typeof HorizontalAlignment.Left;
/** /**
* Vertical Alignment of the legend when it is set inside chart * Vertical Alignment of the legend when it is set inside chart
*/ */
verticalAlignment?: VerticalAlignment; verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom;
/** /**
* Number of columns when legend is set inside chart * Number of columns when legend is set inside chart
*/ */
@ -155,8 +181,54 @@ export interface LabelsOrientationConfig {
// Arguments to XY chart expression, with computed properties // Arguments to XY chart expression, with computed properties
export interface XYArgs { export interface XYArgs {
title?: string; xTitle: string;
description?: string; yTitle: string;
yRightTitle: string;
yLeftExtent: AxisExtentConfigResult;
yRightExtent: AxisExtentConfigResult;
legend: LegendConfigResult;
endValue?: EndValue;
emphasizeFitting?: boolean;
valueLabels: ValueLabelMode;
dataLayers: DataLayerConfigResult[];
referenceLineLayers: ReferenceLineLayerConfigResult[];
annotationLayers: AnnotationLayerConfigResult[];
fittingFunction?: FittingFunction;
axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult;
tickLabelsVisibilitySettings?: TickLabelsConfigResult;
gridlinesVisibilitySettings?: GridlinesConfigResult;
labelsOrientation?: LabelsOrientationConfigResult;
curveType?: XYCurveType;
fillOpacity?: number;
hideEndzones?: boolean;
valuesInLegend?: boolean;
ariaLabel?: string;
}
export interface LayeredXYArgs {
xTitle: string;
yTitle: string;
yRightTitle: string;
yLeftExtent: AxisExtentConfigResult;
yRightExtent: AxisExtentConfigResult;
legend: LegendConfigResult;
endValue?: EndValue;
emphasizeFitting?: boolean;
valueLabels: ValueLabelMode;
layers?: XYExtendedLayerConfigResult[];
fittingFunction?: FittingFunction;
axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult;
tickLabelsVisibilitySettings?: TickLabelsConfigResult;
gridlinesVisibilitySettings?: GridlinesConfigResult;
labelsOrientation?: LabelsOrientationConfigResult;
curveType?: XYCurveType;
fillOpacity?: number;
hideEndzones?: boolean;
valuesInLegend?: boolean;
ariaLabel?: string;
}
export interface XYProps {
xTitle: string; xTitle: string;
yTitle: string; yTitle: string;
yRightTitle: string; yRightTitle: string;
@ -164,7 +236,7 @@ export interface XYArgs {
yRightExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult;
legend: LegendConfigResult; legend: LegendConfigResult;
valueLabels: ValueLabelMode; valueLabels: ValueLabelMode;
layers: XYLayerConfigResult[]; layers: CommonXYLayerConfig[];
endValue?: EndValue; endValue?: EndValue;
emphasizeFitting?: boolean; emphasizeFitting?: boolean;
fittingFunction?: FittingFunction; fittingFunction?: FittingFunction;
@ -181,28 +253,48 @@ export interface XYArgs {
export interface AnnotationLayerArgs { export interface AnnotationLayerArgs {
annotations: EventAnnotationOutput[]; annotations: EventAnnotationOutput[];
layerId: string;
hide?: boolean; hide?: boolean;
} }
export type ExtendedAnnotationLayerArgs = AnnotationLayerArgs & {
layerId?: string;
};
export type AnnotationLayerConfigResult = AnnotationLayerArgs & { export type AnnotationLayerConfigResult = AnnotationLayerArgs & {
type: typeof ANNOTATION_LAYER; type: typeof ANNOTATION_LAYER;
layerType: typeof LayerTypes.ANNOTATIONS; layerType: typeof LayerTypes.ANNOTATIONS;
}; };
export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & {
type: typeof EXTENDED_ANNOTATION_LAYER;
layerType: typeof LayerTypes.ANNOTATIONS;
};
export interface ReferenceLineLayerArgs { export interface ReferenceLineLayerArgs {
layerId: string;
accessors: string[]; accessors: string[];
columnToLabel?: string; columnToLabel?: string;
yConfig?: YConfigResult[]; yConfig?: ExtendedYConfigResult[];
}
export interface ExtendedReferenceLineLayerArgs {
layerId?: string;
accessors: string[];
columnToLabel?: string;
yConfig?: ExtendedYConfigResult[];
table?: Datatable;
} }
export type XYLayerArgs = DataLayerArgs | ReferenceLineLayerArgs | AnnotationLayerArgs; export type XYLayerArgs = DataLayerArgs | ReferenceLineLayerArgs | AnnotationLayerArgs;
export type XYLayerConfig = DataLayerConfig | ReferenceLineLayerConfig | AnnotationLayerConfig;
export type XYExtendedLayerConfig =
| ExtendedDataLayerConfig
| ExtendedReferenceLineLayerConfig
| ExtendedAnnotationLayerConfig;
export type XYLayerConfigResult = export type XYExtendedLayerConfigResult =
| DataLayerConfigResult | ExtendedDataLayerConfigResult
| ReferenceLineLayerConfigResult | ExtendedReferenceLineLayerConfigResult
| AnnotationLayerConfigResult; | ExtendedAnnotationLayerConfigResult;
export interface LensMultiTable { export interface LensMultiTable {
type: typeof MULTITABLE; type: typeof MULTITABLE;
@ -216,14 +308,43 @@ export interface LensMultiTable {
export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & {
type: typeof REFERENCE_LINE_LAYER; type: typeof REFERENCE_LINE_LAYER;
layerType: typeof LayerTypes.REFERENCELINE; layerType: typeof LayerTypes.REFERENCELINE;
table: Datatable;
}; };
export type DataLayerConfigResult = DataLayerArgs & { export type ExtendedReferenceLineLayerConfigResult = ExtendedReferenceLineLayerArgs & {
type: typeof EXTENDED_REFERENCE_LINE_LAYER;
layerType: typeof LayerTypes.REFERENCELINE;
table: Datatable;
};
export type DataLayerConfigResult = Omit<DataLayerArgs, 'palette'> & {
type: typeof DATA_LAYER; type: typeof DATA_LAYER;
layerType: typeof LayerTypes.DATA; layerType: typeof LayerTypes.DATA;
palette: PaletteOutput;
table: Datatable;
};
export interface WithLayerId {
layerId: string;
}
export type DataLayerConfig = DataLayerConfigResult & WithLayerId;
export type ReferenceLineLayerConfig = ReferenceLineLayerConfigResult & WithLayerId;
export type AnnotationLayerConfig = AnnotationLayerConfigResult & WithLayerId;
export type ExtendedDataLayerConfig = ExtendedDataLayerConfigResult & WithLayerId;
export type ExtendedReferenceLineLayerConfig = ExtendedReferenceLineLayerConfigResult & WithLayerId;
export type ExtendedAnnotationLayerConfig = ExtendedAnnotationLayerConfigResult & WithLayerId;
export type ExtendedDataLayerConfigResult = Omit<ExtendedDataLayerArgs, 'palette'> & {
type: typeof EXTENDED_DATA_LAYER;
layerType: typeof LayerTypes.DATA;
palette: PaletteOutput;
table: Datatable;
}; };
export type YConfigResult = YConfig & { type: typeof Y_CONFIG }; export type YConfigResult = YConfig & { type: typeof Y_CONFIG };
export type ExtendedYConfigResult = ExtendedYConfig & { type: typeof EXTENDED_Y_CONFIG };
export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & { export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & {
type: typeof AXIS_TITLES_VISIBILITY_CONFIG; type: typeof AXIS_TITLES_VISIBILITY_CONFIG;
@ -237,3 +358,70 @@ export type LegendConfigResult = LegendConfig & { type: typeof LEGEND_CONFIG };
export type AxisExtentConfigResult = AxisExtentConfig & { type: typeof AXIS_EXTENT_CONFIG }; export type AxisExtentConfigResult = AxisExtentConfig & { type: typeof AXIS_EXTENT_CONFIG };
export type GridlinesConfigResult = AxesSettingsConfig & { type: typeof GRID_LINES_CONFIG }; export type GridlinesConfigResult = AxesSettingsConfig & { type: typeof GRID_LINES_CONFIG };
export type TickLabelsConfigResult = AxesSettingsConfig & { type: typeof TICK_LABELS_CONFIG }; export type TickLabelsConfigResult = AxesSettingsConfig & { type: typeof TICK_LABELS_CONFIG };
export type CommonXYLayerConfig = XYLayerConfig | XYExtendedLayerConfig;
export type CommonXYDataLayerConfigResult = DataLayerConfigResult | ExtendedDataLayerConfigResult;
export type CommonXYReferenceLineLayerConfigResult =
| ReferenceLineLayerConfigResult
| ExtendedReferenceLineLayerConfigResult;
export type CommonXYDataLayerConfig = DataLayerConfig | ExtendedDataLayerConfig;
export type CommonXYReferenceLineLayerConfig =
| ReferenceLineLayerConfig
| ExtendedReferenceLineLayerConfig;
export type CommonXYAnnotationLayerConfig = AnnotationLayerConfig | ExtendedAnnotationLayerConfig;
export type XyVisFn = ExpressionFunctionDefinition<
typeof XY_VIS,
Datatable,
XYArgs,
Promise<XYRender>
>;
export type LayeredXyVisFn = ExpressionFunctionDefinition<
typeof LAYERED_XY_VIS,
Datatable,
LayeredXYArgs,
Promise<XYRender>
>;
export type DataLayerFn = ExpressionFunctionDefinition<
typeof DATA_LAYER,
Datatable,
DataLayerArgs,
DataLayerConfigResult
>;
export type ExtendedDataLayerFn = ExpressionFunctionDefinition<
typeof EXTENDED_DATA_LAYER,
Datatable,
ExtendedDataLayerArgs,
ExtendedDataLayerConfigResult
>;
export type ReferenceLineLayerFn = ExpressionFunctionDefinition<
typeof REFERENCE_LINE_LAYER,
Datatable,
ReferenceLineLayerArgs,
ReferenceLineLayerConfigResult
>;
export type ExtendedReferenceLineLayerFn = ExpressionFunctionDefinition<
typeof EXTENDED_REFERENCE_LINE_LAYER,
Datatable,
ExtendedReferenceLineLayerArgs,
ExtendedReferenceLineLayerConfigResult
>;
export type YConfigFn = ExpressionFunctionDefinition<typeof Y_CONFIG, null, YConfig, YConfigResult>;
export type ExtendedYConfigFn = ExpressionFunctionDefinition<
typeof EXTENDED_Y_CONFIG,
null,
ExtendedYConfig,
ExtendedYConfigResult
>;
export type LegendConfigFn = ExpressionFunctionDefinition<
typeof LEGEND_CONFIG,
null,
LegendConfig,
Promise<LegendConfigResult>
>;

View file

@ -6,12 +6,16 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { AnnotationTooltipFormatter } from '@elastic/charts';
import {
AvailableAnnotationIcon,
ManualPointEventAnnotationArgs,
} from '@kbn/event-annotation-plugin/common';
import { XY_VIS_RENDERER } from '../constants'; import { XY_VIS_RENDERER } from '../constants';
import { LensMultiTable, XYArgs } from './expression_functions'; import { XYProps } from './expression_functions';
export interface XYChartProps { export interface XYChartProps {
data: LensMultiTable; args: XYProps;
args: XYArgs;
} }
export interface XYRender { export interface XYRender {
@ -19,3 +23,10 @@ export interface XYRender {
as: typeof XY_VIS_RENDERER; as: typeof XY_VIS_RENDERER;
value: XYChartProps; value: XYChartProps;
} }
export interface CollectiveConfig extends Omit<ManualPointEventAnnotationArgs, 'icon'> {
roundedTimestamp: number;
axisMode: 'bottom';
icon?: AvailableAnnotationIcon | string;
customTooltipDetails?: AnnotationTooltipFormatter | undefined;
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { logDatatables, getLayerDimensions } from './log_datatables';

View file

@ -0,0 +1,55 @@
/*
* 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 { ExecutionContext } from '@kbn/expressions-plugin';
import { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils';
import { LayerTypes } from '../constants';
import { strings } from '../i18n';
import {
CommonXYDataLayerConfig,
CommonXYLayerConfig,
CommonXYReferenceLineLayerConfig,
} from '../types';
export const logDatatables = (layers: CommonXYLayerConfig[], handlers: ExecutionContext) => {
if (!handlers?.inspectorAdapters?.tables) {
return;
}
handlers.inspectorAdapters.tables.reset();
handlers.inspectorAdapters.tables.allowCsvExport = true;
layers.forEach((layer) => {
if (layer.layerType === LayerTypes.ANNOTATIONS) {
return;
}
const logTable = prepareLogTable(layer.table, getLayerDimensions(layer), true);
handlers.inspectorAdapters.tables.logDatatable(layer.layerId, logTable);
});
};
export const getLayerDimensions = (
layer: CommonXYDataLayerConfig | CommonXYReferenceLineLayerConfig
): Dimension[] => {
let xAccessor;
let splitAccessor;
if (layer.layerType === LayerTypes.DATA) {
xAccessor = layer.xAccessor;
splitAccessor = layer.splitAccessor;
}
const { accessors, layerType } = layer;
return [
[
accessors ? accessors : undefined,
layerType === LayerTypes.DATA ? strings.getMetricHelp() : strings.getReferenceLineHelp(),
],
[xAccessor ? [xAccessor] : undefined, strings.getXAxisHelp()],
[splitAccessor ? [splitAccessor] : undefined, strings.getBreakdownHelp()],
];
};

View file

@ -6,9 +6,11 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { Datatable } from '@kbn/expressions-plugin/common';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../../common'; import { LensMultiTable } from '../../common';
import { LayerTypes } from '../../common/constants'; import { LayerTypes } from '../../common/constants';
import { DataLayerConfig, XYProps } from '../../common/types';
import { mockPaletteOutput, sampleArgs } from '../../common/__mocks__'; import { mockPaletteOutput, sampleArgs } from '../../common/__mocks__';
const chartSetupContract = chartPluginMock.createSetupContract(); const chartSetupContract = chartPluginMock.createSetupContract();
@ -166,9 +168,9 @@ export const dateHistogramData: LensMultiTable = {
}, },
}; };
export const dateHistogramLayer: DataLayerConfigResult = { export const dateHistogramLayer: DataLayerConfig = {
layerId: 'dateHistogramLayer',
type: 'dataLayer', type: 'dataLayer',
layerId: 'timeLayer',
layerType: LayerTypes.DATA, layerType: LayerTypes.DATA,
hide: false, hide: false,
xAccessor: 'xAccessorId', xAccessor: 'xAccessorId',
@ -179,17 +181,12 @@ export const dateHistogramLayer: DataLayerConfigResult = {
seriesType: 'bar_stacked', seriesType: 'bar_stacked',
accessors: ['yAccessorId'], accessors: ['yAccessorId'],
palette: mockPaletteOutput, palette: mockPaletteOutput,
table: dateHistogramData.tables.timeLayer,
}; };
export function sampleArgsWithReferenceLine(value: number = 150) { export function sampleArgsWithReferenceLine(value: number = 150) {
const { data, args } = sampleArgs(); const { args: sArgs } = sampleArgs();
const data: Datatable = {
return {
data: {
...data,
tables: {
...data.tables,
referenceLine: {
type: 'datatable', type: 'datatable',
columns: [ columns: [
{ {
@ -199,26 +196,22 @@ export function sampleArgsWithReferenceLine(value: number = 150) {
}, },
], ],
rows: [{ 'referenceLine-a': value }], rows: [{ 'referenceLine-a': value }],
}, };
},
} as LensMultiTable, const args: XYProps = {
args: { ...sArgs,
...args,
layers: [ layers: [
...args.layers, ...sArgs.layers,
{ {
layerId: 'referenceLine-a',
type: 'referenceLineLayer',
layerType: LayerTypes.REFERENCELINE, layerType: LayerTypes.REFERENCELINE,
accessors: ['referenceLine-a'], accessors: ['referenceLine-a'],
layerId: 'referenceLine', yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'extendedYConfig' }],
seriesType: 'line', table: data,
xScaleType: 'linear',
yScaleType: 'linear',
palette: mockPaletteOutput,
isHistogram: false,
hide: true,
yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'yConfig' }],
}, },
], ],
} as XYArgs,
}; };
return { data, args };
} }

View file

@ -29,7 +29,12 @@ import {
defaultAnnotationColor, defaultAnnotationColor,
defaultAnnotationRangeColor, defaultAnnotationRangeColor,
} from '@kbn/event-annotation-plugin/public'; } from '@kbn/event-annotation-plugin/public';
import type { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../../common/types'; import type {
AnnotationLayerArgs,
CommonXYAnnotationLayerConfig,
CollectiveConfig,
} from '../../common';
import { AnnotationIcon, hasIcon, Marker, MarkerBody } from '../helpers'; import { AnnotationIcon, hasIcon, Marker, MarkerBody } from '../helpers';
import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers'; import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers';
@ -52,12 +57,6 @@ export interface AnnotationsProps {
outsideDimension: number; outsideDimension: number;
} }
interface CollectiveConfig extends ManualPointEventAnnotationArgs {
roundedTimestamp: number;
axisMode: 'bottom';
customTooltipDetails?: AnnotationTooltipFormatter | undefined;
}
const groupVisibleConfigsByInterval = ( const groupVisibleConfigsByInterval = (
layers: AnnotationLayerArgs[], layers: AnnotationLayerArgs[],
minInterval?: number, minInterval?: number,
@ -131,7 +130,7 @@ const getCommonStyles = (configArr: ManualPointEventAnnotationArgs[]) => {
}; };
}; };
export const getRangeAnnotations = (layers: AnnotationLayerConfigResult[]) => { export const getRangeAnnotations = (layers: CommonXYAnnotationLayerConfig[]) => {
return layers return layers
.flatMap(({ annotations }) => .flatMap(({ annotations }) =>
annotations.filter( annotations.filter(
@ -146,7 +145,7 @@ export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8;
export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2; export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2;
export const getAnnotationsGroupedByInterval = ( export const getAnnotationsGroupedByInterval = (
layers: AnnotationLayerConfigResult[], layers: CommonXYAnnotationLayerConfig[],
minInterval?: number, minInterval?: number,
firstTimestamp?: number, firstTimestamp?: number,
formatter?: FieldFormat formatter?: FieldFormat

View file

@ -0,0 +1,166 @@
/*
* 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 {
AreaSeries,
BarSeries,
CurveType,
LabelOverflowConstraint,
LineSeries,
} from '@elastic/charts';
import React, { FC } from 'react';
import { PaletteRegistry } from '@kbn/coloring';
import { FormatFactory } from '@kbn/field-formats-plugin/common';
import {
CommonXYDataLayerConfig,
EndValue,
FittingFunction,
ValueLabelMode,
XYCurveType,
} from '../../common';
import { SeriesTypes, ValueLabelModes } from '../../common/constants';
import {
getColorAssignments,
getFitOptions,
GroupsConfiguration,
getSeriesProps,
DatatablesWithFormatInfo,
} from '../helpers';
interface Props {
layers: CommonXYDataLayerConfig[];
formatFactory: FormatFactory;
chartHasMoreThanOneBarSeries?: boolean;
yAxesConfiguration: GroupsConfiguration;
curveType?: XYCurveType;
fittingFunction?: FittingFunction;
endValue?: EndValue | undefined;
paletteService: PaletteRegistry;
formattedDatatables: DatatablesWithFormatInfo;
syncColors?: boolean;
timeZone?: string;
emphasizeFitting?: boolean;
fillOpacity?: number;
shouldShowValueLabels?: boolean;
valueLabels: ValueLabelMode;
}
export const DataLayers: FC<Props> = ({
layers,
endValue,
timeZone,
curveType,
syncColors,
valueLabels,
fillOpacity,
formatFactory,
paletteService,
fittingFunction,
emphasizeFitting,
yAxesConfiguration,
shouldShowValueLabels,
formattedDatatables,
chartHasMoreThanOneBarSeries,
}) => {
const colorAssignments = getColorAssignments(layers, formatFactory);
return (
<>
{layers.flatMap((layer) =>
layer.accessors.map((accessor, accessorIndex) => {
const { seriesType, columnToLabel, layerId } = layer;
const columnToLabelMap: Record<string, string> = columnToLabel
? JSON.parse(columnToLabel)
: {};
// what if row values are not primitive? That is the case of, for instance, Ranges
// remaps them to their serialized version with the formatHint metadata
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
const formattedDatatableInfo = formattedDatatables[layerId];
const isPercentage = seriesType.includes('percentage');
const yAxis = yAxesConfiguration.find((axisConfiguration) =>
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
);
const seriesProps = getSeriesProps({
layer,
accessor,
chartHasMoreThanOneBarSeries,
colorAssignments,
formatFactory,
columnToLabelMap,
paletteService,
formattedDatatableInfo,
syncColors,
yAxis,
timeZone,
emphasizeFitting,
fillOpacity,
});
const index = `${layer.layerId}-${accessorIndex}`;
const curve = curveType ? CurveType[curveType] : undefined;
switch (seriesType) {
case SeriesTypes.LINE:
return (
<LineSeries
key={index}
{...seriesProps}
fit={getFitOptions(fittingFunction, endValue)}
curve={curve}
/>
);
case SeriesTypes.BAR:
case SeriesTypes.BAR_STACKED:
case SeriesTypes.BAR_PERCENTAGE_STACKED:
case SeriesTypes.BAR_HORIZONTAL:
case SeriesTypes.BAR_HORIZONTAL_STACKED:
case SeriesTypes.BAR_HORIZONTAL_PERCENTAGE_STACKED:
const valueLabelsSettings = {
displayValueSettings: {
// This format double fixes two issues in elastic-chart
// * when rotating the chart, the formatter is not correctly picked
// * in some scenarios value labels are not strings, and this breaks the elastic-chart lib
valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '',
showValueLabel: shouldShowValueLabels && valueLabels !== ValueLabelModes.HIDE,
isValueContainedInElement: false,
isAlternatingValueLabel: false,
overflowConstraints: [
LabelOverflowConstraint.ChartEdges,
LabelOverflowConstraint.BarGeometry,
],
},
};
return <BarSeries key={index} {...seriesProps} {...valueLabelsSettings} />;
case SeriesTypes.AREA_STACKED:
case SeriesTypes.AREA_PERCENTAGE_STACKED:
return (
<AreaSeries
key={index}
{...seriesProps}
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction, endValue)}
curve={curve}
/>
);
case SeriesTypes.AREA:
return (
<AreaSeries
key={index}
{...seriesProps}
fit={getFitOptions(fittingFunction, endValue)}
curve={curve}
/>
);
}
})
)}
</>
);
};

View file

@ -11,27 +11,12 @@ import { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
import { EuiPopover } from '@elastic/eui'; import { EuiPopover } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test-jest-helpers'; import { mountWithIntl } from '@kbn/test-jest-helpers';
import { ComponentType, ReactWrapper } from 'enzyme'; import { ComponentType, ReactWrapper } from 'enzyme';
import type { LensMultiTable } from '../../common'; import type { DataLayerConfig, LensMultiTable } from '../../common';
import { LayerTypes } from '../../common/constants'; import { LayerTypes } from '../../common/constants';
import type { DataLayerArgs } from '../../common';
import { getLegendAction } from './legend_action'; import { getLegendAction } from './legend_action';
import { LegendActionPopover } from './legend_action_popover'; import { LegendActionPopover } from './legend_action_popover';
import { mockPaletteOutput } from '../../common/__mocks__'; import { mockPaletteOutput } from '../../common/__mocks__';
const sampleLayer = {
layerId: 'first',
layerType: LayerTypes.DATA,
seriesType: 'line',
xAccessor: 'c',
accessors: ['a', 'b'],
splitAccessor: 'splitAccessorId',
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
xScaleType: 'ordinal',
yScaleType: 'linear',
isHistogram: false,
palette: mockPaletteOutput,
} as DataLayerArgs;
const tables = { const tables = {
first: { first: {
type: 'datatable', type: 'datatable',
@ -168,11 +153,26 @@ const tables = {
}, },
} as LensMultiTable['tables']; } as LensMultiTable['tables'];
const sampleLayer: DataLayerConfig = {
layerId: 'first',
type: 'dataLayer',
layerType: LayerTypes.DATA,
seriesType: 'line',
xAccessor: 'c',
accessors: ['a', 'b'],
splitAccessor: 'splitAccessorId',
columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
xScaleType: 'ordinal',
yScaleType: 'linear',
isHistogram: false,
palette: mockPaletteOutput,
table: tables.first,
};
describe('getLegendAction', function () { describe('getLegendAction', function () {
let wrapperProps: LegendActionProps; let wrapperProps: LegendActionProps;
const Component: ComponentType<LegendActionProps> = getLegendAction( const Component: ComponentType<LegendActionProps> = getLegendAction(
[sampleLayer], [sampleLayer],
tables,
jest.fn(), jest.fn(),
jest.fn(), jest.fn(),
{} {}

View file

@ -9,23 +9,28 @@
import React from 'react'; import React from 'react';
import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts';
import type { FilterEvent } from '../types'; import type { FilterEvent } from '../types';
import type { LensMultiTable, DataLayerArgs } from '../../common'; import type { CommonXYDataLayerConfig } from '../../common';
import type { FormatFactory } from '../types'; import type { FormatFactory } from '../types';
import { LegendActionPopover } from './legend_action_popover'; import { LegendActionPopover } from './legend_action_popover';
import { DatatablesWithFormatInfo } from '../helpers';
export const getLegendAction = ( export const getLegendAction = (
filteredLayers: DataLayerArgs[], dataLayers: CommonXYDataLayerConfig[],
tables: LensMultiTable['tables'],
onFilter: (data: FilterEvent['data']) => void, onFilter: (data: FilterEvent['data']) => void,
formatFactory: FormatFactory, formatFactory: FormatFactory,
layersAlreadyFormatted: Record<string, boolean> formattedDatatables: DatatablesWithFormatInfo
): LegendAction => ): LegendAction =>
React.memo(({ series: [xySeries] }) => { React.memo(({ series: [xySeries] }) => {
const series = xySeries as XYChartSeriesIdentifier; const series = xySeries as XYChartSeriesIdentifier;
const layer = filteredLayers.find((l) => const layerIndex = dataLayers.findIndex((l) =>
series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString())) series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
); );
if (layerIndex === -1) {
return null;
}
const layer = dataLayers[layerIndex];
if (!layer || !layer.splitAccessor) { if (!layer || !layer.splitAccessor) {
return null; return null;
} }
@ -33,12 +38,12 @@ export const getLegendAction = (
const splitLabel = series.seriesKeys[0] as string; const splitLabel = series.seriesKeys[0] as string;
const accessor = layer.splitAccessor; const accessor = layer.splitAccessor;
const table = tables[layer.layerId]; const { table } = layer;
const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor); const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor);
const formatter = formatFactory(splitColumn && splitColumn.meta?.params); const formatter = formatFactory(splitColumn && splitColumn.meta?.params);
const rowIndex = table.rows.findIndex((row) => { const rowIndex = table.rows.findIndex((row) => {
if (layersAlreadyFormatted[accessor]) { if (formattedDatatables[layer.layerId]?.formattedColumns[accessor]) {
// stringify the value to compare with the chart value // stringify the value to compare with the chart value
return formatter.convert(row[accessor]) === splitLabel; return formatter.convert(row[accessor]) === splitLabel;
} }
@ -63,7 +68,7 @@ export const getLegendAction = (
return ( return (
<LegendActionPopover <LegendActionPopover
label={ label={
!layersAlreadyFormatted[accessor] && formatter !formattedDatatables[layer.layerId]?.formattedColumns[accessor] && formatter
? formatter.convert(splitLabel) ? formatter.convert(splitLabel)
: splitLabel : splitLabel
} }

View file

@ -9,9 +9,14 @@
import { LineAnnotation, RectAnnotation } from '@elastic/charts'; import { LineAnnotation, RectAnnotation } from '@elastic/charts';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import { Datatable } from '@kbn/expressions-plugin/common';
import { FieldFormat } from '@kbn/field-formats-plugin/common'; import { FieldFormat } from '@kbn/field-formats-plugin/common';
import { LensMultiTable } from '../../common'; import { LayerTypes } from '../../common/constants';
import { ReferenceLineLayerArgs, YConfig } from '../../common/types'; import {
ReferenceLineLayerArgs,
ReferenceLineLayerConfig,
ExtendedYConfig,
} from '../../common/types';
import { ReferenceLineAnnotations, ReferenceLineAnnotationsProps } from './reference_lines'; import { ReferenceLineAnnotations, ReferenceLineAnnotationsProps } from './reference_lines';
const row: Record<string, number> = { const row: Record<string, number> = {
@ -23,10 +28,7 @@ const row: Record<string, number> = {
yAccessorRightSecondId: 10, yAccessorRightSecondId: 10,
}; };
const histogramData: LensMultiTable = { const data: Datatable = {
type: 'lens_multitable',
tables: {
firstLayer: {
type: 'datatable', type: 'datatable',
rows: [row], rows: [row],
columns: Object.keys(row).map((id) => ({ columns: Object.keys(row).map((id) => ({
@ -37,20 +39,17 @@ const histogramData: LensMultiTable = {
params: { id: 'number' }, params: { id: 'number' },
}, },
})), })),
},
},
dateRange: {
fromDate: new Date('2020-04-01T16:14:16.246Z'),
toDate: new Date('2020-04-01T17:15:41.263Z'),
},
}; };
function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerArgs[] { function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] {
return [ return [
{ {
layerId: 'firstLayer', layerId: 'first',
accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor),
yConfig: yConfigs, yConfig: yConfigs,
type: 'referenceLineLayer',
layerType: LayerTypes.REFERENCELINE,
table: data,
}, },
]; ];
} }
@ -64,7 +63,7 @@ interface XCoords {
x1: number | undefined; x1: number | undefined;
} }
function getAxisFromId(layerPrefix: string): YConfig['axisMode'] { function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] {
return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom';
} }
@ -95,21 +94,20 @@ describe('ReferenceLineAnnotations', () => {
['yAccessorLeft', 'below'], ['yAccessorLeft', 'below'],
['yAccessorRight', 'above'], ['yAccessorRight', 'above'],
['yAccessorRight', 'below'], ['yAccessorRight', 'below'],
] as Array<[string, YConfig['fill']]>)( ] as Array<[string, ExtendedYConfig['fill']]>)(
'should render a RectAnnotation for a reference line with fill set: %s %s', 'should render a RectAnnotation for a reference line with fill set: %s %s',
(layerPrefix, fill) => { (layerPrefix, fill) => {
const axisMode = getAxisFromId(layerPrefix); const axisMode = getAxisFromId(layerPrefix);
const wrapper = shallow( const wrapper = shallow(
<ReferenceLineAnnotations <ReferenceLineAnnotations
{...defaultProps} {...defaultProps}
data={histogramData}
layers={createLayers([ layers={createLayers([
{ {
forAccessor: `${layerPrefix}FirstId`, forAccessor: `${layerPrefix}FirstId`,
axisMode, axisMode,
lineStyle: 'solid', lineStyle: 'solid',
fill, fill,
type: 'yConfig', type: 'extendedYConfig',
}, },
])} ])}
/> />
@ -135,19 +133,18 @@ describe('ReferenceLineAnnotations', () => {
it.each([ it.each([
['xAccessor', 'above'], ['xAccessor', 'above'],
['xAccessor', 'below'], ['xAccessor', 'below'],
] as Array<[string, YConfig['fill']]>)( ] as Array<[string, ExtendedYConfig['fill']]>)(
'should render a RectAnnotation for a reference line with fill set: %s %s', 'should render a RectAnnotation for a reference line with fill set: %s %s',
(layerPrefix, fill) => { (layerPrefix, fill) => {
const wrapper = shallow( const wrapper = shallow(
<ReferenceLineAnnotations <ReferenceLineAnnotations
{...defaultProps} {...defaultProps}
data={histogramData}
layers={createLayers([ layers={createLayers([
{ {
forAccessor: `${layerPrefix}FirstId`, forAccessor: `${layerPrefix}FirstId`,
axisMode: 'bottom', axisMode: 'bottom',
lineStyle: 'solid', lineStyle: 'solid',
type: 'yConfig', type: 'extendedYConfig',
fill, fill,
}, },
])} ])}
@ -176,27 +173,26 @@ describe('ReferenceLineAnnotations', () => {
['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }],
['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }],
['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }],
] as Array<[string, YConfig['fill'], YCoords, YCoords]>)( ] as Array<[string, ExtendedYConfig['fill'], YCoords, YCoords]>)(
'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s',
(layerPrefix, fill, coordsA, coordsB) => { (layerPrefix, fill, coordsA, coordsB) => {
const axisMode = getAxisFromId(layerPrefix); const axisMode = getAxisFromId(layerPrefix);
const wrapper = shallow( const wrapper = shallow(
<ReferenceLineAnnotations <ReferenceLineAnnotations
{...defaultProps} {...defaultProps}
data={histogramData}
layers={createLayers([ layers={createLayers([
{ {
forAccessor: `${layerPrefix}FirstId`, forAccessor: `${layerPrefix}FirstId`,
axisMode, axisMode,
lineStyle: 'solid', lineStyle: 'solid',
type: 'yConfig', type: 'extendedYConfig',
fill, fill,
}, },
{ {
forAccessor: `${layerPrefix}SecondId`, forAccessor: `${layerPrefix}SecondId`,
axisMode, axisMode,
lineStyle: 'solid', lineStyle: 'solid',
type: 'yConfig', type: 'extendedYConfig',
fill, fill,
}, },
])} ])}
@ -227,26 +223,25 @@ describe('ReferenceLineAnnotations', () => {
it.each([ it.each([
['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }],
['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }],
] as Array<[string, YConfig['fill'], XCoords, XCoords]>)( ] as Array<[string, ExtendedYConfig['fill'], XCoords, XCoords]>)(
'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s',
(layerPrefix, fill, coordsA, coordsB) => { (layerPrefix, fill, coordsA, coordsB) => {
const wrapper = shallow( const wrapper = shallow(
<ReferenceLineAnnotations <ReferenceLineAnnotations
{...defaultProps} {...defaultProps}
data={histogramData}
layers={createLayers([ layers={createLayers([
{ {
forAccessor: `${layerPrefix}FirstId`, forAccessor: `${layerPrefix}FirstId`,
axisMode: 'bottom', axisMode: 'bottom',
lineStyle: 'solid', lineStyle: 'solid',
type: 'yConfig', type: 'extendedYConfig',
fill, fill,
}, },
{ {
forAccessor: `${layerPrefix}SecondId`, forAccessor: `${layerPrefix}SecondId`,
axisMode: 'bottom', axisMode: 'bottom',
lineStyle: 'solid', lineStyle: 'solid',
type: 'yConfig', type: 'extendedYConfig',
fill, fill,
}, },
])} ])}
@ -282,21 +277,20 @@ describe('ReferenceLineAnnotations', () => {
const wrapper = shallow( const wrapper = shallow(
<ReferenceLineAnnotations <ReferenceLineAnnotations
{...defaultProps} {...defaultProps}
data={histogramData}
layers={createLayers([ layers={createLayers([
{ {
forAccessor: `${layerPrefix}FirstId`, forAccessor: `${layerPrefix}FirstId`,
axisMode, axisMode,
lineStyle: 'solid', lineStyle: 'solid',
fill: 'above', fill: 'above',
type: 'yConfig', type: 'extendedYConfig',
}, },
{ {
forAccessor: `${layerPrefix}SecondId`, forAccessor: `${layerPrefix}SecondId`,
axisMode, axisMode,
lineStyle: 'solid', lineStyle: 'solid',
fill: 'below', fill: 'below',
type: 'yConfig', type: 'extendedYConfig',
}, },
])} ])}
/> />
@ -326,27 +320,26 @@ describe('ReferenceLineAnnotations', () => {
it.each([ it.each([
['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }],
['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }],
] as Array<[YConfig['fill'], YCoords, YCoords]>)( ] as Array<[ExtendedYConfig['fill'], YCoords, YCoords]>)(
'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s',
(fill, coordsA, coordsB) => { (fill, coordsA, coordsB) => {
const wrapper = shallow( const wrapper = shallow(
<ReferenceLineAnnotations <ReferenceLineAnnotations
{...defaultProps} {...defaultProps}
data={histogramData}
layers={createLayers([ layers={createLayers([
{ {
forAccessor: `yAccessorLeftFirstId`, forAccessor: `yAccessorLeftFirstId`,
axisMode: 'left', axisMode: 'left',
lineStyle: 'solid', lineStyle: 'solid',
fill, fill,
type: 'yConfig', type: 'extendedYConfig',
}, },
{ {
forAccessor: `yAccessorRightSecondId`, forAccessor: `yAccessorRightSecondId`,
axisMode: 'right', axisMode: 'right',
lineStyle: 'solid', lineStyle: 'solid',
fill, fill,
type: 'yConfig', type: 'extendedYConfig',
}, },
])} ])}
/> />

View file

@ -13,8 +13,7 @@ import { groupBy } from 'lodash';
import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts';
import { euiLightVars } from '@kbn/ui-theme'; import { euiLightVars } from '@kbn/ui-theme';
import type { FieldFormat } from '@kbn/field-formats-plugin/common'; import type { FieldFormat } from '@kbn/field-formats-plugin/common';
import type { IconPosition, ReferenceLineLayerArgs, YAxisMode } from '../../common/types'; import type { CommonXYReferenceLineLayerConfig, IconPosition, YAxisMode } from '../../common/types';
import type { LensMultiTable } from '../../common/types';
import { import {
LINES_MARKER_SIZE, LINES_MARKER_SIZE,
mapVerticalToHorizontalPlacement, mapVerticalToHorizontalPlacement,
@ -89,8 +88,7 @@ export function getBaseIconPlacement(
} }
export interface ReferenceLineAnnotationsProps { export interface ReferenceLineAnnotationsProps {
layers: ReferenceLineLayerArgs[]; layers: CommonXYReferenceLineLayerConfig[];
data: LensMultiTable;
formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>;
axesMap: Record<'left' | 'right', boolean>; axesMap: Record<'left' | 'right', boolean>;
isHorizontal: boolean; isHorizontal: boolean;
@ -99,7 +97,6 @@ export interface ReferenceLineAnnotationsProps {
export const ReferenceLineAnnotations = ({ export const ReferenceLineAnnotations = ({
layers, layers,
data,
formatters, formatters,
axesMap, axesMap,
isHorizontal, isHorizontal,
@ -111,11 +108,10 @@ export const ReferenceLineAnnotations = ({
if (!layer.yConfig) { if (!layer.yConfig) {
return []; return [];
} }
const { columnToLabel, yConfig: yConfigs, layerId } = layer; const { columnToLabel, yConfig: yConfigs, table } = layer;
const columnToLabelMap: Record<string, string> = columnToLabel const columnToLabelMap: Record<string, string> = columnToLabel
? JSON.parse(columnToLabel) ? JSON.parse(columnToLabel)
: {}; : {};
const table = data.tables[layerId];
const row = table.rows[0]; const row = table.rows[0];
@ -194,8 +190,8 @@ export const ReferenceLineAnnotations = ({
annotations.push( annotations.push(
<LineAnnotation <LineAnnotation
{...props} {...props}
id={`${layerId}-${yConfig.forAccessor}-line`} id={`${layer.layerId}-${yConfig.forAccessor}-line`}
key={`${layerId}-${yConfig.forAccessor}-line`} key={`${layer.layerId}-${yConfig.forAccessor}-line`}
dataValues={table.rows.map(() => ({ dataValues={table.rows.map(() => ({
dataValue: row[yConfig.forAccessor], dataValue: row[yConfig.forAccessor],
header: columnToLabelMap[yConfig.forAccessor], header: columnToLabelMap[yConfig.forAccessor],
@ -225,8 +221,8 @@ export const ReferenceLineAnnotations = ({
annotations.push( annotations.push(
<RectAnnotation <RectAnnotation
{...props} {...props}
id={`${layerId}-${yConfig.forAccessor}-rect`} id={`${layer.layerId}-${yConfig.forAccessor}-rect`}
key={`${layerId}-${yConfig.forAccessor}-rect`} key={`${layer.layerId}-${yConfig.forAccessor}-rect`}
dataValues={table.rows.map(() => { dataValues={table.rows.map(() => {
const nextValue = shouldCheckNextReferenceLine const nextValue = shouldCheckNextReferenceLine
? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor]

View file

@ -11,7 +11,7 @@ import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { Endzones } from '@kbn/charts-plugin/public'; import { Endzones } from '@kbn/charts-plugin/public';
import { search } from '@kbn/data-plugin/public'; import { search } from '@kbn/data-plugin/public';
import type { LensMultiTable, DataLayerArgs } from '../../common'; import type { CommonXYDataLayerConfig } from '../../common';
export interface XDomain { export interface XDomain {
min?: number; min?: number;
@ -19,17 +19,16 @@ export interface XDomain {
minInterval?: number; minInterval?: number;
} }
export const getAppliedTimeRange = (layers: DataLayerArgs[], data: LensMultiTable) => { export const getAppliedTimeRange = (layers: CommonXYDataLayerConfig[]) => {
return Object.entries(data.tables) return layers
.map(([tableId, table]) => { .map(({ xAccessor, table }) => {
const layer = layers.find((l) => l.layerId === tableId); const xColumn = table.columns.find((col) => col.id === xAccessor);
const xColumn = table.columns.find((col) => col.id === layer?.xAccessor);
const timeRange = const timeRange =
xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange; xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange;
if (timeRange) { if (timeRange) {
return { return {
timeRange, timeRange,
field: xColumn.meta.field, field: xColumn?.meta.field,
}; };
} }
}) })
@ -37,13 +36,12 @@ export const getAppliedTimeRange = (layers: DataLayerArgs[], data: LensMultiTabl
}; };
export const getXDomain = ( export const getXDomain = (
layers: DataLayerArgs[], layers: CommonXYDataLayerConfig[],
data: LensMultiTable,
minInterval: number | undefined, minInterval: number | undefined,
isTimeViz: boolean, isTimeViz: boolean,
isHistogram: boolean isHistogram: boolean
) => { ) => {
const appliedTimeRange = getAppliedTimeRange(layers, data)?.timeRange; const appliedTimeRange = getAppliedTimeRange(layers)?.timeRange;
const from = appliedTimeRange?.from; const from = appliedTimeRange?.from;
const to = appliedTimeRange?.to; const to = appliedTimeRange?.to;
const baseDomain = isTimeViz const baseDomain = isTimeViz
@ -59,8 +57,8 @@ export const getXDomain = (
if (isHistogram && isFullyQualified(baseDomain)) { if (isHistogram && isFullyQualified(baseDomain)) {
const xValues = uniq( const xValues = uniq(
layers layers
.flatMap((layer) => .flatMap<number>(({ table, xAccessor }) =>
data.tables[layer.layerId].rows.map((row) => row[layer.xAccessor!].valueOf() as number) table.rows.map((row) => row[xAccessor!].valueOf())
) )
.sort() .sort()
); );

View file

@ -6,66 +6,56 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import React, { useRef } from 'react'; import React, { useMemo, useRef } from 'react';
import { import {
Chart, Chart,
Settings, Settings,
Axis, Axis,
LineSeries,
AreaSeries,
BarSeries,
Position, Position,
GeometryValue, GeometryValue,
XYChartSeriesIdentifier, XYChartSeriesIdentifier,
StackMode,
VerticalAlignment, VerticalAlignment,
HorizontalAlignment, HorizontalAlignment,
LayoutDirection, LayoutDirection,
ElementClickListener, ElementClickListener,
BrushEndListener, BrushEndListener,
XYBrushEvent, XYBrushEvent,
CurveType,
LegendPositionConfig, LegendPositionConfig,
LabelOverflowConstraint,
DisplayValueStyle, DisplayValueStyle,
RecursivePartial, RecursivePartial,
AxisStyle, AxisStyle,
ScaleType,
AreaSeriesProps,
BarSeriesProps,
LineSeriesProps,
ColorVariant,
Placement, Placement,
} from '@elastic/charts'; } from '@elastic/charts';
import { IconType } from '@elastic/eui'; import { IconType } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { PaletteRegistry } from '@kbn/coloring';
import { PaletteRegistry, SeriesLayer } from '@kbn/coloring';
import type { Datatable, DatatableRow, DatatableColumn } from '@kbn/expressions-plugin/public';
import { RenderMode } from '@kbn/expressions-plugin/common'; import { RenderMode } from '@kbn/expressions-plugin/common';
import { FieldFormat } from '@kbn/field-formats-plugin/common';
import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public'; import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public';
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; import type { FilterEvent, BrushEvent, FormatFactory } from '../types';
import type { SeriesType, XYChartProps } from '../../common/types'; import type { CommonXYDataLayerConfig, SeriesType, XYChartProps } from '../../common/types';
import { isHorizontalChart, getSeriesColor, getAnnotationsLayers, getDataLayers } from '../helpers'; import {
isHorizontalChart,
getAnnotationsLayers,
getDataLayers,
Series,
getFormattedTablesByLayers,
validateExtent,
} from '../helpers';
import { import {
getFilteredLayers, getFilteredLayers,
getReferenceLayers, getReferenceLayers,
isDataLayer, isDataLayer,
getFitOptions,
getAxesConfiguration, getAxesConfiguration,
GroupsConfiguration, GroupsConfiguration,
validateExtent,
getColorAssignments,
getLinesCausedPaddings, getLinesCausedPaddings,
} from '../helpers'; } from '../helpers';
import { getXDomain, XyEndzones } from './x_domain'; import { getXDomain, XyEndzones } from './x_domain';
import { getLegendAction } from './legend_action'; import { getLegendAction } from './legend_action';
import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines'; import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines';
import { visualizationDefinitions } from '../definitions'; import { visualizationDefinitions } from '../definitions';
import { XYLayerConfigResult } from '../../common/types'; import { CommonXYLayerConfig } from '../../common/types';
import { import {
Annotations, Annotations,
getAnnotationsGroupedByInterval, getAnnotationsGroupedByInterval,
@ -73,7 +63,8 @@ import {
OUTSIDE_RECT_ANNOTATION_WIDTH, OUTSIDE_RECT_ANNOTATION_WIDTH,
OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION, OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION,
} from './annotations'; } from './annotations';
import { AxisExtentModes, SeriesTypes, ValueLabelModes } from '../../common/constants';
import { DataLayers } from './data_layers';
import './xy_chart.scss'; import './xy_chart.scss';
declare global { declare global {
@ -85,8 +76,6 @@ declare global {
} }
} }
type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps;
export type XYChartRenderProps = XYChartProps & { export type XYChartRenderProps = XYChartProps & {
chartsThemeService: ChartsPluginSetup['theme']; chartsThemeService: ChartsPluginSetup['theme'];
chartsActiveCursorService: ChartsPluginStart['activeCursor']; chartsActiveCursorService: ChartsPluginStart['activeCursor'];
@ -104,8 +93,6 @@ export type XYChartRenderProps = XYChartProps & {
eventAnnotationService: EventAnnotationServiceType; eventAnnotationService: EventAnnotationServiceType;
}; };
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
function getValueLabelsStyling(isHorizontal: boolean): { function getValueLabelsStyling(isHorizontal: boolean): {
displayValue: RecursivePartial<DisplayValueStyle>; displayValue: RecursivePartial<DisplayValueStyle>;
} { } {
@ -134,7 +121,6 @@ function getIconForSeriesType(seriesType: SeriesType): IconType {
export const XYChartReportable = React.memo(XYChart); export const XYChartReportable = React.memo(XYChart);
export function XYChart({ export function XYChart({
data,
args, args,
formatFactory, formatFactory,
timeZone, timeZone,
@ -166,50 +152,47 @@ export function XYChart({
const chartTheme = chartsThemeService.useChartsTheme(); const chartTheme = chartsThemeService.useChartsTheme();
const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
const darkMode = chartsThemeService.useDarkMode(); const darkMode = chartsThemeService.useDarkMode();
const filteredLayers = getFilteredLayers(layers, data); const filteredLayers = getFilteredLayers(layers);
const layersById = filteredLayers.reduce<Record<string, XYLayerConfigResult>>( const layersById = filteredLayers.reduce<Record<string, CommonXYLayerConfig>>(
(hashMap, layer) => { (hashMap, layer) => ({ ...hashMap, [layer.layerId]: layer }),
hashMap[layer.layerId] = layer;
return hashMap;
},
{} {}
); );
const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, { const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, {
datatables: Object.values(data.tables), datatables: filteredLayers.map(({ table }) => table),
}); });
const dataLayers: CommonXYDataLayerConfig[] = filteredLayers.filter(isDataLayer);
const formattedDatatables = useMemo(
() => getFormattedTablesByLayers(dataLayers, formatFactory),
[dataLayers, formatFactory]
);
if (filteredLayers.length === 0) { if (filteredLayers.length === 0) {
const icon: IconType = getIconForSeriesType(getDataLayers(layers)?.[0]?.seriesType || 'bar'); const icon: IconType = getIconForSeriesType(
getDataLayers(layers)?.[0]?.seriesType || SeriesTypes.BAR
);
return <EmptyPlaceholder className="xyChart__empty" icon={icon} />; return <EmptyPlaceholder className="xyChart__empty" icon={icon} />;
} }
// use formatting hint of first x axis column to format ticks // use formatting hint of first x axis column to format ticks
const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find( const xAxisColumn = dataLayers[0]?.table.columns.find(({ id }) => id === dataLayers[0].xAccessor);
({ id }) => id === filteredLayers[0].xAccessor
);
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params); const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params);
const layersAlreadyFormatted: Record<string, boolean> = {};
// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
const safeXAccessorLabelRenderer = (value: unknown): string => const safeXAccessorLabelRenderer = (value: unknown): string =>
xAxisColumn && layersAlreadyFormatted[xAxisColumn.id] xAxisColumn && formattedDatatables[dataLayers[0]?.layerId]?.formattedColumns[xAxisColumn.id]
? String(value) ? String(value)
: String(xAxisFormatter.convert(value)); : String(xAxisFormatter.convert(value));
const chartHasMoreThanOneSeries = const chartHasMoreThanOneSeries =
filteredLayers.length > 1 || filteredLayers.length > 1 ||
filteredLayers.some((layer) => layer.accessors.length > 1) || filteredLayers.some((layer) => layer.accessors.length > 1) ||
filteredLayers.some((layer) => layer.splitAccessor); filteredLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor);
const shouldRotate = isHorizontalChart(filteredLayers); const shouldRotate = isHorizontalChart(dataLayers);
const yAxesConfiguration = getAxesConfiguration( const yAxesConfiguration = getAxesConfiguration(dataLayers, shouldRotate, formatFactory);
filteredLayers,
shouldRotate,
data.tables,
formatFactory
);
const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name); const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name);
const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || { const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || {
@ -223,25 +206,20 @@ export function XYChart({
yRight: true, yRight: true,
}; };
const labelsOrientation = args.labelsOrientation || { const labelsOrientation = args.labelsOrientation || { x: 0, yLeft: 0, yRight: 0 };
x: 0,
yLeft: 0,
yRight: 0,
};
const filteredBarLayers = filteredLayers.filter((layer) => layer.seriesType.includes('bar')); const filteredBarLayers = dataLayers.filter((layer) => layer.seriesType.includes('bar'));
const chartHasMoreThanOneBarSeries = const chartHasMoreThanOneBarSeries =
filteredBarLayers.length > 1 || filteredBarLayers.length > 1 ||
filteredBarLayers.some((layer) => layer.accessors.length > 1) || filteredBarLayers.some((layer) => layer.accessors.length > 1) ||
filteredBarLayers.some((layer) => layer.splitAccessor); filteredBarLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor);
const isTimeViz = Boolean(filteredLayers.every((l) => l.xScaleType === 'time')); const isTimeViz = Boolean(dataLayers.every((l) => l.xScaleType === 'time'));
const isHistogramViz = filteredLayers.every((l) => l.isHistogram); const isHistogramViz = dataLayers.every((l) => l.isHistogram);
const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain(
filteredLayers, dataLayers,
data,
minInterval, minInterval,
isTimeViz, isTimeViz,
isHistogramViz isHistogramViz
@ -252,17 +230,16 @@ export function XYChart({
right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'), right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'),
}; };
const getYAxesTitles = ( const getYAxesTitles = (axisSeries: Series[], groupId: 'right' | 'left') => {
axisSeries: Array<{ layer: string; accessor: string }>,
groupId: string
) => {
const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle; const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle;
return ( return (
yTitle || yTitle ||
axisSeries axisSeries
.map( .map(
(series) => (series) =>
data.tables[series.layer].columns.find((column) => column.id === series.accessor)?.name filteredLayers
.find(({ layerId }) => series.layer === layerId)
?.table.columns.find((column) => column.id === series.accessor)?.name
) )
.filter((name) => Boolean(name))[0] .filter((name) => Boolean(name))[0]
); );
@ -270,9 +247,9 @@ export function XYChart({
const referenceLineLayers = getReferenceLayers(layers); const referenceLineLayers = getReferenceLayers(layers);
const annotationsLayers = getAnnotationsLayers(layers); const annotationsLayers = getAnnotationsLayers(layers);
const firstTable = data.tables[filteredLayers[0].layerId]; const firstTable = dataLayers[0]?.table;
const xColumnId = firstTable.columns.find((col) => col.id === filteredLayers[0].xAccessor)?.id; const xColumnId = firstTable.columns.find((col) => col.id === dataLayers[0]?.xAccessor)?.id;
const groupedLineAnnotations = getAnnotationsGroupedByInterval( const groupedLineAnnotations = getAnnotationsGroupedByInterval(
annotationsLayers, annotationsLayers,
@ -339,10 +316,11 @@ export function XYChart({
return layer.seriesType.includes('bar') || layer.seriesType.includes('area'); return layer.seriesType.includes('bar') || layer.seriesType.includes('area');
}) })
); );
const fit = !hasBarOrArea && extent.mode === 'dataBounds';
const fit = !hasBarOrArea && extent.mode === AxisExtentModes.DATA_BOUNDS;
let min: number = NaN; let min: number = NaN;
let max: number = NaN; let max: number = NaN;
if (extent.mode === 'custom') { if (extent.mode === 'custom') {
const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent); const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent);
if (!inclusiveZeroError && !boundaryError) { if (!inclusiveZeroError && !boundaryError) {
@ -369,38 +347,42 @@ export function XYChart({
const shouldShowValueLabels = const shouldShowValueLabels =
// No stacked bar charts // No stacked bar charts
filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) && dataLayers.every((layer) => !layer.seriesType.includes('stacked')) &&
// No histogram charts // No histogram charts
!isHistogramViz; !isHistogramViz;
const valueLabelsStyling = const valueLabelsStyling =
shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate); shouldShowValueLabels &&
valueLabels !== ValueLabelModes.HIDE &&
const colorAssignments = getColorAssignments(getDataLayers(args.layers), data, formatFactory); getValueLabelsStyling(shouldRotate);
const clickHandler: ElementClickListener = ([[geometry, series]]) => { const clickHandler: ElementClickListener = ([[geometry, series]]) => {
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
const xySeries = series as XYChartSeriesIdentifier; const xySeries = series as XYChartSeriesIdentifier;
const xyGeometry = geometry as GeometryValue; const xyGeometry = geometry as GeometryValue;
const layer = filteredLayers.find((l) => const layerIndex = dataLayers.findIndex((l) =>
xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString())) xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
); );
if (!layer) {
if (layerIndex === -1) {
return; return;
} }
const table = data.tables[layer.layerId]; const layer = dataLayers[layerIndex];
const { table } = layer;
const xColumn = table.columns.find((col) => col.id === layer.xAccessor); const xColumn = table.columns.find((col) => col.id === layer.xAccessor);
const currentXFormatter = const currentXFormatter =
layer.xAccessor && layersAlreadyFormatted[layer.xAccessor] && xColumn layer.xAccessor &&
formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor] &&
xColumn
? formatFactory(xColumn.meta.params) ? formatFactory(xColumn.meta.params)
: xAxisFormatter; : xAxisFormatter;
const rowIndex = table.rows.findIndex((row) => { const rowIndex = table.rows.findIndex((row) => {
if (layer.xAccessor) { if (layer.xAccessor) {
if (layersAlreadyFormatted[layer.xAccessor]) { if (formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor]) {
// stringify the value to compare with the chart value // stringify the value to compare with the chart value
return currentXFormatter.convert(row[layer.xAccessor]) === xyGeometry.x; return currentXFormatter.convert(row[layer.xAccessor]) === xyGeometry.x;
} }
@ -425,7 +407,7 @@ export function XYChart({
points.push({ points.push({
row: table.rows.findIndex((row) => { row: table.rows.findIndex((row) => {
if (layer.splitAccessor) { if (layer.splitAccessor) {
if (layersAlreadyFormatted[layer.splitAccessor]) { if (formattedDatatables[layer.layerId]?.formattedColumns[layer.splitAccessor]) {
return splitFormatter.convert(row[layer.splitAccessor]) === pointValue; return splitFormatter.convert(row[layer.splitAccessor]) === pointValue;
} }
return row[layer.splitAccessor] === pointValue; return row[layer.splitAccessor] === pointValue;
@ -436,12 +418,7 @@ export function XYChart({
}); });
} }
const context: FilterEvent['data'] = { const context: FilterEvent['data'] = {
data: points.map((point) => ({ data: points.map(({ row, column, value }) => ({ row, column, value, table })),
row: point.row,
column: point.column,
value: point.value,
table,
})),
}; };
onClickValue(context); onClickValue(context);
}; };
@ -455,27 +432,23 @@ export function XYChart({
return; return;
} }
const table = data.tables[filteredLayers[0].layerId]; const { table } = dataLayers[0];
const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor); const xAxisColumnIndex = table.columns.findIndex((el) => el.id === dataLayers[0].xAccessor);
const context: BrushEvent['data'] = { const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex };
range: [min, max],
table,
column: xAxisColumnIndex,
};
onSelectRange(context); onSelectRange(context);
}; };
const legendInsideParams = { const legendInsideParams: LegendPositionConfig = {
vAlign: legend.verticalAlignment ?? VerticalAlignment.Top, vAlign: legend.verticalAlignment ?? VerticalAlignment.Top,
hAlign: legend?.horizontalAlignment ?? HorizontalAlignment.Right, hAlign: legend?.horizontalAlignment ?? HorizontalAlignment.Right,
direction: LayoutDirection.Vertical, direction: LayoutDirection.Vertical,
floating: true, floating: true,
floatingColumns: legend?.floatingColumns ?? 1, floatingColumns: legend?.floatingColumns ?? 1,
} as LegendPositionConfig; };
const isHistogramModeEnabled = filteredLayers.some( const isHistogramModeEnabled = dataLayers.some(
({ isHistogram, seriesType }) => ({ isHistogram, seriesType }) =>
isHistogram && isHistogram &&
(seriesType.includes('stacked') || (seriesType.includes('stacked') ||
@ -570,13 +543,7 @@ export function XYChart({
onElementClick={interactive ? clickHandler : undefined} onElementClick={interactive ? clickHandler : undefined}
legendAction={ legendAction={
interactive interactive
? getLegendAction( ? getLegendAction(dataLayers, onClickValue, formatFactory, formattedDatatables)
filteredLayers,
data.tables,
onClickValue,
formatFactory,
layersAlreadyFormatted
)
: undefined : undefined
} }
showLegendExtra={isHistogramViz && valuesInLegend} showLegendExtra={isHistogramViz && valuesInLegend}
@ -589,7 +556,7 @@ export function XYChart({
position={shouldRotate ? Position.Left : Position.Bottom} position={shouldRotate ? Position.Left : Position.Bottom}
title={xTitle} title={xTitle}
gridLine={gridLineStyle} gridLine={gridLineStyle}
hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor} hide={dataLayers[0]?.hide || !dataLayers[0]?.xAccessor}
tickFormat={(d) => safeXAccessorLabelRenderer(d)} tickFormat={(d) => safeXAccessorLabelRenderer(d)}
style={xAxisStyle} style={xAxisStyle}
timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0} timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0}
@ -609,9 +576,9 @@ export function XYChart({
? gridlinesVisibilitySettings?.yRight ? gridlinesVisibilitySettings?.yRight
: gridlinesVisibilitySettings?.yLeft, : gridlinesVisibilitySettings?.yLeft,
}} }}
hide={filteredLayers[0].hide} hide={dataLayers[0]?.hide}
tickFormat={(d) => axis.formatter?.convert(d) || ''} tickFormat={(d) => axis.formatter?.convert(d) || ''}
style={getYAxesStyle(axis.groupId as 'left' | 'right')} style={getYAxesStyle(axis.groupId)}
domain={getYAxisDomain(axis)} domain={getYAxisDomain(axis)}
ticks={5} ticks={5}
/> />
@ -623,7 +590,7 @@ export function XYChart({
baseDomain={rawXDomain} baseDomain={rawXDomain}
extendedDomain={xDomain} extendedDomain={xDomain}
darkMode={darkMode} darkMode={darkMode}
histogramMode={filteredLayers.every( histogramMode={dataLayers.every(
(layer) => (layer) =>
layer.isHistogram && layer.isHistogram &&
(layer.seriesType.includes('stacked') || !layer.splitAccessor) && (layer.seriesType.includes('stacked') || !layer.splitAccessor) &&
@ -634,282 +601,28 @@ export function XYChart({
/> />
)} )}
{filteredLayers.flatMap((layer, layerIndex) => {dataLayers.length && (
layer.accessors.map((accessor, accessorIndex) => { <DataLayers
const { layers={dataLayers}
splitAccessor, endValue={endValue}
seriesType, timeZone={timeZone}
accessors, curveType={args.curveType}
xAccessor, syncColors={syncColors}
layerId, valueLabels={valueLabels}
columnToLabel, fillOpacity={args.fillOpacity}
yScaleType, formatFactory={formatFactory}
xScaleType, paletteService={paletteService}
isHistogram, fittingFunction={fittingFunction}
palette, emphasizeFitting={emphasizeFitting}
} = layer; yAxesConfiguration={yAxesConfiguration}
const columnToLabelMap: Record<string, string> = columnToLabel shouldShowValueLabels={shouldShowValueLabels}
? JSON.parse(columnToLabel) formattedDatatables={formattedDatatables}
: {}; chartHasMoreThanOneBarSeries={chartHasMoreThanOneBarSeries}
const table = data.tables[layerId];
const formatterPerColumn = new Map<DatatableColumn, FieldFormat>();
for (const column of table.columns) {
formatterPerColumn.set(column, formatFactory(column.meta.params));
}
// what if row values are not primitive? That is the case of, for instance, Ranges
// remaps them to their serialized version with the formatHint metadata
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
const tableConverted: Datatable = {
...table,
rows: table.rows.map((row: DatatableRow) => {
const newRow = { ...row };
for (const column of table.columns) {
const record = newRow[column.id];
if (
record != null &&
// pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level
(!isPrimitive(record) || (column.id === xAccessor && xScaleType === 'ordinal'))
) {
newRow[column.id] = formatterPerColumn.get(column)!.convert(record);
}
}
return newRow;
}),
};
// save the id of the layer with the custom table
table.columns.reduce<Record<string, boolean>>(
(alreadyFormatted: Record<string, boolean>, { id }) => {
if (alreadyFormatted[id]) {
return alreadyFormatted;
}
alreadyFormatted[id] = table.rows.some(
(row, i) => row[id] !== tableConverted.rows[i][id]
);
return alreadyFormatted;
},
layersAlreadyFormatted
);
const isStacked = seriesType.includes('stacked');
const isPercentage = seriesType.includes('percentage');
const isBarChart = seriesType.includes('bar');
const enableHistogramMode =
isHistogram &&
(isStacked || !splitAccessor) &&
(isStacked || !isBarChart || !chartHasMoreThanOneBarSeries);
// For date histogram chart type, we're getting the rows that represent intervals without data.
// To not display them in the legend, they need to be filtered out.
const rows = tableConverted.rows.filter(
(row) =>
!(xAccessor && typeof row[xAccessor] === 'undefined') &&
!(
splitAccessor &&
typeof row[splitAccessor] === 'undefined' &&
typeof row[accessor] === 'undefined'
)
);
if (!xAccessor) {
rows.forEach((row) => {
row.unifiedX = i18n.translate('expressionXY.xyChart.emptyXLabel', {
defaultMessage: '(empty)',
});
});
}
const yAxis = yAxesConfiguration.find((axisConfiguration) =>
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
);
const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params;
const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params;
const splitFormatter = formatFactory(splitHint);
const seriesProps: SeriesSpec = {
splitSeriesAccessors: splitAccessor ? [splitAccessor] : [],
stackAccessors: isStacked ? [xAccessor as string] : [],
id: `${splitAccessor}-${accessor}`,
xAccessor: xAccessor || 'unifiedX',
yAccessors: [accessor],
data: rows,
xScaleType: xAccessor ? xScaleType : 'ordinal',
yScaleType:
formatter?.id === 'bytes' && yScaleType === ScaleType.Linear
? ScaleType.LinearBinary
: yScaleType,
color: ({ yAccessor, seriesKeys }) => {
const overwriteColor = getSeriesColor(layer, accessor);
if (overwriteColor !== null) {
return overwriteColor;
}
const colorAssignment = colorAssignments[palette.name];
const seriesLayers: SeriesLayer[] = [
{
name: splitAccessor ? String(seriesKeys[0]) : columnToLabelMap[seriesKeys[0]],
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
rankAtDepth: colorAssignment.getRank(
layer,
String(seriesKeys[0]),
String(yAccessor)
),
},
];
return paletteService.get(palette.name).getCategoricalColor(
seriesLayers,
{
maxDepth: 1,
behindText: false,
totalSeries: colorAssignment.totalSeriesCount,
syncColors,
},
palette.params
);
},
groupId: yAxis?.groupId,
enableHistogramMode,
stackMode: isPercentage ? StackMode.Percentage : undefined,
timeZone,
areaSeriesStyle: {
point: {
visible: !xAccessor,
radius: xAccessor && !emphasizeFitting ? 5 : 0,
},
...(args.fillOpacity && { area: { opacity: args.fillOpacity } }),
...(emphasizeFitting && {
fit: {
area: {
opacity: args.fillOpacity || 0.5,
},
line: {
visible: true,
stroke: ColorVariant.Series,
opacity: 1,
dash: [],
},
},
}),
},
lineSeriesStyle: {
point: {
visible: !xAccessor,
radius: xAccessor && !emphasizeFitting ? 5 : 0,
},
...(emphasizeFitting && {
fit: {
line: {
visible: true,
stroke: ColorVariant.Series,
opacity: 1,
dash: [],
},
},
}),
},
name(d) {
// For multiple y series, the name of the operation is used on each, either:
// * Key - Y name
// * Formatted value - Y name
if (accessors.length > 1) {
const result = d.seriesKeys
.map((key: string | number, i) => {
if (
i === 0 &&
splitHint &&
splitAccessor &&
!layersAlreadyFormatted[splitAccessor]
) {
return splitFormatter.convert(key);
}
return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? '';
})
.join(' - ');
return result;
}
// For formatted split series, format the key
// This handles splitting by dates, for example
if (splitHint) {
if (splitAccessor && layersAlreadyFormatted[splitAccessor]) {
return d.seriesKeys[0];
}
return splitFormatter.convert(d.seriesKeys[0]);
}
// This handles both split and single-y cases:
// * If split series without formatting, show the value literally
// * If single Y, the seriesKey will be the accessor, so we show the human-readable name
return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? '';
},
};
const index = `${layerIndex}-${accessorIndex}`;
const curveType = args.curveType ? CurveType[args.curveType] : undefined;
switch (seriesType) {
case 'line':
return (
<LineSeries
key={index}
{...seriesProps}
fit={getFitOptions(fittingFunction, endValue)}
curve={curveType}
/> />
);
case 'bar':
case 'bar_stacked':
case 'bar_percentage_stacked':
case 'bar_horizontal':
case 'bar_horizontal_stacked':
case 'bar_horizontal_percentage_stacked':
const valueLabelsSettings = {
displayValueSettings: {
// This format double fixes two issues in elastic-chart
// * when rotating the chart, the formatter is not correctly picked
// * in some scenarios value labels are not strings, and this breaks the elastic-chart lib
valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '',
showValueLabel: shouldShowValueLabels && valueLabels !== 'hide',
isValueContainedInElement: false,
isAlternatingValueLabel: false,
overflowConstraints: [
LabelOverflowConstraint.ChartEdges,
LabelOverflowConstraint.BarGeometry,
],
},
};
return <BarSeries key={index} {...seriesProps} {...valueLabelsSettings} />;
case 'area_stacked':
case 'area_percentage_stacked':
return (
<AreaSeries
key={index}
{...seriesProps}
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction, endValue)}
curve={curveType}
/>
);
case 'area':
return (
<AreaSeries
key={index}
{...seriesProps}
fit={getFitOptions(fittingFunction, endValue)}
curve={curveType}
/>
);
default:
return assertNever(seriesType);
}
})
)} )}
{referenceLineLayers.length ? ( {referenceLineLayers.length ? (
<ReferenceLineAnnotations <ReferenceLineAnnotations
layers={referenceLineLayers} layers={referenceLineLayers}
data={data}
formatters={{ formatters={{
left: yAxesMap.left?.formatter, left: yAxesMap.left?.formatter,
right: yAxesMap.right?.formatter, right: yAxesMap.right?.formatter,
@ -946,7 +659,3 @@ export function XYChart({
</Chart> </Chart>
); );
} }
function assertNever(x: never): never {
throw new Error('Unexpected series type: ' + x);
}

View file

@ -18,7 +18,6 @@ import { ExpressionRenderDefinition } from '@kbn/expressions-plugin';
import { FormatFactory } from '@kbn/field-formats-plugin/common'; import { FormatFactory } from '@kbn/field-formats-plugin/common';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { XYChartProps } from '../../common'; import type { XYChartProps } from '../../common';
import { calculateMinInterval } from '../helpers/interval';
import type { BrushEvent, FilterEvent } from '../types'; import type { BrushEvent, FilterEvent } from '../types';
export type GetStartDepsFn = () => Promise<{ export type GetStartDepsFn = () => Promise<{
@ -56,7 +55,10 @@ export const getXyChartRenderer = ({
}; };
const deps = await getStartDeps(); const deps = await getStartDeps();
const { XYChartReportable } = await import('../components/xy_chart'); const [{ XYChartReportable }, { calculateMinInterval }] = await Promise.all([
import('../components/xy_chart'),
import('../helpers/interval'),
]);
ReactDOM.render( ReactDOM.render(
<KibanaThemeProvider theme$={deps.kibanaTheme.theme$}> <KibanaThemeProvider theme$={deps.kibanaTheme.theme$}>

View file

@ -9,19 +9,32 @@ import React from 'react';
import { Position } from '@elastic/charts'; import { Position } from '@elastic/charts';
import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui';
import classnames from 'classnames'; import classnames from 'classnames';
import type { IconPosition, YAxisMode, YConfig } from '../../common/types'; import type {
IconPosition,
YAxisMode,
ExtendedYConfig,
CollectiveConfig,
} from '../../common/types';
import { getBaseIconPlacement } from '../components'; import { getBaseIconPlacement } from '../components';
import { hasIcon } from './icon'; import { hasIcon, iconSet } from './icon';
import { annotationsIconSet } from './annotations_icon_set';
export const LINES_MARKER_SIZE = 20; export const LINES_MARKER_SIZE = 20;
// Note: it does not take into consideration whether the reference line is in view or not type PartialExtendedYConfig = Pick<
ExtendedYConfig,
'axisMode' | 'icon' | 'iconPosition' | 'textVisibility'
>;
type PartialCollectiveConfig = Pick<CollectiveConfig, 'axisMode' | 'icon' | 'textVisibility'>;
const isExtendedYConfig = (
config: PartialExtendedYConfig | PartialCollectiveConfig | undefined
): config is PartialExtendedYConfig =>
(config as PartialExtendedYConfig)?.iconPosition ? true : false;
// Note: it does not take into consideration whether the reference line is in view or not
export const getLinesCausedPaddings = ( export const getLinesCausedPaddings = (
visualConfigs: Array< visualConfigs: Array<PartialExtendedYConfig | PartialCollectiveConfig | undefined>,
Pick<YConfig, 'axisMode' | 'icon' | 'iconPosition' | 'textVisibility'> | undefined
>,
axesMap: Record<'left' | 'right', unknown> axesMap: Record<'left' | 'right', unknown>
) => { ) => {
// collect all paddings for the 4 axis: if any text is detected double it. // collect all paddings for the 4 axis: if any text is detected double it.
@ -31,7 +44,9 @@ export const getLinesCausedPaddings = (
if (!config) { if (!config) {
return; return;
} }
const { axisMode, icon, iconPosition, textVisibility } = config; const { axisMode, icon, textVisibility } = config;
const iconPosition = isExtendedYConfig(config) ? config.iconPosition : undefined;
if (axisMode && (hasIcon(icon) || textVisibility)) { if (axisMode && (hasIcon(icon) || textVisibility)) {
const placement = getBaseIconPlacement(iconPosition, axesMap, axisMode); const placement = getBaseIconPlacement(iconPosition, axesMap, axisMode);
paddings[placement] = Math.max( paddings[placement] = Math.max(
@ -48,6 +63,7 @@ export const getLinesCausedPaddings = (
paddings[placement] = LINES_MARKER_SIZE; paddings[placement] = LINES_MARKER_SIZE;
} }
}); });
return paddings; return paddings;
}; };
@ -138,7 +154,7 @@ export const AnnotationIcon = ({
if (isNumericalString(type)) { if (isNumericalString(type)) {
return <NumberIcon number={Number(type)} />; return <NumberIcon number={Number(type)} />;
} }
const iconConfig = annotationsIconSet.find((i) => i.value === type); const iconConfig = iconSet.find((i) => i.value === type);
if (!iconConfig) { if (!iconConfig) {
return null; return null;
} }

View file

@ -1,101 +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 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 { TriangleIcon, CircleIcon } from '../icons';
export const annotationsIconSet = [
{
value: 'asterisk',
label: i18n.translate('expressionXY.xyChart.iconSelect.asteriskIconLabel', {
defaultMessage: 'Asterisk',
}),
},
{
value: 'alert',
label: i18n.translate('expressionXY.xyChart.iconSelect.alertIconLabel', {
defaultMessage: 'Alert',
}),
},
{
value: 'bell',
label: i18n.translate('expressionXY.xyChart.iconSelect.bellIconLabel', {
defaultMessage: 'Bell',
}),
},
{
value: 'bolt',
label: i18n.translate('expressionXY.xyChart.iconSelect.boltIconLabel', {
defaultMessage: 'Bolt',
}),
},
{
value: 'bug',
label: i18n.translate('expressionXY.xyChart.iconSelect.bugIconLabel', {
defaultMessage: 'Bug',
}),
},
{
value: 'circle',
label: i18n.translate('expressionXY.xyChart.iconSelect.circleIconLabel', {
defaultMessage: 'Circle',
}),
icon: CircleIcon,
canFill: true,
},
{
value: 'editorComment',
label: i18n.translate('expressionXY.xyChart.iconSelect.commentIconLabel', {
defaultMessage: 'Comment',
}),
},
{
value: 'flag',
label: i18n.translate('expressionXY.xyChart.iconSelect.flagIconLabel', {
defaultMessage: 'Flag',
}),
},
{
value: 'heart',
label: i18n.translate('expressionXY.xyChart.iconSelect.heartLabel', {
defaultMessage: 'Heart',
}),
},
{
value: 'mapMarker',
label: i18n.translate('expressionXY.xyChart.iconSelect.mapMarkerLabel', {
defaultMessage: 'Map Marker',
}),
},
{
value: 'pinFilled',
label: i18n.translate('expressionXY.xyChart.iconSelect.mapPinLabel', {
defaultMessage: 'Map Pin',
}),
},
{
value: 'starEmpty',
label: i18n.translate('expressionXY.xyChart.iconSelect.starLabel', { defaultMessage: 'Star' }),
},
{
value: 'tag',
label: i18n.translate('expressionXY.xyChart.iconSelect.tagIconLabel', {
defaultMessage: 'Tag',
}),
},
{
value: 'triangle',
label: i18n.translate('expressionXY.xyChart.iconSelect.triangleIconLabel', {
defaultMessage: 'Triangle',
}),
icon: TriangleIcon,
shouldRotate: true,
canFill: true,
},
];

View file

@ -6,7 +6,7 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { DataLayerConfigResult } from '../../common'; import { DataLayerConfig } from '../../common';
import { LayerTypes } from '../../common/constants'; import { LayerTypes } from '../../common/constants';
import { Datatable } from '@kbn/expressions-plugin/public'; import { Datatable } from '@kbn/expressions-plugin/public';
import { getAxesConfiguration } from './axes_configuration'; import { getAxesConfiguration } from './axes_configuration';
@ -220,9 +220,9 @@ describe('axes_configuration', () => {
}, },
}; };
const sampleLayer: DataLayerConfigResult = { const sampleLayer: DataLayerConfig = {
type: 'dataLayer',
layerId: 'first', layerId: 'first',
type: 'dataLayer',
layerType: LayerTypes.DATA, layerType: LayerTypes.DATA,
seriesType: 'line', seriesType: 'line',
xAccessor: 'c', xAccessor: 'c',
@ -233,11 +233,12 @@ describe('axes_configuration', () => {
yScaleType: 'linear', yScaleType: 'linear',
isHistogram: false, isHistogram: false,
palette: { type: 'palette', name: 'default' }, palette: { type: 'palette', name: 'default' },
table: tables.first,
}; };
it('should map auto series to left axis', () => { it('should map auto series to left axis', () => {
const formatFactory = jest.fn(); const formatFactory = jest.fn();
const groups = getAxesConfiguration([sampleLayer], false, tables, formatFactory); const groups = getAxesConfiguration([sampleLayer], false, formatFactory);
expect(groups.length).toEqual(1); expect(groups.length).toEqual(1);
expect(groups[0].position).toEqual('left'); expect(groups[0].position).toEqual('left');
expect(groups[0].series[0].accessor).toEqual('yAccessorId'); expect(groups[0].series[0].accessor).toEqual('yAccessorId');
@ -247,7 +248,7 @@ describe('axes_configuration', () => {
it('should map auto series to right axis if formatters do not match', () => { it('should map auto series to right axis if formatters do not match', () => {
const formatFactory = jest.fn(); const formatFactory = jest.fn();
const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] }; const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] };
const groups = getAxesConfiguration([twoSeriesLayer], false, tables, formatFactory); const groups = getAxesConfiguration([twoSeriesLayer], false, formatFactory);
expect(groups.length).toEqual(2); expect(groups.length).toEqual(2);
expect(groups[0].position).toEqual('left'); expect(groups[0].position).toEqual('left');
expect(groups[1].position).toEqual('right'); expect(groups[1].position).toEqual('right');
@ -261,7 +262,7 @@ describe('axes_configuration', () => {
...sampleLayer, ...sampleLayer,
accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'], accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'],
}; };
const groups = getAxesConfiguration([threeSeriesLayer], false, tables, formatFactory); const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory);
expect(groups.length).toEqual(2); expect(groups.length).toEqual(2);
expect(groups[0].position).toEqual('left'); expect(groups[0].position).toEqual('left');
expect(groups[1].position).toEqual('right'); expect(groups[1].position).toEqual('right');
@ -280,7 +281,6 @@ describe('axes_configuration', () => {
}, },
], ],
false, false,
tables,
formatFactory formatFactory
); );
expect(groups.length).toEqual(1); expect(groups.length).toEqual(1);
@ -300,7 +300,6 @@ describe('axes_configuration', () => {
}, },
], ],
false, false,
tables,
formatFactory formatFactory
); );
expect(groups.length).toEqual(2); expect(groups.length).toEqual(2);
@ -324,7 +323,6 @@ describe('axes_configuration', () => {
}, },
], ],
false, false,
tables,
formatFactory formatFactory
); );
expect(formatFactory).toHaveBeenCalledTimes(2); expect(formatFactory).toHaveBeenCalledTimes(2);

View file

@ -6,22 +6,25 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { Datatable } from '@kbn/expressions-plugin/public';
import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
import { FormatFactory } from '../types'; import { FormatFactory } from '../types';
import { AxisExtentConfig, DataLayerConfigResult } from '../../common'; import { AxisExtentConfig, CommonXYDataLayerConfig, ExtendedYConfig, YConfig } from '../../common';
import { isDataLayer } from './visualization';
interface FormattedMetric { export interface Series {
layer: string; layer: string;
accessor: string; accessor: string;
}
interface FormattedMetric extends Series {
fieldFormat: SerializedFieldFormat; fieldFormat: SerializedFieldFormat;
} }
export type GroupsConfiguration = Array<{ export type GroupsConfiguration = Array<{
groupId: string; groupId: 'left' | 'right';
position: 'left' | 'right' | 'bottom' | 'top'; position: 'left' | 'right' | 'bottom' | 'top';
formatter?: IFieldFormat; formatter?: IFieldFormat;
series: Array<{ layer: string; accessor: string }>; series: Series[];
}>; }>;
export function isFormatterCompatible( export function isFormatterCompatible(
@ -31,10 +34,7 @@ export function isFormatterCompatible(
return formatter1.id === formatter2.id; return formatter1.id === formatter2.id;
} }
export function groupAxesByType( export function groupAxesByType(layers: CommonXYDataLayerConfig[]) {
layers: DataLayerConfigResult[],
tables?: Record<string, Datatable>
) {
const series: { const series: {
auto: FormattedMetric[]; auto: FormattedMetric[];
left: FormattedMetric[]; left: FormattedMetric[];
@ -47,15 +47,19 @@ export function groupAxesByType(
bottom: [], bottom: [],
}; };
layers?.forEach((layer) => { layers.forEach((layer) => {
const table = tables?.[layer.layerId]; const { table } = layer;
layer.accessors.forEach((accessor) => { layer.accessors.forEach((accessor) => {
const yConfig: Array<YConfig | ExtendedYConfig> | undefined = layer.yConfig;
const mode = const mode =
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto';
'auto'; let formatter: SerializedFieldFormat = table.columns?.find((column) => column.id === accessor)
let formatter: SerializedFieldFormat = table?.columns.find((column) => column.id === accessor)
?.meta?.params || { id: 'number' }; ?.meta?.params || { id: 'number' };
if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') { if (
isDataLayer(layer) &&
layer.seriesType.includes('percentage') &&
formatter.id !== 'percent'
) {
formatter = { formatter = {
id: 'percent', id: 'percent',
params: { params: {
@ -71,10 +75,12 @@ export function groupAxesByType(
}); });
}); });
const tablesExist = layers.filter(({ table }) => Boolean(table)).length > 0;
series.auto.forEach((currentSeries) => { series.auto.forEach((currentSeries) => {
if ( if (
series.left.length === 0 || series.left.length === 0 ||
(tables && (tablesExist &&
series.left.every((leftSeries) => series.left.every((leftSeries) =>
isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat)
)) ))
@ -82,7 +88,7 @@ export function groupAxesByType(
series.left.push(currentSeries); series.left.push(currentSeries);
} else if ( } else if (
series.right.length === 0 || series.right.length === 0 ||
(tables && (tablesExist &&
series.left.every((leftSeries) => series.left.every((leftSeries) =>
isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat)
)) ))
@ -98,12 +104,11 @@ export function groupAxesByType(
} }
export function getAxesConfiguration( export function getAxesConfiguration(
layers: DataLayerConfigResult[], layers: CommonXYDataLayerConfig[],
shouldRotate: boolean, shouldRotate: boolean,
tables?: Record<string, Datatable>,
formatFactory?: FormatFactory formatFactory?: FormatFactory
): GroupsConfiguration { ): GroupsConfiguration {
const series = groupAxesByType(layers, tables); const series = groupAxesByType(layers);
const axisGroups: GroupsConfiguration = []; const axisGroups: GroupsConfiguration = [];

View file

@ -7,41 +7,13 @@
*/ */
import { getColorAssignments } from './color_assignment'; import { getColorAssignments } from './color_assignment';
import type { DataLayerConfigResult, LensMultiTable } from '../../common'; import type { DataLayerConfig } from '../../common';
import type { FormatFactory } from '../types'; import type { FormatFactory } from '../types';
import { LayerTypes } from '../../common/constants'; import { LayerTypes } from '../../common/constants';
import { Datatable } from '@kbn/expressions-plugin';
describe('color_assignment', () => { describe('color_assignment', () => {
const layers: DataLayerConfigResult[] = [ const tables: Record<string, Datatable> = {
{
type: 'dataLayer',
yScaleType: 'linear',
xScaleType: 'linear',
isHistogram: true,
seriesType: 'bar',
palette: { type: 'palette', name: 'palette1' },
layerId: '1',
layerType: LayerTypes.DATA,
splitAccessor: 'split1',
accessors: ['y1', 'y2'],
},
{
type: 'dataLayer',
yScaleType: 'linear',
xScaleType: 'linear',
isHistogram: true,
seriesType: 'bar',
palette: { type: 'palette', name: 'palette2' },
layerId: '2',
layerType: LayerTypes.DATA,
splitAccessor: 'split2',
accessors: ['y3', 'y4'],
},
];
const data: LensMultiTable = {
type: 'lens_multitable',
tables: {
'1': { '1': {
type: 'datatable', type: 'datatable',
columns: [ columns: [
@ -74,9 +46,37 @@ describe('color_assignment', () => {
{ split2: 3 }, { split2: 3 },
], ],
}, },
},
}; };
const layers: DataLayerConfig[] = [
{
layerId: 'first',
type: 'dataLayer',
yScaleType: 'linear',
xScaleType: 'linear',
isHistogram: true,
seriesType: 'bar',
palette: { type: 'palette', name: 'palette1' },
layerType: LayerTypes.DATA,
splitAccessor: 'split1',
accessors: ['y1', 'y2'],
table: tables['1'],
},
{
layerId: 'second',
type: 'dataLayer',
yScaleType: 'linear',
xScaleType: 'linear',
isHistogram: true,
seriesType: 'bar',
palette: { type: 'palette', name: 'palette2' },
layerType: LayerTypes.DATA,
splitAccessor: 'split2',
accessors: ['y3', 'y4'],
table: tables['2'],
},
];
const formatFactory = (() => const formatFactory = (() =>
({ ({
convert(x: unknown) { convert(x: unknown) {
@ -86,7 +86,7 @@ describe('color_assignment', () => {
describe('totalSeriesCount', () => { describe('totalSeriesCount', () => {
it('should calculate total number of series per palette', () => { it('should calculate total number of series per palette', () => {
const assignments = getColorAssignments(layers, data, formatFactory); const assignments = getColorAssignments(layers, formatFactory);
// two y accessors, with 3 splitted series // two y accessors, with 3 splitted series
expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3); expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3);
expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3);
@ -95,7 +95,6 @@ describe('color_assignment', () => {
it('should calculate total number of series spanning multible layers', () => { it('should calculate total number of series spanning multible layers', () => {
const assignments = getColorAssignments( const assignments = getColorAssignments(
[layers[0], { ...layers[1], palette: layers[0].palette }], [layers[0], { ...layers[1], palette: layers[0].palette }],
data,
formatFactory formatFactory
); );
// two y accessors, with 3 splitted series, two times // two y accessors, with 3 splitted series, two times
@ -106,7 +105,6 @@ describe('color_assignment', () => {
it('should calculate total number of series for non split series', () => { it('should calculate total number of series for non split series', () => {
const assignments = getColorAssignments( const assignments = getColorAssignments(
[layers[0], { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }], [layers[0], { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }],
data,
formatFactory formatFactory
); );
// two y accessors, with 3 splitted series for the first layer, 2 non splitted y accessors for the second layer // two y accessors, with 3 splitted series for the first layer, 2 non splitted y accessors for the second layer
@ -117,15 +115,16 @@ describe('color_assignment', () => {
it('should format non-primitive values and count them correctly', () => { it('should format non-primitive values and count them correctly', () => {
const complexObject = { aProp: 123 }; const complexObject = { aProp: 123 };
const formatMock = jest.fn((x) => 'formatted'); const formatMock = jest.fn((x) => 'formatted');
const assignments = getColorAssignments( const newLayers = [
layers,
{ {
...data, ...layers[0],
tables: { table: { ...tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] },
...data.tables,
'1': { ...data.tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] },
},
}, },
layers[1],
];
const assignments = getColorAssignments(
newLayers,
(() => (() =>
({ ({
convert: formatMock, convert: formatMock,
@ -137,26 +136,18 @@ describe('color_assignment', () => {
}); });
it('should handle missing tables', () => { it('should handle missing tables', () => {
const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory); const assignments = getColorAssignments(
layers.map((l) => ({ ...l, table: {} as any })),
formatFactory
);
// if there is no data, just assume a single split // if there is no data, just assume a single split
expect(assignments.palette1.totalSeriesCount).toEqual(2); expect(assignments.palette1.totalSeriesCount).toEqual(2);
}); });
it('should handle missing columns', () => { it('should handle missing columns', () => {
const assignments = getColorAssignments( const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]];
layers, const assignments = getColorAssignments(newLayers, formatFactory);
{
...data,
tables: {
...data.tables,
'1': {
...data.tables['1'],
columns: [],
},
},
},
formatFactory
);
// if the split column is missing, just assume a single split // if the split column is missing, just assume a single split
expect(assignments.palette1.totalSeriesCount).toEqual(2); expect(assignments.palette1.totalSeriesCount).toEqual(2);
}); });
@ -164,7 +155,7 @@ describe('color_assignment', () => {
describe('getRank', () => { describe('getRank', () => {
it('should return the correct rank for a series key', () => { it('should return the correct rank for a series key', () => {
const assignments = getColorAssignments(layers, data, formatFactory); const assignments = getColorAssignments(layers, formatFactory);
// 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(3); expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(3);
// 1 series in front of 1/y4 - 1/y3 // 1 series in front of 1/y4 - 1/y3
@ -173,7 +164,7 @@ describe('color_assignment', () => {
it('should return the correct rank for a series key spanning multiple layers', () => { it('should return the correct rank for a series key spanning multiple layers', () => {
const newLayers = [layers[0], { ...layers[1], palette: layers[0].palette }]; const newLayers = [layers[0], { ...layers[1], palette: layers[0].palette }];
const assignments = getColorAssignments(newLayers, data, formatFactory); const assignments = getColorAssignments(newLayers, formatFactory);
// 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1
expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3); expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3);
// 2 series in front for the current layer (1/y3, 1/y4), plus all 6 series from the first layer // 2 series in front for the current layer (1/y3, 1/y4), plus all 6 series from the first layer
@ -185,7 +176,7 @@ describe('color_assignment', () => {
layers[0], layers[0],
{ ...layers[1], palette: layers[0].palette, splitAccessor: undefined }, { ...layers[1], palette: layers[0].palette, splitAccessor: undefined },
]; ];
const assignments = getColorAssignments(newLayers, data, formatFactory); const assignments = getColorAssignments(newLayers, formatFactory);
// 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1 // 3 series in front of 2/y2 - 1/y1, 1/y2 and 2/y1
expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3); expect(assignments.palette1.getRank(newLayers[0], '2', 'y2')).toEqual(3);
// 1 series in front for the current layer (y3), plus all 6 series from the first layer // 1 series in front for the current layer (y3), plus all 6 series from the first layer
@ -193,15 +184,16 @@ describe('color_assignment', () => {
}); });
it('should return the correct rank for a series with a non-primitive value', () => { it('should return the correct rank for a series with a non-primitive value', () => {
const assignments = getColorAssignments( const newLayers = [
layers,
{ {
...data, ...layers[0],
tables: { table: { ...tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] },
...data.tables,
'1': { ...data.tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] },
},
}, },
layers[1],
];
const assignments = getColorAssignments(
newLayers,
(() => (() =>
({ ({
convert: () => 'formatted', convert: () => 'formatted',
@ -212,26 +204,19 @@ describe('color_assignment', () => {
}); });
it('should handle missing tables', () => { it('should handle missing tables', () => {
const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory); const assignments = getColorAssignments(
layers.map((l) => ({ ...l, table: {} as any })),
formatFactory
);
// if there is no data, assume it is the first splitted series. One series in front - 0/y1 // if there is no data, assume it is the first splitted series. One series in front - 0/y1
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1);
}); });
it('should handle missing columns', () => { it('should handle missing columns', () => {
const assignments = getColorAssignments( const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]];
layers,
{ const assignments = getColorAssignments(newLayers, formatFactory);
...data,
tables: {
...data.tables,
'1': {
...data.tables['1'],
columns: [],
},
},
},
formatFactory
);
// if the split column is missing, assume it is the first splitted series. One series in front - 0/y1 // if the split column is missing, assume it is the first splitted series. One series in front - 0/y1
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1);
}); });

View file

@ -8,10 +8,9 @@
import { uniq, mapValues } from 'lodash'; import { uniq, mapValues } from 'lodash';
import { euiLightVars } from '@kbn/ui-theme'; import { euiLightVars } from '@kbn/ui-theme';
import type { Datatable } from '@kbn/expressions-plugin';
import { FormatFactory } from '../types'; import { FormatFactory } from '../types';
import { isDataLayer } from './visualization'; import { isDataLayer } from './visualization';
import { DataLayerConfigResult, XYLayerConfigResult } from '../../common'; import { CommonXYDataLayerConfig, CommonXYLayerConfig } from '../../common';
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
@ -21,20 +20,21 @@ export type ColorAssignments = Record<
string, string,
{ {
totalSeriesCount: number; totalSeriesCount: number;
getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string): number; getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string): number;
} }
>; >;
export function getColorAssignments( export function getColorAssignments(
layers: XYLayerConfigResult[], layers: CommonXYLayerConfig[],
data: { tables: Record<string, Datatable> },
formatFactory: FormatFactory formatFactory: FormatFactory
): ColorAssignments { ): ColorAssignments {
const layersPerPalette: Record<string, DataLayerConfigResult[]> = {}; const layersPerPalette: Record<string, CommonXYDataLayerConfig[]> = {};
layers.forEach((layer) => {
if (!isDataLayer(layer)) {
return;
}
layers
.filter((layer): layer is DataLayerConfigResult => isDataLayer(layer))
.forEach((layer) => {
const palette = layer.palette?.name || 'default'; const palette = layer.palette?.name || 'default';
if (!layersPerPalette[palette]) { if (!layersPerPalette[palette]) {
layersPerPalette[palette] = []; layersPerPalette[palette] = [];
@ -43,18 +43,18 @@ export function getColorAssignments(
}); });
return mapValues(layersPerPalette, (paletteLayers) => { return mapValues(layersPerPalette, (paletteLayers) => {
const seriesPerLayer = paletteLayers.map((layer, layerIndex) => { const seriesPerLayer = paletteLayers.map((layer) => {
if (!layer.splitAccessor) { if (!layer.splitAccessor) {
return { numberOfSeries: layer.accessors.length, splits: [] }; return { numberOfSeries: layer.accessors.length, splits: [] };
} }
const splitAccessor = layer.splitAccessor; const splitAccessor = layer.splitAccessor;
const column = data.tables[layer.layerId]?.columns.find(({ id }) => id === splitAccessor); const column = layer.table.columns?.find(({ id }) => id === splitAccessor);
const columnFormatter = column && formatFactory(column.meta.params); const columnFormatter = column && formatFactory(column.meta.params);
const splits = const splits =
!column || !data.tables[layer.layerId] !column || !layer.table
? [] ? []
: uniq( : uniq(
data.tables[layer.layerId].rows.map((row) => { layer.table.rows.map((row) => {
let value = row[splitAccessor]; let value = row[splitAccessor];
if (value && !isPrimitive(value)) { if (value && !isPrimitive(value)) {
value = columnFormatter?.convert(value) ?? value; value = columnFormatter?.convert(value) ?? value;
@ -72,8 +72,10 @@ export function getColorAssignments(
); );
return { return {
totalSeriesCount, totalSeriesCount,
getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string) { getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string) {
const layerIndex = paletteLayers.findIndex((l) => sortedLayer.layerId === l.layerId); const layerIndex = paletteLayers.findIndex(
(layer) => sortedLayer.layerId === layer.layerId
);
const currentSeriesPerLayer = seriesPerLayer[layerIndex]; const currentSeriesPerLayer = seriesPerLayer[layerIndex];
const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey);
return ( return (

View file

@ -0,0 +1,332 @@
/*
* 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 {
AreaSeriesProps,
BarSeriesProps,
ColorVariant,
LineSeriesProps,
ScaleType,
SeriesName,
StackMode,
XYChartSeriesIdentifier,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import {
FieldFormat,
FieldFormatParams,
IFieldFormat,
SerializedFieldFormat,
} from '@kbn/field-formats-plugin/common';
import { Datatable } from '@kbn/expressions-plugin';
import { PaletteRegistry, SeriesLayer } from '@kbn/coloring';
import { CommonXYDataLayerConfig, XScaleType } from '../../common';
import { FormatFactory } from '../types';
import { getSeriesColor } from './state';
import { ColorAssignments } from './color_assignment';
import { GroupsConfiguration } from './axes_configuration';
type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps;
type GetSeriesPropsFn = (config: {
layer: CommonXYDataLayerConfig;
accessor: string;
chartHasMoreThanOneBarSeries?: boolean;
formatFactory: FormatFactory;
colorAssignments: ColorAssignments;
columnToLabelMap: Record<string, string>;
paletteService: PaletteRegistry;
syncColors?: boolean;
yAxis?: GroupsConfiguration[number];
timeZone?: string;
emphasizeFitting?: boolean;
fillOpacity?: number;
formattedDatatableInfo: DatatableWithFormatInfo;
}) => SeriesSpec;
type GetSeriesNameFn = (
data: XYChartSeriesIdentifier,
config: {
layer: CommonXYDataLayerConfig;
splitHint: SerializedFieldFormat<FieldFormatParams> | undefined;
splitFormatter: FieldFormat;
alreadyFormattedColumns: Record<string, boolean>;
columnToLabelMap: Record<string, string>;
}
) => SeriesName;
type GetColorFn = (
seriesIdentifier: XYChartSeriesIdentifier,
config: {
layer: CommonXYDataLayerConfig;
accessor: string;
colorAssignments: ColorAssignments;
columnToLabelMap: Record<string, string>;
paletteService: PaletteRegistry;
syncColors?: boolean;
}
) => string | null;
export interface DatatableWithFormatInfo {
table: Datatable;
formattedColumns: Record<string, true>;
}
export type DatatablesWithFormatInfo = Record<string, DatatableWithFormatInfo>;
export type FormattedDatatables = Record<string, Datatable>;
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
export const getFormattedRow = (
row: Datatable['rows'][number],
columns: Datatable['columns'],
columnsFormatters: Record<string, IFieldFormat>,
xAccessor: string | undefined,
xScaleType: XScaleType
): { row: Datatable['rows'][number]; formattedColumns: Record<string, true> } =>
columns.reduce(
(formattedInfo, { id }) => {
const record = formattedInfo.row[id];
if (
record != null &&
// pre-format values for ordinal x axes because there can only be a single x axis formatter on chart level
(!isPrimitive(record) || (id === xAccessor && xScaleType === 'ordinal'))
) {
return {
row: { ...formattedInfo.row, [id]: columnsFormatters[id]!.convert(record) },
formattedColumns: { ...formattedInfo.formattedColumns, [id]: true },
};
}
return formattedInfo;
},
{ row, formattedColumns: {} }
);
export const getFormattedTable = (
table: Datatable,
formatFactory: FormatFactory,
xAccessor: string | undefined,
xScaleType: XScaleType
): { table: Datatable; formattedColumns: Record<string, true> } => {
const columnsFormatters = table.columns.reduce<Record<string, IFieldFormat>>(
(formatters, { id, meta }) => ({ ...formatters, [id]: formatFactory(meta.params) }),
{}
);
const formattedTableInfo = table.rows.reduce<{
rows: Datatable['rows'];
formattedColumns: Record<string, true>;
}>(
({ rows: formattedRows, formattedColumns }, row) => {
const formattedRowInfo = getFormattedRow(
row,
table.columns,
columnsFormatters,
xAccessor,
xScaleType
);
return {
rows: [...formattedRows, formattedRowInfo.row],
formattedColumns: { ...formattedColumns, ...formattedRowInfo.formattedColumns },
};
},
{
rows: [],
formattedColumns: {},
}
);
return {
table: { ...table, rows: formattedTableInfo.rows },
formattedColumns: formattedTableInfo.formattedColumns,
};
};
export const getFormattedTablesByLayers = (
layers: CommonXYDataLayerConfig[],
formatFactory: FormatFactory
): DatatablesWithFormatInfo =>
layers.reduce(
(formattedDatatables, { layerId, table, xAccessor, xScaleType }) => ({
...formattedDatatables,
[layerId]: getFormattedTable(table, formatFactory, xAccessor, xScaleType),
}),
{}
);
const getSeriesName: GetSeriesNameFn = (
data,
{ layer, splitHint, splitFormatter, alreadyFormattedColumns, columnToLabelMap }
) => {
// For multiple y series, the name of the operation is used on each, either:
// * Key - Y name
// * Formatted value - Y name
if (layer.splitAccessor && layer.accessors.length > 1) {
const formatted = alreadyFormattedColumns[layer.splitAccessor];
const result = data.seriesKeys
.map((key: string | number, i) => {
if (i === 0 && splitHint && layer.splitAccessor && !formatted) {
return splitFormatter.convert(key);
}
return layer.splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? null;
})
.join(' - ');
return result;
}
// For formatted split series, format the key
// This handles splitting by dates, for example
if (splitHint) {
if (layer.splitAccessor && alreadyFormattedColumns[layer.splitAccessor]) {
return data.seriesKeys[0];
}
return splitFormatter.convert(data.seriesKeys[0]);
}
// This handles both split and single-y cases:
// * If split series without formatting, show the value literally
// * If single Y, the seriesKey will be the accessor, so we show the human-readable name
return layer.splitAccessor ? data.seriesKeys[0] : columnToLabelMap[data.seriesKeys[0]] ?? null;
};
const getPointConfig = (xAccessor?: string, emphasizeFitting?: boolean) => ({
visible: !xAccessor,
radius: xAccessor && !emphasizeFitting ? 5 : 0,
});
const getLineConfig = () => ({ visible: true, stroke: ColorVariant.Series, opacity: 1, dash: [] });
const getColor: GetColorFn = (
{ yAccessor, seriesKeys },
{ layer, accessor, colorAssignments, columnToLabelMap, paletteService, syncColors }
) => {
const overwriteColor = getSeriesColor(layer, accessor);
if (overwriteColor !== null) {
return overwriteColor;
}
const colorAssignment = colorAssignments[layer.palette.name];
const seriesLayers: SeriesLayer[] = [
{
name: layer.splitAccessor ? String(seriesKeys[0]) : columnToLabelMap[seriesKeys[0]],
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
rankAtDepth: colorAssignment.getRank(layer, String(seriesKeys[0]), String(yAccessor)),
},
];
return paletteService.get(layer.palette.name).getCategoricalColor(
seriesLayers,
{
maxDepth: 1,
behindText: false,
totalSeries: colorAssignment.totalSeriesCount,
syncColors,
},
layer.palette.params
);
};
export const getSeriesProps: GetSeriesPropsFn = ({
layer,
accessor,
chartHasMoreThanOneBarSeries,
colorAssignments,
formatFactory,
columnToLabelMap,
paletteService,
syncColors,
yAxis,
timeZone,
emphasizeFitting,
fillOpacity,
formattedDatatableInfo,
}): SeriesSpec => {
const { table } = layer;
const isStacked = layer.seriesType.includes('stacked');
const isPercentage = layer.seriesType.includes('percentage');
const isBarChart = layer.seriesType.includes('bar');
const enableHistogramMode =
layer.isHistogram &&
(isStacked || !layer.splitAccessor) &&
(isStacked || !isBarChart || !chartHasMoreThanOneBarSeries);
const formatter = table?.columns.find((column) => column.id === accessor)?.meta?.params;
const splitHint = table?.columns.find((col) => col.id === layer.splitAccessor)?.meta?.params;
const splitFormatter = formatFactory(splitHint);
// what if row values are not primitive? That is the case of, for instance, Ranges
// remaps them to their serialized version with the formatHint metadata
// In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on
const { table: formattedTable, formattedColumns } = formattedDatatableInfo;
// For date histogram chart type, we're getting the rows that represent intervals without data.
// To not display them in the legend, they need to be filtered out.
let rows = formattedTable.rows.filter(
(row) =>
!(layer.xAccessor && typeof row[layer.xAccessor] === 'undefined') &&
!(
layer.splitAccessor &&
typeof row[layer.splitAccessor] === 'undefined' &&
typeof row[accessor] === 'undefined'
)
);
if (!layer.xAccessor) {
rows = rows.map((row) => ({
...row,
unifiedX: i18n.translate('expressionXY.xyChart.emptyXLabel', {
defaultMessage: '(empty)',
}),
}));
}
return {
splitSeriesAccessors: layer.splitAccessor ? [layer.splitAccessor] : [],
stackAccessors: isStacked ? [layer.xAccessor as string] : [],
id: layer.splitAccessor ? `${layer.splitAccessor}-${accessor}` : `${accessor}`,
xAccessor: layer.xAccessor || 'unifiedX',
yAccessors: [accessor],
data: rows,
xScaleType: layer.xAccessor ? layer.xScaleType : 'ordinal',
yScaleType:
formatter?.id === 'bytes' && layer.yScaleType === ScaleType.Linear
? ScaleType.LinearBinary
: layer.yScaleType,
color: (series) =>
getColor(series, {
layer,
accessor,
colorAssignments,
columnToLabelMap,
paletteService,
syncColors,
}),
groupId: yAxis?.groupId,
enableHistogramMode,
stackMode: isPercentage ? StackMode.Percentage : undefined,
timeZone,
areaSeriesStyle: {
point: getPointConfig(layer.xAccessor, emphasizeFitting),
...(fillOpacity && { area: { opacity: fillOpacity } }),
...(emphasizeFitting && {
fit: { area: { opacity: fillOpacity || 0.5 }, line: getLineConfig() },
}),
},
lineSeriesStyle: {
point: getPointConfig(layer.xAccessor, emphasizeFitting),
...(emphasizeFitting && { fit: { line: getLineConfig() } }),
},
name(d) {
return getSeriesName(d, {
layer,
splitHint,
splitFormatter,
alreadyFormattedColumns: formattedColumns,
columnToLabelMap,
});
},
};
};

View file

@ -8,6 +8,7 @@
import { Fit } from '@elastic/charts'; import { Fit } from '@elastic/charts';
import { EndValue, FittingFunction } from '../../common'; import { EndValue, FittingFunction } from '../../common';
import { EndValues } from '../../common/constants';
export function getFitEnum(fittingFunction?: FittingFunction | EndValue) { export function getFitEnum(fittingFunction?: FittingFunction | EndValue) {
if (fittingFunction) { if (fittingFunction) {
@ -17,10 +18,10 @@ export function getFitEnum(fittingFunction?: FittingFunction | EndValue) {
} }
export function getEndValue(endValue?: EndValue) { export function getEndValue(endValue?: EndValue) {
if (endValue === 'Nearest') { if (endValue === EndValues.NEAREST) {
return Fit[endValue]; return Fit[endValue];
} }
if (endValue === 'Zero') { if (endValue === EndValues.ZERO) {
return 0; return 0;
} }
return undefined; return undefined;

View file

@ -6,6 +6,107 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { i18n } from '@kbn/i18n';
import { TriangleIcon, CircleIcon } from '../icons';
import { AvailableReferenceLineIcons } from '../../common/constants';
export function hasIcon(icon: string | undefined): icon is string { export function hasIcon(icon: string | undefined): icon is string {
return icon != null && icon !== 'empty'; return icon != null && icon !== 'empty';
} }
export const iconSet = [
{
value: AvailableReferenceLineIcons.EMPTY,
label: i18n.translate('expressionXY.xyChart.iconSelect.noIconLabel', {
defaultMessage: 'None',
}),
},
{
value: AvailableReferenceLineIcons.ASTERISK,
label: i18n.translate('expressionXY.xyChart.iconSelect.asteriskIconLabel', {
defaultMessage: 'Asterisk',
}),
},
{
value: AvailableReferenceLineIcons.ALERT,
label: i18n.translate('expressionXY.xyChart.iconSelect.alertIconLabel', {
defaultMessage: 'Alert',
}),
},
{
value: AvailableReferenceLineIcons.BELL,
label: i18n.translate('expressionXY.xyChart.iconSelect.bellIconLabel', {
defaultMessage: 'Bell',
}),
},
{
value: AvailableReferenceLineIcons.BOLT,
label: i18n.translate('expressionXY.xyChart.iconSelect.boltIconLabel', {
defaultMessage: 'Bolt',
}),
},
{
value: AvailableReferenceLineIcons.BUG,
label: i18n.translate('expressionXY.xyChart.iconSelect.bugIconLabel', {
defaultMessage: 'Bug',
}),
},
{
value: AvailableReferenceLineIcons.CIRCLE,
label: i18n.translate('expressionXY.xyChart.iconSelect.circleIconLabel', {
defaultMessage: 'Circle',
}),
icon: CircleIcon,
canFill: true,
},
{
value: AvailableReferenceLineIcons.EDITOR_COMMENT,
label: i18n.translate('expressionXY.xyChart.iconSelect.commentIconLabel', {
defaultMessage: 'Comment',
}),
},
{
value: AvailableReferenceLineIcons.FLAG,
label: i18n.translate('expressionXY.xyChart.iconSelect.flagIconLabel', {
defaultMessage: 'Flag',
}),
},
{
value: AvailableReferenceLineIcons.HEART,
label: i18n.translate('expressionXY.xyChart.iconSelect.heartLabel', {
defaultMessage: 'Heart',
}),
},
{
value: AvailableReferenceLineIcons.MAP_MARKER,
label: i18n.translate('expressionXY.xyChart.iconSelect.mapMarkerLabel', {
defaultMessage: 'Map Marker',
}),
},
{
value: AvailableReferenceLineIcons.PIN_FILLED,
label: i18n.translate('expressionXY.xyChart.iconSelect.mapPinLabel', {
defaultMessage: 'Map Pin',
}),
},
{
value: AvailableReferenceLineIcons.STAR_EMPTY,
label: i18n.translate('expressionXY.xyChart.iconSelect.starLabel', { defaultMessage: 'Star' }),
},
{
value: AvailableReferenceLineIcons.TAG,
label: i18n.translate('expressionXY.xyChart.iconSelect.tagIconLabel', {
defaultMessage: 'Tag',
}),
},
{
value: AvailableReferenceLineIcons.TRIANGLE,
label: i18n.translate('expressionXY.xyChart.iconSelect.triangleIconLabel', {
defaultMessage: 'Triangle',
}),
icon: TriangleIcon,
shouldRotate: true,
canFill: true,
},
];

View file

@ -14,5 +14,5 @@ export * from './fitting_functions';
export * from './axes_configuration'; export * from './axes_configuration';
export * from './icon'; export * from './icon';
export * from './color_assignment'; export * from './color_assignment';
export * from './annotations_icon_set';
export * from './annotations'; export * from './annotations';
export * from './data_layers';

View file

@ -6,32 +6,36 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { DataLayerConfigResult, XYChartProps } from '../../common'; import { DataLayerConfig, XYChartProps } from '../../common';
import { sampleArgs } from '../../common/__mocks__'; import { sampleArgs } from '../../common/__mocks__';
import { calculateMinInterval } from './interval'; import { calculateMinInterval } from './interval';
describe('calculateMinInterval', () => { describe('calculateMinInterval', () => {
let xyProps: XYChartProps; let xyProps: XYChartProps;
let layer: DataLayerConfig;
beforeEach(() => { beforeEach(() => {
xyProps = sampleArgs(); const { layers, ...restArgs } = sampleArgs().args;
(xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'time';
xyProps = { args: { ...restArgs, layers } };
layer = xyProps.args.layers[0] as DataLayerConfig;
layer.xScaleType = 'time';
}); });
it('should use first valid layer and determine interval', async () => { it('should use first valid layer and determine interval', async () => {
xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; layer.table.columns[2].meta.source = 'esaggs';
xyProps.data.tables.first.columns[2].meta.sourceParams = { layer.table.columns[2].meta.sourceParams = {
type: 'date_histogram', type: 'date_histogram',
params: { params: {
used_interval: '5m', used_interval: '5m',
}, },
}; };
xyProps.args.layers[0] = layer;
const result = await calculateMinInterval(xyProps); const result = await calculateMinInterval(xyProps);
expect(result).toEqual(5 * 60 * 1000); expect(result).toEqual(5 * 60 * 1000);
}); });
it('should return interval of number histogram if available on first x axis columns', async () => { it('should return interval of number histogram if available on first x axis columns', async () => {
(xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'linear'; layer.xScaleType = 'linear';
xyProps.data.tables.first.columns[2].meta = { layer.table.columns[2].meta = {
source: 'esaggs', source: 'esaggs',
type: 'number', type: 'number',
field: 'someField', field: 'someField',
@ -43,19 +47,22 @@ describe('calculateMinInterval', () => {
}, },
}, },
}; };
xyProps.args.layers[0] = layer;
const result = await calculateMinInterval(xyProps); const result = await calculateMinInterval(xyProps);
expect(result).toEqual(5); expect(result).toEqual(5);
}); });
it('should return undefined if data table is empty', async () => { it('should return undefined if data table is empty', async () => {
xyProps.data.tables.first.rows = []; layer.table.rows = [];
xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; layer.table.columns[2].meta.source = 'esaggs';
xyProps.data.tables.first.columns[2].meta.sourceParams = { layer.table.columns[2].meta.sourceParams = {
type: 'date_histogram', type: 'date_histogram',
params: { params: {
used_interval: '5m', used_interval: '5m',
}, },
}; };
xyProps.args.layers[0] = layer;
const result = await calculateMinInterval(xyProps); const result = await calculateMinInterval(xyProps);
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
}); });
@ -66,14 +73,16 @@ describe('calculateMinInterval', () => {
}); });
it('should return undefined if date column is not found', async () => { it('should return undefined if date column is not found', async () => {
xyProps.data.tables.first.columns.splice(2, 1); layer.table.columns.splice(2, 1);
xyProps.args.layers[0] = layer;
const result = await calculateMinInterval(xyProps); const result = await calculateMinInterval(xyProps);
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
}); });
it('should return undefined if x axis is not a date', async () => { it('should return undefined if x axis is not a date', async () => {
(xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'ordinal'; layer.xScaleType = 'ordinal';
xyProps.data.tables.first.columns.splice(2, 1); xyProps.args.layers[0] = layer;
xyProps.args.layers[0].table.columns.splice(2, 1);
const result = await calculateMinInterval(xyProps); const result = await calculateMinInterval(xyProps);
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
}); });

View file

@ -11,11 +11,11 @@ import { XYChartProps } from '../../common';
import { getFilteredLayers } from './layers'; import { getFilteredLayers } from './layers';
import { isDataLayer } from './visualization'; import { isDataLayer } from './visualization';
export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { export function calculateMinInterval({ args: { layers } }: XYChartProps) {
const filteredLayers = getFilteredLayers(layers, data); const filteredLayers = getFilteredLayers(layers);
if (filteredLayers.length === 0) return; if (filteredLayers.length === 0) return;
const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time'); const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time');
const xColumn = data.tables[filteredLayers[0].layerId].columns.find( const xColumn = filteredLayers[0].table.columns.find(
(column) => isDataLayer(filteredLayers[0]) && column.id === filteredLayers[0].xAccessor (column) => isDataLayer(filteredLayers[0]) && column.id === filteredLayers[0].xAccessor
); );

View file

@ -6,24 +6,42 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { LensMultiTable } from '../../common'; import { Datatable } from '@kbn/expressions-plugin/common';
import { DataLayerConfigResult, XYLayerConfigResult } from '../../common/types'; import {
import { getDataLayers } from './visualization'; CommonXYDataLayerConfig,
CommonXYLayerConfig,
CommonXYReferenceLineLayerConfig,
} from '../../common/types';
import { isDataLayer, isReferenceLayer } from './visualization';
export function getFilteredLayers(layers: CommonXYLayerConfig[]) {
return layers.filter<CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig>(
(layer): layer is CommonXYReferenceLineLayerConfig | CommonXYDataLayerConfig => {
let table: Datatable | undefined;
let accessors: string[] = [];
let xAccessor: undefined | string | number;
let splitAccessor: undefined | string | number;
if (isDataLayer(layer)) {
xAccessor = layer.xAccessor;
splitAccessor = layer.splitAccessor;
}
if (isDataLayer(layer) || isReferenceLayer(layer)) {
table = layer.table;
accessors = layer.accessors;
}
export function getFilteredLayers(layers: XYLayerConfigResult[], data: LensMultiTable) {
return getDataLayers(layers).filter<DataLayerConfigResult>(
(layer): layer is DataLayerConfigResult => {
const { layerId, xAccessor, accessors, splitAccessor } = layer;
return !( return !(
!accessors.length || !accessors.length ||
!data.tables[layerId] || !table ||
data.tables[layerId].rows.length === 0 || table.rows.length === 0 ||
(xAccessor && (xAccessor &&
data.tables[layerId].rows.every((row) => typeof row[xAccessor] === 'undefined')) || table.rows.every((row) => xAccessor && typeof row[xAccessor] === 'undefined')) ||
// stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty // stacked percentage bars have no xAccessors but splitAccessor with undefined values in them when empty
(!xAccessor && (!xAccessor &&
splitAccessor && splitAccessor &&
data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined')) table.rows.every((row) => splitAccessor && typeof row[splitAccessor] === 'undefined'))
); );
} }
); );

View file

@ -6,7 +6,7 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import type { SeriesType, XYLayerConfigResult, YConfig } from '../../common'; import type { CommonXYLayerConfig, SeriesType, ExtendedYConfig, YConfig } from '../../common';
import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization'; import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization';
export function isHorizontalSeries(seriesType: SeriesType) { export function isHorizontalSeries(seriesType: SeriesType) {
@ -21,16 +21,14 @@ export function isStackedChart(seriesType: SeriesType) {
return seriesType.includes('stacked'); return seriesType.includes('stacked');
} }
export function isHorizontalChart(layers: XYLayerConfigResult[]) { export function isHorizontalChart(layers: CommonXYLayerConfig[]) {
return getDataLayers(layers).every((l) => isHorizontalSeries(l.seriesType)); return getDataLayers(layers).every((l) => isHorizontalSeries(l.seriesType));
} }
export const getSeriesColor = (layer: XYLayerConfigResult, accessor: string) => { export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => {
if ((isDataLayer(layer) && layer.splitAccessor) || isAnnotationsLayer(layer)) { if ((isDataLayer(layer) && layer.splitAccessor) || isAnnotationsLayer(layer)) {
return null; return null;
} }
const yConfig: Array<YConfig | ExtendedYConfig> | undefined = layer?.yConfig;
return ( return yConfig?.find((yConf) => yConf.forAccessor === accessor)?.color || null;
layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null
);
}; };

View file

@ -6,38 +6,40 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import {
DataLayerConfigResult,
ReferenceLineLayerConfigResult,
XYLayerConfigResult,
AnnotationLayerConfigResult,
} from '../../common/types';
import { LayerTypes } from '../../common/constants'; import { LayerTypes } from '../../common/constants';
import {
CommonXYLayerConfig,
CommonXYDataLayerConfig,
CommonXYReferenceLineLayerConfig,
CommonXYAnnotationLayerConfig,
} from '../../common/types';
export const isDataLayer = (layer: XYLayerConfigResult): layer is DataLayerConfigResult => export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLayerConfig =>
layer.layerType === LayerTypes.DATA || !layer.layerType; layer.layerType === LayerTypes.DATA || !layer.layerType;
export const getDataLayers = (layers: XYLayerConfigResult[]) => export const getDataLayers = (layers: CommonXYLayerConfig[]) =>
(layers || []).filter((layer): layer is DataLayerConfigResult => isDataLayer(layer)); (layers || []).filter((layer): layer is CommonXYDataLayerConfig => isDataLayer(layer));
export const isReferenceLayer = ( export const isReferenceLayer = (
layer: XYLayerConfigResult layer: CommonXYLayerConfig
): layer is ReferenceLineLayerConfigResult => layer.layerType === LayerTypes.REFERENCELINE; ): layer is CommonXYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE;
export const getReferenceLayers = (layers: XYLayerConfigResult[]) => export const getReferenceLayers = (layers: CommonXYLayerConfig[]) =>
(layers || []).filter((layer): layer is ReferenceLineLayerConfigResult => (layers || []).filter((layer): layer is CommonXYReferenceLineLayerConfig =>
isReferenceLayer(layer) isReferenceLayer(layer)
); );
const isAnnotationLayerCommon = ( const isAnnotationLayerCommon = (
layer: XYLayerConfigResult layer: CommonXYLayerConfig
): layer is AnnotationLayerConfigResult => layer.layerType === LayerTypes.ANNOTATIONS; ): layer is CommonXYAnnotationLayerConfig => layer.layerType === LayerTypes.ANNOTATIONS;
export const isAnnotationsLayer = ( export const isAnnotationsLayer = (
layer: XYLayerConfigResult layer: CommonXYLayerConfig
): layer is AnnotationLayerConfigResult => isAnnotationLayerCommon(layer); ): layer is CommonXYAnnotationLayerConfig => isAnnotationLayerCommon(layer);
export const getAnnotationsLayers = ( export const getAnnotationsLayers = (
layers: XYLayerConfigResult[] layers: CommonXYLayerConfig[]
): AnnotationLayerConfigResult[] => ): CommonXYAnnotationLayerConfig[] =>
(layers || []).filter((layer): layer is AnnotationLayerConfigResult => isAnnotationsLayer(layer)); (layers || []).filter((layer): layer is CommonXYAnnotationLayerConfig =>
isAnnotationsLayer(layer)
);

View file

@ -16,17 +16,22 @@ import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public'
import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types'; import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types';
import { import {
xyVisFunction, xyVisFunction,
layeredXyVisFunction,
dataLayerFunction,
extendedDataLayerFunction,
yAxisConfigFunction, yAxisConfigFunction,
extendedYAxisConfigFunction,
legendConfigFunction, legendConfigFunction,
gridlinesConfigFunction, gridlinesConfigFunction,
dataLayerConfigFunction,
axisExtentConfigFunction, axisExtentConfigFunction,
tickLabelsConfigFunction, tickLabelsConfigFunction,
annotationLayerConfigFunction, referenceLineLayerFunction,
extendedReferenceLineLayerFunction,
annotationLayerFunction,
labelsOrientationConfigFunction, labelsOrientationConfigFunction,
referenceLineLayerConfigFunction,
axisTitlesVisibilityConfigFunction, axisTitlesVisibilityConfigFunction,
} from '../common'; extendedAnnotationLayerFunction,
} from '../common/expression_functions';
import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers'; import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers';
export interface XYPluginStartDependencies { export interface XYPluginStartDependencies {
@ -51,16 +56,21 @@ export class ExpressionXyPlugin {
{ expressions, charts }: SetupDeps { expressions, charts }: SetupDeps
): ExpressionXyPluginSetup { ): ExpressionXyPluginSetup {
expressions.registerFunction(yAxisConfigFunction); expressions.registerFunction(yAxisConfigFunction);
expressions.registerFunction(extendedYAxisConfigFunction);
expressions.registerFunction(legendConfigFunction); expressions.registerFunction(legendConfigFunction);
expressions.registerFunction(gridlinesConfigFunction); expressions.registerFunction(gridlinesConfigFunction);
expressions.registerFunction(dataLayerConfigFunction); expressions.registerFunction(dataLayerFunction);
expressions.registerFunction(extendedDataLayerFunction);
expressions.registerFunction(axisExtentConfigFunction); expressions.registerFunction(axisExtentConfigFunction);
expressions.registerFunction(tickLabelsConfigFunction); expressions.registerFunction(tickLabelsConfigFunction);
expressions.registerFunction(annotationLayerConfigFunction); expressions.registerFunction(annotationLayerFunction);
expressions.registerFunction(extendedAnnotationLayerFunction);
expressions.registerFunction(labelsOrientationConfigFunction); expressions.registerFunction(labelsOrientationConfigFunction);
expressions.registerFunction(referenceLineLayerConfigFunction); expressions.registerFunction(referenceLineLayerFunction);
expressions.registerFunction(extendedReferenceLineLayerFunction);
expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction);
expressions.registerFunction(xyVisFunction); expressions.registerFunction(xyVisFunction);
expressions.registerFunction(layeredXyVisFunction);
const getStartDeps: GetStartDepsFn = async () => { const getStartDeps: GetStartDepsFn = async () => {
const [coreStart, deps] = await core.getStartServices(); const [coreStart, deps] = await core.getStartServices();

View file

@ -12,16 +12,21 @@ import { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types';
import { import {
xyVisFunction, xyVisFunction,
yAxisConfigFunction, yAxisConfigFunction,
extendedYAxisConfigFunction,
legendConfigFunction, legendConfigFunction,
gridlinesConfigFunction, gridlinesConfigFunction,
dataLayerConfigFunction, dataLayerFunction,
axisExtentConfigFunction, axisExtentConfigFunction,
tickLabelsConfigFunction, tickLabelsConfigFunction,
annotationLayerConfigFunction, annotationLayerFunction,
labelsOrientationConfigFunction, labelsOrientationConfigFunction,
referenceLineLayerConfigFunction, referenceLineLayerFunction,
axisTitlesVisibilityConfigFunction, axisTitlesVisibilityConfigFunction,
} from '../common'; extendedDataLayerFunction,
extendedReferenceLineLayerFunction,
layeredXyVisFunction,
extendedAnnotationLayerFunction,
} from '../common/expression_functions';
import { SetupDeps } from './types'; import { SetupDeps } from './types';
export class ExpressionXyPlugin export class ExpressionXyPlugin
@ -29,16 +34,21 @@ export class ExpressionXyPlugin
{ {
public setup(core: CoreSetup, { expressions }: SetupDeps) { public setup(core: CoreSetup, { expressions }: SetupDeps) {
expressions.registerFunction(yAxisConfigFunction); expressions.registerFunction(yAxisConfigFunction);
expressions.registerFunction(extendedYAxisConfigFunction);
expressions.registerFunction(legendConfigFunction); expressions.registerFunction(legendConfigFunction);
expressions.registerFunction(gridlinesConfigFunction); expressions.registerFunction(gridlinesConfigFunction);
expressions.registerFunction(dataLayerConfigFunction); expressions.registerFunction(dataLayerFunction);
expressions.registerFunction(extendedDataLayerFunction);
expressions.registerFunction(axisExtentConfigFunction); expressions.registerFunction(axisExtentConfigFunction);
expressions.registerFunction(tickLabelsConfigFunction); expressions.registerFunction(tickLabelsConfigFunction);
expressions.registerFunction(annotationLayerConfigFunction); expressions.registerFunction(annotationLayerFunction);
expressions.registerFunction(extendedAnnotationLayerFunction);
expressions.registerFunction(labelsOrientationConfigFunction); expressions.registerFunction(labelsOrientationConfigFunction);
expressions.registerFunction(referenceLineLayerConfigFunction); expressions.registerFunction(referenceLineLayerFunction);
expressions.registerFunction(extendedReferenceLineLayerFunction);
expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction);
expressions.registerFunction(xyVisFunction); expressions.registerFunction(xyVisFunction);
expressions.registerFunction(layeredXyVisFunction);
} }
public start(core: CoreStart) {} public start(core: CoreStart) {}

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
export const AvailableAnnotationIcons = {
ASTERISK: 'asterisk',
ALERT: 'alert',
BELL: 'bell',
BOLT: 'bolt',
BUG: 'bug',
CIRCLE: 'circle',
EDITOR_COMMENT: 'editorComment',
FLAG: 'flag',
HEART: 'heart',
MAP_MARKER: 'mapMarker',
PIN_FILLED: 'pinFilled',
STAR_EMPTY: 'starEmpty',
TAG: 'tag',
TRIANGLE: 'triangle',
} as const;

View file

@ -17,4 +17,8 @@ export type {
export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation'; export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation';
export { eventAnnotationGroup } from './event_annotation_group'; export { eventAnnotationGroup } from './event_annotation_group';
export type { EventAnnotationGroupArgs } from './event_annotation_group'; export type { EventAnnotationGroupArgs } from './event_annotation_group';
export type { EventAnnotationConfig, RangeEventAnnotationConfig } from './types'; export type {
EventAnnotationConfig,
RangeEventAnnotationConfig,
AvailableAnnotationIcon,
} from './types';

View file

@ -8,6 +8,8 @@
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { AvailableAnnotationIcons } from '../constants';
import type { import type {
ManualRangeEventAnnotationArgs, ManualRangeEventAnnotationArgs,
ManualRangeEventAnnotationOutput, ManualRangeEventAnnotationOutput,
@ -65,6 +67,8 @@ export const manualPointEventAnnotation: ExpressionFunctionDefinition<
help: i18n.translate('eventAnnotation.manualAnnotation.args.icon', { help: i18n.translate('eventAnnotation.manualAnnotation.args.icon', {
defaultMessage: 'An optional icon used for annotation lines', defaultMessage: 'An optional icon used for annotation lines',
}), }),
options: [...Object.values(AvailableAnnotationIcons)],
strict: true,
}, },
textVisibility: { textVisibility: {
types: ['boolean'], types: ['boolean'],

View file

@ -6,15 +6,18 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { $Values } from '@kbn/utility-types';
import { AvailableAnnotationIcons } from './constants';
export type LineStyle = 'solid' | 'dashed' | 'dotted'; export type LineStyle = 'solid' | 'dashed' | 'dotted';
export type Fill = 'inside' | 'outside' | 'none'; export type Fill = 'inside' | 'outside' | 'none';
export type AnnotationType = 'manual'; export type AnnotationType = 'manual';
export type KeyType = 'point_in_time' | 'range'; export type KeyType = 'point_in_time' | 'range';
export type AvailableAnnotationIcon = $Values<typeof AvailableAnnotationIcons>;
export interface PointStyleProps { export interface PointStyleProps {
label: string; label: string;
color?: string; color?: string;
icon?: string; icon?: AvailableAnnotationIcon;
lineWidth?: number; lineWidth?: number;
lineStyle?: LineStyle; lineStyle?: LineStyle;
textVisibility?: boolean; textVisibility?: boolean;

View file

@ -714,7 +714,7 @@ describe('Execution', () => {
expect(result).toMatchObject({ expect(result).toMatchObject({
type: 'error', type: 'error',
error: { error: {
message: '[requiredArg] > requiredArg requires an argument', message: '[requiredArg] > requiredArg requires the "arg" argument',
}, },
}); });
}); });
@ -725,7 +725,7 @@ describe('Execution', () => {
expect(result).toMatchObject({ expect(result).toMatchObject({
type: 'error', type: 'error',
error: { error: {
message: '[var_set] > var_set requires an "name" argument', message: '[var_set] > var_set requires the "name" argument',
}, },
}); });
}); });

View file

@ -481,7 +481,7 @@ export class Execution<
); );
// Check for missing required arguments. // Check for missing required arguments.
for (const { aliases, default: argDefault, name, required } of Object.values(argDefs)) { for (const { default: argDefault, name, required } of Object.values(argDefs)) {
if (!(name in dealiasedArgAsts) && typeof argDefault !== 'undefined') { if (!(name in dealiasedArgAsts) && typeof argDefault !== 'undefined') {
dealiasedArgAsts[name] = [parse(argDefault as string, 'argument')]; dealiasedArgAsts[name] = [parse(argDefault as string, 'argument')];
} }
@ -490,13 +490,7 @@ export class Execution<
continue; continue;
} }
if (!aliases?.length) { throw new Error(`${fnDef.name} requires the "${name}" argument`);
throw new Error(`${fnDef.name} requires an argument`);
}
// use an alias if _ is the missing arg
const errorArg = name === '_' ? aliases[0] : name;
throw new Error(`${fnDef.name} requires an "${errorArg}" argument`);
} }
// Create the functions to resolve the argument ASTs into values // Create the functions to resolve the argument ASTs into values

View file

@ -57,8 +57,7 @@ export type CustomPaletteParamsConfig = CustomPaletteParams & {
export type LayerType = typeof layerTypes[keyof typeof layerTypes]; export type LayerType = typeof layerTypes[keyof typeof layerTypes];
// Shared by XY Chart and Heatmap as for now export type ValueLabelConfig = 'hide' | 'show';
export type ValueLabelConfig = 'hide' | 'inside' | 'outside';
export type PieChartType = $Values<typeof PieChartTypes>; export type PieChartType = $Values<typeof PieChartTypes>;
export type CategoryDisplayType = $Values<typeof CategoryDisplay>; export type CategoryDisplayType = $Values<typeof CategoryDisplay>;

View file

@ -145,8 +145,6 @@ export function LayerPanel(
const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); const isEmptyLayer = !groups.some((d) => d.accessors.length > 0);
const { activeId, activeGroup } = activeDimension; const { activeId, activeGroup } = activeDimension;
const { setDimension, removeDimension } = activeVisualization;
const allAccessors = groups.flatMap((group) => const allAccessors = groups.flatMap((group) =>
group.accessors.map((accessor) => accessor.columnId) group.accessors.map((accessor) => accessor.columnId)
); );
@ -209,7 +207,7 @@ export function LayerPanel(
previousColumn = typeof dropResult === 'object' ? dropResult.deleted : undefined; previousColumn = typeof dropResult === 'object' ? dropResult.deleted : undefined;
} }
} }
const newVisState = setDimension({ const newVisState = activeVisualization.setDimension({
columnId, columnId,
groupId, groupId,
layerId: targetLayerId, layerId: targetLayerId,
@ -221,7 +219,7 @@ export function LayerPanel(
if (typeof dropResult === 'object') { if (typeof dropResult === 'object') {
// When a column is moved, we delete the reference to the old // When a column is moved, we delete the reference to the old
updateVisualization( updateVisualization(
removeDimension({ activeVisualization.removeDimension({
columnId: dropResult.deleted, columnId: dropResult.deleted,
layerId: targetLayerId, layerId: targetLayerId,
prevState: newVisState, prevState: newVisState,
@ -234,7 +232,7 @@ export function LayerPanel(
} }
} else { } else {
if (dropType === 'duplicate_compatible' || dropType === 'reorder') { if (dropType === 'duplicate_compatible' || dropType === 'reorder') {
const newVisState = setDimension({ const newVisState = activeVisualization.setDimension({
columnId, columnId,
groupId, groupId,
layerId: targetLayerId, layerId: targetLayerId,
@ -247,16 +245,15 @@ export function LayerPanel(
} }
}; };
}, [ }, [
framePublicAPI, layerDatasource,
setNextFocusedButtonId,
groups, groups,
layerDatasourceOnDrop, layerDatasourceOnDrop,
props.visualizationState,
updateVisualization,
setDimension,
removeDimension,
layerDatasourceDropProps, layerDatasourceDropProps,
setNextFocusedButtonId, activeVisualization,
layerDatasource, props.visualizationState,
framePublicAPI,
updateVisualization,
]); ]);
const isDimensionPanelOpen = Boolean(activeId); const isDimensionPanelOpen = Boolean(activeId);

View file

@ -9,11 +9,10 @@ import { Ast, AstFunction, fromExpression } from '@kbn/interpreter';
import { DatasourceStates } from '../../state_management'; import { DatasourceStates } from '../../state_management';
import { Visualization, DatasourceMap, DatasourceLayers } from '../../types'; import { Visualization, DatasourceMap, DatasourceLayers } from '../../types';
export function prependDatasourceExpression( export function getDatasourceExpressionsByLayers(
visualizationExpression: Ast | string | null,
datasourceMap: DatasourceMap, datasourceMap: DatasourceMap,
datasourceStates: DatasourceStates datasourceStates: DatasourceStates
): Ast | null { ): null | Record<string, Ast> {
const datasourceExpressions: Array<[string, Ast | string]> = []; const datasourceExpressions: Array<[string, Ast | string]> = [];
Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => { Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => {
@ -28,19 +27,41 @@ export function prependDatasourceExpression(
}); });
}); });
if (datasourceExpressions.length === 0 || visualizationExpression === null) { if (datasourceExpressions.length === 0) {
return null; return null;
} }
const parsedDatasourceExpressions: Array<[string, Ast]> = datasourceExpressions.map(
([layerId, expr]) => [layerId, typeof expr === 'string' ? fromExpression(expr) : expr] return datasourceExpressions.reduce(
(exprs, [layerId, expr]) => ({
...exprs,
[layerId]: typeof expr === 'string' ? fromExpression(expr) : expr,
}),
{}
); );
}
export function prependDatasourceExpression(
visualizationExpression: Ast | string | null,
datasourceMap: DatasourceMap,
datasourceStates: DatasourceStates
): Ast | null {
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
datasourceMap,
datasourceStates
);
if (datasourceExpressionsByLayers === null || visualizationExpression === null) {
return null;
}
const parsedDatasourceExpressions = Object.entries(datasourceExpressionsByLayers);
const datafetchExpression: AstFunction = { const datafetchExpression: AstFunction = {
type: 'function', type: 'function',
function: 'lens_merge_tables', function: 'lens_merge_tables',
arguments: { arguments: {
layerIds: parsedDatasourceExpressions.map(([id]) => id), layerIds: parsedDatasourceExpressions.map(([id]) => id),
tables: parsedDatasourceExpressions.map(([id, expr]) => expr), tables: parsedDatasourceExpressions.map(([, expr]) => expr),
}, },
}; };
@ -79,16 +100,32 @@ export function buildExpression({
if (visualization === null) { if (visualization === null) {
return null; return null;
} }
if (visualization.shouldBuildDatasourceExpressionManually?.()) {
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
datasourceMap,
datasourceStates
);
const visualizationExpression = visualization.toExpression(
visualizationState,
datasourceLayers,
{
title,
description,
},
datasourceExpressionsByLayers ?? undefined
);
return typeof visualizationExpression === 'string'
? fromExpression(visualizationExpression)
: visualizationExpression;
}
const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers, { const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers, {
title, title,
description, description,
}); });
const completeExpression = prependDatasourceExpression( return prependDatasourceExpression(visualizationExpression, datasourceMap, datasourceStates);
visualizationExpression,
datasourceMap,
datasourceStates
);
return completeExpression;
} }

View file

@ -22,7 +22,7 @@ import {
EuiText, EuiText,
} from '@elastic/eui'; } from '@elastic/eui';
import { IconType } from '@elastic/eui/src/components/icon/icon'; import { IconType } from '@elastic/eui/src/components/icon/icon';
import { Ast, toExpression } from '@kbn/interpreter'; import { Ast, fromExpression, toExpression } from '@kbn/interpreter';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import classNames from 'classnames'; import classNames from 'classnames';
import { ExecutionContextSearch } from '@kbn/data-plugin/public'; import { ExecutionContextSearch } from '@kbn/data-plugin/public';
@ -39,7 +39,10 @@ import {
DatasourceLayers, DatasourceLayers,
} from '../../types'; } from '../../types';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
import { prependDatasourceExpression } from './expression_helpers'; import {
getDatasourceExpressionsByLayers,
prependDatasourceExpression,
} from './expression_helpers';
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
import { import {
getMissingIndexPattern, getMissingIndexPattern,
@ -485,6 +488,7 @@ function getPreviewExpression(
visualizableState: VisualizableState, visualizableState: VisualizableState,
visualization: Visualization, visualization: Visualization,
datasources: Record<string, Datasource>, datasources: Record<string, Datasource>,
datasourceStates: DatasourceStates,
frame: FramePublicAPI frame: FramePublicAPI
) { ) {
if (!visualization.toPreviewExpression) { if (!visualization.toPreviewExpression) {
@ -518,6 +522,19 @@ function getPreviewExpression(
}); });
} }
if (visualization.shouldBuildDatasourceExpressionManually?.()) {
const datasourceExpressionsByLayers = getDatasourceExpressionsByLayers(
datasources,
datasourceStates
);
return visualization.toPreviewExpression(
visualizableState.visualizationState,
suggestionFrameApi.datasourceLayers,
datasourceExpressionsByLayers ?? undefined
);
}
return visualization.toPreviewExpression( return visualization.toPreviewExpression(
visualizableState.visualizationState, visualizableState.visualizationState,
suggestionFrameApi.datasourceLayers suggestionFrameApi.datasourceLayers
@ -534,21 +551,7 @@ function preparePreviewExpression(
const suggestionDatasourceId = visualizableState.datasourceId; const suggestionDatasourceId = visualizableState.datasourceId;
const suggestionDatasourceState = visualizableState.datasourceState; const suggestionDatasourceState = visualizableState.datasourceState;
const expression = getPreviewExpression( const datasourceStatesWithSuggestions = suggestionDatasourceId
visualizableState,
visualization,
datasourceMap,
framePublicAPI
);
if (!expression) {
return;
}
const expressionWithDatasource = prependDatasourceExpression(
expression,
datasourceMap,
suggestionDatasourceId
? { ? {
...datasourceStates, ...datasourceStates,
[suggestionDatasourceId]: { [suggestionDatasourceId]: {
@ -556,8 +559,27 @@ function preparePreviewExpression(
state: suggestionDatasourceState, state: suggestionDatasourceState,
}, },
} }
: datasourceStates : datasourceStates;
const previewExprDatasourcesStates = visualization.shouldBuildDatasourceExpressionManually?.()
? datasourceStatesWithSuggestions
: datasourceStates;
const expression = getPreviewExpression(
visualizableState,
visualization,
datasourceMap,
previewExprDatasourcesStates,
framePublicAPI
); );
return expressionWithDatasource; if (!expression) {
return;
}
if (visualization.shouldBuildDatasourceExpressionManually?.()) {
return typeof expression === 'string' ? fromExpression(expression) : expression;
}
return prependDatasourceExpression(expression, datasourceMap, datasourceStatesWithSuggestions);
} }

View file

@ -348,6 +348,7 @@ export class Embeddable
if (!this.savedVis || !this.savedVis.visualizationType) { if (!this.savedVis || !this.savedVis.visualizationType) {
return []; return [];
} }
return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || []; return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || [];
} }
@ -458,6 +459,7 @@ export class Embeddable
this.embeddableTitle = this.getTitle(); this.embeddableTitle = this.getTitle();
isDirty = true; isDirty = true;
} }
return isDirty; return isDirty;
} }

View file

@ -62,11 +62,11 @@ export const HeatmapToolbar = memo(
buttonDataTestSubj="lnsVisualOptionsButton" buttonDataTestSubj="lnsVisualOptionsButton"
> >
<ValueLabelsSettings <ValueLabelsSettings
valueLabels={state?.gridConfig.isCellLabelVisible ? 'inside' : 'hide'} valueLabels={state?.gridConfig.isCellLabelVisible ? 'show' : 'hide'}
onValueLabelChange={(newMode) => { onValueLabelChange={(newMode) => {
setState({ setState({
...state, ...state,
gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'inside' }, gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'show' },
}); });
}} }}
/> />

View file

@ -15,6 +15,7 @@ export type {
XYState, XYState,
XYReferenceLineLayerConfig, XYReferenceLineLayerConfig,
XYLayerConfig, XYLayerConfig,
ValidLayer,
XYDataLayerConfig, XYDataLayerConfig,
XYAnnotationLayerConfig, XYAnnotationLayerConfig,
} from './xy_visualization/types'; } from './xy_visualization/types';
@ -70,7 +71,7 @@ export type {
} from './indexpattern_datasource/types'; } from './indexpattern_datasource/types';
export type { export type {
XYArgs, XYArgs,
YConfig, ExtendedYConfig,
XYRender, XYRender,
LayerType, LayerType,
YAxisMode, YAxisMode,
@ -80,28 +81,27 @@ export type {
YScaleType, YScaleType,
XScaleType, XScaleType,
AxisConfig, AxisConfig,
ValidLayer,
XYCurveType, XYCurveType,
XYChartProps, XYChartProps,
LegendConfig, LegendConfig,
IconPosition, IconPosition,
YConfigResult, ExtendedYConfigResult,
DataLayerArgs, DataLayerArgs,
LensMultiTable, LensMultiTable,
ValueLabelMode, ValueLabelMode,
AxisExtentMode, AxisExtentMode,
DataLayerConfig,
FittingFunction, FittingFunction,
AxisExtentConfig, AxisExtentConfig,
LegendConfigResult, LegendConfigResult,
AxesSettingsConfig, AxesSettingsConfig,
GridlinesConfigResult, GridlinesConfigResult,
DataLayerConfigResult,
TickLabelsConfigResult, TickLabelsConfigResult,
AxisExtentConfigResult, AxisExtentConfigResult,
ReferenceLineLayerArgs, ReferenceLineLayerArgs,
LabelsOrientationConfig, LabelsOrientationConfig,
ReferenceLineLayerConfig,
LabelsOrientationConfigResult, LabelsOrientationConfigResult,
ReferenceLineLayerConfigResult,
AxisTitlesVisibilityConfigResult, AxisTitlesVisibilityConfigResult,
} from '@kbn/expression-xy-plugin/common'; } from '@kbn/expression-xy-plugin/common';
export type { LensEmbeddableInput } from './embeddable'; export type { LensEmbeddableInput } from './embeddable';

View file

@ -235,6 +235,7 @@ export function getIndexPatternDatasource({
if (staticValue == null) { if (staticValue == null) {
return state; return state;
} }
return mergeLayer({ return mergeLayer({
state, state,
layerId, layerId,

View file

@ -30,11 +30,11 @@ export interface LegendLocationSettingsProps {
/** /**
* Sets the vertical alignment for legend inside chart * Sets the vertical alignment for legend inside chart
*/ */
verticalAlignment?: VerticalAlignment; verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom;
/** /**
* Sets the vertical alignment for legend inside chart * Sets the vertical alignment for legend inside chart
*/ */
horizontalAlignment?: HorizontalAlignment; horizontalAlignment?: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right;
/** /**
* Callback on horizontal alignment option change * Callback on horizontal alignment option change
*/ */

View file

@ -58,11 +58,11 @@ export interface LegendSettingsPopoverProps {
/** /**
* Sets the vertical alignment for legend inside chart * Sets the vertical alignment for legend inside chart
*/ */
verticalAlignment?: VerticalAlignment; verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom;
/** /**
* Sets the vertical alignment for legend inside chart * Sets the vertical alignment for legend inside chart
*/ */
horizontalAlignment?: HorizontalAlignment; horizontalAlignment?: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right;
/** /**
* Callback on horizontal alignment option change * Callback on horizontal alignment option change
*/ */
@ -225,6 +225,7 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
position={position} position={position}
onPositionChange={onPositionChange} onPositionChange={onPositionChange}
/> />
{location !== 'inside' && (
<LegendSizeSettings <LegendSizeSettings
legendSize={legendSize} legendSize={legendSize}
onLegendSizeChange={onLegendSizeChange} onLegendSizeChange={onLegendSizeChange}
@ -232,6 +233,7 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
!position || position === Position.Left || position === Position.Right !position || position === Position.Left || position === Position.Right
} }
/> />
)}
{location && ( {location && (
<ColumnsNumberSetting <ColumnsNumberSetting
floatingColumns={floatingColumns} floatingColumns={floatingColumns}

View file

@ -38,7 +38,7 @@ describe('Value labels Settings', () => {
}); });
it('should render the passed value if given', () => { it('should render the passed value if given', () => {
const component = shallow(<ValueLabelsSettings {...props} valueLabels="inside" />); const component = shallow(<ValueLabelsSettings {...props} valueLabels="show" />);
expect( expect(
component.find('[data-test-subj="lens-value-labels-visibility-btn"]').prop('idSelected') component.find('[data-test-subj="lens-value-labels-visibility-btn"]').prop('idSelected')
).toEqual(`value_labels_inside`); ).toEqual(`value_labels_inside`);

View file

@ -26,7 +26,7 @@ const valueLabelsOptions: Array<{
}, },
{ {
id: `value_labels_inside`, id: `value_labels_inside`,
value: 'inside', value: 'show',
label: i18n.translate('xpack.lens.shared.valueLabelsVisibility.inside', { label: i18n.translate('xpack.lens.shared.valueLabelsVisibility.inside', {
defaultMessage: 'Show', defaultMessage: 'Show',
}), }),

View file

@ -588,7 +588,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
labels?: { buttonAriaLabel: string; buttonLabel: string }; labels?: { buttonAriaLabel: string; buttonLabel: string };
}; };
interface VisualizationDimensionChangeProps<T> { export interface VisualizationDimensionChangeProps<T> {
layerId: string; layerId: string;
columnId: string; columnId: string;
prevState: T; prevState: T;
@ -887,7 +887,8 @@ export interface Visualization<T = unknown> {
toExpression: ( toExpression: (
state: T, state: T,
datasourceLayers: DatasourceLayers, datasourceLayers: DatasourceLayers,
attributes?: Partial<{ title: string; description: string }> attributes?: Partial<{ title: string; description: string }>,
datasourceExpressionsByLayers?: Record<string, Ast>
) => ExpressionAstExpression | string | null; ) => ExpressionAstExpression | string | null;
/** /**
* Expression to render a preview version of the chart in very constrained space. * Expression to render a preview version of the chart in very constrained space.
@ -895,7 +896,8 @@ export interface Visualization<T = unknown> {
*/ */
toPreviewExpression?: ( toPreviewExpression?: (
state: T, state: T,
datasourceLayers: DatasourceLayers datasourceLayers: DatasourceLayers,
datasourceExpressionsByLayers?: Record<string, Ast>
) => ExpressionAstExpression | string | null; ) => ExpressionAstExpression | string | null;
/** /**
* The frame will call this function on all visualizations at few stages (pre-build/build error) in order * The frame will call this function on all visualizations at few stages (pre-build/build error) in order
@ -920,6 +922,12 @@ export interface Visualization<T = unknown> {
* On Edit events the frame will call this to know what's going to be the next visualization state * On Edit events the frame will call this to know what's going to be the next visualization state
*/ */
onEditAction?: (state: T, event: LensEditEvent<LensEditSupportedActions>) => T; onEditAction?: (state: T, event: LensEditEvent<LensEditSupportedActions>) => T;
/**
* `datasourceExpressionsByLayers` will be passed to the params of `toExpression` and `toPreviewExpression`
* functions and datasource expressions will not be appended to the expression automatically.
*/
shouldBuildDatasourceExpressionManually?: () => boolean;
} }
export interface LensFilterEvent { export interface LensFilterEvent {

View file

@ -30,9 +30,6 @@ Object {
"curveType": Array [ "curveType": Array [
"LINEAR", "LINEAR",
], ],
"description": Array [
"",
],
"emphasizeFitting": Array [ "emphasizeFitting": Array [
true, true,
], ],
@ -135,6 +132,18 @@ Object {
"splitAccessor": Array [ "splitAccessor": Array [
"d", "d",
], ],
"table": Array [
Object {
"chain": Array [
Object {
"arguments": Object {},
"function": "kibana",
"type": "function",
},
],
"type": "expression",
},
],
"xAccessor": Array [ "xAccessor": Array [
"a", "a",
], ],
@ -146,7 +155,7 @@ Object {
"linear", "linear",
], ],
}, },
"function": "dataLayer", "function": "extendedDataLayer",
"type": "function", "type": "function",
}, },
], ],
@ -204,9 +213,6 @@ Object {
"type": "expression", "type": "expression",
}, },
], ],
"title": Array [
"",
],
"valueLabels": Array [ "valueLabels": Array [
"hide", "hide",
], ],
@ -263,7 +269,7 @@ Object {
"", "",
], ],
}, },
"function": "xyVis", "function": "layeredXyVis",
"type": "function", "type": "function",
}, },
], ],

View file

@ -135,6 +135,7 @@ export const setAnnotationsDimension: Visualization<XYState>['setDimension'] = (
: undefined; : undefined;
let resultAnnotations = [...inputAnnotations] as XYAnnotationLayerConfig['annotations']; let resultAnnotations = [...inputAnnotations] as XYAnnotationLayerConfig['annotations'];
if (!currentConfig) { if (!currentConfig) {
resultAnnotations.push({ resultAnnotations.push({
label: defaultAnnotationLabel, label: defaultAnnotationLabel,

View file

@ -5,10 +5,10 @@
* 2.0. * 2.0.
*/ */
import { DataLayerConfigResult } from '@kbn/expression-xy-plugin/common';
import { layerTypes } from '../../common'; import { layerTypes } from '../../common';
import { Datatable } from '@kbn/expressions-plugin/public'; import { Datatable } from '@kbn/expressions-plugin/public';
import { getAxesConfiguration } from './axes_configuration'; import { getAxesConfiguration } from './axes_configuration';
import { XYDataLayerConfig } from './types';
describe('axes_configuration', () => { describe('axes_configuration', () => {
const tables: Record<string, Datatable> = { const tables: Record<string, Datatable> = {
@ -219,8 +219,7 @@ describe('axes_configuration', () => {
}, },
}; };
const sampleLayer: DataLayerConfigResult = { const sampleLayer: XYDataLayerConfig = {
type: 'dataLayer',
layerId: 'first', layerId: 'first',
layerType: layerTypes.DATA, layerType: layerTypes.DATA,
seriesType: 'line', seriesType: 'line',

View file

@ -7,7 +7,7 @@
import { groupBy, partition } from 'lodash'; import { groupBy, partition } from 'lodash';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { YAxisMode, YConfig } from '@kbn/expression-xy-plugin/common'; import type { YAxisMode, ExtendedYConfig } from '@kbn/expression-xy-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/public'; import { Datatable } from '@kbn/expressions-plugin/public';
import { layerTypes } from '../../common'; import { layerTypes } from '../../common';
import type { DatasourceLayers, FramePublicAPI, Visualization } from '../types'; import type { DatasourceLayers, FramePublicAPI, Visualization } from '../types';
@ -34,7 +34,7 @@ export interface ReferenceLineBase {
* * what groups are current defined in data layers * * what groups are current defined in data layers
* * what existing reference line are currently defined in reference layers * * what existing reference line are currently defined in reference layers
*/ */
export function getGroupsToShow<T extends ReferenceLineBase & { config?: YConfig[] }>( export function getGroupsToShow<T extends ReferenceLineBase & { config?: ExtendedYConfig[] }>(
referenceLayers: T[], referenceLayers: T[],
state: XYState | undefined, state: XYState | undefined,
datasourceLayers: DatasourceLayers, datasourceLayers: DatasourceLayers,
@ -104,6 +104,7 @@ export function getStaticValue(
untouchedDataLayers, untouchedDataLayers,
accessors, accessors,
} = getAccessorCriteriaForGroup(groupId, dataLayers, activeData); } = getAccessorCriteriaForGroup(groupId, dataLayers, activeData);
if ( if (
groupId === 'x' && groupId === 'x' &&
filteredLayers.length && filteredLayers.length &&
@ -111,6 +112,7 @@ export function getStaticValue(
) { ) {
return fallbackValue; return fallbackValue;
} }
const computedValue = computeStaticValueForGroup( const computedValue = computeStaticValueForGroup(
filteredLayers, filteredLayers,
accessors, accessors,
@ -118,6 +120,7 @@ export function getStaticValue(
groupId !== 'x', // histogram axis should compute the min based on the current data groupId !== 'x', // histogram axis should compute the min based on the current data
groupId !== 'x' groupId !== 'x'
); );
return computedValue ?? fallbackValue; return computedValue ?? fallbackValue;
} }
@ -165,6 +168,7 @@ export function computeOverallDataDomain(
const accessorMap = new Set(accessorIds); const accessorMap = new Set(accessorIds);
let min: number | undefined; let min: number | undefined;
let max: number | undefined; let max: number | undefined;
const [stacked, unstacked] = partition( const [stacked, unstacked] = partition(
dataLayers, dataLayers,
({ seriesType }) => isStackedChart(seriesType) && allowStacking ({ seriesType }) => isStackedChart(seriesType) && allowStacking
@ -268,13 +272,17 @@ export const getReferenceSupportedLayer = (
label: 'x' as const, label: 'x' as const,
}, },
]; ];
const referenceLineGroups = getGroupsRelatedToData( const referenceLineGroups = getGroupsRelatedToData(
referenceLineGroupIds, referenceLineGroupIds,
state, state,
frame?.datasourceLayers || {}, frame?.datasourceLayers || {},
frame?.activeData frame?.activeData
); );
const dataLayers = getDataLayers(state?.layers || []);
const layers = state?.layers || [];
const dataLayers = getDataLayers(layers);
const filledDataLayers = dataLayers.filter( const filledDataLayers = dataLayers.filter(
({ accessors, xAccessor }) => accessors.length || xAccessor ({ accessors, xAccessor }) => accessors.length || xAccessor
); );
@ -289,7 +297,7 @@ export const getReferenceSupportedLayer = (
groupId: id, groupId: id,
columnId: generateId(), columnId: generateId(),
dataType: 'number', dataType: 'number',
label: getAxisName(label, { isHorizontal: isHorizontalChart(state?.layers || []) }), label: getAxisName(label, { isHorizontal: isHorizontalChart(layers) }),
staticValue: getStaticValue( staticValue: getStaticValue(
dataLayers, dataLayers,
label, label,
@ -317,6 +325,7 @@ export const getReferenceSupportedLayer = (
initialDimensions, initialDimensions,
}; };
}; };
export const setReferenceDimension: Visualization<XYState>['setDimension'] = ({ export const setReferenceDimension: Visualization<XYState>['setDimension'] = ({
prevState, prevState,
layerId, layerId,
@ -397,6 +406,7 @@ export const getReferenceConfiguration = ({
return axisMode; return axisMode;
} }
); );
const groupsToShow = getGroupsToShow( const groupsToShow = getGroupsToShow(
[ [
// When a reference layer panel is added, a static reference line should automatically be included by default // When a reference layer panel is added, a static reference line should automatically be included by default
@ -422,7 +432,7 @@ export const getReferenceConfiguration = ({
], ],
state, state,
frame.datasourceLayers, frame.datasourceLayers,
frame?.activeData frame.activeData
); );
const isHorizontal = isHorizontalChart(state.layers); const isHorizontal = isHorizontalChart(state.layers);
return { return {

View file

@ -6,7 +6,7 @@
*/ */
import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
import type { SeriesType, YConfig, ValidLayer } from '@kbn/expression-xy-plugin/common'; import type { SeriesType, ExtendedYConfig } from '@kbn/expression-xy-plugin/common';
import type { FramePublicAPI, DatasourcePublicAPI } from '../types'; import type { FramePublicAPI, DatasourcePublicAPI } from '../types';
import { import {
visualizationTypes, visualizationTypes,
@ -58,7 +58,8 @@ export const getSeriesColor = (layer: XYLayerConfig, accessor: string) => {
return null; return null;
} }
return ( return (
layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null layer?.yConfig?.find((yConfig: ExtendedYConfig) => yConfig.forAccessor === accessor)?.color ||
null
); );
}; };
@ -79,7 +80,7 @@ export const getColumnToLabelMap = (
}; };
export function hasHistogramSeries( export function hasHistogramSeries(
layers: ValidLayer[] = [], layers: XYDataLayerConfig[] = [],
datasourceLayers?: FramePublicAPI['datasourceLayers'] datasourceLayers?: FramePublicAPI['datasourceLayers']
) { ) {
if (!datasourceLayers) { if (!datasourceLayers) {
@ -87,7 +88,11 @@ export function hasHistogramSeries(
} }
const validLayers = layers.filter(({ accessors }) => accessors.length); const validLayers = layers.filter(({ accessors }) => accessors.length);
return validLayers.some(({ layerId, xAccessor }: ValidLayer) => { return validLayers.some(({ layerId, xAccessor }: XYDataLayerConfig) => {
if (!xAccessor) {
return false;
}
const xAxisOperation = datasourceLayers[layerId].getOperationForColumnId(xAccessor); const xAxisOperation = datasourceLayers[layerId].getOperationForColumnId(xAccessor);
return ( return (
xAxisOperation && xAxisOperation &&

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { Ast } from '@kbn/interpreter'; import { Ast, fromExpression } from '@kbn/interpreter';
import { Position } from '@elastic/charts'; import { Position } from '@elastic/charts';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { getXyVisualization } from './xy_visualization'; import { getXyVisualization } from './xy_visualization';
@ -28,6 +28,8 @@ describe('#toExpression', () => {
let mockDatasource: ReturnType<typeof createMockDatasource>; let mockDatasource: ReturnType<typeof createMockDatasource>;
let frame: ReturnType<typeof createMockFramePublicAPI>; let frame: ReturnType<typeof createMockFramePublicAPI>;
let datasourceExpressionsByLayers: Record<string, Ast>;
beforeEach(() => { beforeEach(() => {
frame = createMockFramePublicAPI(); frame = createMockFramePublicAPI();
mockDatasource = createMockDatasource('testDatasource'); mockDatasource = createMockDatasource('testDatasource');
@ -46,6 +48,23 @@ describe('#toExpression', () => {
frame.datasourceLayers = { frame.datasourceLayers = {
first: mockDatasource.publicAPIMock, first: mockDatasource.publicAPIMock,
}; };
const datasourceExpression = mockDatasource.toExpression(
frame.datasourceLayers.first,
'first'
) ?? {
type: 'expression',
chain: [],
};
const exprAst =
typeof datasourceExpression === 'string'
? fromExpression(datasourceExpression)
: datasourceExpression;
datasourceExpressionsByLayers = {
first: exprAst,
referenceLine: exprAst,
};
}); });
it('should map to a valid AST', () => { it('should map to a valid AST', () => {
@ -82,7 +101,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) )
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
@ -106,7 +127,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast ) as Ast
).chain[0].arguments.fittingFunction[0] ).chain[0].arguments.fittingFunction[0]
).toEqual('None'); ).toEqual('None');
@ -129,7 +152,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
expect( expect(
(expression.chain[0].arguments.axisTitlesVisibilitySettings[0] as Ast).chain[0].arguments (expression.chain[0].arguments.axisTitlesVisibilitySettings[0] as Ast).chain[0].arguments
@ -157,7 +182,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
expect((expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.xAccessor).toEqual( expect((expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.xAccessor).toEqual(
[] []
@ -182,7 +209,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) )
).toBeNull(); ).toBeNull();
}); });
@ -204,7 +233,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
)! as Ast; )! as Ast;
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b');
@ -241,7 +272,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
expect( expect(
(expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments (expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments
@ -269,7 +302,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
expect((expression.chain[0].arguments.labelsOrientation[0] as Ast).chain[0].arguments).toEqual({ expect((expression.chain[0].arguments.labelsOrientation[0] as Ast).chain[0].arguments).toEqual({
x: [0], x: [0],
@ -295,7 +330,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
expect( expect(
(expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments
@ -310,7 +347,7 @@ describe('#toExpression', () => {
const expression = xyVisualization.toExpression( const expression = xyVisualization.toExpression(
{ {
legend: { position: Position.Bottom, isVisible: true }, legend: { position: Position.Bottom, isVisible: true },
valueLabels: 'inside', valueLabels: 'show',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
layers: [ layers: [
{ {
@ -323,16 +360,18 @@ describe('#toExpression', () => {
}, },
], ],
}, },
frame.datasourceLayers frame.datasourceLayers,
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('inside'); expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('show');
}); });
it('should compute the correct series color fallback based on the layer type', () => { it('should compute the correct series color fallback based on the layer type', () => {
const expression = xyVisualization.toExpression( const expression = xyVisualization.toExpression(
{ {
legend: { position: Position.Bottom, isVisible: true }, legend: { position: Position.Bottom, isVisible: true },
valueLabels: 'inside', valueLabels: 'show',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
layers: [ layers: [
{ {
@ -352,7 +391,9 @@ describe('#toExpression', () => {
}, },
], ],
}, },
{ ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock } { ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock },
undefined,
datasourceExpressionsByLayers
) as Ast; ) as Ast;
function getYConfigColorForLayer(ast: Ast, index: number) { function getYConfigColorForLayer(ast: Ast, index: number) {

View file

@ -10,13 +10,15 @@ import { ScaleType } from '@elastic/charts';
import type { PaletteRegistry } from '@kbn/coloring'; import type { PaletteRegistry } from '@kbn/coloring';
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import type { ValidLayer, YConfig } from '@kbn/expression-xy-plugin/common'; import type { AxisExtentConfig, ExtendedYConfig, YConfig } from '@kbn/expression-xy-plugin/common';
import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
import { import {
State, State,
XYDataLayerConfig, XYDataLayerConfig,
XYReferenceLineLayerConfig, XYReferenceLineLayerConfig,
XYAnnotationLayerConfig, XYAnnotationLayerConfig,
} from './types'; } from './types';
import type { ValidXYDataLayerConfig } from './types';
import { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../types'; import { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../types';
import { getColumnToLabelMap } from './state_helpers'; import { getColumnToLabelMap } from './state_helpers';
import { hasIcon } from './xy_config_panel/shared/icon_select'; import { hasIcon } from './xy_config_panel/shared/icon_select';
@ -50,6 +52,7 @@ export const toExpression = (
datasourceLayers: DatasourceLayers, datasourceLayers: DatasourceLayers,
paletteService: PaletteRegistry, paletteService: PaletteRegistry,
attributes: Partial<{ title: string; description: string }> = {}, attributes: Partial<{ title: string; description: string }> = {},
datasourceExpressionsByLayers: Record<string, Ast>,
eventAnnotationService: EventAnnotationServiceType eventAnnotationService: EventAnnotationServiceType
): Ast | null => { ): Ast | null => {
if (!state || !state.layers.length) { if (!state || !state.layers.length) {
@ -73,7 +76,7 @@ export const toExpression = (
metadata, metadata,
datasourceLayers, datasourceLayers,
paletteService, paletteService,
attributes, datasourceExpressionsByLayers,
eventAnnotationService eventAnnotationService
); );
}; };
@ -100,6 +103,7 @@ export function toPreviewExpression(
state: State, state: State,
datasourceLayers: DatasourceLayers, datasourceLayers: DatasourceLayers,
paletteService: PaletteRegistry, paletteService: PaletteRegistry,
datasourceExpressionsByLayers: Record<string, Ast>,
eventAnnotationService: EventAnnotationServiceType eventAnnotationService: EventAnnotationServiceType
) { ) {
return toExpression( return toExpression(
@ -116,6 +120,7 @@ export function toPreviewExpression(
datasourceLayers, datasourceLayers,
paletteService, paletteService,
{}, {},
datasourceExpressionsByLayers,
eventAnnotationService eventAnnotationService
); );
} }
@ -151,11 +156,13 @@ export const buildExpression = (
metadata: Record<string, Record<string, OperationMetadata | null>>, metadata: Record<string, Record<string, OperationMetadata | null>>,
datasourceLayers: DatasourceLayers, datasourceLayers: DatasourceLayers,
paletteService: PaletteRegistry, paletteService: PaletteRegistry,
attributes: Partial<{ title: string; description: string }> = {}, datasourceExpressionsByLayers: Record<string, Ast>,
eventAnnotationService: EventAnnotationServiceType eventAnnotationService: EventAnnotationServiceType
): Ast | null => { ): Ast | null => {
const validDataLayers = getDataLayers(state.layers) const validDataLayers: ValidXYDataLayerConfig[] = getDataLayers(state.layers)
.filter((layer): layer is ValidLayer => Boolean(layer.accessors.length)) .filter<ValidXYDataLayerConfig>((layer): layer is ValidXYDataLayerConfig =>
Boolean(layer.accessors.length)
)
.map((layer) => ({ .map((layer) => ({
...layer, ...layer,
accessors: getSortedAccessors(datasourceLayers[layer.layerId], layer), accessors: getSortedAccessors(datasourceLayers[layer.layerId], layer),
@ -188,10 +195,8 @@ export const buildExpression = (
chain: [ chain: [
{ {
type: 'function', type: 'function',
function: 'xyVis', function: 'layeredXyVis',
arguments: { arguments: {
title: [attributes?.title || ''],
description: [attributes?.description || ''],
xTitle: [state.xTitle || ''], xTitle: [state.xTitle || ''],
yTitle: [state.yTitle || ''], yTitle: [state.yTitle || ''],
yRightTitle: [state.yRightTitle || ''], yRightTitle: [state.yRightTitle || ''],
@ -207,18 +212,24 @@ export const buildExpression = (
showSingleSeries: state.legend.showSingleSeries showSingleSeries: state.legend.showSingleSeries
? [state.legend.showSingleSeries] ? [state.legend.showSingleSeries]
: [], : [],
position: [state.legend.position], position: !state.legend.isInside ? [state.legend.position] : [],
isInside: state.legend.isInside ? [state.legend.isInside] : [], isInside: state.legend.isInside ? [state.legend.isInside] : [],
legendSize: state.legend.legendSize ? [state.legend.legendSize] : [], legendSize:
horizontalAlignment: state.legend.horizontalAlignment !state.legend.isInside && state.legend.legendSize
? [state.legend.legendSize]
: [],
horizontalAlignment:
state.legend.horizontalAlignment && state.legend.isInside
? [state.legend.horizontalAlignment] ? [state.legend.horizontalAlignment]
: [], : [],
verticalAlignment: state.legend.verticalAlignment verticalAlignment:
state.legend.verticalAlignment && state.legend.isInside
? [state.legend.verticalAlignment] ? [state.legend.verticalAlignment]
: [], : [],
// ensure that even if the user types more than 5 columns // ensure that even if the user types more than 5 columns
// we will only show 5 // we will only show 5
floatingColumns: state.legend.floatingColumns floatingColumns:
state.legend.floatingColumns && state.legend.isInside
? [Math.min(5, state.legend.floatingColumns)] ? [Math.min(5, state.legend.floatingColumns)]
: [], : [],
maxLines: state.legend.maxLines ? [state.legend.maxLines] : [], maxLines: state.legend.maxLines ? [state.legend.maxLines] : [],
@ -236,50 +247,8 @@ export const buildExpression = (
emphasizeFitting: [state.emphasizeFitting || false], emphasizeFitting: [state.emphasizeFitting || false],
curveType: [state.curveType || 'LINEAR'], curveType: [state.curveType || 'LINEAR'],
fillOpacity: [state.fillOpacity || 0.3], fillOpacity: [state.fillOpacity || 0.3],
yLeftExtent: [ yLeftExtent: [axisExtentConfigToExpression(state.yLeftExtent, validDataLayers)],
{ yRightExtent: [axisExtentConfigToExpression(state.yRightExtent, validDataLayers)],
type: 'expression',
chain: [
{
type: 'function',
function: 'axisExtentConfig',
arguments: {
mode: [state?.yLeftExtent?.mode || 'full'],
lowerBound:
state?.yLeftExtent?.lowerBound !== undefined
? [state?.yLeftExtent?.lowerBound]
: [],
upperBound:
state?.yLeftExtent?.upperBound !== undefined
? [state?.yLeftExtent?.upperBound]
: [],
},
},
],
},
],
yRightExtent: [
{
type: 'expression',
chain: [
{
type: 'function',
function: 'axisExtentConfig',
arguments: {
mode: [state?.yRightExtent?.mode || 'full'],
lowerBound:
state?.yRightExtent?.lowerBound !== undefined
? [state?.yRightExtent?.lowerBound]
: [],
upperBound:
state?.yRightExtent?.upperBound !== undefined
? [state?.yRightExtent?.upperBound]
: [],
},
},
],
},
],
axisTitlesVisibilitySettings: [ axisTitlesVisibilitySettings: [
{ {
type: 'expression', type: 'expression',
@ -353,13 +322,15 @@ export const buildExpression = (
layer, layer,
datasourceLayers[layer.layerId], datasourceLayers[layer.layerId],
metadata, metadata,
paletteService paletteService,
datasourceExpressionsByLayers[layer.layerId]
) )
), ),
...validReferenceLayers.map((layer) => ...validReferenceLayers.map((layer) =>
referenceLineLayerToExpression( referenceLineLayerToExpression(
layer, layer,
datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId] datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId],
datasourceExpressionsByLayers[layer.layerId]
) )
), ),
...validAnnotationsLayers.map((layer) => ...validAnnotationsLayers.map((layer) =>
@ -372,25 +343,32 @@ export const buildExpression = (
}; };
}; };
const buildTableExpression = (datasourceExpression: Ast): ExpressionAstExpression => ({
type: 'expression',
chain: [{ type: 'function', function: 'kibana', arguments: {} }, ...datasourceExpression.chain],
});
const referenceLineLayerToExpression = ( const referenceLineLayerToExpression = (
layer: XYReferenceLineLayerConfig, layer: XYReferenceLineLayerConfig,
datasourceLayer: DatasourcePublicAPI datasourceLayer: DatasourcePublicAPI,
datasourceExpression: Ast
): Ast => { ): Ast => {
return { return {
type: 'expression', type: 'expression',
chain: [ chain: [
{ {
type: 'function', type: 'function',
function: 'referenceLineLayer', function: 'extendedReferenceLineLayer',
arguments: { arguments: {
layerId: [layer.layerId], layerId: [layer.layerId],
yConfig: layer.yConfig yConfig: layer.yConfig
? layer.yConfig.map((yConfig) => ? layer.yConfig.map((yConfig) =>
yConfigToExpression(yConfig, defaultReferenceLineColor) extendedYConfigToExpression(yConfig, defaultReferenceLineColor)
) )
: [], : [],
accessors: layer.accessors, accessors: layer.accessors,
columnToLabel: [JSON.stringify(getColumnToLabelMap(layer, datasourceLayer))], columnToLabel: [JSON.stringify(getColumnToLabelMap(layer, datasourceLayer))],
...(datasourceExpression ? { table: [buildTableExpression(datasourceExpression)] } : {}),
}, },
}, },
], ],
@ -406,7 +384,7 @@ const annotationLayerToExpression = (
chain: [ chain: [
{ {
type: 'function', type: 'function',
function: 'annotationLayer', function: 'extendedAnnotationLayer',
arguments: { arguments: {
hide: [Boolean(layer.hide)], hide: [Boolean(layer.hide)],
layerId: [layer.layerId], layerId: [layer.layerId],
@ -420,10 +398,11 @@ const annotationLayerToExpression = (
}; };
const dataLayerToExpression = ( const dataLayerToExpression = (
layer: ValidLayer, layer: ValidXYDataLayerConfig,
datasourceLayer: DatasourcePublicAPI, datasourceLayer: DatasourcePublicAPI,
metadata: Record<string, Record<string, OperationMetadata | null>>, metadata: Record<string, Record<string, OperationMetadata | null>>,
paletteService: PaletteRegistry paletteService: PaletteRegistry,
datasourceExpression: Ast
): Ast => { ): Ast => {
const columnToLabel = getColumnToLabelMap(layer, datasourceLayer); const columnToLabel = getColumnToLabelMap(layer, datasourceLayer);
@ -441,7 +420,7 @@ const dataLayerToExpression = (
chain: [ chain: [
{ {
type: 'function', type: 'function',
function: 'dataLayer', function: 'extendedDataLayer',
arguments: { arguments: {
layerId: [layer.layerId], layerId: [layer.layerId],
hide: [Boolean(layer.hide)], hide: [Boolean(layer.hide)],
@ -458,6 +437,7 @@ const dataLayerToExpression = (
seriesType: [layer.seriesType], seriesType: [layer.seriesType],
accessors: layer.accessors, accessors: layer.accessors,
columnToLabel: [JSON.stringify(columnToLabel)], columnToLabel: [JSON.stringify(columnToLabel)],
...(datasourceExpression ? { table: [buildTableExpression(datasourceExpression)] } : {}),
palette: [ palette: [
{ {
type: 'expression', type: 'expression',
@ -496,6 +476,23 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => {
{ {
type: 'function', type: 'function',
function: 'yConfig', function: 'yConfig',
arguments: {
forAccessor: [yConfig.forAccessor],
axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
color: yConfig.color ? [yConfig.color] : defaultColor ? [defaultColor] : [],
},
},
],
};
};
const extendedYConfigToExpression = (yConfig: ExtendedYConfig, defaultColor?: string): Ast => {
return {
type: 'expression',
chain: [
{
type: 'function',
function: 'extendedYConfig',
arguments: { arguments: {
forAccessor: [yConfig.forAccessor], forAccessor: [yConfig.forAccessor],
axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
@ -514,3 +511,21 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => {
], ],
}; };
}; };
const axisExtentConfigToExpression = (
extent: AxisExtentConfig | undefined,
layers: ValidXYDataLayerConfig[]
): Ast => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'axisExtentConfig',
arguments: {
mode: [extent?.mode ?? 'full'],
lowerBound: extent?.lowerBound !== undefined ? [extent?.lowerBound] : [],
upperBound: extent?.upperBound !== undefined ? [extent?.upperBound] : [],
},
},
],
});

View file

@ -16,7 +16,10 @@ import type {
FittingFunction, FittingFunction,
LabelsOrientationConfig, LabelsOrientationConfig,
EndValue, EndValue,
ExtendedYConfig,
YConfig, YConfig,
YScaleType,
XScaleType,
} from '@kbn/expression-xy-plugin/common'; } from '@kbn/expression-xy-plugin/common';
import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
import { LensIconChartArea } from '../assets/chart_area'; import { LensIconChartArea } from '../assets/chart_area';
@ -43,12 +46,16 @@ export interface XYDataLayerConfig {
yConfig?: YConfig[]; yConfig?: YConfig[];
splitAccessor?: string; splitAccessor?: string;
palette?: PaletteOutput; palette?: PaletteOutput;
yScaleType?: YScaleType;
xScaleType?: XScaleType;
isHistogram?: boolean;
columnToLabel?: string;
} }
export interface XYReferenceLineLayerConfig { export interface XYReferenceLineLayerConfig {
layerId: string; layerId: string;
accessors: string[]; accessors: string[];
yConfig?: YConfig[]; yConfig?: ExtendedYConfig[];
layerType: 'referenceLine'; layerType: 'referenceLine';
} }
@ -64,6 +71,13 @@ export type XYLayerConfig =
| XYReferenceLineLayerConfig | XYReferenceLineLayerConfig
| XYAnnotationLayerConfig; | XYAnnotationLayerConfig;
export interface ValidXYDataLayerConfig extends XYDataLayerConfig {
xAccessor: NonNullable<XYDataLayerConfig['xAccessor']>;
layerId: string;
}
export type ValidLayer = ValidXYDataLayerConfig | XYReferenceLineLayerConfig;
// Persisted parts of the state // Persisted parts of the state
export interface XYState { export interface XYState {
preferredSeriesType: SeriesType; preferredSeriesType: SeriesType;

View file

@ -16,7 +16,12 @@ import { ThemeServiceStart } from '@kbn/core/public';
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
import { FillStyle, SeriesType, YAxisMode, YConfig } from '@kbn/expression-xy-plugin/common'; import {
FillStyle,
SeriesType,
YAxisMode,
ExtendedYConfig,
} from '@kbn/expression-xy-plugin/common';
import { getSuggestions } from './xy_suggestions'; import { getSuggestions } from './xy_suggestions';
import { XyToolbar } from './xy_config_panel'; import { XyToolbar } from './xy_config_panel';
import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { DimensionEditor } from './xy_config_panel/dimension_editor';
@ -61,7 +66,7 @@ import {
} from './visualization_helpers'; } from './visualization_helpers';
import { groupAxesByType } from './axes_configuration'; import { groupAxesByType } from './axes_configuration';
import { XYState } from './types'; import { XYState } from './types';
import { ReferenceLinePanel } from './xy_config_panel/reference_line_panel'; import { ReferenceLinePanel } from './xy_config_panel/reference_line_config_panel';
import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel';
import { DimensionTrigger } from '../shared_components/dimension_trigger'; import { DimensionTrigger } from '../shared_components/dimension_trigger';
import { defaultAnnotationLabel } from './annotations/helpers'; import { defaultAnnotationLabel } from './annotations/helpers';
@ -295,6 +300,7 @@ export const getXyVisualization = ({
setDimension(props) { setDimension(props) {
const { prevState, layerId, columnId, groupId } = props; const { prevState, layerId, columnId, groupId } = props;
const foundLayer: XYLayerConfig | undefined = prevState.layers.find( const foundLayer: XYLayerConfig | undefined = prevState.layers.find(
(l) => l.layerId === layerId (l) => l.layerId === layerId
); );
@ -333,7 +339,7 @@ export const getXyVisualization = ({
} }
const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value'); const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value');
const axisMode = axisPosition as YAxisMode; const axisMode = axisPosition as YAxisMode;
const yConfig = metrics.map<YConfig>((metric, idx) => { const yConfig = metrics.map<ExtendedYConfig>((metric, idx) => {
return { return {
color: metric.color, color: metric.color,
forAccessor: metric.accessor ?? foundLayer.accessors[idx], forAccessor: metric.accessor ?? foundLayer.accessors[idx],
@ -444,7 +450,7 @@ export const getXyVisualization = ({
const groupsAvailable = getGroupsAvailableInData( const groupsAvailable = getGroupsAvailableInData(
getDataLayers(prevState.layers), getDataLayers(prevState.layers),
frame.datasourceLayers, frame.datasourceLayers,
frame?.activeData frame.activeData
); );
if ( if (
@ -508,10 +514,26 @@ export const getXyVisualization = ({
); );
}, },
toExpression: (state, layers, attributes) => shouldBuildDatasourceExpressionManually: () => true,
toExpression(state, layers, paletteService, attributes, eventAnnotationService),
toPreviewExpression: (state, layers) => toExpression: (state, layers, attributes, datasourceExpressionsByLayers = {}) =>
toPreviewExpression(state, layers, paletteService, eventAnnotationService), toExpression(
state,
layers,
paletteService,
attributes,
datasourceExpressionsByLayers,
eventAnnotationService
),
toPreviewExpression: (state, layers, datasourceExpressionsByLayers = {}) =>
toPreviewExpression(
state,
layers,
paletteService,
datasourceExpressionsByLayers,
eventAnnotationService
),
getErrorMessages(state, datasourceLayers) { getErrorMessages(state, datasourceLayers) {
// Data error handling below here // Data error handling below here
@ -592,10 +614,12 @@ export const getXyVisualization = ({
...getDataLayers(state.layers), ...getDataLayers(state.layers),
...getReferenceLayers(state.layers), ...getReferenceLayers(state.layers),
].filter(({ accessors }) => accessors.length > 0); ].filter(({ accessors }) => accessors.length > 0);
const accessorsWithArrayValues = []; const accessorsWithArrayValues = [];
for (const layer of filteredLayers) { for (const layer of filteredLayers) {
const { layerId, accessors } = layer; const { layerId, accessors } = layer;
const rows = frame.activeData[layerId] && frame.activeData[layerId].rows; const rows = frame.activeData?.[layerId] && frame.activeData[layerId].rows;
if (!rows) { if (!rows) {
break; break;
} }
@ -607,6 +631,7 @@ export const getXyVisualization = ({
} }
} }
} }
return accessorsWithArrayValues.map((label) => ( return accessorsWithArrayValues.map((label) => (
<FormattedMessage <FormattedMessage
key={label} key={label}

View file

@ -0,0 +1,505 @@
/*
* 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 './index.scss';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiDatePicker,
EuiFormRow,
EuiSwitch,
EuiSwitchEvent,
EuiButtonGroup,
EuiFormLabel,
EuiFormControlLayout,
EuiText,
transparentize,
} from '@elastic/eui';
import type { PaletteRegistry } from '@kbn/coloring';
import moment from 'moment';
import {
EventAnnotationConfig,
PointInTimeEventAnnotationConfig,
RangeEventAnnotationConfig,
} from '@kbn/event-annotation-plugin/common/types';
import { pick } from 'lodash';
import { search } from '@kbn/data-plugin/public';
import {
defaultAnnotationColor,
defaultAnnotationRangeColor,
isRangeAnnotation,
} from '@kbn/event-annotation-plugin/public';
import Color from 'color';
import { getDataLayers } from '../../visualization_helpers';
import { FormatFactory } from '../../../../common';
import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components';
import { isHorizontalChart } from '../../state_helpers';
import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers';
import { ColorPicker } from '../color_picker';
import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings';
import { LineStyleSettings } from '../shared/line_style_settings';
import { updateLayer } from '..';
import { annotationsIconSet } from './icon_set';
import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types';
import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types';
export const toRangeAnnotationColor = (color = defaultAnnotationColor) => {
return new Color(transparentize(color, 0.1)).hexa();
};
export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => {
return new Color(transparentize(color, 1)).hex();
};
export const getEndTimestamp = (
startTime: string,
{ activeData, dateRange }: FramePublicAPI,
dataLayers: XYDataLayerConfig[]
) => {
const startTimeNumber = moment(startTime).valueOf();
const dateRangeFraction =
(moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1;
const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString();
const dataLayersId = dataLayers.map(({ layerId }) => layerId);
if (
!dataLayersId.length ||
!activeData ||
Object.entries(activeData)
.filter(([key]) => dataLayersId.includes(key))
.every(([, { rows }]) => !rows || !rows.length)
) {
return fallbackValue;
}
const xColumn = activeData?.[dataLayersId[0]].columns.find(
(column) => column.id === dataLayers[0].xAccessor
);
if (!xColumn) {
return fallbackValue;
}
const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval;
if (!dateInterval) return fallbackValue;
const intervalDuration = search.aggs.parseInterval(dateInterval);
if (!intervalDuration) return fallbackValue;
return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString();
};
const sanitizeProperties = (annotation: EventAnnotationConfig) => {
if (isRangeAnnotation(annotation)) {
const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [
'label',
'key',
'id',
'isHidden',
'color',
'outside',
]);
return rangeAnnotation;
} else {
const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [
'id',
'label',
'key',
'isHidden',
'lineStyle',
'lineWidth',
'color',
'icon',
'textVisibility',
]);
return lineAnnotation;
}
};
export const AnnotationsPanel = (
props: VisualizationDimensionEditorProps<State> & {
formatFactory: FormatFactory;
paletteService: PaletteRegistry;
}
) => {
const { state, setState, layerId, accessor, frame } = props;
const isHorizontal = isHorizontalChart(state.layers);
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue<XYState>({
value: state,
onChange: setState,
});
const index = localState.layers.findIndex((l) => l.layerId === layerId);
const localLayer = localState.layers.find(
(l) => l.layerId === layerId
) as XYAnnotationLayerConfig;
const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor);
const isRange = isRangeAnnotation(currentAnnotation);
const setAnnotations = useCallback(
(annotation) => {
if (annotation == null) {
return;
}
const newConfigs = [...(localLayer.annotations || [])];
const existingIndex = newConfigs.findIndex((c) => c.id === accessor);
if (existingIndex !== -1) {
newConfigs[existingIndex] = sanitizeProperties({
...newConfigs[existingIndex],
...annotation,
});
} else {
throw new Error(
'should never happen because annotation is created before config panel is opened'
);
}
setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index));
},
[accessor, index, localState, localLayer, setLocalState]
);
return (
<>
<DimensionEditorSection
title={i18n.translate('xpack.lens.xyChart.placement', {
defaultMessage: 'Placement',
})}
>
{isRange ? (
<>
<ConfigPanelRangeDatePicker
dataTestSubj="lns-xyAnnotation-fromTime"
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.from', {
defaultMessage: 'From',
})}
value={moment(currentAnnotation?.key.timestamp)}
onChange={(date) => {
if (date) {
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
if (currentEndTime < date.valueOf()) {
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
const dif = currentEndTime - currentStartTime;
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
timestamp: date.toISOString(),
endTimestamp: moment(date.valueOf() + dif).toISOString(),
},
});
} else {
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
timestamp: date.toISOString(),
},
});
}
}
}}
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
defaultMessage: 'Annotation date',
})}
/>
<ConfigPanelRangeDatePicker
dataTestSubj="lns-xyAnnotation-toTime"
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.to', {
defaultMessage: 'To',
})}
value={moment(currentAnnotation?.key.endTimestamp)}
onChange={(date) => {
if (date) {
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
if (currentStartTime > date.valueOf()) {
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
const dif = currentEndTime - currentStartTime;
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
endTimestamp: date.toISOString(),
timestamp: moment(date.valueOf() - dif).toISOString(),
},
});
} else {
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
endTimestamp: date.toISOString(),
},
});
}
}
}}
/>
</>
) : (
<ConfigPanelRangeDatePicker
dataTestSubj="lns-xyAnnotation-time"
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
defaultMessage: 'Annotation date',
})}
value={moment(currentAnnotation?.key.timestamp)}
onChange={(date) => {
if (date) {
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'point_in_time' }),
timestamp: date.toISOString(),
},
});
}
}}
/>
)}
<ConfigPanelApplyAsRangeSwitch
annotation={currentAnnotation}
onChange={setAnnotations}
frame={frame}
state={state}
/>
</DimensionEditorSection>
<DimensionEditorSection
title={i18n.translate('xpack.lens.xyChart.appearance', {
defaultMessage: 'Appearance',
})}
>
<NameInput
value={currentAnnotation?.label || defaultAnnotationLabel}
defaultValue={defaultAnnotationLabel}
onChange={(value) => {
setAnnotations({ label: value });
}}
/>
{!isRange && (
<IconSelectSetting
setConfig={setAnnotations}
defaultIcon="triangle"
currentConfig={{
axisMode: 'bottom',
...currentAnnotation,
}}
customIconSet={annotationsIconSet}
/>
)}
{!isRange && (
<TextDecorationSetting
setConfig={setAnnotations}
currentConfig={{
axisMode: 'bottom',
...currentAnnotation,
}}
/>
)}
{!isRange && (
<LineStyleSettings
isHorizontal={isHorizontal}
setConfig={setAnnotations}
currentConfig={currentAnnotation}
/>
)}
{isRange && (
<EuiFormRow
label={i18n.translate('xpack.lens.xyChart.fillStyle', {
defaultMessage: 'Fill',
})}
display="columnCompressed"
fullWidth
>
<EuiButtonGroup
legend={i18n.translate('xpack.lens.xyChart.fillStyle', {
defaultMessage: 'Fill',
})}
data-test-subj="lns-xyAnnotation-fillStyle"
name="fillStyle"
buttonSize="compressed"
options={[
{
id: `lens_xyChart_fillStyle_inside`,
label: i18n.translate('xpack.lens.xyChart.fillStyle.inside', {
defaultMessage: 'Inside',
}),
'data-test-subj': 'lnsXY_fillStyle_inside',
},
{
id: `lens_xyChart_fillStyle_outside`,
label: i18n.translate('xpack.lens.xyChart.fillStyle.outside', {
defaultMessage: 'Outside',
}),
'data-test-subj': 'lnsXY_fillStyle_inside',
},
]}
idSelected={`lens_xyChart_fillStyle_${
Boolean(currentAnnotation?.outside) ? 'outside' : 'inside'
}`}
onChange={(id) => {
setAnnotations({
outside: id === `lens_xyChart_fillStyle_outside`,
});
}}
isFullWidth
/>
</EuiFormRow>
)}
<ColorPicker
{...props}
defaultColor={isRange ? defaultAnnotationRangeColor : defaultAnnotationColor}
showAlpha={isRange}
setConfig={setAnnotations}
disableHelpTooltip
label={i18n.translate('xpack.lens.xyChart.lineColor.label', {
defaultMessage: 'Color',
})}
/>
<ConfigPanelHideSwitch
value={Boolean(currentAnnotation?.isHidden)}
onChange={(ev) => setAnnotations({ isHidden: ev.target.checked })}
/>
</DimensionEditorSection>
</>
);
};
const ConfigPanelApplyAsRangeSwitch = ({
annotation,
onChange,
frame,
state,
}: {
annotation?: EventAnnotationConfig;
onChange: (annotations: Partial<EventAnnotationConfig> | undefined) => void;
frame: FramePublicAPI;
state: XYState;
}) => {
const isRange = isRangeAnnotation(annotation);
return (
<EuiFormRow display="columnCompressed" className="lnsRowCompressedMargin">
<EuiSwitch
data-test-subj="lns-xyAnnotation-rangeSwitch"
label={
<EuiText size="xs">
{i18n.translate('xpack.lens.xyChart.applyAsRange', {
defaultMessage: 'Apply as range',
})}
</EuiText>
}
checked={isRange}
onChange={() => {
if (isRange) {
const newPointAnnotation: PointInTimeEventAnnotationConfig = {
key: {
type: 'point_in_time',
timestamp: annotation.key.timestamp,
},
id: annotation.id,
label:
annotation.label === defaultRangeAnnotationLabel
? defaultAnnotationLabel
: annotation.label,
color: toLineAnnotationColor(annotation.color),
isHidden: annotation.isHidden,
};
onChange(newPointAnnotation);
} else if (annotation) {
const fromTimestamp = moment(annotation?.key.timestamp);
const dataLayers = getDataLayers(state.layers);
const newRangeAnnotation: RangeEventAnnotationConfig = {
key: {
type: 'range',
timestamp: annotation.key.timestamp,
endTimestamp: getEndTimestamp(fromTimestamp.toISOString(), frame, dataLayers),
},
id: annotation.id,
label:
annotation.label === defaultAnnotationLabel
? defaultRangeAnnotationLabel
: annotation.label,
color: toRangeAnnotationColor(annotation.color),
isHidden: annotation.isHidden,
};
onChange(newRangeAnnotation);
}
}}
compressed
/>
</EuiFormRow>
);
};
const ConfigPanelRangeDatePicker = ({
value,
label,
prependLabel,
onChange,
dataTestSubj = 'lnsXY_annotation_date_picker',
}: {
value: moment.Moment;
prependLabel?: string;
label?: string;
onChange: (val: moment.Moment | null) => void;
dataTestSubj?: string;
}) => {
return (
<EuiFormRow display="rowCompressed" fullWidth label={label} className="lnsRowCompressedMargin">
{prependLabel ? (
<EuiFormControlLayout
fullWidth
className="lnsConfigPanelNoPadding"
prepend={
<EuiFormLabel className="lnsConfigPanelDate__label">{prependLabel}</EuiFormLabel>
}
>
<EuiDatePicker
fullWidth
showTimeSelect
selected={value}
onChange={onChange}
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
data-test-subj={dataTestSubj}
/>
</EuiFormControlLayout>
) : (
<EuiDatePicker
fullWidth
showTimeSelect
selected={value}
onChange={onChange}
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
data-test-subj={dataTestSubj}
/>
)}
</EuiFormRow>
);
};
const ConfigPanelHideSwitch = ({
value,
onChange,
}: {
value: boolean;
onChange: (event: EuiSwitchEvent) => void;
}) => {
return (
<EuiFormRow
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
defaultMessage: 'Hide annotation',
})}
display="columnCompressedSwitch"
fullWidth
>
<EuiSwitch
compressed
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
defaultMessage: 'Hide annotation',
})}
showLabel={false}
data-test-subj="lns-annotations-hide-annotation"
checked={value}
onChange={onChange}
/>
</EuiFormRow>
);
};

View file

@ -4,10 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { i18n } from '@kbn/i18n';
import { IconTriangle, IconCircle } from '../../../assets/annotation_icons';
export const annotationsIconSet = [ import { i18n } from '@kbn/i18n';
import { AvailableAnnotationIcon } from '@kbn/event-annotation-plugin/common';
import { IconTriangle, IconCircle } from '../../../assets/annotation_icons';
import { IconSet } from '../shared/icon_select';
export const annotationsIconSet: IconSet<AvailableAnnotationIcon> = [
{ {
value: 'asterisk', value: 'asterisk',
label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', { label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', {

View file

@ -29,7 +29,7 @@ const customLineStaticAnnotation = {
id: 'ann1', id: 'ann1',
key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' },
label: 'Event', label: 'Event',
icon: 'triangle', icon: 'triangle' as const,
color: 'red', color: 'red',
lineStyle: 'dashed' as const, lineStyle: 'dashed' as const,
lineWidth: 3, lineWidth: 3,

View file

@ -5,501 +5,4 @@
* 2.0. * 2.0.
*/ */
import './index.scss'; export { AnnotationsPanel } from './annotations_panel';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import type { PaletteRegistry } from '@kbn/coloring';
import {
EuiDatePicker,
EuiFormRow,
EuiSwitch,
EuiSwitchEvent,
EuiButtonGroup,
EuiFormLabel,
EuiFormControlLayout,
EuiText,
transparentize,
} from '@elastic/eui';
import { pick } from 'lodash';
import moment from 'moment';
import {
EventAnnotationConfig,
PointInTimeEventAnnotationConfig,
RangeEventAnnotationConfig,
} from '@kbn/event-annotation-plugin/common/types';
import { search } from '@kbn/data-plugin/public';
import {
defaultAnnotationColor,
defaultAnnotationRangeColor,
isRangeAnnotation,
} from '@kbn/event-annotation-plugin/public';
import Color from 'color';
import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types';
import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types';
import { FormatFactory } from '../../../../common';
import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components';
import { isHorizontalChart } from '../../state_helpers';
import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers';
import { ColorPicker } from '../color_picker';
import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings';
import { LineStyleSettings } from '../shared/line_style_settings';
import { updateLayer } from '..';
import { annotationsIconSet } from './icon_set';
import { getDataLayers } from '../../visualization_helpers';
export const toRangeAnnotationColor = (color = defaultAnnotationColor) => {
return new Color(transparentize(color, 0.1)).hexa();
};
export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => {
return new Color(transparentize(color, 1)).hex();
};
export const getEndTimestamp = (
startTime: string,
{ activeData, dateRange }: FramePublicAPI,
dataLayers: XYDataLayerConfig[]
) => {
const startTimeNumber = moment(startTime).valueOf();
const dateRangeFraction =
(moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1;
const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString();
const dataLayersId = dataLayers.map(({ layerId }) => layerId);
if (
!dataLayersId.length ||
!activeData ||
Object.entries(activeData)
.filter(([key]) => dataLayersId.includes(key))
.every(([, { rows }]) => !rows || !rows.length)
) {
return fallbackValue;
}
const xColumn = activeData?.[dataLayersId[0]].columns.find(
(column) => column.id === dataLayers[0].xAccessor
);
if (!xColumn) {
return fallbackValue;
}
const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval;
if (!dateInterval) return fallbackValue;
const intervalDuration = search.aggs.parseInterval(dateInterval);
if (!intervalDuration) return fallbackValue;
return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString();
};
const sanitizeProperties = (annotation: EventAnnotationConfig) => {
if (isRangeAnnotation(annotation)) {
const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [
'label',
'key',
'id',
'isHidden',
'color',
'outside',
]);
return rangeAnnotation;
} else {
const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [
'id',
'label',
'key',
'isHidden',
'lineStyle',
'lineWidth',
'color',
'icon',
'textVisibility',
]);
return lineAnnotation;
}
};
export const AnnotationsPanel = (
props: VisualizationDimensionEditorProps<State> & {
formatFactory: FormatFactory;
paletteService: PaletteRegistry;
}
) => {
const { state, setState, layerId, accessor, frame } = props;
const isHorizontal = isHorizontalChart(state.layers);
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue<XYState>({
value: state,
onChange: setState,
});
const index = localState.layers.findIndex((l) => l.layerId === layerId);
const localLayer = localState.layers.find(
(l) => l.layerId === layerId
) as XYAnnotationLayerConfig;
const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor);
const isRange = isRangeAnnotation(currentAnnotation);
const setAnnotations = useCallback(
(annotation) => {
if (annotation == null) {
return;
}
const newConfigs = [...(localLayer.annotations || [])];
const existingIndex = newConfigs.findIndex((c) => c.id === accessor);
if (existingIndex !== -1) {
newConfigs[existingIndex] = sanitizeProperties({
...newConfigs[existingIndex],
...annotation,
});
} else {
throw new Error(
'should never happen because annotation is created before config panel is opened'
);
}
setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index));
},
[accessor, index, localState, localLayer, setLocalState]
);
return (
<>
<DimensionEditorSection
title={i18n.translate('xpack.lens.xyChart.placement', {
defaultMessage: 'Placement',
})}
>
{isRange ? (
<>
<ConfigPanelRangeDatePicker
dataTestSubj="lns-xyAnnotation-fromTime"
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.from', {
defaultMessage: 'From',
})}
value={moment(currentAnnotation?.key.timestamp)}
onChange={(date) => {
if (date) {
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
if (currentEndTime < date.valueOf()) {
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
const dif = currentEndTime - currentStartTime;
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
timestamp: date.toISOString(),
endTimestamp: moment(date.valueOf() + dif).toISOString(),
},
});
} else {
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
timestamp: date.toISOString(),
},
});
}
}
}}
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
defaultMessage: 'Annotation date',
})}
/>
<ConfigPanelRangeDatePicker
dataTestSubj="lns-xyAnnotation-toTime"
prependLabel={i18n.translate('xpack.lens.xyChart.annotationDate.to', {
defaultMessage: 'To',
})}
value={moment(currentAnnotation?.key.endTimestamp)}
onChange={(date) => {
if (date) {
const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf();
if (currentStartTime > date.valueOf()) {
const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf();
const dif = currentEndTime - currentStartTime;
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
endTimestamp: date.toISOString(),
timestamp: moment(date.valueOf() - dif).toISOString(),
},
});
} else {
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'range' }),
endTimestamp: date.toISOString(),
},
});
}
}
}}
/>
</>
) : (
<ConfigPanelRangeDatePicker
dataTestSubj="lns-xyAnnotation-time"
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
defaultMessage: 'Annotation date',
})}
value={moment(currentAnnotation?.key.timestamp)}
onChange={(date) => {
if (date) {
setAnnotations({
key: {
...(currentAnnotation?.key || { type: 'point_in_time' }),
timestamp: date.toISOString(),
},
});
}
}}
/>
)}
<ConfigPanelApplyAsRangeSwitch
annotation={currentAnnotation}
onChange={setAnnotations}
frame={frame}
state={state}
/>
</DimensionEditorSection>
<DimensionEditorSection
title={i18n.translate('xpack.lens.xyChart.appearance', {
defaultMessage: 'Appearance',
})}
>
<NameInput
value={currentAnnotation?.label || defaultAnnotationLabel}
defaultValue={defaultAnnotationLabel}
onChange={(value) => {
setAnnotations({ label: value });
}}
/>
{!isRange && (
<IconSelectSetting
setConfig={setAnnotations}
defaultIcon="triangle"
currentConfig={{
axisMode: 'bottom',
...currentAnnotation,
}}
customIconSet={annotationsIconSet}
/>
)}
{!isRange && (
<TextDecorationSetting
setConfig={setAnnotations}
currentConfig={{
axisMode: 'bottom',
...currentAnnotation,
}}
/>
)}
{!isRange && (
<LineStyleSettings
isHorizontal={isHorizontal}
setConfig={setAnnotations}
currentConfig={currentAnnotation}
/>
)}
{isRange && (
<EuiFormRow
label={i18n.translate('xpack.lens.xyChart.fillStyle', {
defaultMessage: 'Fill',
})}
display="columnCompressed"
fullWidth
>
<EuiButtonGroup
legend={i18n.translate('xpack.lens.xyChart.fillStyle', {
defaultMessage: 'Fill',
})}
data-test-subj="lns-xyAnnotation-fillStyle"
name="fillStyle"
buttonSize="compressed"
options={[
{
id: `lens_xyChart_fillStyle_inside`,
label: i18n.translate('xpack.lens.xyChart.fillStyle.inside', {
defaultMessage: 'Inside',
}),
'data-test-subj': 'lnsXY_fillStyle_inside',
},
{
id: `lens_xyChart_fillStyle_outside`,
label: i18n.translate('xpack.lens.xyChart.fillStyle.outside', {
defaultMessage: 'Outside',
}),
'data-test-subj': 'lnsXY_fillStyle_inside',
},
]}
idSelected={`lens_xyChart_fillStyle_${
Boolean(currentAnnotation?.outside) ? 'outside' : 'inside'
}`}
onChange={(id) => {
setAnnotations({
outside: id === `lens_xyChart_fillStyle_outside`,
});
}}
isFullWidth
/>
</EuiFormRow>
)}
<ColorPicker
{...props}
defaultColor={isRange ? defaultAnnotationRangeColor : defaultAnnotationColor}
showAlpha={isRange}
setConfig={setAnnotations}
disableHelpTooltip
label={i18n.translate('xpack.lens.xyChart.lineColor.label', {
defaultMessage: 'Color',
})}
/>
<ConfigPanelHideSwitch
value={Boolean(currentAnnotation?.isHidden)}
onChange={(ev) => setAnnotations({ isHidden: ev.target.checked })}
/>
</DimensionEditorSection>
</>
);
};
const ConfigPanelApplyAsRangeSwitch = ({
annotation,
onChange,
frame,
state,
}: {
annotation?: EventAnnotationConfig;
onChange: (annotations: Partial<EventAnnotationConfig> | undefined) => void;
frame: FramePublicAPI;
state: XYState;
}) => {
const isRange = isRangeAnnotation(annotation);
return (
<EuiFormRow display="columnCompressed" className="lnsRowCompressedMargin">
<EuiSwitch
data-test-subj="lns-xyAnnotation-rangeSwitch"
label={
<EuiText size="xs">
{i18n.translate('xpack.lens.xyChart.applyAsRange', {
defaultMessage: 'Apply as range',
})}
</EuiText>
}
checked={isRange}
onChange={() => {
if (isRange) {
const newPointAnnotation: PointInTimeEventAnnotationConfig = {
key: {
type: 'point_in_time',
timestamp: annotation.key.timestamp,
},
id: annotation.id,
label:
annotation.label === defaultRangeAnnotationLabel
? defaultAnnotationLabel
: annotation.label,
color: toLineAnnotationColor(annotation.color),
isHidden: annotation.isHidden,
};
onChange(newPointAnnotation);
} else if (annotation) {
const fromTimestamp = moment(annotation?.key.timestamp);
const dataLayers = getDataLayers(state.layers);
const newRangeAnnotation: RangeEventAnnotationConfig = {
key: {
type: 'range',
timestamp: annotation.key.timestamp,
endTimestamp: getEndTimestamp(fromTimestamp.toISOString(), frame, dataLayers),
},
id: annotation.id,
label:
annotation.label === defaultAnnotationLabel
? defaultRangeAnnotationLabel
: annotation.label,
color: toRangeAnnotationColor(annotation.color),
isHidden: annotation.isHidden,
};
onChange(newRangeAnnotation);
}
}}
compressed
/>
</EuiFormRow>
);
};
const ConfigPanelRangeDatePicker = ({
value,
label,
prependLabel,
onChange,
dataTestSubj = 'lnsXY_annotation_date_picker',
}: {
value: moment.Moment;
prependLabel?: string;
label?: string;
onChange: (val: moment.Moment | null) => void;
dataTestSubj?: string;
}) => {
return (
<EuiFormRow display="rowCompressed" fullWidth label={label} className="lnsRowCompressedMargin">
{prependLabel ? (
<EuiFormControlLayout
fullWidth
className="lnsConfigPanelNoPadding"
prepend={
<EuiFormLabel className="lnsConfigPanelDate__label">{prependLabel}</EuiFormLabel>
}
>
<EuiDatePicker
fullWidth
showTimeSelect
selected={value}
onChange={onChange}
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
data-test-subj={dataTestSubj}
/>
</EuiFormControlLayout>
) : (
<EuiDatePicker
fullWidth
showTimeSelect
selected={value}
onChange={onChange}
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
data-test-subj={dataTestSubj}
/>
)}
</EuiFormRow>
);
};
const ConfigPanelHideSwitch = ({
value,
onChange,
}: {
value: boolean;
onChange: (event: EuiSwitchEvent) => void;
}) => {
return (
<EuiFormRow
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
defaultMessage: 'Hide annotation',
})}
display="columnCompressedSwitch"
fullWidth
>
<EuiSwitch
compressed
label={i18n.translate('xpack.lens.xyChart.annotation.name', {
defaultMessage: 'Hide annotation',
})}
showLabel={false}
data-test-subj="lns-annotations-hide-annotation"
checked={value}
onChange={onChange}
/>
</EuiFormRow>
);
};

View file

@ -76,10 +76,9 @@ export const ColorPicker = ({
frame.datasourceLayers[layer.layerId] ?? layer.accessors, frame.datasourceLayers[layer.layerId] ?? layer.accessors,
layer layer
); );
const colorAssignments = getColorAssignments( const colorAssignments = getColorAssignments(
getDataLayers(state.layers), getDataLayers(state.layers),
{ tables: frame.activeData }, { tables: frame.activeData ?? {} },
formatFactory formatFactory
); );
const mappedAccessors = getAccessorColorConfig( const mappedAccessors = getAccessorColorConfig(
@ -91,7 +90,6 @@ export const ColorPicker = ({
}, },
paletteService paletteService
); );
return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; return mappedAccessors.find((a) => a.columnId === accessor)?.color || null;
} }
}, [ }, [
@ -105,12 +103,12 @@ export const ColorPicker = ({
defaultColor, defaultColor,
]); ]);
const [color, setColor] = useState(currentColor);
useEffect(() => { useEffect(() => {
setColor(currentColor); setColor(currentColor);
}, [currentColor]); }, [currentColor]);
const [color, setColor] = useState(currentColor);
const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { const handleColor: EuiColorPickerProps['onChange'] = (text, output) => {
setColor(text); setColor(text);
if (output.isValid || text === '') { if (output.isValid || text === '') {

Some files were not shown because too many files have changed in this diff Show more