From b29b46896128fabe9b8bc4d727a57192fe76ad8b Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 3 May 2022 17:29:01 +0300 Subject: [PATCH] [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 Co-authored-by: Marta Bondyra --- packages/kbn-optimizer/limits.yml | 2 +- .../expression_xy/common/__mocks__/index.ts | 37 +- .../expression_xy/common/constants.ts | 26 +- ...on_layer_config.ts => annotation_layer.ts} | 20 +- .../axis_extent_config.ts | 21 +- .../common_data_layer_args.ts | 72 + .../common_reference_line_layer_args.ts | 30 + .../expression_functions/common_xy_args.ts | 118 + .../common_y_config_args.ts | 30 + ...ayer_config.test.ts => data_layer.test.ts} | 15 +- .../common/expression_functions/data_layer.ts | 30 + .../expression_functions/data_layer_config.ts | 123 - .../extended_annotation_layer.ts | 51 + .../extended_data_layer.ts | 40 + .../extended_reference_line_layer.ts | 40 + .../extended_y_axis_config.ts | 80 + .../common/expression_functions/index.ts | 11 +- .../expression_functions/layered_xy_vis.ts | 39 + .../expression_functions/layered_xy_vis_fn.ts | 33 + .../legend_config.test.ts | 4 +- .../expression_functions/legend_config.ts | 22 +- .../expression_functions/legend_config_fn.ts | 68 + .../reference_line_layer.ts | 30 + .../reference_line_layer_config.ts | 62 - .../common/expression_functions/validate.ts | 103 + .../expression_functions/xy_vis.test.ts | 17 +- .../common/expression_functions/xy_vis.ts | 251 +- .../common/expression_functions/xy_vis_fn.ts | 69 + .../expression_functions/y_axis_config.ts | 80 +- .../expression_xy/common/helpers/index.ts | 9 + .../common/helpers/layers.test.ts | 49 + .../expression_xy/common/helpers/layers.ts | 27 + .../expression_xy/common/i18n/index.tsx | 212 + .../expression_xy/common/index.ts | 32 +- .../common/types/expression_functions.ts | 232 +- .../common/types/expression_renderers.ts | 17 +- .../expression_xy/common/utils/index.tsx | 9 + .../common/utils/log_datatables.ts | 55 + .../expression_xy/public/__mocks__/index.tsx | 71 +- .../__snapshots__/xy_chart.test.tsx.snap | 3592 +++++++++++++---- .../public/components/annotations.tsx | 17 +- .../public/components/data_layers.tsx | 166 + .../public/components/legend_action.test.tsx | 34 +- .../public/components/legend_action.tsx | 21 +- .../components/reference_lines.test.tsx | 83 +- .../public/components/reference_lines.tsx | 18 +- .../public/components/x_domain.tsx | 22 +- .../public/components/xy_chart.test.tsx | 1391 ++++--- .../public/components/xy_chart.tsx | 475 +-- .../xy_chart_renderer.tsx | 6 +- .../public/helpers/annotations.tsx | 34 +- .../public/helpers/annotations_icon_set.tsx | 101 - .../public/helpers/axes_configuration.test.ts | 16 +- .../public/helpers/axes_configuration.ts | 45 +- .../public/helpers/color_assignment.test.ts | 169 +- .../public/helpers/color_assignment.ts | 44 +- .../public/helpers/data_layers.tsx | 332 ++ .../public/helpers/fitting_functions.ts | 5 +- .../expression_xy/public/helpers/icon.ts | 101 + .../expression_xy/public/helpers/index.ts | 2 +- .../public/helpers/interval.test.ts | 37 +- .../expression_xy/public/helpers/interval.ts | 6 +- .../expression_xy/public/helpers/layers.ts | 40 +- .../expression_xy/public/helpers/state.ts | 12 +- .../public/helpers/visualization.ts | 40 +- .../expression_xy/public/plugin.ts | 24 +- .../expression_xy/server/plugin.ts | 24 +- .../event_annotation/common/constants.ts | 24 + src/plugins/event_annotation/common/index.ts | 6 +- .../common/manual_event_annotation/index.ts | 4 + src/plugins/event_annotation/common/types.ts | 7 +- .../common/execution/execution.test.ts | 4 +- .../expressions/common/execution/execution.ts | 10 +- x-pack/plugins/lens/common/types.ts | 3 +- .../editor_frame/config_panel/layer_panel.tsx | 19 +- .../editor_frame/expression_helpers.ts | 65 +- .../editor_frame/suggestion_panel.tsx | 54 +- .../lens/public/embeddable/embeddable.tsx | 2 + .../toolbar_component.tsx | 4 +- x-pack/plugins/lens/public/index.ts | 10 +- .../indexpattern_datasource/indexpattern.tsx | 1 + .../legend_location_settings.tsx | 4 +- .../legend_settings_popover.tsx | 20 +- .../value_labels_settings.test.tsx | 2 +- .../value_labels_settings.tsx | 2 +- x-pack/plugins/lens/public/types.ts | 14 +- .../__snapshots__/to_expression.test.ts.snap | 22 +- .../xy_visualization/annotations/helpers.tsx | 1 + .../axes_configuration.test.ts | 5 +- .../reference_line_helpers.tsx | 20 +- .../public/xy_visualization/state_helpers.ts | 13 +- .../xy_visualization/to_expression.test.ts | 71 +- .../public/xy_visualization/to_expression.ts | 159 +- .../lens/public/xy_visualization/types.ts | 16 +- .../public/xy_visualization/visualization.tsx | 43 +- .../annotations_panel.tsx | 505 +++ .../annotations_config_panel/icon_set.ts | 5 +- .../annotations_config_panel/index.test.tsx | 2 +- .../annotations_config_panel/index.tsx | 499 +-- .../xy_config_panel/color_picker.tsx | 8 +- .../xy_config_panel/dimension_editor.tsx | 6 +- .../xy_config_panel/index.tsx | 9 +- .../xy_config_panel/layer_header.tsx | 6 +- .../reference_line_config_panel/icon_set.ts | 67 + .../reference_line_config_panel/index.tsx | 8 + .../reference_line_panel.tsx | 39 +- .../xy_config_panel/shared/icon_select.tsx | 81 +- .../shared/marker_decoration_settings.tsx | 38 +- .../visual_options_popover/index.tsx | 5 +- .../make_lens_embeddable_factory.ts | 19 +- .../server/migrations/common_migrations.ts | 41 +- .../saved_object_migrations.test.ts | 49 + .../migrations/saved_object_migrations.ts | 21 +- .../plugins/lens/server/migrations/types.ts | 17 +- .../shared/exploratory_view/types.ts | 4 +- .../translations/translations/fr-FR.json | 9 +- .../translations/translations/ja-JP.json | 9 +- .../translations/translations/zh-CN.json | 8 +- 118 files changed, 7374 insertions(+), 3780 deletions(-) rename src/plugins/chart_expressions/expression_xy/common/expression_functions/{annotation_layer_config.ts => annotation_layer.ts} (69%) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts rename src/plugins/chart_expressions/expression_xy/common/expression_functions/{data_layer_config.test.ts => data_layer.test.ts} (70%) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config_fn.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/helpers/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/common/utils/index.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx delete mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/annotations_icon_set.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx create mode 100644 src/plugins/event_annotation/common/constants.ts create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/icon_set.ts create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/index.tsx rename x-pack/plugins/lens/public/xy_visualization/xy_config_panel/{ => reference_line_config_panel}/reference_line_panel.tsx (81%) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f5a6744845188..9f73dcd620d30 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -124,7 +124,7 @@ pageLoadAssetSize: visTypeGauge: 24113 unifiedSearch: 71059 data: 454087 - expressionXY: 26500 eventAnnotation: 19334 screenshotting: 22870 synthetics: 40958 + expressionXY: 29000 diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index f225846687e14..3a4a1fdb813fc 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -10,7 +10,7 @@ import { Position } from '@elastic/charts'; import type { PaletteOutput } from '@kbn/coloring'; import { Datatable, DatatableRow } from '@kbn/expressions-plugin'; import { LayerTypes } from '../constants'; -import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../types'; +import { DataLayerConfig, XYProps } from '../types'; export const mockPaletteOutput: PaletteOutput = { type: 'palette', @@ -46,9 +46,9 @@ export const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable = rows, }); -export const sampleLayer: DataLayerConfigResult = { - type: 'dataLayer', +export const sampleLayer: DataLayerConfig = { layerId: 'first', + type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'c', @@ -59,9 +59,12 @@ export const sampleLayer: DataLayerConfigResult = { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: createSampleDatatableWithRows([]), }; -export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLayer]): XYArgs => ({ +export const createArgsWithLayers = ( + layers: DataLayerConfig | DataLayerConfig[] = sampleLayer +): XYProps => ({ xTitle: '', yTitle: '', yRightTitle: '', @@ -104,25 +107,17 @@ export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLa mode: 'full', type: 'axisExtentConfig', }, - layers, + layers: Array.isArray(layers) ? layers : [layers], }); export function sampleArgs() { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: 'I', d: 'Foo' }, - { a: 1, b: 5, c: 'J', d: 'Bar' }, - ]), - }, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), - }, - }; - - const args: XYArgs = createArgsWithLayers(); + const data = createSampleDatatableWithRows([ + { a: 1, b: 2, c: 'I', d: 'Foo' }, + { a: 1, b: 5, c: 'J', d: 'Bar' }, + ]); - return { data, args }; + return { + data, + args: createArgsWithLayers({ ...sampleLayer, table: data }), + }; } diff --git a/src/plugins/chart_expressions/expression_xy/common/constants.ts b/src/plugins/chart_expressions/expression_xy/common/constants.ts index bf1e43b205843..931ece6ef8a78 100644 --- a/src/plugins/chart_expressions/expression_xy/common/constants.ts +++ b/src/plugins/chart_expressions/expression_xy/common/constants.ts @@ -7,16 +7,21 @@ */ export const XY_VIS = 'xyVis'; +export const LAYERED_XY_VIS = 'layeredXyVis'; export const Y_CONFIG = 'yConfig'; +export const EXTENDED_Y_CONFIG = 'extendedYConfig'; export const MULTITABLE = 'lens_multitable'; export const DATA_LAYER = 'dataLayer'; +export const EXTENDED_DATA_LAYER = 'extendedDataLayer'; export const LEGEND_CONFIG = 'legendConfig'; export const XY_VIS_RENDERER = 'xyVis'; export const GRID_LINES_CONFIG = 'gridlinesConfig'; export const ANNOTATION_LAYER = 'annotationLayer'; +export const EXTENDED_ANNOTATION_LAYER = 'extendedAnnotationLayer'; export const TICK_LABELS_CONFIG = 'tickLabelsConfig'; export const AXIS_EXTENT_CONFIG = 'axisExtentConfig'; export const REFERENCE_LINE_LAYER = 'referenceLineLayer'; +export const EXTENDED_REFERENCE_LINE_LAYER = 'extendedReferenceLineLayer'; export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig'; export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig'; @@ -106,6 +111,23 @@ export const XYCurveTypes = { export const ValueLabelModes = { HIDE: 'hide', - INSIDE: 'inside', - OUTSIDE: 'outside', + SHOW: 'show', +} 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; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts similarity index 69% rename from src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts index 1f46f12626f0b..6174b9d40e452 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts @@ -6,13 +6,14 @@ * 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 { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../types'; +import { strings } from '../i18n'; -export function annotationLayerConfigFunction(): ExpressionFunctionDefinition< +export function annotationLayerFunction(): ExpressionFunctionDefinition< typeof ANNOTATION_LAYER, - null, + Datatable, AnnotationLayerArgs, AnnotationLayerConfigResult > { @@ -20,21 +21,17 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition< name: ANNOTATION_LAYER, aliases: [], type: ANNOTATION_LAYER, - inputTypes: ['null'], - help: 'Annotation layer in lens', + inputTypes: ['datatable'], + help: strings.getAnnotationLayerFnHelp(), args: { - layerId: { - types: ['string'], - help: '', - }, hide: { types: ['boolean'], default: false, - help: 'Show details', + help: strings.getAnnotationLayerHideHelp(), }, annotations: { types: ['manual_point_event_annotation', 'manual_range_event_annotation'], - help: '', + help: strings.getAnnotationLayerAnnotationsHelp(), multi: true, }, }, @@ -42,6 +39,7 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition< return { type: ANNOTATION_LAYER, ...args, + annotations: args.annotations ?? [], layerType: LayerTypes.ANNOTATIONS, }; }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts index e65550c7aeeef..c1a4070225a64 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts @@ -11,6 +11,13 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/commo import { AxisExtentConfig, AxisExtentConfigResult } from '../types'; 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< typeof AXIS_EXTENT_CONFIG, null, @@ -27,10 +34,12 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition< args: { mode: { types: ['string'], - options: [...Object.values(AxisExtentModes)], help: i18n.translate('expressionXY.axisExtentConfig.extentMode.help', { defaultMessage: 'The extent mode', }), + options: [...Object.values(AxisExtentModes)], + strict: true, + default: AxisExtentModes.FULL, }, lowerBound: { types: ['number'], @@ -46,6 +55,16 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition< }, }, 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 { type: AXIS_EXTENT_CONFIG, ...args, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts new file mode 100644 index 0000000000000..49446310a894b --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -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}', + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts new file mode 100644 index 0000000000000..f338e08a88940 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_reference_line_layer_args.ts @@ -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(), + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts new file mode 100644 index 0000000000000..f80d814571076 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts @@ -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(), + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts new file mode 100644 index 0000000000000..76ac6ba2a1a97 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts @@ -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(), + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.test.ts similarity index 70% rename from src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.test.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.test.ts index 7f8bf30d956ff..518690d47bfcb 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.test.ts @@ -7,15 +7,15 @@ */ import { DataLayerArgs } from '../types'; -import { dataLayerConfigFunction } from '.'; import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; -import { mockPaletteOutput } from '../__mocks__'; +import { mockPaletteOutput, sampleArgs } from '../__mocks__'; import { LayerTypes } from '../constants'; +import { dataLayerFunction } from './data_layer'; describe('dataLayerConfig', () => { test('produces the correct arguments', () => { + const { data } = sampleArgs(); const args: DataLayerArgs = { - layerId: 'first', seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], @@ -26,8 +26,13 @@ describe('dataLayerConfig', () => { 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, + }); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts new file mode 100644 index 0000000000000..f36a0ea4c101f --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts @@ -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, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.ts deleted file mode 100644 index d09b8481a6842..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.ts +++ /dev/null @@ -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, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts new file mode 100644 index 0000000000000..539c11854355c --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts @@ -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, + }; + }, + }; +} diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts new file mode 100644 index 0000000000000..84c1213fc069d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.ts @@ -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, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts new file mode 100644 index 0000000000000..4f75838bea114 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_reference_line_layer.ts @@ -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, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts new file mode 100644 index 0000000000000..606cdd84ac710 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts @@ -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, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts index 5c7e013a91332..ab1d570a07351 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts @@ -7,13 +7,18 @@ */ export * from './xy_vis'; +export * from './layered_xy_vis'; 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 './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 './axis_extent_config'; export * from './tick_labels_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'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts new file mode 100644 index 0000000000000..6b926e1ceff05 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis.ts @@ -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); + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts new file mode 100644 index 0000000000000..4b7de0eba3166 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts @@ -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, + }, + }, + }; +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.test.ts index 9d58903e93c62..48e6d1c956acb 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.test.ts @@ -12,9 +12,9 @@ import { LegendConfig } from '../types'; import { legendConfigFunction } from './legend_config'; describe('legendConfigFunction', () => { - test('produces the correct arguments', () => { + test('produces the correct arguments', async () => { 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 }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.ts index 65f8a725518a3..2b383f1899d44 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.ts @@ -8,16 +8,10 @@ import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { LEGEND_CONFIG } from '../constants'; -import { LegendConfig, LegendConfigResult } from '../types'; +import { LegendConfigFn } from '../types'; -export const legendConfigFunction: ExpressionFunctionDefinition< - typeof LEGEND_CONFIG, - null, - LegendConfig, - LegendConfigResult -> = { +export const legendConfigFunction: LegendConfigFn = { name: LEGEND_CONFIG, aliases: [], type: LEGEND_CONFIG, @@ -31,6 +25,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition< help: i18n.translate('expressionXY.legendConfig.isVisible.help', { defaultMessage: 'Specifies whether or not the legend is visible.', }), + default: true, }, position: { types: ['string'], @@ -38,6 +33,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition< help: i18n.translate('expressionXY.legendConfig.position.help', { defaultMessage: 'Specifies the legend position.', }), + strict: true, }, showSingleSeries: { types: ['boolean'], @@ -58,6 +54,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition< defaultMessage: 'Specifies the horizontal alignment of the legend when it is displayed inside chart.', }), + strict: true, }, verticalAlignment: { types: ['string'], @@ -66,6 +63,7 @@ export const legendConfigFunction: ExpressionFunctionDefinition< defaultMessage: 'Specifies the vertical alignment of the legend when it is displayed inside chart.', }), + strict: true, }, floatingColumns: { types: ['number'], @@ -93,10 +91,8 @@ export const legendConfigFunction: ExpressionFunctionDefinition< }), }, }, - fn(input, args) { - return { - type: LEGEND_CONFIG, - ...args, - }; + async fn(input, args, handlers) { + const { legendConfigFn } = await import('./legend_config_fn'); + return await legendConfigFn(input, args, handlers); }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config_fn.ts new file mode 100644 index 0000000000000..35df125ae230f --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config_fn.ts @@ -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 }; +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts new file mode 100644 index 0000000000000..9c6e27c958530 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts @@ -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, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_config.ts deleted file mode 100644 index 46f6e7671c0ab..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_config.ts +++ /dev/null @@ -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, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts new file mode 100644 index 0000000000000..55d7cb12382c0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -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) => + layers.filter(({ seriesType }) => seriesType.includes('bar')).length > 0; + +export const hasAreaLayer = (layers: Array) => + layers.filter(({ seriesType }) => seriesType.includes('area')).length > 0; + +export const hasHistogramBarLayer = ( + layers: Array +) => + 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 +) => { + 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 +) => { + 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()); + } +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 27e68f0a49891..688efbe122f3e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -8,14 +8,23 @@ import { xyVisFunction } from '.'; import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; -import { sampleArgs } from '../__mocks__'; +import { sampleArgs, sampleLayer } from '../__mocks__'; import { XY_VIS } from '../constants'; 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 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] } }, + }); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 227b7553d6414..2e97cb00b3e55 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -6,243 +6,36 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin'; -import { prepareLogTable } from '@kbn/visualizations-plugin/common/utils'; -import { LensMultiTable, XYArgs, XYRender } from '../types'; -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'; +import { XyVisFn } from '../types'; +import { XY_VIS, DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants'; +import { strings } from '../i18n'; +import { commonXYArgs } from './common_xy_args'; -const strings = { - 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 -> = { +export const xyVisFunction: XyVisFn = { name: XY_VIS, type: 'render', - inputTypes: [MULTITABLE], - help: i18n.translate('expressionXY.xyVis.help', { - defaultMessage: 'An X/Y chart', - }), + inputTypes: ['datatable'], + help: strings.getXYHelp(), args: { - title: { - types: ['string'], - help: 'The chart title.', - }, - 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', - }), + ...commonXYArgs, + dataLayers: { + types: [DATA_LAYER], + help: strings.getDataLayerHelp(), multi: true, }, - curveType: { - types: ['string'], - options: [...Object.values(XYCurveTypes)], - help: i18n.translate('expressionXY.xyVis.curveType.help', { - defaultMessage: 'Define how curve type is rendered for a line chart', - }), - }, - fillOpacity: { - types: ['number'], - help: i18n.translate('expressionXY.xyVis.fillOpacity.help', { - defaultMessage: 'Define the area chart fill opacity', - }), - }, - 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', - }), + referenceLineLayers: { + types: [REFERENCE_LINE_LAYER], + help: strings.getReferenceLineLayerHelp(), + multi: true, }, - ariaLabel: { - types: ['string'], - help: i18n.translate('expressionXY.xyVis.ariaLabel.help', { - defaultMessage: 'Specifies the aria label of the xy chart', - }), - required: false, + annotationLayers: { + types: [ANNOTATION_LAYER], + help: strings.getAnnotationLayerHelp(), + multi: true, }, }, - fn(data, args, handlers) { - if (handlers?.inspectorAdapters?.tables) { - args.layers.forEach((layer) => { - 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, - }, - }, - }; + async fn(data, args, handlers) { + const { xyVisFn } = await import('./xy_vis_fn'); + return await xyVisFn(data, args, handlers); }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts new file mode 100644 index 0000000000000..1bd75e1296c6c --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -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((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, + }, + }, + }; +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts index 943c6910952a0..882a3231148f5 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts @@ -6,84 +6,18 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; -import { FillStyles, IconPositions, LineStyles, YAxisModes, Y_CONFIG } from '../constants'; -import { YConfig, YConfigResult } from '../types'; +import { Y_CONFIG } from '../constants'; +import { YConfigFn } from '../types'; +import { strings } from '../i18n'; +import { commonYConfigArgs } from './common_y_config_args'; -export const yAxisConfigFunction: ExpressionFunctionDefinition< - typeof Y_CONFIG, - null, - YConfig, - YConfigResult -> = { +export const yAxisConfigFunction: YConfigFn = { name: Y_CONFIG, aliases: [], type: Y_CONFIG, - help: i18n.translate('expressionXY.yConfig.help', { - defaultMessage: `Configure the behavior of a xy chart's y axis metric`, - }), + help: strings.getYConfigFnHelp(), inputTypes: ['null'], - args: { - 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', - }), - }, - }, + args: { ...commonYConfigArgs }, fn(input, args) { return { type: Y_CONFIG, diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts new file mode 100644 index 0000000000000..55c4136e0c00d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/index.ts @@ -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'; diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts new file mode 100644 index 0000000000000..ac44ef18fc505 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts @@ -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); + }); +}); diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts new file mode 100644 index 0000000000000..d62ea264acb1a --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts @@ -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(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( + layers: Array, + keyword: string +): Array { + return layers + .filter((l): l is T => l !== undefined) + .map((l, index) => ({ + ...l, + layerId: isWithLayerId(l) ? l.layerId : generateLayerId(keyword, index), + })); +} diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx new file mode 100644 index 0000000000000..225f9de0d6a7c --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -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', + }), +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts index 68f9f946baeb0..4bee4a3e7f2b6 100755 --- a/src/plugins/chart_expressions/expression_xy/common/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -9,20 +9,6 @@ export const PLUGIN_ID = 'expressionXy'; export const PLUGIN_NAME = 'expressionXy'; -export { - xyVisFunction, - yAxisConfigFunction, - legendConfigFunction, - gridlinesConfigFunction, - dataLayerConfigFunction, - axisExtentConfigFunction, - tickLabelsConfigFunction, - annotationLayerConfigFunction, - labelsOrientationConfigFunction, - referenceLineLayerConfigFunction, - axisTitlesVisibilityConfigFunction, -} from './expression_functions'; - export type { XYArgs, YConfig, @@ -42,25 +28,37 @@ export type { XYChartProps, LegendConfig, IconPosition, - YConfigResult, DataLayerArgs, LensMultiTable, ValueLabelMode, AxisExtentMode, + DataLayerConfig, FittingFunction, + ExtendedYConfig, AxisExtentConfig, + CollectiveConfig, LegendConfigResult, AxesSettingsConfig, + CommonXYLayerConfig, AnnotationLayerArgs, - XYLayerConfigResult, + ExtendedYConfigResult, GridlinesConfigResult, DataLayerConfigResult, TickLabelsConfigResult, AxisExtentConfigResult, ReferenceLineLayerArgs, + CommonXYDataLayerConfig, LabelsOrientationConfig, - AnnotationLayerConfigResult, + ReferenceLineLayerConfig, + AvailableReferenceLineIcon, + XYExtendedLayerConfigResult, + CommonXYAnnotationLayerConfig, + ExtendedDataLayerConfigResult, LabelsOrientationConfigResult, + CommonXYDataLayerConfigResult, ReferenceLineLayerConfigResult, + CommonXYReferenceLineLayerConfig, AxisTitlesVisibilityConfigResult, + ExtendedReferenceLineLayerConfigResult, + CommonXYReferenceLineLayerConfigResult, } from './types'; diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index a1984c78fe0ac..b3c7bca93ca29 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -9,7 +9,7 @@ import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; import { $Values } from '@kbn/utility-types'; 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 { AxisExtentModes, @@ -34,9 +34,17 @@ import { LEGEND_CONFIG, DATA_LAYER, AXIS_EXTENT_CONFIG, + EXTENDED_DATA_LAYER, + EXTENDED_REFERENCE_LINE_LAYER, ANNOTATION_LAYER, EndValues, + EXTENDED_Y_CONFIG, + AvailableReferenceLineIcons, + XY_VIS, + LAYERED_XY_VIS, + EXTENDED_ANNOTATION_LAYER, } from '../constants'; +import { XYRender } from './expression_renderers'; export type EndValue = $Values; export type LayerType = $Values; @@ -51,6 +59,7 @@ export type IconPosition = $Values; export type ValueLabelMode = $Values; export type AxisExtentMode = $Values; export type FittingFunction = $Values; +export type AvailableReferenceLineIcon = $Values; export interface AxesSettingsConfig { x: boolean; @@ -69,11 +78,8 @@ export interface AxisConfig { hide?: boolean; } -export interface YConfig { - forAccessor: string; - axisMode?: YAxisMode; - color?: string; - icon?: string; +export interface ExtendedYConfig extends YConfig { + icon?: AvailableReferenceLineIcon; lineWidth?: number; lineStyle?: LineStyle; fill?: FillStyle; @@ -81,12 +87,32 @@ export interface YConfig { textVisibility?: boolean; } +export interface YConfig { + forAccessor: string; + axisMode?: YAxisMode; + color?: string; +} + +export interface DataLayerArgs { + 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[]; +} + export interface ValidLayer extends DataLayerConfigResult { xAccessor: NonNullable; } -export interface DataLayerArgs { - layerId: string; +export interface ExtendedDataLayerArgs { + layerId?: string; accessors: string[]; seriesType: SeriesType; xAccessor?: string; @@ -96,9 +122,9 @@ export interface DataLayerArgs { yScaleType: YScaleType; xScaleType: XScaleType; isHistogram: boolean; - // palette will always be set on the expression palette: PaletteOutput; yConfig?: YConfigResult[]; + table?: Datatable; } export interface LegendConfig { @@ -121,11 +147,11 @@ export interface LegendConfig { /** * 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 */ - verticalAlignment?: VerticalAlignment; + verticalAlignment?: typeof VerticalAlignment.Top | typeof VerticalAlignment.Bottom; /** * Number of columns when legend is set inside chart */ @@ -155,16 +181,62 @@ export interface LabelsOrientationConfig { // Arguments to XY chart expression, with computed properties export interface XYArgs { - title?: string; - description?: string; xTitle: string; yTitle: string; yRightTitle: string; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; legend: LegendConfigResult; + endValue?: EndValue; + emphasizeFitting?: boolean; valueLabels: ValueLabelMode; - layers: XYLayerConfigResult[]; + 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; + yTitle: string; + yRightTitle: string; + yLeftExtent: AxisExtentConfigResult; + yRightExtent: AxisExtentConfigResult; + legend: LegendConfigResult; + valueLabels: ValueLabelMode; + layers: CommonXYLayerConfig[]; endValue?: EndValue; emphasizeFitting?: boolean; fittingFunction?: FittingFunction; @@ -181,28 +253,48 @@ export interface XYArgs { export interface AnnotationLayerArgs { annotations: EventAnnotationOutput[]; - layerId: string; hide?: boolean; } +export type ExtendedAnnotationLayerArgs = AnnotationLayerArgs & { + layerId?: string; +}; + export type AnnotationLayerConfigResult = AnnotationLayerArgs & { type: typeof ANNOTATION_LAYER; layerType: typeof LayerTypes.ANNOTATIONS; }; +export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & { + type: typeof EXTENDED_ANNOTATION_LAYER; + layerType: typeof LayerTypes.ANNOTATIONS; +}; + export interface ReferenceLineLayerArgs { - layerId: string; accessors: 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 XYLayerConfig = DataLayerConfig | ReferenceLineLayerConfig | AnnotationLayerConfig; +export type XYExtendedLayerConfig = + | ExtendedDataLayerConfig + | ExtendedReferenceLineLayerConfig + | ExtendedAnnotationLayerConfig; -export type XYLayerConfigResult = - | DataLayerConfigResult - | ReferenceLineLayerConfigResult - | AnnotationLayerConfigResult; +export type XYExtendedLayerConfigResult = + | ExtendedDataLayerConfigResult + | ExtendedReferenceLineLayerConfigResult + | ExtendedAnnotationLayerConfigResult; export interface LensMultiTable { type: typeof MULTITABLE; @@ -216,14 +308,43 @@ export interface LensMultiTable { export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { type: typeof REFERENCE_LINE_LAYER; 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 & { type: typeof DATA_LAYER; 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 & { + type: typeof EXTENDED_DATA_LAYER; + layerType: typeof LayerTypes.DATA; + palette: PaletteOutput; + table: Datatable; }; export type YConfigResult = YConfig & { type: typeof Y_CONFIG }; +export type ExtendedYConfigResult = ExtendedYConfig & { type: typeof EXTENDED_Y_CONFIG }; export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & { 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 GridlinesConfigResult = AxesSettingsConfig & { type: typeof GRID_LINES_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 +>; +export type LayeredXyVisFn = ExpressionFunctionDefinition< + typeof LAYERED_XY_VIS, + Datatable, + LayeredXYArgs, + Promise +>; + +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; +export type ExtendedYConfigFn = ExpressionFunctionDefinition< + typeof EXTENDED_Y_CONFIG, + null, + ExtendedYConfig, + ExtendedYConfigResult +>; + +export type LegendConfigFn = ExpressionFunctionDefinition< + typeof LEGEND_CONFIG, + null, + LegendConfig, + Promise +>; diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts index 1acb98903d06b..b03ea975b0143 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -6,12 +6,16 @@ * 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 { LensMultiTable, XYArgs } from './expression_functions'; +import { XYProps } from './expression_functions'; export interface XYChartProps { - data: LensMultiTable; - args: XYArgs; + args: XYProps; } export interface XYRender { @@ -19,3 +23,10 @@ export interface XYRender { as: typeof XY_VIS_RENDERER; value: XYChartProps; } + +export interface CollectiveConfig extends Omit { + roundedTimestamp: number; + axisMode: 'bottom'; + icon?: AvailableAnnotationIcon | string; + customTooltipDetails?: AnnotationTooltipFormatter | undefined; +} diff --git a/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx b/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx new file mode 100644 index 0000000000000..ba40d5768f50c --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/utils/index.tsx @@ -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'; diff --git a/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts b/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts new file mode 100644 index 0000000000000..79a3cbd2eef19 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/utils/log_datatables.ts @@ -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()], + ]; +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx index 60b9f6a5788b4..194bfc2bf5c9d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx @@ -6,9 +6,11 @@ * Side Public License, v 1. */ +import { Datatable } from '@kbn/expressions-plugin/common'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../../common'; +import { LensMultiTable } from '../../common'; import { LayerTypes } from '../../common/constants'; +import { DataLayerConfig, XYProps } from '../../common/types'; import { mockPaletteOutput, sampleArgs } from '../../common/__mocks__'; const chartSetupContract = chartPluginMock.createSetupContract(); @@ -166,9 +168,9 @@ export const dateHistogramData: LensMultiTable = { }, }; -export const dateHistogramLayer: DataLayerConfigResult = { +export const dateHistogramLayer: DataLayerConfig = { + layerId: 'dateHistogramLayer', type: 'dataLayer', - layerId: 'timeLayer', layerType: LayerTypes.DATA, hide: false, xAccessor: 'xAccessorId', @@ -179,46 +181,37 @@ export const dateHistogramLayer: DataLayerConfigResult = { seriesType: 'bar_stacked', accessors: ['yAccessorId'], palette: mockPaletteOutput, + table: dateHistogramData.tables.timeLayer, }; export function sampleArgsWithReferenceLine(value: number = 150) { - const { data, args } = sampleArgs(); + const { args: sArgs } = sampleArgs(); + const data: Datatable = { + type: 'datatable', + columns: [ + { + id: 'referenceLine-a', + meta: { params: { id: 'number' }, type: 'number' }, + name: 'Static value', + }, + ], + rows: [{ 'referenceLine-a': value }], + }; - return { - data: { - ...data, - tables: { - ...data.tables, - referenceLine: { - type: 'datatable', - columns: [ - { - id: 'referenceLine-a', - meta: { params: { id: 'number' }, type: 'number' }, - name: 'Static value', - }, - ], - rows: [{ 'referenceLine-a': value }], - }, + const args: XYProps = { + ...sArgs, + layers: [ + ...sArgs.layers, + { + layerId: 'referenceLine-a', + type: 'referenceLineLayer', + layerType: LayerTypes.REFERENCELINE, + accessors: ['referenceLine-a'], + yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'extendedYConfig' }], + table: data, }, - } as LensMultiTable, - args: { - ...args, - layers: [ - ...args.layers, - { - layerType: LayerTypes.REFERENCELINE, - accessors: ['referenceLine-a'], - layerId: 'referenceLine', - seriesType: 'line', - xScaleType: 'linear', - yScaleType: 'linear', - palette: mockPaletteOutput, - isHistogram: false, - hide: true, - yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'yConfig' }], - }, - ], - } as XYArgs, + ], }; + + return { data, args }; } diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index a90577d8bca03..3b11ee812da6f 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -398,127 +398,425 @@ exports[`XYChart component it renders area 1`] = ` darkMode={false} histogramMode={false} /> - - `; @@ -641,139 +939,425 @@ exports[`XYChart component it renders bar 1`] = ` darkMode={false} histogramMode={false} /> - - `; @@ -896,139 +1480,425 @@ exports[`XYChart component it renders horizontal bar 1`] = ` darkMode={false} histogramMode={false} /> - - `; @@ -1151,127 +2021,425 @@ exports[`XYChart component it renders line 1`] = ` darkMode={false} histogramMode={false} /> - - `; @@ -1394,135 +2562,425 @@ exports[`XYChart component it renders stacked area 1`] = ` darkMode={false} histogramMode={false} /> - - `; @@ -1645,147 +3103,425 @@ exports[`XYChart component it renders stacked bar 1`] = ` darkMode={false} histogramMode={false} /> - - `; @@ -1908,147 +3644,425 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = ` darkMode={false} histogramMode={false} /> - - `; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index ffed221b921c1..fa2c081f08700 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -29,7 +29,12 @@ import { defaultAnnotationColor, defaultAnnotationRangeColor, } 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 { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers'; @@ -52,12 +57,6 @@ export interface AnnotationsProps { outsideDimension: number; } -interface CollectiveConfig extends ManualPointEventAnnotationArgs { - roundedTimestamp: number; - axisMode: 'bottom'; - customTooltipDetails?: AnnotationTooltipFormatter | undefined; -} - const groupVisibleConfigsByInterval = ( layers: AnnotationLayerArgs[], minInterval?: number, @@ -131,7 +130,7 @@ const getCommonStyles = (configArr: ManualPointEventAnnotationArgs[]) => { }; }; -export const getRangeAnnotations = (layers: AnnotationLayerConfigResult[]) => { +export const getRangeAnnotations = (layers: CommonXYAnnotationLayerConfig[]) => { return layers .flatMap(({ annotations }) => annotations.filter( @@ -146,7 +145,7 @@ export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8; export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2; export const getAnnotationsGroupedByInterval = ( - layers: AnnotationLayerConfigResult[], + layers: CommonXYAnnotationLayerConfig[], minInterval?: number, firstTimestamp?: number, formatter?: FieldFormat diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx new file mode 100644 index 0000000000000..1166d41a9e402 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -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 = ({ + 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 = 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 ( + + ); + 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 ; + case SeriesTypes.AREA_STACKED: + case SeriesTypes.AREA_PERCENTAGE_STACKED: + return ( + + ); + case SeriesTypes.AREA: + return ( + + ); + } + }) + )} + + ); +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx index 0f1cdebc5bf59..7c60a6a3a5769 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx @@ -11,27 +11,12 @@ import { LegendActionProps, SeriesIdentifier } from '@elastic/charts'; import { EuiPopover } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { ComponentType, ReactWrapper } from 'enzyme'; -import type { LensMultiTable } from '../../common'; +import type { DataLayerConfig, LensMultiTable } from '../../common'; import { LayerTypes } from '../../common/constants'; -import type { DataLayerArgs } from '../../common'; import { getLegendAction } from './legend_action'; import { LegendActionPopover } from './legend_action_popover'; 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 = { first: { type: 'datatable', @@ -168,11 +153,26 @@ const 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 () { let wrapperProps: LegendActionProps; const Component: ComponentType = getLegendAction( [sampleLayer], - tables, jest.fn(), jest.fn(), {} diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx index 9bbdec3635fa8..da1939f223649 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx @@ -9,23 +9,28 @@ import React from 'react'; import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; import type { FilterEvent } from '../types'; -import type { LensMultiTable, DataLayerArgs } from '../../common'; +import type { CommonXYDataLayerConfig } from '../../common'; import type { FormatFactory } from '../types'; import { LegendActionPopover } from './legend_action_popover'; +import { DatatablesWithFormatInfo } from '../helpers'; export const getLegendAction = ( - filteredLayers: DataLayerArgs[], - tables: LensMultiTable['tables'], + dataLayers: CommonXYDataLayerConfig[], onFilter: (data: FilterEvent['data']) => void, formatFactory: FormatFactory, - layersAlreadyFormatted: Record + formattedDatatables: DatatablesWithFormatInfo ): LegendAction => React.memo(({ series: [xySeries] }) => { 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())) ); + if (layerIndex === -1) { + return null; + } + + const layer = dataLayers[layerIndex]; if (!layer || !layer.splitAccessor) { return null; } @@ -33,12 +38,12 @@ export const getLegendAction = ( const splitLabel = series.seriesKeys[0] as string; const accessor = layer.splitAccessor; - const table = tables[layer.layerId]; + const { table } = layer; const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor); const formatter = formatFactory(splitColumn && splitColumn.meta?.params); const rowIndex = table.rows.findIndex((row) => { - if (layersAlreadyFormatted[accessor]) { + if (formattedDatatables[layer.layerId]?.formattedColumns[accessor]) { // stringify the value to compare with the chart value return formatter.convert(row[accessor]) === splitLabel; } @@ -63,7 +68,7 @@ export const getLegendAction = ( return ( = { @@ -23,34 +28,28 @@ const row: Record = { yAccessorRightSecondId: 10, }; -const histogramData: LensMultiTable = { - type: 'lens_multitable', - tables: { - firstLayer: { - type: 'datatable', - rows: [row], - columns: Object.keys(row).map((id) => ({ - id, - name: `Static value: ${row[id]}`, - meta: { - type: 'number', - params: { id: 'number' }, - }, - })), +const data: Datatable = { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((id) => ({ + id, + name: `Static value: ${row[id]}`, + meta: { + type: '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 [ { - layerId: 'firstLayer', + layerId: 'first', accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), yConfig: yConfigs, + type: 'referenceLineLayer', + layerType: LayerTypes.REFERENCELINE, + table: data, }, ]; } @@ -64,7 +63,7 @@ interface XCoords { 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'; } @@ -95,21 +94,20 @@ describe('ReferenceLineAnnotations', () => { ['yAccessorLeft', 'below'], ['yAccessorRight', 'above'], ['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', (layerPrefix, fill) => { const axisMode = getAxisFromId(layerPrefix); const wrapper = shallow( @@ -135,19 +133,18 @@ describe('ReferenceLineAnnotations', () => { it.each([ ['xAccessor', 'above'], ['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', (layerPrefix, fill) => { const wrapper = shallow( { ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['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', (layerPrefix, fill, coordsA, coordsB) => { const axisMode = getAxisFromId(layerPrefix); const wrapper = shallow( { it.each([ ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], ['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', (layerPrefix, fill, coordsA, coordsB) => { const wrapper = shallow( { const wrapper = shallow( @@ -326,27 +320,26 @@ describe('ReferenceLineAnnotations', () => { it.each([ ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['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', (fill, coordsA, coordsB) => { const wrapper = shallow( diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx index 6146cc379227d..d17dbf2a70ad1 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx @@ -13,8 +13,7 @@ import { groupBy } from 'lodash'; import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; import { euiLightVars } from '@kbn/ui-theme'; import type { FieldFormat } from '@kbn/field-formats-plugin/common'; -import type { IconPosition, ReferenceLineLayerArgs, YAxisMode } from '../../common/types'; -import type { LensMultiTable } from '../../common/types'; +import type { CommonXYReferenceLineLayerConfig, IconPosition, YAxisMode } from '../../common/types'; import { LINES_MARKER_SIZE, mapVerticalToHorizontalPlacement, @@ -89,8 +88,7 @@ export function getBaseIconPlacement( } export interface ReferenceLineAnnotationsProps { - layers: ReferenceLineLayerArgs[]; - data: LensMultiTable; + layers: CommonXYReferenceLineLayerConfig[]; formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; axesMap: Record<'left' | 'right', boolean>; isHorizontal: boolean; @@ -99,7 +97,6 @@ export interface ReferenceLineAnnotationsProps { export const ReferenceLineAnnotations = ({ layers, - data, formatters, axesMap, isHorizontal, @@ -111,11 +108,10 @@ export const ReferenceLineAnnotations = ({ if (!layer.yConfig) { return []; } - const { columnToLabel, yConfig: yConfigs, layerId } = layer; + const { columnToLabel, yConfig: yConfigs, table } = layer; const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; - const table = data.tables[layerId]; const row = table.rows[0]; @@ -194,8 +190,8 @@ export const ReferenceLineAnnotations = ({ annotations.push( ({ dataValue: row[yConfig.forAccessor], header: columnToLabelMap[yConfig.forAccessor], @@ -225,8 +221,8 @@ export const ReferenceLineAnnotations = ({ annotations.push( { const nextValue = shouldCheckNextReferenceLine ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] diff --git a/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx index 257d299616946..78b6ef91926a8 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx @@ -11,7 +11,7 @@ import React from 'react'; import moment from 'moment'; import { Endzones } from '@kbn/charts-plugin/public'; import { search } from '@kbn/data-plugin/public'; -import type { LensMultiTable, DataLayerArgs } from '../../common'; +import type { CommonXYDataLayerConfig } from '../../common'; export interface XDomain { min?: number; @@ -19,17 +19,16 @@ export interface XDomain { minInterval?: number; } -export const getAppliedTimeRange = (layers: DataLayerArgs[], data: LensMultiTable) => { - return Object.entries(data.tables) - .map(([tableId, table]) => { - const layer = layers.find((l) => l.layerId === tableId); - const xColumn = table.columns.find((col) => col.id === layer?.xAccessor); +export const getAppliedTimeRange = (layers: CommonXYDataLayerConfig[]) => { + return layers + .map(({ xAccessor, table }) => { + const xColumn = table.columns.find((col) => col.id === xAccessor); const timeRange = xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange; if (timeRange) { return { timeRange, - field: xColumn.meta.field, + field: xColumn?.meta.field, }; } }) @@ -37,13 +36,12 @@ export const getAppliedTimeRange = (layers: DataLayerArgs[], data: LensMultiTabl }; export const getXDomain = ( - layers: DataLayerArgs[], - data: LensMultiTable, + layers: CommonXYDataLayerConfig[], minInterval: number | undefined, isTimeViz: boolean, isHistogram: boolean ) => { - const appliedTimeRange = getAppliedTimeRange(layers, data)?.timeRange; + const appliedTimeRange = getAppliedTimeRange(layers)?.timeRange; const from = appliedTimeRange?.from; const to = appliedTimeRange?.to; const baseDomain = isTimeViz @@ -59,8 +57,8 @@ export const getXDomain = ( if (isHistogram && isFullyQualified(baseDomain)) { const xValues = uniq( layers - .flatMap((layer) => - data.tables[layer.layerId].rows.map((row) => row[layer.xAccessor!].valueOf() as number) + .flatMap(({ table, xAccessor }) => + table.rows.map((row) => row[xAccessor!].valueOf()) ) .sort() ); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 10f53ec2572a8..de67e814d5b78 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -9,13 +9,6 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { - AnnotationLayerConfigResult, - DataLayerConfigResult, - LensMultiTable, - XYArgs, -} from '../../common'; -import { LayerTypes } from '../../common/constants'; import { AreaSeries, Axis, @@ -34,7 +27,12 @@ import { VerticalAlignment, XYChartSeriesIdentifier, } from '@elastic/charts'; +import { Datatable } from '@kbn/expressions-plugin/common'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; +import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; +import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; +import { DataLayerConfig } from '../../common'; +import { LayerTypes } from '../../common/constants'; import { XyEndzones } from './x_domain'; import { chartsActiveCursorService, @@ -52,8 +50,12 @@ import { sampleLayer, } from '../../common/__mocks__'; import { XYChart, XYChartRenderProps } from './xy_chart'; -import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; -import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; +import { + CommonXYAnnotationLayerConfig, + ExtendedDataLayerConfig, + XYProps, +} from '../../common/types'; +import { DataLayers } from './data_layers'; import { Annotations } from './annotations'; const onClickValue = jest.fn(); @@ -64,45 +66,36 @@ describe('XYChart component', () => { let convertSpy: jest.Mock; let defaultProps: Omit; - const dataWithoutFormats: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'number' } }, - { id: 'c', name: 'c', meta: { type: 'string' } }, - { id: 'd', name: 'd', meta: { type: 'string' } }, - ], - rows: [ - { a: 1, b: 2, c: 'I', d: 'Row 1' }, - { a: 1, b: 5, c: 'J', d: 'Row 2' }, - ], - }, - }, + const dataWithoutFormats: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + { id: 'd', name: 'd', meta: { type: 'string' } }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], }; - const dataWithFormats: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'number' } }, - { id: 'c', name: 'c', meta: { type: 'string' } }, - { id: 'd', name: 'd', meta: { type: 'string', params: { id: 'custom' } } }, - ], - rows: [ - { a: 1, b: 2, c: 'I', d: 'Row 1' }, - { a: 1, b: 5, c: 'J', d: 'Row 2' }, - ], - }, - }, + + const dataWithFormats: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + { id: 'd', name: 'd', meta: { type: 'string', params: { id: 'custom' } } }, + ], + rows: [ + { a: 1, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], }; - const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { - return shallow(); + const getRenderedComponent = (args: XYProps) => { + return shallow(); }; beforeEach(() => { @@ -128,28 +121,31 @@ describe('XYChart component', () => { }); test('it renders line', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(LineSeries)).toHaveLength(2); - expect(component.find(LineSeries).at(0).prop('yAccessors')).toEqual(['a']); - expect(component.find(LineSeries).at(1).prop('yAccessors')).toEqual(['b']); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries).toHaveLength(2); + expect(lineSeries.at(0).prop('yAccessors')).toEqual(['a']); + expect(lineSeries.at(1).prop('yAccessors')).toEqual(['b']); }); describe('date range', () => { - const timeSampleLayer: DataLayerConfigResult = { + const { data, args } = sampleArgs(); + + const timeSampleLayer: DataLayerConfig = { + layerId: 'timeLayer', type: 'dataLayer', - layerId: 'first', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'c', @@ -160,56 +156,47 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: { + ...data, + columns: data.columns.map((c) => + c.id !== 'c' + ? c + : { + ...c, + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2019-01-02T05:00:00.000Z', + to: '2019-01-03T05:00:00.000Z', + }, + }, + }, + } + ), + }, }; + const multiLayerArgs = createArgsWithLayers([ timeSampleLayer, - { - ...timeSampleLayer, - layerId: 'second', - seriesType: 'bar', - xScaleType: 'time', - }, + { ...timeSampleLayer, seriesType: 'bar', xScaleType: 'time' }, ]); - test('it uses the full date range', () => { - const { data, args } = sampleArgs(); + test('it uses the full date range', () => { const component = shallow( - c.id !== 'c' - ? c - : { - ...c, - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: 'date_histogram', - params: {}, - appliedTimeRange: { - from: '2019-01-02T05:00:00.000Z', - to: '2019-01-03T05:00:00.000Z', - }, - }, - }, - } - ), - }, - }, - }} args={{ ...args, layers: [ { - ...(args.layers[0] as DataLayerConfigResult), + ...(args.layers[0] as DataLayerConfig), seriesType: 'line', xScaleType: 'time', + table: timeSampleLayer.table, }, ], }} @@ -226,15 +213,21 @@ describe('XYChart component', () => { }); test('it uses passed in minInterval', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]), - second: createSampleDatatableWithRows([]), - }, - }; + const table1 = createSampleDatatableWithRows([{ a: 1, b: 2, c: 'I', d: 'Foo' }]); + const table2 = createSampleDatatableWithRows([]); - const component = shallow(); + const component = shallow( + + ); // real auto interval is 30mins = 1800000 expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` @@ -247,9 +240,9 @@ describe('XYChart component', () => { }); describe('axis time', () => { - const defaultTimeLayer: DataLayerConfigResult = { + const defaultTimeLayer: DataLayerConfig = { + layerId: 'defaultTimeLayer', type: 'dataLayer', - layerId: 'first', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'c', @@ -260,21 +253,28 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: true, palette: mockPaletteOutput, + table: data, + }; + + const newData = { + ...data, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, }; - test('it should disable the new time axis for a line time layer when isHistogram is set to false', () => { - const { data } = sampleArgs(); + test('it should disable the new time axis for a line time layer when isHistogram is set to false', () => { const instance = shallow( ({ + ...layer, + table: newData, + })), }} - args={multiLayerArgs} /> ); @@ -283,20 +283,18 @@ describe('XYChart component', () => { expect(axisStyle).toBe(0); }); test('it should enable the new time axis for a line time layer when isHistogram is set to true', () => { - const { data } = sampleArgs(); const timeLayerArgs = createArgsWithLayers([defaultTimeLayer]); const instance = shallow( ({ + ...layer, + table: newData, + })), }} - args={timeLayerArgs} /> ); @@ -305,8 +303,7 @@ describe('XYChart component', () => { expect(axisStyle).toBe(3); }); test('it should disable the new time axis for a vertical bar with break down dimension', () => { - const { data } = sampleArgs(); - const timeLayer: DataLayerConfigResult = { + const timeLayer: DataLayerConfig = { ...defaultTimeLayer, seriesType: 'bar', }; @@ -315,14 +312,13 @@ describe('XYChart component', () => { const instance = shallow( ({ + ...layer, + table: newData, + })), }} - args={timeLayerArgs} /> ); @@ -332,8 +328,7 @@ describe('XYChart component', () => { }); test('it should enable the new time axis for a stacked vertical bar with break down dimension', () => { - const { data } = sampleArgs(); - const timeLayer: DataLayerConfigResult = { + const timeLayer: DataLayerConfig = { ...defaultTimeLayer, seriesType: 'bar_stacked', }; @@ -342,14 +337,13 @@ describe('XYChart component', () => { const instance = shallow( ({ + ...layer, + table: newData, + })), }} - args={timeLayerArgs} /> ); @@ -359,65 +353,53 @@ describe('XYChart component', () => { }); }); describe('endzones', () => { - const { args } = sampleArgs(); const table = createSampleDatatableWithRows([ { a: 1, b: 2, c: new Date('2021-04-22').valueOf(), d: 'Foo' }, { a: 1, b: 2, c: new Date('2021-04-23').valueOf(), d: 'Foo' }, { a: 1, b: 2, c: new Date('2021-04-24').valueOf(), d: 'Foo' }, ]); - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - ...table, - columns: table.columns.map((c) => - c.id !== 'c' - ? c - : { - ...c, - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: 'date_histogram', - params: {}, - appliedTimeRange: { - from: '2021-04-22T12:00:00.000Z', - to: '2021-04-24T12:00:00.000Z', - }, - }, + const newData = { + ...table, + type: 'datatable', + + columns: table.columns.map((c) => + c.id !== 'c' + ? c + : { + ...c, + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2021-04-22T12:00:00.000Z', + to: '2021-04-24T12:00:00.000Z', }, - } - ), - }, - }, - dateRange: { - // first and last bucket are partial - fromDate: new Date('2021-04-22T12:00:00.000Z'), - toDate: new Date('2021-04-24T12:00:00.000Z'), - }, + }, + }, + } + ), }; - const timeArgs: XYArgs = { + const timeArgs: XYProps = { ...args, layers: [ { - ...(args.layers[0] as DataLayerConfigResult), + ...args.layers[0], + type: 'dataLayer', seriesType: 'line', xScaleType: 'time', isHistogram: true, splitAccessor: undefined, - }, + table: newData, + } as DataLayerConfig, ], }; test('it extends interval if data is exceeding it', () => { const component = shallow( - + ); expect(component.find(Settings).prop('xDomain')).toEqual({ @@ -429,14 +411,17 @@ describe('XYChart component', () => { }); }); + const defaultTimeArgs = { + ...timeArgs, + layers: timeArgs.layers.map((layer) => ({ + ...layer, + table: data, + })), + }; + test('it renders endzone component bridging gap between domain and extended domain', () => { const component = shallow( - + ); expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( @@ -451,12 +436,7 @@ describe('XYChart component', () => { test('should pass enabled histogram mode and min interval to endzones component', () => { const component = shallow( - + ); expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( @@ -472,12 +452,11 @@ describe('XYChart component', () => { { ); @@ -511,13 +489,12 @@ describe('XYChart component', () => { }); describe('y axis extents', () => { - test('it passes custom y axis extents to elastic-charts axis spec', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); + test('it passes custom y axis extents to elastic-charts axis spec', () => { const component = shallow( { }); test('it passes fit to bounds y axis extents to elastic-charts axis spec', () => { - const { data, args } = sampleArgs(); - const component = shallow( { }); test('it does not allow fit for area chart', () => { - const { data, args } = sampleArgs(); - const component = shallow( { }, layers: [ { - ...(args.layers[0] as DataLayerConfigResult), + ...(args.layers[0] as DataLayerConfig), seriesType: 'area', }, ], @@ -592,12 +563,9 @@ describe('XYChart component', () => { }); test('it does not allow positive lower bound for bar', () => { - const { data, args } = sampleArgs(); - const component = shallow( { }, layers: [ { - ...(args.layers[0] as DataLayerConfigResult), + ...(args.layers[0] as DataLayerConfig), seriesType: 'bar', }, ], @@ -624,36 +592,29 @@ describe('XYChart component', () => { }); test('it does include referenceLine values when in full extent mode', () => { - const { data, args } = sampleArgsWithReferenceLine(); + const { args: refArgs } = sampleArgsWithReferenceLine(); - const component = shallow(); + const component = shallow(); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ fit: false, min: NaN, max: NaN, - includeDataFromIds: ['referenceLine-referenceLine-a-rect'], + includeDataFromIds: ['referenceLine-a-referenceLine-a-rect'], }); }); }); test('it has xDomain undefined if the x is not a time scale or a histogram', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }); test('it uses min interval if interval is passed in and visualization is histogram', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }); test('disabled legend extra by default', () => { - const { data, args } = sampleArgs(); - const component = shallow(); + const { args } = sampleArgs(); + const component = shallow(); expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); }); test('ignores legend extra for ordinal chart', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( - + ); expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); }); @@ -712,7 +672,6 @@ describe('XYChart component', () => { const component = shallow( { }); test('it renders bar', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(2); - expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']); - expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries).toHaveLength(2); + expect(barSeries.at(0).prop('yAccessors')).toEqual(['a']); + expect(barSeries.at(1).prop('yAccessors')).toEqual(['b']); }); test('it renders area', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(AreaSeries)).toHaveLength(2); - expect(component.find(AreaSeries).at(0).prop('yAccessors')).toEqual(['a']); - expect(component.find(AreaSeries).at(1).prop('yAccessors')).toEqual(['b']); + + const areaSeries = component.find(DataLayers).dive().find(AreaSeries); + expect(areaSeries).toHaveLength(2); + expect(areaSeries.at(0).prop('yAccessors')).toEqual(['a']); + expect(areaSeries.at(1).prop('yAccessors')).toEqual(['b']); }); test('it renders horizontal bar', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(2); - expect(component.find(BarSeries).at(0).prop('yAccessors')).toEqual(['a']); - expect(component.find(BarSeries).at(1).prop('yAccessors')).toEqual(['b']); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries).toHaveLength(2); + expect(barSeries.at(0).prop('yAccessors')).toEqual(['a']); + expect(barSeries.at(1).prop('yAccessors')).toEqual(['b']); expect(component.find(Settings).prop('rotation')).toEqual(90); }); test('it renders regular bar empty placeholder for no results', () => { const { data, args } = sampleArgs(); - - // send empty data to the chart - data.tables.first.rows = []; - - const component = shallow(); + const component = shallow( + ({ ...layer, table: { ...data, rows: [] } })), + }} + /> + ); expect(component.find(BarSeries)).toHaveLength(0); expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined(); @@ -796,7 +762,6 @@ describe('XYChart component', () => { const wrapper = mountWithIntl( { test('onBrushEnd returns correct context data for number histogram data', () => { const { args } = sampleArgs(); - const numberLayer: DataLayerConfigResult = { - type: 'dataLayer', + const numberHistogramData: Datatable = { + type: 'datatable', + rows: [ + { + xAccessorId: 5, + yAccessorId: 1, + }, + { + xAccessorId: 7, + yAccessorId: 1, + }, + { + xAccessorId: 8, + yAccessorId: 1, + }, + { + xAccessorId: 10, + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'bytes', + meta: { type: 'number' }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { type: 'number' }, + }, + ], + }; + + const numberLayer: DataLayerConfig = { layerId: 'numberLayer', + type: 'dataLayer', layerType: LayerTypes.DATA, hide: false, xAccessor: 'xAccessorId', @@ -827,55 +826,12 @@ describe('XYChart component', () => { seriesType: 'bar_stacked', accessors: ['yAccessorId'], palette: mockPaletteOutput, - }; - - const numberHistogramData: LensMultiTable = { - type: 'lens_multitable', - tables: { - numberLayer: { - type: 'datatable', - rows: [ - { - xAccessorId: 5, - yAccessorId: 1, - }, - { - xAccessorId: 7, - yAccessorId: 1, - }, - { - xAccessorId: 8, - yAccessorId: 1, - }, - { - xAccessorId: 10, - yAccessorId: 1, - }, - ], - columns: [ - { - id: 'xAccessorId', - name: 'bytes', - meta: { type: 'number' }, - }, - { - id: 'yAccessorId', - name: 'Count of records', - meta: { type: 'number' }, - }, - ], - }, - }, - dateRange: { - fromDate: new Date('2020-04-01T16:14:16.246Z'), - toDate: new Date('2020-04-01T17:15:41.263Z'), - }, + table: numberHistogramData, }; const wrapper = mountWithIntl( { expect(onSelectRange).toHaveBeenCalledWith({ column: 0, - table: numberHistogramData.tables.numberLayer, + table: numberHistogramData, range: [5, 8], }); }); test('onBrushEnd is not set on non-interactive mode', () => { - const { args, data } = sampleArgs(); + const { args } = sampleArgs(); - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); }); @@ -908,7 +862,6 @@ describe('XYChart component', () => { const wrapper = mountWithIntl( { const wrapper = mountWithIntl( { accessors: ['d'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', palette: mockPaletteOutput, + table: data, }, ], }} @@ -965,13 +918,13 @@ describe('XYChart component', () => { { column: 1, row: 1, - table: data.tables.first, + table: data, value: 5, }, { column: 1, row: 0, - table: data.tables.first, + table: data, value: 2, }, ], @@ -999,7 +952,6 @@ describe('XYChart component', () => { const wrapper = mountWithIntl( { test('onElementClick returns correct context data for numeric histogram', () => { const { args } = sampleArgs(); - const numberLayer: DataLayerConfigResult = { - type: 'dataLayer', + const numberHistogramData: Datatable = { + type: 'datatable', + rows: [ + { + xAccessorId: 5, + yAccessorId: 1, + }, + { + xAccessorId: 7, + yAccessorId: 1, + }, + { + xAccessorId: 8, + yAccessorId: 1, + }, + { + xAccessorId: 10, + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'bytes', + meta: { type: 'number' }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { type: 'number' }, + }, + ], + }; + + const numberLayer: DataLayerConfig = { layerId: 'numberLayer', + type: 'dataLayer', layerType: LayerTypes.DATA, hide: false, xAccessor: 'xAccessorId', @@ -1038,50 +1024,9 @@ describe('XYChart component', () => { seriesType: 'bar_stacked', accessors: ['yAccessorId'], palette: mockPaletteOutput, + table: numberHistogramData, }; - const numberHistogramData: LensMultiTable = { - type: 'lens_multitable', - tables: { - numberLayer: { - type: 'datatable', - rows: [ - { - xAccessorId: 5, - yAccessorId: 1, - }, - { - xAccessorId: 7, - yAccessorId: 1, - }, - { - xAccessorId: 8, - yAccessorId: 1, - }, - { - xAccessorId: 10, - yAccessorId: 1, - }, - ], - columns: [ - { - id: 'xAccessorId', - name: 'bytes', - meta: { type: 'number' }, - }, - { - id: 'yAccessorId', - name: 'Count of records', - meta: { type: 'number' }, - }, - ], - }, - }, - dateRange: { - fromDate: new Date('2020-04-01T16:14:16.246Z'), - toDate: new Date('2020-04-01T17:15:41.263Z'), - }, - }; const geometry: GeometryValue = { x: 5, y: 1, @@ -1100,7 +1045,6 @@ describe('XYChart component', () => { const wrapper = mountWithIntl( { { column: 0, row: 0, - table: numberHistogramData.tables.numberLayer, + table: numberHistogramData, value: 5, }, ], @@ -1142,13 +1086,12 @@ describe('XYChart component', () => { const wrapper = mountWithIntl( { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data, }, ], }} @@ -1173,7 +1117,7 @@ describe('XYChart component', () => { { column: 3, row: 1, - table: data.tables.first, + table: data, value: 'Bar', }, ], @@ -1182,21 +1126,31 @@ describe('XYChart component', () => { test('sets up correct yScaleType equal to binary_linear for bytes formatting', () => { const { args, data } = sampleArgs(); - data.tables.first.columns[0].meta = { - type: 'number', - params: { id: 'bytes', params: { pattern: '0,0.00b' } }, + + const [firstCol, ...rest] = data.columns; + const newData: Datatable = { + ...data, + columns: [ + { + ...firstCol, + meta: { + type: 'number', + params: { id: 'bytes', params: { pattern: '0,0.00b' } }, + }, + }, + ...rest, + ], }; const wrapper = mountWithIntl( { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: newData, }, ], }} @@ -1221,13 +1176,12 @@ describe('XYChart component', () => { const wrapper = mountWithIntl( { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data, }, ], }} @@ -1247,94 +1202,92 @@ describe('XYChart component', () => { }); test('onElementClick is not triggering event on non-interactive mode', () => { - const { args, data } = sampleArgs(); + const { args } = sampleArgs(); - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined(); }); test('legendAction is not triggering event on non-interactive mode', () => { - const { args, data } = sampleArgs(); + const { args } = sampleArgs(); - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(Settings).first().prop('legendAction')).toBeUndefined(); }); test('it renders stacked bar', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(2); - expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1); - expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries).toHaveLength(2); + expect(barSeries.at(0).prop('stackAccessors')).toHaveLength(1); + expect(barSeries.at(1).prop('stackAccessors')).toHaveLength(1); }); test('it renders stacked area', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(AreaSeries)).toHaveLength(2); - expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toHaveLength(1); - expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toHaveLength(1); + + const areaSeries = component.find(DataLayers).dive().find(AreaSeries); + expect(areaSeries).toHaveLength(2); + expect(areaSeries.at(0).prop('stackAccessors')).toHaveLength(1); + expect(areaSeries.at(1).prop('stackAccessors')).toHaveLength(1); }); test('it renders stacked horizontal bar', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); expect(component).toMatchSnapshot(); - expect(component.find(BarSeries)).toHaveLength(2); - expect(component.find(BarSeries).at(0).prop('stackAccessors')).toHaveLength(1); - expect(component.find(BarSeries).at(1).prop('stackAccessors')).toHaveLength(1); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries).toHaveLength(2); + expect(barSeries.at(0).prop('stackAccessors')).toHaveLength(1); + expect(barSeries.at(1).prop('stackAccessors')).toHaveLength(1); expect(component.find(Settings).prop('rotation')).toEqual(90); }); test('it renders stacked bar empty placeholder for no results', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { /> ); - expect(component.find(BarSeries)).toHaveLength(0); + expect(component.find(DataLayers)).toHaveLength(0); expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined(); }); test('it passes time zone to the series', () => { - const { data, args } = sampleArgs(); - const component = shallow( - - ); - expect(component.find(LineSeries).at(0).prop('timeZone')).toEqual('CEST'); - expect(component.find(LineSeries).at(1).prop('timeZone')).toEqual('CEST'); + const { args } = sampleArgs(); + const component = shallow(); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries.at(0).prop('timeZone')).toEqual('CEST'); + expect(lineSeries.at(1).prop('timeZone')).toEqual('CEST'); }); test('it applies histogram mode to the series for single series', () => { - const { data, args } = sampleArgs(); - const firstLayer: DataLayerConfigResult = { + const { args } = sampleArgs(); + const firstLayer: DataLayerConfig = { ...args.layers[0], accessors: ['b'], seriesType: 'bar', isHistogram: true, - } as DataLayerConfigResult; + } as DataLayerConfig; delete firstLayer.splitAccessor; const component = shallow( - + ); - expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true); + expect( + component.find(DataLayers).dive().find(BarSeries).at(0).prop('enableHistogramMode') + ).toEqual(true); }); test('it does not apply histogram mode to more than one bar series for unstacked bar chart', () => { - const { data, args } = sampleArgs(); - const firstLayer: DataLayerConfigResult = { + const { args } = sampleArgs(); + const firstLayer: DataLayerConfig = { ...args.layers[0], seriesType: 'bar', isHistogram: true, - } as DataLayerConfigResult; + } as DataLayerConfig; delete firstLayer.splitAccessor; const component = shallow( - + ); - expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false); - expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries.at(0).prop('enableHistogramMode')).toEqual(false); + expect(barSeries.at(1).prop('enableHistogramMode')).toEqual(false); }); test('it applies histogram mode to more than one the series for unstacked line/area chart', () => { - const { data, args } = sampleArgs(); - const firstLayer: DataLayerConfigResult = { + const { args } = sampleArgs(); + const firstLayer: DataLayerConfig = { ...args.layers[0], seriesType: 'line', isHistogram: true, - } as DataLayerConfigResult; + } as DataLayerConfig; delete firstLayer.splitAccessor; - const secondLayer: DataLayerConfigResult = { + const secondLayer: DataLayerConfig = { ...args.layers[0], seriesType: 'line', isHistogram: true, - } as DataLayerConfigResult; + } as DataLayerConfig; delete secondLayer.splitAccessor; const component = shallow( - + ); - expect(component.find(LineSeries).at(0).prop('enableHistogramMode')).toEqual(true); - expect(component.find(LineSeries).at(1).prop('enableHistogramMode')).toEqual(true); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries.at(0).prop('enableHistogramMode')).toEqual(true); + expect(lineSeries.at(1).prop('enableHistogramMode')).toEqual(true); }); test('it applies histogram mode to the series for stacked series', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }} /> ); - expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true); - expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(true); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries.at(0).prop('enableHistogramMode')).toEqual(true); + expect(barSeries.at(1).prop('enableHistogramMode')).toEqual(true); }); test('it does not apply histogram mode for splitted series', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); - expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false); - expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false); + + const barSeries = component.find(DataLayers).dive().find(BarSeries); + expect(barSeries.at(0).prop('enableHistogramMode')).toEqual(false); + expect(barSeries.at(1).prop('enableHistogramMode')).toEqual(false); }); describe('y axes', () => { - test('single axis if possible', () => { - const args = createArgsWithLayers(); + const args = createArgsWithLayers(); + const layer = args.layers[0] as DataLayerConfig; - const component = getRenderedComponent(dataWithoutFormats, args); + test('single axis if possible', () => { + const newArgs = { + ...args, + layers: args.layers.map((l) => ({ + ...layer, + table: dataWithoutFormats, + })), + }; + const component = getRenderedComponent(newArgs); const axes = component.find(Axis); expect(axes).toHaveLength(2); }); test('multiple axes because of config', () => { - const args = createArgsWithLayers(); - const newArgs = { + const newArgs: XYProps = { ...args, layers: [ { - ...args.layers[0], + ...layer, accessors: ['a', 'b'], yConfig: [ { + type: 'yConfig', forAccessor: 'a', axisMode: 'left', }, { + type: 'yConfig', forAccessor: 'b', axisMode: 'right', }, ], + table: dataWithoutFormats, }, ], - } as XYArgs; + }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); + const component = getRenderedComponent(newArgs); const axes = component.find(Axis); expect(axes).toHaveLength(3); - expect(component.find(LineSeries).at(0).prop('groupId')).toEqual(axes.at(1).prop('groupId')); - expect(component.find(LineSeries).at(1).prop('groupId')).toEqual(axes.at(2).prop('groupId')); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries.at(0).prop('groupId')).toEqual(axes.at(1).prop('groupId')); + expect(lineSeries.at(1).prop('groupId')).toEqual(axes.at(2).prop('groupId')); }); test('multiple axes because of incompatible formatters', () => { - const args = createArgsWithLayers(); - const newArgs = { + const newArgs: XYProps = { ...args, layers: [ { - ...args.layers[0], + ...layer, accessors: ['c', 'd'], + table: dataWithFormats, }, ], - } as XYArgs; + }; - const component = getRenderedComponent(dataWithFormats, newArgs); + const component = getRenderedComponent(newArgs); const axes = component.find(Axis); expect(axes).toHaveLength(3); - expect(component.find(LineSeries).at(0).prop('groupId')).toEqual(axes.at(1).prop('groupId')); - expect(component.find(LineSeries).at(1).prop('groupId')).toEqual(axes.at(2).prop('groupId')); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries.at(0).prop('groupId')).toEqual(axes.at(1).prop('groupId')); + expect(lineSeries.at(1).prop('groupId')).toEqual(axes.at(2).prop('groupId')); }); test('single axis despite different formatters if enforced', () => { - const args = createArgsWithLayers(); - const newArgs = { + const newArgs: XYProps = { ...args, layers: [ { - ...args.layers[0], + ...layer, accessors: ['c', 'd'], yConfig: [ { + type: 'yConfig', forAccessor: 'c', axisMode: 'left', }, { + type: 'yConfig', forAccessor: 'd', axisMode: 'left', }, ], + table: dataWithoutFormats, }, ], - } as XYArgs; + }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); + const component = getRenderedComponent(newArgs); const axes = component.find(Axis); expect(axes).toHaveLength(2); }); }); describe('y series coloring', () => { + const args = createArgsWithLayers(); + const layer = args.layers[0] as DataLayerConfig; + test('color is applied to chart for multiple series', () => { - const args = createArgsWithLayers(); - const newArgs = { + const newArgs: XYProps = { ...args, layers: [ { - ...args.layers[0], - splitAccessor: undefined, + ...layer, + type: 'extendedDataLayer', accessors: ['a', 'b'], + splitAccessor: undefined, yConfig: [ { + type: 'yConfig', forAccessor: 'a', color: '#550000', }, { + type: 'yConfig', forAccessor: 'b', color: '#FFFF00', }, ], - }, + table: dataWithoutFormats, + } as ExtendedDataLayerConfig, { - ...args.layers[0], - splitAccessor: undefined, + ...layer, + type: 'extendedDataLayer', accessors: ['c'], + splitAccessor: undefined, yConfig: [ { + type: 'yConfig', forAccessor: 'c', color: '#FEECDF', }, ], - }, + table: dataWithoutFormats, + } as ExtendedDataLayerConfig, ], - } as XYArgs; + }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); + const component = getRenderedComponent(newArgs); + const lineSeries = component.find(DataLayers).dive().find(LineSeries); expect( - (component.find(LineSeries).at(0).prop('color') as Function)!({ + (lineSeries.at(0).prop('color') as Function)!({ yAccessor: 'a', seriesKeys: ['a'], }) ).toEqual('#550000'); expect( - (component.find(LineSeries).at(1).prop('color') as Function)!({ + (lineSeries.at(1).prop('color') as Function)!({ yAccessor: 'b', seriesKeys: ['b'], }) ).toEqual('#FFFF00'); expect( - (component.find(LineSeries).at(2).prop('color') as Function)!({ + (lineSeries.at(2).prop('color') as Function)!({ yAccessor: 'c', seriesKeys: ['c'], }) ).toEqual('#FEECDF'); }); test('color is not applied to chart when splitAccessor is defined or when yConfig is not configured', () => { - const args = createArgsWithLayers(); - const newArgs = { + const newArgs: XYProps = { ...args, layers: [ { - ...args.layers[0], + ...layer, accessors: ['a'], yConfig: [ { + type: 'yConfig', forAccessor: 'a', color: '#550000', }, ], + table: dataWithoutFormats, }, { - ...args.layers[0], - splitAccessor: undefined, + ...layer, accessors: ['c'], + table: dataWithoutFormats, }, ], - } as XYArgs; + }; + + const component = getRenderedComponent(newArgs); - const component = getRenderedComponent(dataWithoutFormats, newArgs); + const lineSeries = component.find(DataLayers).dive().find(LineSeries); expect( - (component.find(LineSeries).at(0).prop('color') as Function)!({ + (lineSeries.at(0).prop('color') as Function)!({ yAccessor: 'a', seriesKeys: ['a'], }) ).toEqual('blue'); expect( - (component.find(LineSeries).at(1).prop('color') as Function)!({ + (lineSeries.at(1).prop('color') as Function)!({ yAccessor: 'c', seriesKeys: ['c'], }) @@ -1650,16 +1636,21 @@ describe('XYChart component', () => { accessors: ['a'], splitAccessor: undefined, columnToLabel: '', + table: dataWithoutFormats, }, ], }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + const nameFn = component + .find(DataLayers) + .dive() + .find(LineSeries) + .prop('name') as SeriesNameFn; // In this case, the ID is used as the name. This shouldn't happen in practice - expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(''); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(null); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(null); }); test('simplest xy chart with empty name', () => { @@ -1672,16 +1663,21 @@ describe('XYChart component', () => { accessors: ['a'], splitAccessor: undefined, columnToLabel: '{"a":""}', + table: dataWithoutFormats, }, ], }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + const nameFn = component + .find(DataLayers) + .dive() + .find(LineSeries) + .prop('name') as SeriesNameFn; // In this case, the ID is used as the name. This shouldn't happen in practice expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual(''); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(null); }); test('simplest xy chart with human-readable name', () => { @@ -1694,12 +1690,17 @@ describe('XYChart component', () => { accessors: ['a'], splitAccessor: undefined, columnToLabel: '{"a":"Column A"}', + table: dataWithoutFormats, }, ], }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + const nameFn = component + .find(DataLayers) + .dive() + .find(LineSeries) + .prop('name') as SeriesNameFn; expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Column A'); }); @@ -1714,19 +1715,22 @@ describe('XYChart component', () => { accessors: ['a', 'b'], splitAccessor: undefined, columnToLabel: '{"a": "Label A"}', + table: dataWithoutFormats, }, ], }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; - const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + const nameFn1 = lineSeries.at(0).prop('name') as SeriesNameFn; + const nameFn2 = lineSeries.at(1).prop('name') as SeriesNameFn; // This accessor has a human-readable name expect(nameFn1({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Label A'); // This accessor does not - expect(nameFn2({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(''); - expect(nameFn1({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(''); + expect(nameFn2({ ...nameFnArgs, seriesKeys: ['b'] }, false)).toEqual(null); + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['nonsense'] }, false)).toEqual(null); }); test('split series without formatting and single y accessor', () => { @@ -1739,12 +1743,17 @@ describe('XYChart component', () => { accessors: ['a'], splitAccessor: 'd', columnToLabel: '{"a": "Label A"}', + table: dataWithoutFormats, }, ], }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + const nameFn = component + .find(DataLayers) + .dive() + .find(LineSeries) + .prop('name') as SeriesNameFn; expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1'); }); @@ -1759,12 +1768,17 @@ describe('XYChart component', () => { accessors: ['a'], splitAccessor: 'd', columnToLabel: '{"a": "Label A"}', + table: dataWithFormats, }, ], }; - const component = getRenderedComponent(dataWithFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + const nameFn = component + .find(DataLayers) + .dive() + .find(LineSeries) + .prop('name') as SeriesNameFn; convertSpy.mockReturnValueOnce('formatted'); expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted'); @@ -1781,13 +1795,16 @@ describe('XYChart component', () => { accessors: ['a', 'b'], splitAccessor: 'd', columnToLabel: '{"a": "Label A","b": "Label B"}', + table: dataWithoutFormats, }, ], }; - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; - const nameFn2 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + const nameFn1 = lineSeries.at(0).prop('name') as SeriesNameFn; + const nameFn2 = lineSeries.at(0).prop('name') as SeriesNameFn; expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( 'split1 - Label A' @@ -1807,13 +1824,16 @@ describe('XYChart component', () => { accessors: ['a', 'b'], splitAccessor: 'd', columnToLabel: '{"a": "Label A","b": "Label B"}', + table: dataWithFormats, }, ], }; - const component = getRenderedComponent(dataWithFormats, newArgs); - const nameFn1 = component.find(LineSeries).at(0).prop('name') as SeriesNameFn; - const nameFn2 = component.find(LineSeries).at(1).prop('name') as SeriesNameFn; + const component = getRenderedComponent(newArgs); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + const nameFn1 = lineSeries.at(0).prop('name') as SeriesNameFn; + const nameFn2 = lineSeries.at(1).prop('name') as SeriesNameFn; convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( @@ -1826,57 +1846,58 @@ describe('XYChart component', () => { }); test('it set the scale of the x axis according to the args prop', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); - expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal); - expect(component.find(LineSeries).at(1).prop('xScaleType')).toEqual(ScaleType.Ordinal); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries.at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal); + expect(lineSeries.at(1).prop('xScaleType')).toEqual(ScaleType.Ordinal); }); test('it set the scale of the y axis according to the args prop', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( ); - expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt); - expect(component.find(LineSeries).at(1).prop('yScaleType')).toEqual(ScaleType.Sqrt); + + const lineSeries = component.find(DataLayers).dive().find(LineSeries); + expect(lineSeries.at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt); + expect(lineSeries.at(1).prop('yScaleType')).toEqual(ScaleType.Sqrt); }); test('it gets the formatter for the x axis', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); - shallow(); + shallow(); expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' }); }); test('it gets the formatter for the y axis if there is only one accessor', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); shallow( ); @@ -1887,9 +1908,9 @@ describe('XYChart component', () => { }); test('it should pass the formatter function to the axis', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); - const instance = shallow(); + const instance = shallow(); const tickFormatter = instance.find(Axis).first().prop('tickFormat'); @@ -1903,7 +1924,7 @@ describe('XYChart component', () => { }); test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.tickLabelsVisibilitySettings = { x: false, @@ -1912,7 +1933,7 @@ describe('XYChart component', () => { type: 'tickLabelsConfig', }; - const instance = shallow(); + const instance = shallow(); const axisStyle = instance.find(Axis).first().prop('style'); @@ -1924,7 +1945,7 @@ describe('XYChart component', () => { }); test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.tickLabelsVisibilitySettings = { x: true, @@ -1933,7 +1954,7 @@ describe('XYChart component', () => { type: 'tickLabelsConfig', }; - const instance = shallow(); + const instance = shallow(); const axisStyle = instance.find(Axis).at(1).prop('style'); @@ -1945,7 +1966,7 @@ describe('XYChart component', () => { }); test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.tickLabelsVisibilitySettings = { x: true, @@ -1954,7 +1975,7 @@ describe('XYChart component', () => { type: 'tickLabelsConfig', }; - const instance = shallow(); + const instance = shallow(); const axisStyle = instance.find(Axis).first().prop('style'); @@ -1966,7 +1987,7 @@ describe('XYChart component', () => { }); test('it should set the tickLabel orientation on the x axis', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.labelsOrientation = { x: -45, @@ -1975,7 +1996,7 @@ describe('XYChart component', () => { type: 'labelsOrientationConfig', }; - const instance = shallow(); + const instance = shallow(); const axisStyle = instance.find(Axis).first().prop('style'); @@ -1987,7 +2008,7 @@ describe('XYChart component', () => { }); test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.tickLabelsVisibilitySettings = { x: false, @@ -1996,7 +2017,7 @@ describe('XYChart component', () => { type: 'tickLabelsConfig', }; - const instance = shallow(); + const instance = shallow(); const axisStyle = instance.find(Axis).at(1).prop('style'); @@ -2008,7 +2029,7 @@ describe('XYChart component', () => { }); test('it should set the tickLabel orientation on the y axis', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.labelsOrientation = { x: -45, @@ -2017,7 +2038,7 @@ describe('XYChart component', () => { type: 'labelsOrientationConfig', }; - const instance = shallow(); + const instance = shallow(); const axisStyle = instance.find(Axis).at(1).prop('style'); @@ -2029,37 +2050,33 @@ describe('XYChart component', () => { }); test('it should remove invalid rows', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'number' } }, - { id: 'c', name: 'c', meta: { type: 'string' } }, - ], - rows: [ - { a: undefined, b: 2, c: 'I', d: 'Row 1' }, - { a: 1, b: 5, c: 'J', d: 'Row 2' }, - ], - }, - second: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'number' } }, - { id: 'c', name: 'c', meta: { type: 'string' } }, - ], - rows: [ - { a: undefined, b: undefined, c: undefined }, - { a: undefined, b: undefined, c: undefined }, - ], - }, - }, + const data1: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + ], + rows: [ + { a: undefined, b: 2, c: 'I', d: 'Row 1' }, + { a: 1, b: 5, c: 'J', d: 'Row 2' }, + ], }; - const args: XYArgs = { + const data2: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + ], + rows: [ + { a: undefined, b: undefined, c: undefined }, + { a: undefined, b: undefined, c: undefined }, + ], + }; + + const args: XYProps = { xTitle: '', yTitle: '', yRightTitle: '', @@ -2093,8 +2110,8 @@ describe('XYChart component', () => { }, layers: [ { - type: 'dataLayer', layerId: 'first', + type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'a', @@ -2105,10 +2122,11 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data1, }, { - type: 'dataLayer', layerId: 'second', + type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'a', @@ -2119,13 +2137,14 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data2, }, ], }; - const component = shallow(); + const component = shallow(); - const series = component.find(LineSeries); + const series = component.find(DataLayers).dive().find(LineSeries); // Only one series should be rendered, even though 2 are configured // This one series should only have one row, even though 2 are sent @@ -2133,25 +2152,20 @@ describe('XYChart component', () => { }); test('it should not remove rows with falsy but non-undefined values', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'number' } }, - { id: 'c', name: 'c', meta: { type: 'number' } }, - ], - rows: [ - { a: 0, b: 2, c: 5 }, - { a: 1, b: 0, c: 7 }, - ], - }, - }, + const data: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'number' } }, + ], + rows: [ + { a: 0, b: 2, c: 5 }, + { a: 1, b: 0, c: 7 }, + ], }; - const args: XYArgs = { + const args: XYProps = { xTitle: '', yTitle: '', yRightTitle: '', @@ -2185,8 +2199,8 @@ describe('XYChart component', () => { }, layers: [ { - type: 'dataLayer', layerId: 'first', + type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'a', @@ -2197,13 +2211,14 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data, }, ], }; - const component = shallow(); + const component = shallow(); - const series = component.find(LineSeries); + const series = component.find(DataLayers).dive().find(LineSeries); expect(series.prop('data')).toEqual([ { a: 0, b: 2, c: 5 }, @@ -2212,22 +2227,17 @@ describe('XYChart component', () => { }); test('it should show legend for split series, even with one row', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'number' } }, - { id: 'c', name: 'c', meta: { type: 'string' } }, - ], - rows: [{ a: 1, b: 5, c: 'J' }], - }, - }, + const data: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + ], + rows: [{ a: 1, b: 5, c: 'J' }], }; - const args: XYArgs = { + const args: XYProps = { xTitle: '', yTitle: '', yRightTitle: '', @@ -2261,8 +2271,8 @@ describe('XYChart component', () => { }, layers: [ { - type: 'dataLayer', layerId: 'first', + type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'a', @@ -2273,27 +2283,27 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data, }, ], }; - const component = shallow(); + const component = shallow(); expect(component.find(Settings).prop('showLegend')).toEqual(true); }); test('it should always show legend if showSingleSeries is set', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }); test('it should populate the correct legendPosition if isInside is set', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }); test('it not show legend if isVisible is set to false', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }); test('it should show legend on right side', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); const component = shallow( { }); test('it should apply the fitting function to all non-bar series', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: 'I', d: 'Foo' }, - { a: 1, b: 5, c: 'J', d: 'Bar' }, - ]), - }, - }; + const data: Datatable = createSampleDatatableWithRows([ + { a: 1, b: 2, c: 'I', d: 'Foo' }, + { a: 1, b: 5, c: 'J', d: 'Bar' }, + ]); - const args: XYArgs = createArgsWithLayers([ - { ...sampleLayer, accessors: ['a'] }, - { ...sampleLayer, seriesType: 'bar', accessors: ['a'] }, - { ...sampleLayer, seriesType: 'area', accessors: ['a'] }, - { ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'] }, + const args: XYProps = createArgsWithLayers([ + { ...sampleLayer, accessors: ['a'], table: data }, + { ...sampleLayer, seriesType: 'bar', accessors: ['a'], table: data }, + { ...sampleLayer, seriesType: 'area', accessors: ['a'], table: data }, + { ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'], table: data }, ]); const component = shallow( - + ); - - expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.Carry }); - expect(component.find(BarSeries).prop('fit')).toEqual(undefined); - expect(component.find(AreaSeries).at(0).prop('fit')).toEqual({ type: Fit.Carry }); - expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toEqual([]); - expect(component.find(AreaSeries).at(1).prop('fit')).toEqual({ type: Fit.Carry }); - expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toEqual(['c']); + const dataLayers = component.find(DataLayers).dive(); + expect(dataLayers.find(LineSeries).prop('fit')).toEqual({ type: Fit.Carry }); + expect(dataLayers.find(BarSeries).prop('fit')).toEqual(undefined); + expect(dataLayers.find(AreaSeries).at(0).prop('fit')).toEqual({ type: Fit.Carry }); + expect(dataLayers.find(AreaSeries).at(0).prop('stackAccessors')).toEqual([]); + expect(dataLayers.find(AreaSeries).at(1).prop('fit')).toEqual({ type: Fit.Carry }); + expect(dataLayers.find(AreaSeries).at(1).prop('stackAccessors')).toEqual(['c']); }); test('it should apply None fitting function if not specified', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); - (args.layers[0] as DataLayerConfigResult).accessors = ['a']; + (args.layers[0] as DataLayerConfig).accessors = ['a']; - const component = shallow(); + const component = shallow(); - expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None }); + expect(component.find(DataLayers).dive().find(LineSeries).prop('fit')).toEqual({ + type: Fit.None, + }); }); test('it should apply the xTitle if is specified', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.xTitle = 'My custom x-axis title'; - const component = shallow(); + const component = shallow(); expect(component.find(Axis).at(0).prop('title')).toEqual('My custom x-axis title'); }); test('it should hide the X axis title if the corresponding switch is off', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.axisTitlesVisibilitySettings = { x: false, @@ -2430,7 +2434,7 @@ describe('XYChart component', () => { type: 'axisTitlesVisibilityConfig', }; - const component = shallow(); + const component = shallow(); const axisStyle = component.find(Axis).first().prop('style'); @@ -2442,7 +2446,7 @@ describe('XYChart component', () => { }); test('it should show the X axis gridlines if the setting is on', () => { - const { data, args } = sampleArgs(); + const { args } = sampleArgs(); args.gridlinesVisibilitySettings = { x: true, @@ -2451,7 +2455,7 @@ describe('XYChart component', () => { type: 'gridlinesConfig', }; - const component = shallow(); + const component = shallow(); expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({ visible: true, @@ -2459,45 +2463,37 @@ describe('XYChart component', () => { }); test('it should format the boolean values correctly', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { - id: 'a', - name: 'a', - meta: { type: 'number', params: { id: 'number', params: { pattern: '0,0.000' } } }, - }, - { - id: 'b', - name: 'b', - meta: { type: 'number', params: { id: 'number', params: { pattern: '000,0' } } }, - }, - { - id: 'c', - name: 'c', - meta: { - type: 'boolean', - params: { id: 'boolean' }, - }, - }, - ], - rows: [ - { a: 5, b: 2, c: 0 }, - { a: 19, b: 5, c: 1 }, - ], + const data: Datatable = { + type: 'datatable', + columns: [ + { + id: 'a', + name: 'a', + meta: { type: 'number', params: { id: 'number', params: { pattern: '0,0.000' } } }, }, - }, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), - }, + { + id: 'b', + name: 'b', + meta: { type: 'number', params: { id: 'number', params: { pattern: '000,0' } } }, + }, + { + id: 'c', + name: 'c', + meta: { + type: 'boolean', + params: { id: 'boolean' }, + }, + }, + ], + rows: [ + { a: 5, b: 2, c: 0 }, + { a: 19, b: 5, c: 1 }, + ], }; - const timeSampleLayer: DataLayerConfigResult = { + + const timeSampleLayer: DataLayerConfig = { + layerId: 'timeLayer', type: 'dataLayer', - layerId: 'first', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'c', @@ -2506,22 +2502,19 @@ describe('XYChart component', () => { yScaleType: 'linear', isHistogram: false, palette: mockPaletteOutput, + table: data, }; + const args = createArgsWithLayers([timeSampleLayer]); const getCustomFormatSpy = jest.fn(); getCustomFormatSpy.mockReturnValue({ convert: jest.fn((x) => Boolean(x)) }); const component = shallow( - + ); - expect(component.find(LineSeries).at(1).prop('data')).toEqual([ + expect(component.find(DataLayers).dive().find(LineSeries).at(1).prop('data')).toEqual([ { a: 5, b: 2, @@ -2545,6 +2538,7 @@ describe('XYChart component', () => { lineStyle: 'dashed', lineWidth: 3, }; + const defaultLineStaticAnnotation = { time: '2022-03-18T08:25:17.140Z', label: 'Annotation', @@ -2558,7 +2552,7 @@ describe('XYChart component', () => { }; const createLayerWithAnnotations = ( annotations: EventAnnotationOutput[] = [defaultLineStaticAnnotation] - ): AnnotationLayerConfigResult => ({ + ): CommonXYAnnotationLayerConfig => ({ type: 'annotationLayer', layerType: LayerTypes.ANNOTATIONS, layerId: 'annotation', @@ -2567,37 +2561,37 @@ describe('XYChart component', () => { function sampleArgsWithAnnotations(annotationLayers = [createLayerWithAnnotations()]) { const { args } = sampleArgs(); return { - data: dateHistogramData, args: { ...args, layers: [dateHistogramLayer, ...annotationLayers], - } as XYArgs, + }, }; } + test('should render basic line annotation', () => { - const { data, args } = sampleArgsWithAnnotations(); - const component = mount(); + const { args } = sampleArgsWithAnnotations(); + const component = mount(); expect(component.find('LineAnnotation')).toMatchSnapshot(); }); test('should render basic range annotation', () => { - const { data, args } = sampleArgsWithAnnotations([ + const { args } = sampleArgsWithAnnotations([ createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]), ]); - const component = mount(); + const component = mount(); expect(component.find(RectAnnotation)).toMatchSnapshot(); }); test('should render simplified annotations when hide is true', () => { - const { data, args } = sampleArgsWithAnnotations([ + const { args } = sampleArgsWithAnnotations([ createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]), ]); - (args.layers[1] as AnnotationLayerConfigResult).hide = true; - const component = mount(); + (args.layers[1] as CommonXYAnnotationLayerConfig).hide = true; + const component = mount(); expect(component.find('LineAnnotation')).toMatchSnapshot(); expect(component.find('RectAnnotation')).toMatchSnapshot(); }); test('should render grouped line annotations preserving the shared styles', () => { - const { data, args } = sampleArgsWithAnnotations([ + const { args } = sampleArgsWithAnnotations([ createLayerWithAnnotations([ customLineStaticAnnotation, { ...customLineStaticAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, @@ -2608,7 +2602,7 @@ describe('XYChart component', () => { }, ]), ]); - const component = mount(); + const component = mount(); const groupedAnnotation = component.find(LineAnnotation); expect(groupedAnnotation.length).toEqual(1); @@ -2626,13 +2620,14 @@ describe('XYChart component', () => { ' Event 1 2022-03-18T08:25:00.000Z Event 3 2022-03-18T08:25:00.001Z Event 2 2022-03-18T08:25:00.020Z' ); }); + test('should render grouped line annotations with default styles', () => { - const { data, args } = sampleArgsWithAnnotations([ + const { args } = sampleArgsWithAnnotations([ createLayerWithAnnotations([customLineStaticAnnotation]), createLayerWithAnnotations([ { ...customLineStaticAnnotation, - icon: 'square', + icon: 'triangle' as const, color: 'blue', lineStyle: 'dotted', lineWidth: 10, @@ -2641,7 +2636,7 @@ describe('XYChart component', () => { }, ]), ]); - const component = mount(); + const component = mount(); const groupedAnnotation = component.find(LineAnnotation); expect(groupedAnnotation.length).toEqual(1); @@ -2649,7 +2644,7 @@ describe('XYChart component', () => { expect(groupedAnnotation).toMatchSnapshot(); }); test('should not render hidden annotations', () => { - const { data, args } = sampleArgsWithAnnotations([ + const { args } = sampleArgsWithAnnotations([ createLayerWithAnnotations([ customLineStaticAnnotation, { ...customLineStaticAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, @@ -2663,7 +2658,7 @@ describe('XYChart component', () => { { ...defaultRangeStaticAnnotation, label: 'range', isHidden: true }, ]), ]); - const component = mount(); + const component = mount(); const lineAnnotations = component.find(LineAnnotation); const rectAnnotations = component.find(Annotations).find(RectAnnotation); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index faac9076e8a8a..db653861a337e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -6,66 +6,56 @@ * Side Public License, v 1. */ -import React, { useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { Chart, Settings, Axis, - LineSeries, - AreaSeries, - BarSeries, Position, GeometryValue, XYChartSeriesIdentifier, - StackMode, VerticalAlignment, HorizontalAlignment, LayoutDirection, ElementClickListener, BrushEndListener, XYBrushEvent, - CurveType, LegendPositionConfig, - LabelOverflowConstraint, DisplayValueStyle, RecursivePartial, AxisStyle, - ScaleType, - AreaSeriesProps, - BarSeriesProps, - LineSeriesProps, - ColorVariant, Placement, } from '@elastic/charts'; import { IconType } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; -import type { Datatable, DatatableRow, DatatableColumn } from '@kbn/expressions-plugin/public'; +import { PaletteRegistry } from '@kbn/coloring'; import { RenderMode } from '@kbn/expressions-plugin/common'; -import { FieldFormat } from '@kbn/field-formats-plugin/common'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { ChartsPluginSetup, ChartsPluginStart, useActiveCursor } from '@kbn/charts-plugin/public'; import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; -import type { SeriesType, XYChartProps } from '../../common/types'; -import { isHorizontalChart, getSeriesColor, getAnnotationsLayers, getDataLayers } from '../helpers'; +import type { CommonXYDataLayerConfig, SeriesType, XYChartProps } from '../../common/types'; +import { + isHorizontalChart, + getAnnotationsLayers, + getDataLayers, + Series, + getFormattedTablesByLayers, + validateExtent, +} from '../helpers'; import { getFilteredLayers, getReferenceLayers, isDataLayer, - getFitOptions, getAxesConfiguration, GroupsConfiguration, - validateExtent, - getColorAssignments, getLinesCausedPaddings, } from '../helpers'; import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './legend_action'; import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines'; import { visualizationDefinitions } from '../definitions'; -import { XYLayerConfigResult } from '../../common/types'; +import { CommonXYLayerConfig } from '../../common/types'; import { Annotations, getAnnotationsGroupedByInterval, @@ -73,7 +63,8 @@ import { OUTSIDE_RECT_ANNOTATION_WIDTH, OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION, } from './annotations'; - +import { AxisExtentModes, SeriesTypes, ValueLabelModes } from '../../common/constants'; +import { DataLayers } from './data_layers'; import './xy_chart.scss'; declare global { @@ -85,8 +76,6 @@ declare global { } } -type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps; - export type XYChartRenderProps = XYChartProps & { chartsThemeService: ChartsPluginSetup['theme']; chartsActiveCursorService: ChartsPluginStart['activeCursor']; @@ -104,8 +93,6 @@ export type XYChartRenderProps = XYChartProps & { eventAnnotationService: EventAnnotationServiceType; }; -const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; - function getValueLabelsStyling(isHorizontal: boolean): { displayValue: RecursivePartial; } { @@ -134,7 +121,6 @@ function getIconForSeriesType(seriesType: SeriesType): IconType { export const XYChartReportable = React.memo(XYChart); export function XYChart({ - data, args, formatFactory, timeZone, @@ -166,50 +152,47 @@ export function XYChart({ const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); const darkMode = chartsThemeService.useDarkMode(); - const filteredLayers = getFilteredLayers(layers, data); - const layersById = filteredLayers.reduce>( - (hashMap, layer) => { - hashMap[layer.layerId] = layer; - return hashMap; - }, + const filteredLayers = getFilteredLayers(layers); + const layersById = filteredLayers.reduce>( + (hashMap, layer) => ({ ...hashMap, [layer.layerId]: layer }), {} ); 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) { - const icon: IconType = getIconForSeriesType(getDataLayers(layers)?.[0]?.seriesType || 'bar'); + const icon: IconType = getIconForSeriesType( + getDataLayers(layers)?.[0]?.seriesType || SeriesTypes.BAR + ); return ; } // use formatting hint of first x axis column to format ticks - const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find( - ({ id }) => id === filteredLayers[0].xAccessor - ); + const xAxisColumn = dataLayers[0]?.table.columns.find(({ id }) => id === dataLayers[0].xAccessor); const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params); - const layersAlreadyFormatted: Record = {}; // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers const safeXAccessorLabelRenderer = (value: unknown): string => - xAxisColumn && layersAlreadyFormatted[xAxisColumn.id] + xAxisColumn && formattedDatatables[dataLayers[0]?.layerId]?.formattedColumns[xAxisColumn.id] ? String(value) : String(xAxisFormatter.convert(value)); const chartHasMoreThanOneSeries = filteredLayers.length > 1 || filteredLayers.some((layer) => layer.accessors.length > 1) || - filteredLayers.some((layer) => layer.splitAccessor); - const shouldRotate = isHorizontalChart(filteredLayers); - - const yAxesConfiguration = getAxesConfiguration( - filteredLayers, - shouldRotate, - data.tables, - formatFactory - ); + filteredLayers.some((layer) => isDataLayer(layer) && layer.splitAccessor); + const shouldRotate = isHorizontalChart(dataLayers); + + const yAxesConfiguration = getAxesConfiguration(dataLayers, shouldRotate, formatFactory); const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name); const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || { @@ -223,25 +206,20 @@ export function XYChart({ yRight: true, }; - const labelsOrientation = args.labelsOrientation || { - x: 0, - yLeft: 0, - yRight: 0, - }; + const labelsOrientation = args.labelsOrientation || { 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 = filteredBarLayers.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 isHistogramViz = filteredLayers.every((l) => l.isHistogram); + const isTimeViz = Boolean(dataLayers.every((l) => l.xScaleType === 'time')); + const isHistogramViz = dataLayers.every((l) => l.isHistogram); const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( - filteredLayers, - data, + dataLayers, minInterval, isTimeViz, isHistogramViz @@ -252,17 +230,16 @@ export function XYChart({ right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'), }; - const getYAxesTitles = ( - axisSeries: Array<{ layer: string; accessor: string }>, - groupId: string - ) => { + const getYAxesTitles = (axisSeries: Series[], groupId: 'right' | 'left') => { const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle; return ( yTitle || axisSeries .map( (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] ); @@ -270,9 +247,9 @@ export function XYChart({ const referenceLineLayers = getReferenceLayers(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( annotationsLayers, @@ -339,10 +316,11 @@ export function XYChart({ 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 max: number = NaN; - if (extent.mode === 'custom') { const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent); if (!inclusiveZeroError && !boundaryError) { @@ -369,38 +347,42 @@ export function XYChart({ const shouldShowValueLabels = // No stacked bar charts - filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) && + dataLayers.every((layer) => !layer.seriesType.includes('stacked')) && // No histogram charts !isHistogramViz; const valueLabelsStyling = - shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate); - - const colorAssignments = getColorAssignments(getDataLayers(args.layers), data, formatFactory); + shouldShowValueLabels && + valueLabels !== ValueLabelModes.HIDE && + getValueLabelsStyling(shouldRotate); const clickHandler: ElementClickListener = ([[geometry, series]]) => { // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue const xySeries = series as XYChartSeriesIdentifier; 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())) ); - if (!layer) { + + if (layerIndex === -1) { 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 currentXFormatter = - layer.xAccessor && layersAlreadyFormatted[layer.xAccessor] && xColumn + layer.xAccessor && + formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor] && + xColumn ? formatFactory(xColumn.meta.params) : xAxisFormatter; const rowIndex = table.rows.findIndex((row) => { if (layer.xAccessor) { - if (layersAlreadyFormatted[layer.xAccessor]) { + if (formattedDatatables[layer.layerId]?.formattedColumns[layer.xAccessor]) { // stringify the value to compare with the chart value return currentXFormatter.convert(row[layer.xAccessor]) === xyGeometry.x; } @@ -425,7 +407,7 @@ export function XYChart({ points.push({ row: table.rows.findIndex((row) => { if (layer.splitAccessor) { - if (layersAlreadyFormatted[layer.splitAccessor]) { + if (formattedDatatables[layer.layerId]?.formattedColumns[layer.splitAccessor]) { return splitFormatter.convert(row[layer.splitAccessor]) === pointValue; } return row[layer.splitAccessor] === pointValue; @@ -436,12 +418,7 @@ export function XYChart({ }); } const context: FilterEvent['data'] = { - data: points.map((point) => ({ - row: point.row, - column: point.column, - value: point.value, - table, - })), + data: points.map(({ row, column, value }) => ({ row, column, value, table })), }; onClickValue(context); }; @@ -455,27 +432,23 @@ export function XYChart({ 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'] = { - range: [min, max], - table, - column: xAxisColumnIndex, - }; + const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex }; onSelectRange(context); }; - const legendInsideParams = { + const legendInsideParams: LegendPositionConfig = { vAlign: legend.verticalAlignment ?? VerticalAlignment.Top, hAlign: legend?.horizontalAlignment ?? HorizontalAlignment.Right, direction: LayoutDirection.Vertical, floating: true, floatingColumns: legend?.floatingColumns ?? 1, - } as LegendPositionConfig; + }; - const isHistogramModeEnabled = filteredLayers.some( + const isHistogramModeEnabled = dataLayers.some( ({ isHistogram, seriesType }) => isHistogram && (seriesType.includes('stacked') || @@ -570,13 +543,7 @@ export function XYChart({ onElementClick={interactive ? clickHandler : undefined} legendAction={ interactive - ? getLegendAction( - filteredLayers, - data.tables, - onClickValue, - formatFactory, - layersAlreadyFormatted - ) + ? getLegendAction(dataLayers, onClickValue, formatFactory, formattedDatatables) : undefined } showLegendExtra={isHistogramViz && valuesInLegend} @@ -589,7 +556,7 @@ export function XYChart({ position={shouldRotate ? Position.Left : Position.Bottom} title={xTitle} gridLine={gridLineStyle} - hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor} + hide={dataLayers[0]?.hide || !dataLayers[0]?.xAccessor} tickFormat={(d) => safeXAccessorLabelRenderer(d)} style={xAxisStyle} timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0} @@ -609,9 +576,9 @@ export function XYChart({ ? gridlinesVisibilitySettings?.yRight : gridlinesVisibilitySettings?.yLeft, }} - hide={filteredLayers[0].hide} + hide={dataLayers[0]?.hide} tickFormat={(d) => axis.formatter?.convert(d) || ''} - style={getYAxesStyle(axis.groupId as 'left' | 'right')} + style={getYAxesStyle(axis.groupId)} domain={getYAxisDomain(axis)} ticks={5} /> @@ -623,7 +590,7 @@ export function XYChart({ baseDomain={rawXDomain} extendedDomain={xDomain} darkMode={darkMode} - histogramMode={filteredLayers.every( + histogramMode={dataLayers.every( (layer) => layer.isHistogram && (layer.seriesType.includes('stacked') || !layer.splitAccessor) && @@ -634,282 +601,28 @@ export function XYChart({ /> )} - {filteredLayers.flatMap((layer, layerIndex) => - layer.accessors.map((accessor, accessorIndex) => { - const { - splitAccessor, - seriesType, - accessors, - xAccessor, - layerId, - columnToLabel, - yScaleType, - xScaleType, - isHistogram, - palette, - } = layer; - const columnToLabelMap: Record = columnToLabel - ? JSON.parse(columnToLabel) - : {}; - - const table = data.tables[layerId]; - - const formatterPerColumn = new Map(); - 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>( - (alreadyFormatted: Record, { 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 ( - - ); - 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 ; - case 'area_stacked': - case 'area_percentage_stacked': - return ( - - ); - case 'area': - return ( - - ); - default: - return assertNever(seriesType); - } - }) + {dataLayers.length && ( + )} {referenceLineLayers.length ? ( ); } - -function assertNever(x: never): never { - throw new Error('Unexpected series type: ' + x); -} diff --git a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx index a703506bd3e11..c6ad977bbad3a 100644 --- a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx @@ -18,7 +18,6 @@ import { ExpressionRenderDefinition } from '@kbn/expressions-plugin'; import { FormatFactory } from '@kbn/field-formats-plugin/common'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import type { XYChartProps } from '../../common'; -import { calculateMinInterval } from '../helpers/interval'; import type { BrushEvent, FilterEvent } from '../types'; export type GetStartDepsFn = () => Promise<{ @@ -56,7 +55,10 @@ export const getXyChartRenderer = ({ }; 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( diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx index 9050bdee4a365..d6746cafc0296 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx @@ -9,19 +9,32 @@ import React from 'react'; import { Position } from '@elastic/charts'; import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; 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 { hasIcon } from './icon'; -import { annotationsIconSet } from './annotations_icon_set'; +import { hasIcon, iconSet } from './icon'; 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; + +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 = ( - visualConfigs: Array< - Pick | undefined - >, + visualConfigs: Array, axesMap: Record<'left' | 'right', unknown> ) => { // collect all paddings for the 4 axis: if any text is detected double it. @@ -31,7 +44,9 @@ export const getLinesCausedPaddings = ( if (!config) { return; } - const { axisMode, icon, iconPosition, textVisibility } = config; + const { axisMode, icon, textVisibility } = config; + const iconPosition = isExtendedYConfig(config) ? config.iconPosition : undefined; + if (axisMode && (hasIcon(icon) || textVisibility)) { const placement = getBaseIconPlacement(iconPosition, axesMap, axisMode); paddings[placement] = Math.max( @@ -48,6 +63,7 @@ export const getLinesCausedPaddings = ( paddings[placement] = LINES_MARKER_SIZE; } }); + return paddings; }; @@ -138,7 +154,7 @@ export const AnnotationIcon = ({ if (isNumericalString(type)) { return ; } - const iconConfig = annotationsIconSet.find((i) => i.value === type); + const iconConfig = iconSet.find((i) => i.value === type); if (!iconConfig) { return null; } diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations_icon_set.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations_icon_set.tsx deleted file mode 100644 index 99b4648e4d556..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations_icon_set.tsx +++ /dev/null @@ -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, - }, -]; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts index c5f2f6d7151e5..f3abf76b2d05a 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataLayerConfigResult } from '../../common'; +import { DataLayerConfig } from '../../common'; import { LayerTypes } from '../../common/constants'; import { Datatable } from '@kbn/expressions-plugin/public'; import { getAxesConfiguration } from './axes_configuration'; @@ -220,9 +220,9 @@ describe('axes_configuration', () => { }, }; - const sampleLayer: DataLayerConfigResult = { - type: 'dataLayer', + const sampleLayer: DataLayerConfig = { layerId: 'first', + type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'c', @@ -233,11 +233,12 @@ describe('axes_configuration', () => { yScaleType: 'linear', isHistogram: false, palette: { type: 'palette', name: 'default' }, + table: tables.first, }; it('should map auto series to left axis', () => { const formatFactory = jest.fn(); - const groups = getAxesConfiguration([sampleLayer], false, tables, formatFactory); + const groups = getAxesConfiguration([sampleLayer], false, formatFactory); expect(groups.length).toEqual(1); expect(groups[0].position).toEqual('left'); 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', () => { const formatFactory = jest.fn(); 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[0].position).toEqual('left'); expect(groups[1].position).toEqual('right'); @@ -261,7 +262,7 @@ describe('axes_configuration', () => { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'], }; - const groups = getAxesConfiguration([threeSeriesLayer], false, tables, formatFactory); + const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory); expect(groups.length).toEqual(2); expect(groups[0].position).toEqual('left'); expect(groups[1].position).toEqual('right'); @@ -280,7 +281,6 @@ describe('axes_configuration', () => { }, ], false, - tables, formatFactory ); expect(groups.length).toEqual(1); @@ -300,7 +300,6 @@ describe('axes_configuration', () => { }, ], false, - tables, formatFactory ); expect(groups.length).toEqual(2); @@ -324,7 +323,6 @@ describe('axes_configuration', () => { }, ], false, - tables, formatFactory ); expect(formatFactory).toHaveBeenCalledTimes(2); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts index 38d2220904226..65f5441d67226 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts @@ -6,22 +6,25 @@ * Side Public License, v 1. */ -import { Datatable } from '@kbn/expressions-plugin/public'; import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; 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; accessor: string; +} + +interface FormattedMetric extends Series { fieldFormat: SerializedFieldFormat; } export type GroupsConfiguration = Array<{ - groupId: string; + groupId: 'left' | 'right'; position: 'left' | 'right' | 'bottom' | 'top'; formatter?: IFieldFormat; - series: Array<{ layer: string; accessor: string }>; + series: Series[]; }>; export function isFormatterCompatible( @@ -31,10 +34,7 @@ export function isFormatterCompatible( return formatter1.id === formatter2.id; } -export function groupAxesByType( - layers: DataLayerConfigResult[], - tables?: Record -) { +export function groupAxesByType(layers: CommonXYDataLayerConfig[]) { const series: { auto: FormattedMetric[]; left: FormattedMetric[]; @@ -47,15 +47,19 @@ export function groupAxesByType( bottom: [], }; - layers?.forEach((layer) => { - const table = tables?.[layer.layerId]; + layers.forEach((layer) => { + const { table } = layer; layer.accessors.forEach((accessor) => { + const yConfig: Array | undefined = layer.yConfig; const mode = - layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || - 'auto'; - let formatter: SerializedFieldFormat = table?.columns.find((column) => column.id === accessor) + yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto'; + let formatter: SerializedFieldFormat = table.columns?.find((column) => column.id === accessor) ?.meta?.params || { id: 'number' }; - if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') { + if ( + isDataLayer(layer) && + layer.seriesType.includes('percentage') && + formatter.id !== 'percent' + ) { formatter = { id: 'percent', params: { @@ -71,10 +75,12 @@ export function groupAxesByType( }); }); + const tablesExist = layers.filter(({ table }) => Boolean(table)).length > 0; + series.auto.forEach((currentSeries) => { if ( series.left.length === 0 || - (tables && + (tablesExist && series.left.every((leftSeries) => isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) )) @@ -82,7 +88,7 @@ export function groupAxesByType( series.left.push(currentSeries); } else if ( series.right.length === 0 || - (tables && + (tablesExist && series.left.every((leftSeries) => isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) )) @@ -98,12 +104,11 @@ export function groupAxesByType( } export function getAxesConfiguration( - layers: DataLayerConfigResult[], + layers: CommonXYDataLayerConfig[], shouldRotate: boolean, - tables?: Record, formatFactory?: FormatFactory ): GroupsConfiguration { - const series = groupAxesByType(layers, tables); + const series = groupAxesByType(layers); const axisGroups: GroupsConfiguration = []; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts index bd13e3217c2af..8b1bdeeadb834 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts @@ -7,76 +7,76 @@ */ import { getColorAssignments } from './color_assignment'; -import type { DataLayerConfigResult, LensMultiTable } from '../../common'; +import type { DataLayerConfig } from '../../common'; import type { FormatFactory } from '../types'; import { LayerTypes } from '../../common/constants'; +import { Datatable } from '@kbn/expressions-plugin'; describe('color_assignment', () => { - const layers: DataLayerConfigResult[] = [ + const tables: Record = { + '1': { + type: 'datatable', + columns: [ + { id: 'split1', name: '', meta: { type: 'number' } }, + { id: 'y1', name: '', meta: { type: 'number' } }, + { id: 'y2', name: '', meta: { type: 'number' } }, + ], + rows: [ + { split1: 1 }, + { split1: 2 }, + { split1: 3 }, + { split1: 1 }, + { split1: 2 }, + { split1: 3 }, + ], + }, + '2': { + type: 'datatable', + columns: [ + { id: 'split2', name: '', meta: { type: 'number' } }, + { id: 'y1', name: '', meta: { type: 'number' } }, + { id: 'y2', name: '', meta: { type: 'number' } }, + ], + rows: [ + { split2: 1 }, + { split2: 2 }, + { split2: 3 }, + { split2: 1 }, + { split2: 2 }, + { split2: 3 }, + ], + }, + }; + + const layers: DataLayerConfig[] = [ { + layerId: 'first', 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'], + table: tables['1'], }, { + layerId: 'second', 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'], + table: tables['2'], }, ]; - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - '1': { - type: 'datatable', - columns: [ - { id: 'split1', name: '', meta: { type: 'number' } }, - { id: 'y1', name: '', meta: { type: 'number' } }, - { id: 'y2', name: '', meta: { type: 'number' } }, - ], - rows: [ - { split1: 1 }, - { split1: 2 }, - { split1: 3 }, - { split1: 1 }, - { split1: 2 }, - { split1: 3 }, - ], - }, - '2': { - type: 'datatable', - columns: [ - { id: 'split2', name: '', meta: { type: 'number' } }, - { id: 'y1', name: '', meta: { type: 'number' } }, - { id: 'y2', name: '', meta: { type: 'number' } }, - ], - rows: [ - { split2: 1 }, - { split2: 2 }, - { split2: 3 }, - { split2: 1 }, - { split2: 2 }, - { split2: 3 }, - ], - }, - }, - }; - const formatFactory = (() => ({ convert(x: unknown) { @@ -86,7 +86,7 @@ describe('color_assignment', () => { describe('totalSeriesCount', () => { 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 expect(assignments.palette1.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', () => { const assignments = getColorAssignments( [layers[0], { ...layers[1], palette: layers[0].palette }], - data, formatFactory ); // 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', () => { const assignments = getColorAssignments( [layers[0], { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }], - data, formatFactory ); // 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', () => { const complexObject = { aProp: 123 }; const formatMock = jest.fn((x) => 'formatted'); - const assignments = getColorAssignments( - layers, + const newLayers = [ { - ...data, - tables: { - ...data.tables, - '1': { ...data.tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] }, - }, + ...layers[0], + table: { ...tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] }, }, + layers[1], + ]; + + const assignments = getColorAssignments( + newLayers, (() => ({ convert: formatMock, @@ -137,26 +136,18 @@ describe('color_assignment', () => { }); 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 expect(assignments.palette1.totalSeriesCount).toEqual(2); }); it('should handle missing columns', () => { - const assignments = getColorAssignments( - layers, - { - ...data, - tables: { - ...data.tables, - '1': { - ...data.tables['1'], - columns: [], - }, - }, - }, - formatFactory - ); + const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]]; + const assignments = getColorAssignments(newLayers, formatFactory); + // if the split column is missing, just assume a single split expect(assignments.palette1.totalSeriesCount).toEqual(2); }); @@ -164,7 +155,7 @@ describe('color_assignment', () => { describe('getRank', () => { 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 expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(3); // 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', () => { 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 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 @@ -185,7 +176,7 @@ describe('color_assignment', () => { layers[0], { ...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 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 @@ -193,15 +184,16 @@ describe('color_assignment', () => { }); it('should return the correct rank for a series with a non-primitive value', () => { - const assignments = getColorAssignments( - layers, + const newLayers = [ { - ...data, - tables: { - ...data.tables, - '1': { ...data.tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] }, - }, + ...layers[0], + table: { ...tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] }, }, + layers[1], + ]; + + const assignments = getColorAssignments( + newLayers, (() => ({ convert: () => 'formatted', @@ -212,26 +204,19 @@ describe('color_assignment', () => { }); 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 expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); }); it('should handle missing columns', () => { - const assignments = getColorAssignments( - layers, - { - ...data, - tables: { - ...data.tables, - '1': { - ...data.tables['1'], - columns: [], - }, - }, - }, - formatFactory - ); + const newLayers = [{ ...layers[0], table: { ...tables['1'], columns: [] } }, layers[1]]; + + const assignments = getColorAssignments(newLayers, formatFactory); + // 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); }); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts index ee2f5a0325287..e94d22471aba9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -8,10 +8,9 @@ import { uniq, mapValues } from 'lodash'; import { euiLightVars } from '@kbn/ui-theme'; -import type { Datatable } from '@kbn/expressions-plugin'; import { FormatFactory } from '../types'; import { isDataLayer } from './visualization'; -import { DataLayerConfigResult, XYLayerConfigResult } from '../../common'; +import { CommonXYDataLayerConfig, CommonXYLayerConfig } from '../../common'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; @@ -21,40 +20,41 @@ export type ColorAssignments = Record< string, { totalSeriesCount: number; - getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string): number; + getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string): number; } >; export function getColorAssignments( - layers: XYLayerConfigResult[], - data: { tables: Record }, + layers: CommonXYLayerConfig[], formatFactory: FormatFactory ): ColorAssignments { - const layersPerPalette: Record = {}; + const layersPerPalette: Record = {}; - layers - .filter((layer): layer is DataLayerConfigResult => isDataLayer(layer)) - .forEach((layer) => { - const palette = layer.palette?.name || 'default'; - if (!layersPerPalette[palette]) { - layersPerPalette[palette] = []; - } - layersPerPalette[palette].push(layer); - }); + layers.forEach((layer) => { + if (!isDataLayer(layer)) { + return; + } + + const palette = layer.palette?.name || 'default'; + if (!layersPerPalette[palette]) { + layersPerPalette[palette] = []; + } + layersPerPalette[palette].push(layer); + }); return mapValues(layersPerPalette, (paletteLayers) => { - const seriesPerLayer = paletteLayers.map((layer, layerIndex) => { + const seriesPerLayer = paletteLayers.map((layer) => { if (!layer.splitAccessor) { return { numberOfSeries: layer.accessors.length, splits: [] }; } 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 splits = - !column || !data.tables[layer.layerId] + !column || !layer.table ? [] : uniq( - data.tables[layer.layerId].rows.map((row) => { + layer.table.rows.map((row) => { let value = row[splitAccessor]; if (value && !isPrimitive(value)) { value = columnFormatter?.convert(value) ?? value; @@ -72,8 +72,10 @@ export function getColorAssignments( ); return { totalSeriesCount, - getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string) { - const layerIndex = paletteLayers.findIndex((l) => sortedLayer.layerId === l.layerId); + getRank(sortedLayer: CommonXYDataLayerConfig, seriesKey: string, yAccessor: string) { + const layerIndex = paletteLayers.findIndex( + (layer) => sortedLayer.layerId === layer.layerId + ); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); return ( diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx new file mode 100644 index 0000000000000..07af8a3c408c2 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -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; + 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 | undefined; + splitFormatter: FieldFormat; + alreadyFormattedColumns: Record; + columnToLabelMap: Record; + } +) => SeriesName; + +type GetColorFn = ( + seriesIdentifier: XYChartSeriesIdentifier, + config: { + layer: CommonXYDataLayerConfig; + accessor: string; + colorAssignments: ColorAssignments; + columnToLabelMap: Record; + paletteService: PaletteRegistry; + syncColors?: boolean; + } +) => string | null; + +export interface DatatableWithFormatInfo { + table: Datatable; + formattedColumns: Record; +} + +export type DatatablesWithFormatInfo = Record; + +export type FormattedDatatables = Record; + +const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; + +export const getFormattedRow = ( + row: Datatable['rows'][number], + columns: Datatable['columns'], + columnsFormatters: Record, + xAccessor: string | undefined, + xScaleType: XScaleType +): { row: Datatable['rows'][number]; formattedColumns: Record } => + 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 } => { + const columnsFormatters = table.columns.reduce>( + (formatters, { id, meta }) => ({ ...formatters, [id]: formatFactory(meta.params) }), + {} + ); + + const formattedTableInfo = table.rows.reduce<{ + rows: Datatable['rows']; + formattedColumns: Record; + }>( + ({ 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, + }); + }, + }; +}; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts index 43d5ad9b4c19f..4c26caf59d8d3 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts @@ -8,6 +8,7 @@ import { Fit } from '@elastic/charts'; import { EndValue, FittingFunction } from '../../common'; +import { EndValues } from '../../common/constants'; export function getFitEnum(fittingFunction?: FittingFunction | EndValue) { if (fittingFunction) { @@ -17,10 +18,10 @@ export function getFitEnum(fittingFunction?: FittingFunction | EndValue) { } export function getEndValue(endValue?: EndValue) { - if (endValue === 'Nearest') { + if (endValue === EndValues.NEAREST) { return Fit[endValue]; } - if (endValue === 'Zero') { + if (endValue === EndValues.ZERO) { return 0; } return undefined; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts index 57e285a07232f..8b4113b3ada11 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts @@ -6,6 +6,107 @@ * 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 { 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, + }, +]; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts index 2bb3a5a927774..773ae4ee22d94 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts @@ -14,5 +14,5 @@ export * from './fitting_functions'; export * from './axes_configuration'; export * from './icon'; export * from './color_assignment'; -export * from './annotations_icon_set'; export * from './annotations'; +export * from './data_layers'; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts index 0fe979b8c3fc1..6721c293dbe57 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts @@ -6,32 +6,36 @@ * Side Public License, v 1. */ -import { DataLayerConfigResult, XYChartProps } from '../../common'; +import { DataLayerConfig, XYChartProps } from '../../common'; import { sampleArgs } from '../../common/__mocks__'; import { calculateMinInterval } from './interval'; describe('calculateMinInterval', () => { let xyProps: XYChartProps; - + let layer: DataLayerConfig; beforeEach(() => { - xyProps = sampleArgs(); - (xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'time'; + const { layers, ...restArgs } = sampleArgs().args; + + xyProps = { args: { ...restArgs, layers } }; + layer = xyProps.args.layers[0] as DataLayerConfig; + layer.xScaleType = 'time'; }); it('should use first valid layer and determine interval', async () => { - xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; - xyProps.data.tables.first.columns[2].meta.sourceParams = { + layer.table.columns[2].meta.source = 'esaggs'; + layer.table.columns[2].meta.sourceParams = { type: 'date_histogram', params: { used_interval: '5m', }, }; + xyProps.args.layers[0] = layer; const result = await calculateMinInterval(xyProps); expect(result).toEqual(5 * 60 * 1000); }); it('should return interval of number histogram if available on first x axis columns', async () => { - (xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'linear'; - xyProps.data.tables.first.columns[2].meta = { + layer.xScaleType = 'linear'; + layer.table.columns[2].meta = { source: 'esaggs', type: 'number', field: 'someField', @@ -43,19 +47,22 @@ describe('calculateMinInterval', () => { }, }, }; + xyProps.args.layers[0] = layer; const result = await calculateMinInterval(xyProps); expect(result).toEqual(5); }); it('should return undefined if data table is empty', async () => { - xyProps.data.tables.first.rows = []; - xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; - xyProps.data.tables.first.columns[2].meta.sourceParams = { + layer.table.rows = []; + layer.table.columns[2].meta.source = 'esaggs'; + layer.table.columns[2].meta.sourceParams = { type: 'date_histogram', params: { used_interval: '5m', }, }; + + xyProps.args.layers[0] = layer; const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); @@ -66,14 +73,16 @@ describe('calculateMinInterval', () => { }); 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); expect(result).toEqual(undefined); }); it('should return undefined if x axis is not a date', async () => { - (xyProps.args.layers[0] as DataLayerConfigResult).xScaleType = 'ordinal'; - xyProps.data.tables.first.columns.splice(2, 1); + layer.xScaleType = 'ordinal'; + xyProps.args.layers[0] = layer; + xyProps.args.layers[0].table.columns.splice(2, 1); const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts index b81fcd33fccad..17e7a9c2aba32 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts @@ -11,11 +11,11 @@ import { XYChartProps } from '../../common'; import { getFilteredLayers } from './layers'; import { isDataLayer } from './visualization'; -export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { - const filteredLayers = getFilteredLayers(layers, data); +export function calculateMinInterval({ args: { layers } }: XYChartProps) { + const filteredLayers = getFilteredLayers(layers); if (filteredLayers.length === 0) return; 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 ); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts index be1701e6b6e4b..4408ebd3feb84 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts @@ -6,24 +6,42 @@ * Side Public License, v 1. */ -import { LensMultiTable } from '../../common'; -import { DataLayerConfigResult, XYLayerConfigResult } from '../../common/types'; -import { getDataLayers } from './visualization'; +import { Datatable } from '@kbn/expressions-plugin/common'; +import { + CommonXYDataLayerConfig, + CommonXYLayerConfig, + CommonXYReferenceLineLayerConfig, +} from '../../common/types'; +import { isDataLayer, isReferenceLayer } from './visualization'; + +export function getFilteredLayers(layers: CommonXYLayerConfig[]) { + return layers.filter( + (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( - (layer): layer is DataLayerConfigResult => { - const { layerId, xAccessor, accessors, splitAccessor } = layer; return !( !accessors.length || - !data.tables[layerId] || - data.tables[layerId].rows.length === 0 || + !table || + table.rows.length === 0 || (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 (!xAccessor && splitAccessor && - data.tables[layerId].rows.every((row) => typeof row[splitAccessor] === 'undefined')) + table.rows.every((row) => splitAccessor && typeof row[splitAccessor] === 'undefined')) ); } ); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts index a5cd66f178b63..e2f95491dbce8 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts @@ -6,7 +6,7 @@ * 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'; export function isHorizontalSeries(seriesType: SeriesType) { @@ -21,16 +21,14 @@ export function isStackedChart(seriesType: SeriesType) { return seriesType.includes('stacked'); } -export function isHorizontalChart(layers: XYLayerConfigResult[]) { +export function isHorizontalChart(layers: CommonXYLayerConfig[]) { 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)) { return null; } - - return ( - layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null - ); + const yConfig: Array | undefined = layer?.yConfig; + return yConfig?.find((yConf) => yConf.forAccessor === accessor)?.color || null; }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts index af2e80948ffdf..db0b431d56fac 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts @@ -6,38 +6,40 @@ * Side Public License, v 1. */ +import { LayerTypes } from '../../common/constants'; import { - DataLayerConfigResult, - ReferenceLineLayerConfigResult, - XYLayerConfigResult, - AnnotationLayerConfigResult, + CommonXYLayerConfig, + CommonXYDataLayerConfig, + CommonXYReferenceLineLayerConfig, + CommonXYAnnotationLayerConfig, } from '../../common/types'; -import { LayerTypes } from '../../common/constants'; -export const isDataLayer = (layer: XYLayerConfigResult): layer is DataLayerConfigResult => +export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLayerConfig => layer.layerType === LayerTypes.DATA || !layer.layerType; -export const getDataLayers = (layers: XYLayerConfigResult[]) => - (layers || []).filter((layer): layer is DataLayerConfigResult => isDataLayer(layer)); +export const getDataLayers = (layers: CommonXYLayerConfig[]) => + (layers || []).filter((layer): layer is CommonXYDataLayerConfig => isDataLayer(layer)); export const isReferenceLayer = ( - layer: XYLayerConfigResult -): layer is ReferenceLineLayerConfigResult => layer.layerType === LayerTypes.REFERENCELINE; + layer: CommonXYLayerConfig +): layer is CommonXYReferenceLineLayerConfig => layer.layerType === LayerTypes.REFERENCELINE; -export const getReferenceLayers = (layers: XYLayerConfigResult[]) => - (layers || []).filter((layer): layer is ReferenceLineLayerConfigResult => +export const getReferenceLayers = (layers: CommonXYLayerConfig[]) => + (layers || []).filter((layer): layer is CommonXYReferenceLineLayerConfig => isReferenceLayer(layer) ); const isAnnotationLayerCommon = ( - layer: XYLayerConfigResult -): layer is AnnotationLayerConfigResult => layer.layerType === LayerTypes.ANNOTATIONS; + layer: CommonXYLayerConfig +): layer is CommonXYAnnotationLayerConfig => layer.layerType === LayerTypes.ANNOTATIONS; export const isAnnotationsLayer = ( - layer: XYLayerConfigResult -): layer is AnnotationLayerConfigResult => isAnnotationLayerCommon(layer); + layer: CommonXYLayerConfig +): layer is CommonXYAnnotationLayerConfig => isAnnotationLayerCommon(layer); export const getAnnotationsLayers = ( - layers: XYLayerConfigResult[] -): AnnotationLayerConfigResult[] => - (layers || []).filter((layer): layer is AnnotationLayerConfigResult => isAnnotationsLayer(layer)); + layers: CommonXYLayerConfig[] +): CommonXYAnnotationLayerConfig[] => + (layers || []).filter((layer): layer is CommonXYAnnotationLayerConfig => + isAnnotationsLayer(layer) + ); diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index d08ffc0fd2ec6..5e68d2c621894 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -16,17 +16,22 @@ import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public' import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types'; import { xyVisFunction, + layeredXyVisFunction, + dataLayerFunction, + extendedDataLayerFunction, yAxisConfigFunction, + extendedYAxisConfigFunction, legendConfigFunction, gridlinesConfigFunction, - dataLayerConfigFunction, axisExtentConfigFunction, tickLabelsConfigFunction, - annotationLayerConfigFunction, + referenceLineLayerFunction, + extendedReferenceLineLayerFunction, + annotationLayerFunction, labelsOrientationConfigFunction, - referenceLineLayerConfigFunction, axisTitlesVisibilityConfigFunction, -} from '../common'; + extendedAnnotationLayerFunction, +} from '../common/expression_functions'; import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers'; export interface XYPluginStartDependencies { @@ -51,16 +56,21 @@ export class ExpressionXyPlugin { { expressions, charts }: SetupDeps ): ExpressionXyPluginSetup { expressions.registerFunction(yAxisConfigFunction); + expressions.registerFunction(extendedYAxisConfigFunction); expressions.registerFunction(legendConfigFunction); expressions.registerFunction(gridlinesConfigFunction); - expressions.registerFunction(dataLayerConfigFunction); + expressions.registerFunction(dataLayerFunction); + expressions.registerFunction(extendedDataLayerFunction); expressions.registerFunction(axisExtentConfigFunction); expressions.registerFunction(tickLabelsConfigFunction); - expressions.registerFunction(annotationLayerConfigFunction); + expressions.registerFunction(annotationLayerFunction); + expressions.registerFunction(extendedAnnotationLayerFunction); expressions.registerFunction(labelsOrientationConfigFunction); - expressions.registerFunction(referenceLineLayerConfigFunction); + expressions.registerFunction(referenceLineLayerFunction); + expressions.registerFunction(extendedReferenceLineLayerFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); + expressions.registerFunction(layeredXyVisFunction); const getStartDeps: GetStartDepsFn = async () => { const [coreStart, deps] = await core.getStartServices(); diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts index c16bd2eb28161..37252a7296580 100755 --- a/src/plugins/chart_expressions/expression_xy/server/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -12,16 +12,21 @@ import { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types'; import { xyVisFunction, yAxisConfigFunction, + extendedYAxisConfigFunction, legendConfigFunction, gridlinesConfigFunction, - dataLayerConfigFunction, + dataLayerFunction, axisExtentConfigFunction, tickLabelsConfigFunction, - annotationLayerConfigFunction, + annotationLayerFunction, labelsOrientationConfigFunction, - referenceLineLayerConfigFunction, + referenceLineLayerFunction, axisTitlesVisibilityConfigFunction, -} from '../common'; + extendedDataLayerFunction, + extendedReferenceLineLayerFunction, + layeredXyVisFunction, + extendedAnnotationLayerFunction, +} from '../common/expression_functions'; import { SetupDeps } from './types'; export class ExpressionXyPlugin @@ -29,16 +34,21 @@ export class ExpressionXyPlugin { public setup(core: CoreSetup, { expressions }: SetupDeps) { expressions.registerFunction(yAxisConfigFunction); + expressions.registerFunction(extendedYAxisConfigFunction); expressions.registerFunction(legendConfigFunction); expressions.registerFunction(gridlinesConfigFunction); - expressions.registerFunction(dataLayerConfigFunction); + expressions.registerFunction(dataLayerFunction); + expressions.registerFunction(extendedDataLayerFunction); expressions.registerFunction(axisExtentConfigFunction); expressions.registerFunction(tickLabelsConfigFunction); - expressions.registerFunction(annotationLayerConfigFunction); + expressions.registerFunction(annotationLayerFunction); + expressions.registerFunction(extendedAnnotationLayerFunction); expressions.registerFunction(labelsOrientationConfigFunction); - expressions.registerFunction(referenceLineLayerConfigFunction); + expressions.registerFunction(referenceLineLayerFunction); + expressions.registerFunction(extendedReferenceLineLayerFunction); expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); + expressions.registerFunction(layeredXyVisFunction); } public start(core: CoreStart) {} diff --git a/src/plugins/event_annotation/common/constants.ts b/src/plugins/event_annotation/common/constants.ts new file mode 100644 index 0000000000000..3338450b64ce5 --- /dev/null +++ b/src/plugins/event_annotation/common/constants.ts @@ -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; diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 3ed43b19a705c..50d7c8b851776 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -17,4 +17,8 @@ export type { export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation'; export { eventAnnotationGroup } from './event_annotation_group'; export type { EventAnnotationGroupArgs } from './event_annotation_group'; -export type { EventAnnotationConfig, RangeEventAnnotationConfig } from './types'; +export type { + EventAnnotationConfig, + RangeEventAnnotationConfig, + AvailableAnnotationIcon, +} from './types'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/index.ts b/src/plugins/event_annotation/common/manual_event_annotation/index.ts index 2401af53df76c..bb02018f5a81a 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/index.ts @@ -8,6 +8,8 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; +import { AvailableAnnotationIcons } from '../constants'; + import type { ManualRangeEventAnnotationArgs, ManualRangeEventAnnotationOutput, @@ -65,6 +67,8 @@ export const manualPointEventAnnotation: ExpressionFunctionDefinition< help: i18n.translate('eventAnnotation.manualAnnotation.args.icon', { defaultMessage: 'An optional icon used for annotation lines', }), + options: [...Object.values(AvailableAnnotationIcons)], + strict: true, }, textVisibility: { types: ['boolean'], diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 664138c9eb9e6..e0b0de3c85c9e 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -6,15 +6,18 @@ * Side Public License, v 1. */ +import { $Values } from '@kbn/utility-types'; +import { AvailableAnnotationIcons } from './constants'; + export type LineStyle = 'solid' | 'dashed' | 'dotted'; export type Fill = 'inside' | 'outside' | 'none'; export type AnnotationType = 'manual'; export type KeyType = 'point_in_time' | 'range'; - +export type AvailableAnnotationIcon = $Values; export interface PointStyleProps { label: string; color?: string; - icon?: string; + icon?: AvailableAnnotationIcon; lineWidth?: number; lineStyle?: LineStyle; textVisibility?: boolean; diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 78d88adfa3dbd..75a95035bb89f 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -714,7 +714,7 @@ describe('Execution', () => { expect(result).toMatchObject({ type: 'error', error: { - message: '[requiredArg] > requiredArg requires an argument', + message: '[requiredArg] > requiredArg requires the "arg" argument', }, }); }); @@ -725,7 +725,7 @@ describe('Execution', () => { expect(result).toMatchObject({ type: 'error', error: { - message: '[var_set] > var_set requires an "name" argument', + message: '[var_set] > var_set requires the "name" argument', }, }); }); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index ef0a268ca314f..2fda462929ff4 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -481,7 +481,7 @@ export class Execution< ); // 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') { dealiasedArgAsts[name] = [parse(argDefault as string, 'argument')]; } @@ -490,13 +490,7 @@ export class Execution< continue; } - if (!aliases?.length) { - 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`); + throw new Error(`${fnDef.name} requires the "${name}" argument`); } // Create the functions to resolve the argument ASTs into values diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index f4111d51e2918..f85c8ba88a076 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -57,8 +57,7 @@ export type CustomPaletteParamsConfig = CustomPaletteParams & { export type LayerType = typeof layerTypes[keyof typeof layerTypes]; -// Shared by XY Chart and Heatmap as for now -export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; +export type ValueLabelConfig = 'hide' | 'show'; export type PieChartType = $Values; export type CategoryDisplayType = $Values; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index e404faacb8f97..c577bf89d6bd1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -145,8 +145,6 @@ export function LayerPanel( const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); const { activeId, activeGroup } = activeDimension; - const { setDimension, removeDimension } = activeVisualization; - const allAccessors = groups.flatMap((group) => group.accessors.map((accessor) => accessor.columnId) ); @@ -209,7 +207,7 @@ export function LayerPanel( previousColumn = typeof dropResult === 'object' ? dropResult.deleted : undefined; } } - const newVisState = setDimension({ + const newVisState = activeVisualization.setDimension({ columnId, groupId, layerId: targetLayerId, @@ -221,7 +219,7 @@ export function LayerPanel( if (typeof dropResult === 'object') { // When a column is moved, we delete the reference to the old updateVisualization( - removeDimension({ + activeVisualization.removeDimension({ columnId: dropResult.deleted, layerId: targetLayerId, prevState: newVisState, @@ -234,7 +232,7 @@ export function LayerPanel( } } else { if (dropType === 'duplicate_compatible' || dropType === 'reorder') { - const newVisState = setDimension({ + const newVisState = activeVisualization.setDimension({ columnId, groupId, layerId: targetLayerId, @@ -247,16 +245,15 @@ export function LayerPanel( } }; }, [ - framePublicAPI, + layerDatasource, + setNextFocusedButtonId, groups, layerDatasourceOnDrop, + layerDatasourceDropProps, + activeVisualization, props.visualizationState, + framePublicAPI, updateVisualization, - setDimension, - removeDimension, - layerDatasourceDropProps, - setNextFocusedButtonId, - layerDatasource, ]); const isDimensionPanelOpen = Boolean(activeId); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index cecf75cd58676..b5fa32cd8e306 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -9,11 +9,10 @@ import { Ast, AstFunction, fromExpression } from '@kbn/interpreter'; import { DatasourceStates } from '../../state_management'; import { Visualization, DatasourceMap, DatasourceLayers } from '../../types'; -export function prependDatasourceExpression( - visualizationExpression: Ast | string | null, +export function getDatasourceExpressionsByLayers( datasourceMap: DatasourceMap, datasourceStates: DatasourceStates -): Ast | null { +): null | Record { const datasourceExpressions: Array<[string, Ast | string]> = []; 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; } - 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 = { type: 'function', function: 'lens_merge_tables', arguments: { 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) { 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, { title, description, }); - const completeExpression = prependDatasourceExpression( - visualizationExpression, - datasourceMap, - datasourceStates - ); - - return completeExpression; + return prependDatasourceExpression(visualizationExpression, datasourceMap, datasourceStates); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 770d25be5388f..dc36e0a671cf0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -22,7 +22,7 @@ import { EuiText, } from '@elastic/eui'; 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 classNames from 'classnames'; import { ExecutionContextSearch } from '@kbn/data-plugin/public'; @@ -39,7 +39,10 @@ import { DatasourceLayers, } from '../../types'; 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 { getMissingIndexPattern, @@ -485,6 +488,7 @@ function getPreviewExpression( visualizableState: VisualizableState, visualization: Visualization, datasources: Record, + datasourceStates: DatasourceStates, frame: FramePublicAPI ) { 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( visualizableState.visualizationState, suggestionFrameApi.datasourceLayers @@ -534,10 +551,25 @@ function preparePreviewExpression( const suggestionDatasourceId = visualizableState.datasourceId; const suggestionDatasourceState = visualizableState.datasourceState; + const datasourceStatesWithSuggestions = suggestionDatasourceId + ? { + ...datasourceStates, + [suggestionDatasourceId]: { + isLoading: false, + state: suggestionDatasourceState, + }, + } + : datasourceStates; + + const previewExprDatasourcesStates = visualization.shouldBuildDatasourceExpressionManually?.() + ? datasourceStatesWithSuggestions + : datasourceStates; + const expression = getPreviewExpression( visualizableState, visualization, datasourceMap, + previewExprDatasourcesStates, framePublicAPI ); @@ -545,19 +577,9 @@ function preparePreviewExpression( return; } - const expressionWithDatasource = prependDatasourceExpression( - expression, - datasourceMap, - suggestionDatasourceId - ? { - ...datasourceStates, - [suggestionDatasourceId]: { - isLoading: false, - state: suggestionDatasourceState, - }, - } - : datasourceStates - ); + if (visualization.shouldBuildDatasourceExpressionManually?.()) { + return typeof expression === 'string' ? fromExpression(expression) : expression; + } - return expressionWithDatasource; + return prependDatasourceExpression(expression, datasourceMap, datasourceStatesWithSuggestions); } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 52f5902b90367..56a24458bbdc0 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -348,6 +348,7 @@ export class Embeddable if (!this.savedVis || !this.savedVis.visualizationType) { return []; } + return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || []; } @@ -458,6 +459,7 @@ export class Embeddable this.embeddableTitle = this.getTitle(); isDirty = true; } + return isDirty; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx index cb6e6e030eaac..a8d94b743adf8 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/toolbar_component.tsx @@ -62,11 +62,11 @@ export const HeatmapToolbar = memo( buttonDataTestSubj="lnsVisualOptionsButton" > { setState({ ...state, - gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'inside' }, + gridConfig: { ...state.gridConfig, isCellLabelVisible: newMode === 'show' }, }); }} /> diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 942df72957fc7..b8d00e7ff61b8 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -15,6 +15,7 @@ export type { XYState, XYReferenceLineLayerConfig, XYLayerConfig, + ValidLayer, XYDataLayerConfig, XYAnnotationLayerConfig, } from './xy_visualization/types'; @@ -70,7 +71,7 @@ export type { } from './indexpattern_datasource/types'; export type { XYArgs, - YConfig, + ExtendedYConfig, XYRender, LayerType, YAxisMode, @@ -80,28 +81,27 @@ export type { YScaleType, XScaleType, AxisConfig, - ValidLayer, XYCurveType, XYChartProps, LegendConfig, IconPosition, - YConfigResult, + ExtendedYConfigResult, DataLayerArgs, LensMultiTable, ValueLabelMode, AxisExtentMode, + DataLayerConfig, FittingFunction, AxisExtentConfig, LegendConfigResult, AxesSettingsConfig, GridlinesConfigResult, - DataLayerConfigResult, TickLabelsConfigResult, AxisExtentConfigResult, ReferenceLineLayerArgs, LabelsOrientationConfig, + ReferenceLineLayerConfig, LabelsOrientationConfigResult, - ReferenceLineLayerConfigResult, AxisTitlesVisibilityConfigResult, } from '@kbn/expression-xy-plugin/common'; export type { LensEmbeddableInput } from './embeddable'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b72519c2191be..bfa28b1d1c1ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -235,6 +235,7 @@ export function getIndexPatternDatasource({ if (staticValue == null) { return state; } + return mergeLayer({ state, layerId, diff --git a/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx b/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx index 7372b727268bd..5e4ef239b4295 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_location_settings.tsx @@ -30,11 +30,11 @@ export interface LegendLocationSettingsProps { /** * 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 */ - horizontalAlignment?: HorizontalAlignment; + horizontalAlignment?: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right; /** * Callback on horizontal alignment option change */ diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx index 560a695eaa089..944c55fb56091 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx @@ -58,11 +58,11 @@ export interface LegendSettingsPopoverProps { /** * 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 */ - horizontalAlignment?: HorizontalAlignment; + horizontalAlignment?: typeof HorizontalAlignment.Left | typeof HorizontalAlignment.Right; /** * Callback on horizontal alignment option change */ @@ -225,13 +225,15 @@ export const LegendSettingsPopover: React.FunctionComponent - + {location !== 'inside' && ( + + )} {location && ( { }); it('should render the passed value if given', () => { - const component = shallow(); + const component = shallow(); expect( component.find('[data-test-subj="lens-value-labels-visibility-btn"]').prop('idSelected') ).toEqual(`value_labels_inside`); diff --git a/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx b/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx index 64d9f5475379a..f5378a2e3ba01 100644 --- a/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/value_labels_settings.tsx @@ -26,7 +26,7 @@ const valueLabelsOptions: Array<{ }, { id: `value_labels_inside`, - value: 'inside', + value: 'show', label: i18n.translate('xpack.lens.shared.valueLabelsVisibility.inside', { defaultMessage: 'Show', }), diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index e4ff1aab1a6c4..8c6c6d9af22dc 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -588,7 +588,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { labels?: { buttonAriaLabel: string; buttonLabel: string }; }; -interface VisualizationDimensionChangeProps { +export interface VisualizationDimensionChangeProps { layerId: string; columnId: string; prevState: T; @@ -887,7 +887,8 @@ export interface Visualization { toExpression: ( state: T, datasourceLayers: DatasourceLayers, - attributes?: Partial<{ title: string; description: string }> + attributes?: Partial<{ title: string; description: string }>, + datasourceExpressionsByLayers?: Record ) => ExpressionAstExpression | string | null; /** * Expression to render a preview version of the chart in very constrained space. @@ -895,7 +896,8 @@ export interface Visualization { */ toPreviewExpression?: ( state: T, - datasourceLayers: DatasourceLayers + datasourceLayers: DatasourceLayers, + datasourceExpressionsByLayers?: Record ) => ExpressionAstExpression | string | null; /** * 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 { * On Edit events the frame will call this to know what's going to be the next visualization state */ onEditAction?: (state: T, event: LensEditEvent) => 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 { diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 256df38ffa5b3..9c4ee0d3b245f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -30,9 +30,6 @@ Object { "curveType": Array [ "LINEAR", ], - "description": Array [ - "", - ], "emphasizeFitting": Array [ true, ], @@ -135,6 +132,18 @@ Object { "splitAccessor": Array [ "d", ], + "table": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "kibana", + "type": "function", + }, + ], + "type": "expression", + }, + ], "xAccessor": Array [ "a", ], @@ -146,7 +155,7 @@ Object { "linear", ], }, - "function": "dataLayer", + "function": "extendedDataLayer", "type": "function", }, ], @@ -204,9 +213,6 @@ Object { "type": "expression", }, ], - "title": Array [ - "", - ], "valueLabels": Array [ "hide", ], @@ -263,7 +269,7 @@ Object { "", ], }, - "function": "xyVis", + "function": "layeredXyVis", "type": "function", }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 9157d310d00b0..f9f4b9da5342a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -135,6 +135,7 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( : undefined; let resultAnnotations = [...inputAnnotations] as XYAnnotationLayerConfig['annotations']; + if (!currentConfig) { resultAnnotations.push({ label: defaultAnnotationLabel, diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts index db8ae7122c538..73b355bce7ed2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { DataLayerConfigResult } from '@kbn/expression-xy-plugin/common'; import { layerTypes } from '../../common'; import { Datatable } from '@kbn/expressions-plugin/public'; import { getAxesConfiguration } from './axes_configuration'; +import { XYDataLayerConfig } from './types'; describe('axes_configuration', () => { const tables: Record = { @@ -219,8 +219,7 @@ describe('axes_configuration', () => { }, }; - const sampleLayer: DataLayerConfigResult = { - type: 'dataLayer', + const sampleLayer: XYDataLayerConfig = { layerId: 'first', layerType: layerTypes.DATA, seriesType: 'line', diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index c7e615bb29e12..5ddb1fcc043e7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -7,7 +7,7 @@ import { groupBy, partition } from 'lodash'; 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 { layerTypes } from '../../common'; import type { DatasourceLayers, FramePublicAPI, Visualization } from '../types'; @@ -34,7 +34,7 @@ export interface ReferenceLineBase { * * what groups are current defined in data layers * * what existing reference line are currently defined in reference layers */ -export function getGroupsToShow( +export function getGroupsToShow( referenceLayers: T[], state: XYState | undefined, datasourceLayers: DatasourceLayers, @@ -104,6 +104,7 @@ export function getStaticValue( untouchedDataLayers, accessors, } = getAccessorCriteriaForGroup(groupId, dataLayers, activeData); + if ( groupId === 'x' && filteredLayers.length && @@ -111,6 +112,7 @@ export function getStaticValue( ) { return fallbackValue; } + const computedValue = computeStaticValueForGroup( filteredLayers, accessors, @@ -118,6 +120,7 @@ export function getStaticValue( groupId !== 'x', // histogram axis should compute the min based on the current data groupId !== 'x' ); + return computedValue ?? fallbackValue; } @@ -165,6 +168,7 @@ export function computeOverallDataDomain( const accessorMap = new Set(accessorIds); let min: number | undefined; let max: number | undefined; + const [stacked, unstacked] = partition( dataLayers, ({ seriesType }) => isStackedChart(seriesType) && allowStacking @@ -268,13 +272,17 @@ export const getReferenceSupportedLayer = ( label: 'x' as const, }, ]; + const referenceLineGroups = getGroupsRelatedToData( referenceLineGroupIds, state, frame?.datasourceLayers || {}, frame?.activeData ); - const dataLayers = getDataLayers(state?.layers || []); + + const layers = state?.layers || []; + const dataLayers = getDataLayers(layers); + const filledDataLayers = dataLayers.filter( ({ accessors, xAccessor }) => accessors.length || xAccessor ); @@ -289,7 +297,7 @@ export const getReferenceSupportedLayer = ( groupId: id, columnId: generateId(), dataType: 'number', - label: getAxisName(label, { isHorizontal: isHorizontalChart(state?.layers || []) }), + label: getAxisName(label, { isHorizontal: isHorizontalChart(layers) }), staticValue: getStaticValue( dataLayers, label, @@ -317,6 +325,7 @@ export const getReferenceSupportedLayer = ( initialDimensions, }; }; + export const setReferenceDimension: Visualization['setDimension'] = ({ prevState, layerId, @@ -397,6 +406,7 @@ export const getReferenceConfiguration = ({ return axisMode; } ); + const groupsToShow = getGroupsToShow( [ // 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, frame.datasourceLayers, - frame?.activeData + frame.activeData ); const isHorizontal = isHorizontalChart(state.layers); return { diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index 06ea897afa1e7..f37ad12f606b1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -6,7 +6,7 @@ */ 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 { visualizationTypes, @@ -58,7 +58,8 @@ export const getSeriesColor = (layer: XYLayerConfig, accessor: string) => { return null; } 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( - layers: ValidLayer[] = [], + layers: XYDataLayerConfig[] = [], datasourceLayers?: FramePublicAPI['datasourceLayers'] ) { if (!datasourceLayers) { @@ -87,7 +88,11 @@ export function hasHistogramSeries( } 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); return ( xAxisOperation && diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 7cd9a43540c0a..d39b9914f0eef 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Ast } from '@kbn/interpreter'; +import { Ast, fromExpression } from '@kbn/interpreter'; import { Position } from '@elastic/charts'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { getXyVisualization } from './xy_visualization'; @@ -28,6 +28,8 @@ describe('#toExpression', () => { let mockDatasource: ReturnType; let frame: ReturnType; + let datasourceExpressionsByLayers: Record; + beforeEach(() => { frame = createMockFramePublicAPI(); mockDatasource = createMockDatasource('testDatasource'); @@ -46,6 +48,23 @@ describe('#toExpression', () => { frame.datasourceLayers = { 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', () => { @@ -82,7 +101,9 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers ) ).toMatchSnapshot(); }); @@ -106,7 +127,9 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers ) as Ast ).chain[0].arguments.fittingFunction[0] ).toEqual('None'); @@ -129,7 +152,9 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers ) as Ast; expect( (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; 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(); }); @@ -204,7 +233,9 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers )! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); @@ -241,7 +272,9 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers ) as Ast; expect( (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; expect((expression.chain[0].arguments.labelsOrientation[0] as Ast).chain[0].arguments).toEqual({ x: [0], @@ -295,7 +330,9 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers ) as Ast; expect( (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments @@ -310,7 +347,7 @@ describe('#toExpression', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, - valueLabels: 'inside', + valueLabels: 'show', preferredSeriesType: 'bar', layers: [ { @@ -323,16 +360,18 @@ describe('#toExpression', () => { }, ], }, - frame.datasourceLayers + frame.datasourceLayers, + undefined, + datasourceExpressionsByLayers ) 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', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, - valueLabels: 'inside', + valueLabels: 'show', preferredSeriesType: 'bar', layers: [ { @@ -352,7 +391,9 @@ describe('#toExpression', () => { }, ], }, - { ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock } + { ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock }, + undefined, + datasourceExpressionsByLayers ) as Ast; function getYConfigColorForLayer(ast: Ast, index: number) { diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 423cc49b68cf7..1c73c455dfe9e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -10,13 +10,15 @@ import { ScaleType } from '@elastic/charts'; import type { PaletteRegistry } from '@kbn/coloring'; 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 { State, XYDataLayerConfig, XYReferenceLineLayerConfig, XYAnnotationLayerConfig, } from './types'; +import type { ValidXYDataLayerConfig } from './types'; import { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import { hasIcon } from './xy_config_panel/shared/icon_select'; @@ -50,6 +52,7 @@ export const toExpression = ( datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, attributes: Partial<{ title: string; description: string }> = {}, + datasourceExpressionsByLayers: Record, eventAnnotationService: EventAnnotationServiceType ): Ast | null => { if (!state || !state.layers.length) { @@ -73,7 +76,7 @@ export const toExpression = ( metadata, datasourceLayers, paletteService, - attributes, + datasourceExpressionsByLayers, eventAnnotationService ); }; @@ -100,6 +103,7 @@ export function toPreviewExpression( state: State, datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, + datasourceExpressionsByLayers: Record, eventAnnotationService: EventAnnotationServiceType ) { return toExpression( @@ -116,6 +120,7 @@ export function toPreviewExpression( datasourceLayers, paletteService, {}, + datasourceExpressionsByLayers, eventAnnotationService ); } @@ -151,11 +156,13 @@ export const buildExpression = ( metadata: Record>, datasourceLayers: DatasourceLayers, paletteService: PaletteRegistry, - attributes: Partial<{ title: string; description: string }> = {}, + datasourceExpressionsByLayers: Record, eventAnnotationService: EventAnnotationServiceType ): Ast | null => { - const validDataLayers = getDataLayers(state.layers) - .filter((layer): layer is ValidLayer => Boolean(layer.accessors.length)) + const validDataLayers: ValidXYDataLayerConfig[] = getDataLayers(state.layers) + .filter((layer): layer is ValidXYDataLayerConfig => + Boolean(layer.accessors.length) + ) .map((layer) => ({ ...layer, accessors: getSortedAccessors(datasourceLayers[layer.layerId], layer), @@ -188,10 +195,8 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'xyVis', + function: 'layeredXyVis', arguments: { - title: [attributes?.title || ''], - description: [attributes?.description || ''], xTitle: [state.xTitle || ''], yTitle: [state.yTitle || ''], yRightTitle: [state.yRightTitle || ''], @@ -207,20 +212,26 @@ export const buildExpression = ( 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] : [], - legendSize: state.legend.legendSize ? [state.legend.legendSize] : [], - horizontalAlignment: state.legend.horizontalAlignment - ? [state.legend.horizontalAlignment] - : [], - verticalAlignment: state.legend.verticalAlignment - ? [state.legend.verticalAlignment] - : [], + legendSize: + !state.legend.isInside && state.legend.legendSize + ? [state.legend.legendSize] + : [], + horizontalAlignment: + state.legend.horizontalAlignment && state.legend.isInside + ? [state.legend.horizontalAlignment] + : [], + verticalAlignment: + state.legend.verticalAlignment && state.legend.isInside + ? [state.legend.verticalAlignment] + : [], // ensure that even if the user types more than 5 columns // we will only show 5 - floatingColumns: state.legend.floatingColumns - ? [Math.min(5, state.legend.floatingColumns)] - : [], + floatingColumns: + state.legend.floatingColumns && state.legend.isInside + ? [Math.min(5, state.legend.floatingColumns)] + : [], maxLines: state.legend.maxLines ? [state.legend.maxLines] : [], shouldTruncate: [ state.legend.shouldTruncate ?? @@ -236,50 +247,8 @@ export const buildExpression = ( emphasizeFitting: [state.emphasizeFitting || false], curveType: [state.curveType || 'LINEAR'], fillOpacity: [state.fillOpacity || 0.3], - yLeftExtent: [ - { - 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] - : [], - }, - }, - ], - }, - ], + yLeftExtent: [axisExtentConfigToExpression(state.yLeftExtent, validDataLayers)], + yRightExtent: [axisExtentConfigToExpression(state.yRightExtent, validDataLayers)], axisTitlesVisibilitySettings: [ { type: 'expression', @@ -353,13 +322,15 @@ export const buildExpression = ( layer, datasourceLayers[layer.layerId], metadata, - paletteService + paletteService, + datasourceExpressionsByLayers[layer.layerId] ) ), ...validReferenceLayers.map((layer) => referenceLineLayerToExpression( layer, - datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId] + datasourceLayers[(layer as XYReferenceLineLayerConfig).layerId], + datasourceExpressionsByLayers[layer.layerId] ) ), ...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 = ( layer: XYReferenceLineLayerConfig, - datasourceLayer: DatasourcePublicAPI + datasourceLayer: DatasourcePublicAPI, + datasourceExpression: Ast ): Ast => { return { type: 'expression', chain: [ { type: 'function', - function: 'referenceLineLayer', + function: 'extendedReferenceLineLayer', arguments: { layerId: [layer.layerId], yConfig: layer.yConfig ? layer.yConfig.map((yConfig) => - yConfigToExpression(yConfig, defaultReferenceLineColor) + extendedYConfigToExpression(yConfig, defaultReferenceLineColor) ) : [], accessors: layer.accessors, columnToLabel: [JSON.stringify(getColumnToLabelMap(layer, datasourceLayer))], + ...(datasourceExpression ? { table: [buildTableExpression(datasourceExpression)] } : {}), }, }, ], @@ -406,7 +384,7 @@ const annotationLayerToExpression = ( chain: [ { type: 'function', - function: 'annotationLayer', + function: 'extendedAnnotationLayer', arguments: { hide: [Boolean(layer.hide)], layerId: [layer.layerId], @@ -420,10 +398,11 @@ const annotationLayerToExpression = ( }; const dataLayerToExpression = ( - layer: ValidLayer, + layer: ValidXYDataLayerConfig, datasourceLayer: DatasourcePublicAPI, metadata: Record>, - paletteService: PaletteRegistry + paletteService: PaletteRegistry, + datasourceExpression: Ast ): Ast => { const columnToLabel = getColumnToLabelMap(layer, datasourceLayer); @@ -441,7 +420,7 @@ const dataLayerToExpression = ( chain: [ { type: 'function', - function: 'dataLayer', + function: 'extendedDataLayer', arguments: { layerId: [layer.layerId], hide: [Boolean(layer.hide)], @@ -458,6 +437,7 @@ const dataLayerToExpression = ( seriesType: [layer.seriesType], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], + ...(datasourceExpression ? { table: [buildTableExpression(datasourceExpression)] } : {}), palette: [ { type: 'expression', @@ -496,6 +476,23 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => { { type: 'function', 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: { forAccessor: [yConfig.forAccessor], 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] : [], + }, + }, + ], +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 276e338807a35..b96ddf1aaee2d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -16,7 +16,10 @@ import type { FittingFunction, LabelsOrientationConfig, EndValue, + ExtendedYConfig, YConfig, + YScaleType, + XScaleType, } from '@kbn/expression-xy-plugin/common'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { LensIconChartArea } from '../assets/chart_area'; @@ -43,12 +46,16 @@ export interface XYDataLayerConfig { yConfig?: YConfig[]; splitAccessor?: string; palette?: PaletteOutput; + yScaleType?: YScaleType; + xScaleType?: XScaleType; + isHistogram?: boolean; + columnToLabel?: string; } export interface XYReferenceLineLayerConfig { layerId: string; accessors: string[]; - yConfig?: YConfig[]; + yConfig?: ExtendedYConfig[]; layerType: 'referenceLine'; } @@ -64,6 +71,13 @@ export type XYLayerConfig = | XYReferenceLineLayerConfig | XYAnnotationLayerConfig; +export interface ValidXYDataLayerConfig extends XYDataLayerConfig { + xAccessor: NonNullable; + layerId: string; +} + +export type ValidLayer = ValidXYDataLayerConfig | XYReferenceLineLayerConfig; + // Persisted parts of the state export interface XYState { preferredSeriesType: SeriesType; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index d67adb3b860e3..97bac36a93465 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -16,7 +16,12 @@ import { ThemeServiceStart } from '@kbn/core/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-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 { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; @@ -61,7 +66,7 @@ import { } from './visualization_helpers'; import { groupAxesByType } from './axes_configuration'; 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 { DimensionTrigger } from '../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; @@ -295,6 +300,7 @@ export const getXyVisualization = ({ setDimension(props) { const { prevState, layerId, columnId, groupId } = props; + const foundLayer: XYLayerConfig | undefined = prevState.layers.find( (l) => l.layerId === layerId ); @@ -333,7 +339,7 @@ export const getXyVisualization = ({ } const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value'); const axisMode = axisPosition as YAxisMode; - const yConfig = metrics.map((metric, idx) => { + const yConfig = metrics.map((metric, idx) => { return { color: metric.color, forAccessor: metric.accessor ?? foundLayer.accessors[idx], @@ -444,7 +450,7 @@ export const getXyVisualization = ({ const groupsAvailable = getGroupsAvailableInData( getDataLayers(prevState.layers), frame.datasourceLayers, - frame?.activeData + frame.activeData ); if ( @@ -508,10 +514,26 @@ export const getXyVisualization = ({ ); }, - toExpression: (state, layers, attributes) => - toExpression(state, layers, paletteService, attributes, eventAnnotationService), - toPreviewExpression: (state, layers) => - toPreviewExpression(state, layers, paletteService, eventAnnotationService), + shouldBuildDatasourceExpressionManually: () => true, + + toExpression: (state, layers, attributes, datasourceExpressionsByLayers = {}) => + toExpression( + state, + layers, + paletteService, + attributes, + datasourceExpressionsByLayers, + eventAnnotationService + ), + + toPreviewExpression: (state, layers, datasourceExpressionsByLayers = {}) => + toPreviewExpression( + state, + layers, + paletteService, + datasourceExpressionsByLayers, + eventAnnotationService + ), getErrorMessages(state, datasourceLayers) { // Data error handling below here @@ -592,10 +614,12 @@ export const getXyVisualization = ({ ...getDataLayers(state.layers), ...getReferenceLayers(state.layers), ].filter(({ accessors }) => accessors.length > 0); + const accessorsWithArrayValues = []; + for (const layer of filteredLayers) { const { layerId, accessors } = layer; - const rows = frame.activeData[layerId] && frame.activeData[layerId].rows; + const rows = frame.activeData?.[layerId] && frame.activeData[layerId].rows; if (!rows) { break; } @@ -607,6 +631,7 @@ export const getXyVisualization = ({ } } } + return accessorsWithArrayValues.map((label) => ( { + 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 & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; + } +) => { + const { state, setState, layerId, accessor, frame } = props; + const isHorizontal = isHorizontalChart(state.layers); + + const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ + 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 ( + <> + + {isRange ? ( + <> + { + 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', + })} + /> + { + 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(), + }, + }); + } + } + }} + /> + + ) : ( + { + if (date) { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'point_in_time' }), + timestamp: date.toISOString(), + }, + }); + } + }} + /> + )} + + + + + { + setAnnotations({ label: value }); + }} + /> + {!isRange && ( + + )} + {!isRange && ( + + )} + {!isRange && ( + + )} + + {isRange && ( + + { + setAnnotations({ + outside: id === `lens_xyChart_fillStyle_outside`, + }); + }} + isFullWidth + /> + + )} + + + setAnnotations({ isHidden: ev.target.checked })} + /> + + + ); +}; + +const ConfigPanelApplyAsRangeSwitch = ({ + annotation, + onChange, + frame, + state, +}: { + annotation?: EventAnnotationConfig; + onChange: (annotations: Partial | undefined) => void; + frame: FramePublicAPI; + state: XYState; +}) => { + const isRange = isRangeAnnotation(annotation); + return ( + + + {i18n.translate('xpack.lens.xyChart.applyAsRange', { + defaultMessage: 'Apply as range', + })} + + } + 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 + /> + + ); +}; + +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 ( + + {prependLabel ? ( + {prependLabel} + } + > + + + ) : ( + + )} + + ); +}; + +const ConfigPanelHideSwitch = ({ + value, + onChange, +}: { + value: boolean; + onChange: (event: EuiSwitchEvent) => void; +}) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/icon_set.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/icon_set.ts index 87813ec12913e..32721285a4477 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/icon_set.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/icon_set.ts @@ -4,10 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + 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 = [ +export const annotationsIconSet: IconSet = [ { value: 'asterisk', label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx index 6194a7f0da120..f97b4009e3e3e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx @@ -29,7 +29,7 @@ const customLineStaticAnnotation = { id: 'ann1', key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, label: 'Event', - icon: 'triangle', + icon: 'triangle' as const, color: 'red', lineStyle: 'dashed' as const, lineWidth: 3, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx index 511d00f33a01c..bd63354936703 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx @@ -5,501 +5,4 @@ * 2.0. */ -import './index.scss'; -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 & { - formatFactory: FormatFactory; - paletteService: PaletteRegistry; - } -) => { - const { state, setState, layerId, accessor, frame } = props; - const isHorizontal = isHorizontalChart(state.layers); - - const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ - 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 ( - <> - - {isRange ? ( - <> - { - 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', - })} - /> - { - 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(), - }, - }); - } - } - }} - /> - - ) : ( - { - if (date) { - setAnnotations({ - key: { - ...(currentAnnotation?.key || { type: 'point_in_time' }), - timestamp: date.toISOString(), - }, - }); - } - }} - /> - )} - - - - - { - setAnnotations({ label: value }); - }} - /> - {!isRange && ( - - )} - {!isRange && ( - - )} - {!isRange && ( - - )} - - {isRange && ( - - { - setAnnotations({ - outside: id === `lens_xyChart_fillStyle_outside`, - }); - }} - isFullWidth - /> - - )} - - - setAnnotations({ isHidden: ev.target.checked })} - /> - - - ); -}; - -const ConfigPanelApplyAsRangeSwitch = ({ - annotation, - onChange, - frame, - state, -}: { - annotation?: EventAnnotationConfig; - onChange: (annotations: Partial | undefined) => void; - frame: FramePublicAPI; - state: XYState; -}) => { - const isRange = isRangeAnnotation(annotation); - return ( - - - {i18n.translate('xpack.lens.xyChart.applyAsRange', { - defaultMessage: 'Apply as range', - })} - - } - 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 - /> - - ); -}; - -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 ( - - {prependLabel ? ( - {prependLabel} - } - > - - - ) : ( - - )} - - ); -}; - -const ConfigPanelHideSwitch = ({ - value, - onChange, -}: { - value: boolean; - onChange: (event: EuiSwitchEvent) => void; -}) => { - return ( - - - - ); -}; +export { AnnotationsPanel } from './annotations_panel'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index 61e6a4f992390..00f03f261ef7b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -76,10 +76,9 @@ export const ColorPicker = ({ frame.datasourceLayers[layer.layerId] ?? layer.accessors, layer ); - const colorAssignments = getColorAssignments( getDataLayers(state.layers), - { tables: frame.activeData }, + { tables: frame.activeData ?? {} }, formatFactory ); const mappedAccessors = getAccessorColorConfig( @@ -91,7 +90,6 @@ export const ColorPicker = ({ }, paletteService ); - return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; } }, [ @@ -105,12 +103,12 @@ export const ColorPicker = ({ defaultColor, ]); + const [color, setColor] = useState(currentColor); + useEffect(() => { setColor(currentColor); }, [currentColor]); - const [color, setColor] = useState(currentColor); - const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { setColor(text); if (output.isValid || text === '') { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx index 39d08e4c710bd..eec3335c5b44c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import { YAxisMode, YConfig } from '@kbn/expression-xy-plugin/common'; +import { YAxisMode, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; import type { VisualizationDimensionEditorProps } from '../../types'; import { State, XYState, XYDataLayerConfig } from '../types'; import { FormatFactory } from '../../../common'; @@ -17,7 +17,7 @@ import { isHorizontalChart } from '../state_helpers'; import { ColorPicker } from './color_picker'; import { PalettePicker, useDebouncedValue } from '../../shared_components'; import { isAnnotationsLayer, isReferenceLayer } from '../visualization_helpers'; -import { ReferenceLinePanel } from './reference_line_panel'; +import { ReferenceLinePanel } from './reference_line_config_panel'; import { AnnotationsPanel } from './annotations_config_panel'; type UnwrapArray = T extends Array ? P : T; @@ -57,7 +57,7 @@ export function DimensionEditor( const axisMode = localYConfig?.axisMode || 'auto'; const setConfig = useCallback( - (yConfig: Partial | undefined) => { + (yConfig: Partial | undefined) => { if (yConfig == null) { return; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index b61f4694f8a91..00c4e9c8eaeb2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -7,7 +7,7 @@ import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { Position, ScaleType, VerticalAlignment, HorizontalAlignment } from '@elastic/charts'; +import { Position, ScaleType } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AxesSettingsConfig, AxisExtentConfig } from '@kbn/expression-xy-plugin/common'; import type { VisualizationToolbarProps, FramePublicAPI } from '../../types'; @@ -21,6 +21,7 @@ import { getScaleType } from '../to_expression'; import { TooltipWrapper } from '../../shared_components'; import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values'; import { getDataLayers } from '../visualization_helpers'; +import { LegendSettingsPopoverProps } from '../../shared_components/legend_settings_popover'; type UnwrapArray = T extends Array ? P : T; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; @@ -380,8 +381,10 @@ export const XyToolbar = memo(function XyToolbar( }} onAlignmentChange={(value) => { const [vertical, horizontal] = value.split('_'); - const verticalAlignment = vertical as VerticalAlignment; - const horizontalAlignment = horizontal as HorizontalAlignment; + const verticalAlignment = vertical as LegendSettingsPopoverProps['verticalAlignment']; + const horizontalAlignment = + horizontal as LegendSettingsPopoverProps['horizontalAlignment']; + setState({ ...state, legend: { ...state.legend, verticalAlignment, horizontalAlignment }, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index 367785da2d6b1..1fbb2af8f1e98 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -11,14 +11,14 @@ import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@e import { SeriesType } from '@kbn/expression-xy-plugin/common'; import { ToolbarButton } from '@kbn/kibana-react-plugin/public'; import type { VisualizationLayerWidgetProps, VisualizationType } from '../../types'; -import { State, visualizationTypes, XYDataLayerConfig } from '../types'; +import { State, visualizationTypes } from '../types'; import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { StaticHeader } from '../../shared_components'; import { LensIconChartBarReferenceLine } from '../../assets/chart_bar_reference_line'; import { LensIconChartBarAnnotations } from '../../assets/chart_bar_annotations'; import { updateLayer } from '.'; -import { isAnnotationsLayer, isReferenceLayer } from '../visualization_helpers'; +import { isAnnotationsLayer, isDataLayer, isReferenceLayer } from '../visualization_helpers'; export function LayerHeader(props: VisualizationLayerWidgetProps) { const layer = props.state.layers.find((l) => l.layerId === props.layerId); @@ -58,7 +58,7 @@ function AnnotationsLayerHeader() { function DataLayerHeader(props: VisualizationLayerWidgetProps) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); const { state, layerId } = props; - const layers = state.layers as XYDataLayerConfig[]; + const layers = state.layers.filter(isDataLayer); const index = layers.findIndex((l) => l.layerId === layerId); const layer = layers[index]; const currentVisType = visualizationTypes.find(({ id }) => id === layer.seriesType)!; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/icon_set.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/icon_set.ts new file mode 100644 index 0000000000000..eda5d06cd3ef1 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/icon_set.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { AvailableReferenceLineIcon } from '@kbn/expression-xy-plugin/common'; +import { IconSet } from '../shared/icon_select'; + +export const referenceLineIconsSet: IconSet = [ + { + value: 'empty', + label: i18n.translate('xpack.lens.xyChart.iconSelect.noIconLabel', { + defaultMessage: 'None', + }), + }, + { + value: 'asterisk', + label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', { + defaultMessage: 'Asterisk', + }), + }, + { + value: 'bell', + label: i18n.translate('xpack.lens.xyChart.iconSelect.bellIconLabel', { + defaultMessage: 'Bell', + }), + }, + { + value: 'bolt', + label: i18n.translate('xpack.lens.xyChart.iconSelect.boltIconLabel', { + defaultMessage: 'Bolt', + }), + }, + { + value: 'bug', + label: i18n.translate('xpack.lens.xyChart.iconSelect.bugIconLabel', { + defaultMessage: 'Bug', + }), + }, + { + value: 'editorComment', + label: i18n.translate('xpack.lens.xyChart.iconSelect.commentIconLabel', { + defaultMessage: 'Comment', + }), + }, + { + value: 'alert', + label: i18n.translate('xpack.lens.xyChart.iconSelect.alertIconLabel', { + defaultMessage: 'Alert', + }), + }, + { + value: 'flag', + label: i18n.translate('xpack.lens.xyChart.iconSelect.flagIconLabel', { + defaultMessage: 'Flag', + }), + }, + { + value: 'tag', + label: i18n.translate('xpack.lens.xyChart.iconSelect.tagIconLabel', { + defaultMessage: 'Tag', + }), + }, +]; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/index.tsx new file mode 100644 index 0000000000000..4297f7d35cd6c --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/index.tsx @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { ReferenceLinePanel } from './reference_line_panel'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx similarity index 81% rename from x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx index 07c9675635a1f..e25c191d2bec1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx @@ -9,23 +9,24 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import { FillStyle, YConfig } from '@kbn/expression-xy-plugin/common'; -import type { VisualizationDimensionEditorProps } from '../../types'; -import { State, XYState, XYReferenceLineLayerConfig } from '../types'; -import { FormatFactory } from '../../../common'; +import { FillStyle, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; +import type { VisualizationDimensionEditorProps } from '../../../types'; +import { State, XYState, XYReferenceLineLayerConfig } from '../../types'; +import { FormatFactory } from '../../../../common'; -import { ColorPicker } from './color_picker'; -import { updateLayer } from '.'; -import { useDebouncedValue } from '../../shared_components'; -import { idPrefix } from './dimension_editor'; -import { isHorizontalChart } from '../state_helpers'; +import { ColorPicker } from '../color_picker'; +import { updateLayer } from '..'; +import { useDebouncedValue } from '../../../shared_components'; +import { idPrefix } from '../dimension_editor'; +import { isHorizontalChart } from '../../state_helpers'; import { IconSelectSetting, MarkerDecorationPosition, TextDecorationSetting, -} from './shared/marker_decoration_settings'; -import { LineStyleSettings } from './shared/line_style_settings'; -import { defaultReferenceLineColor } from '../color_assignment'; +} from '../shared/marker_decoration_settings'; +import { LineStyleSettings } from '../shared/line_style_settings'; +import { referenceLineIconsSet } from './icon_set'; +import { defaultReferenceLineColor } from '../../color_assignment'; export const ReferenceLinePanel = ( props: VisualizationDimensionEditorProps & { @@ -51,7 +52,7 @@ export const ReferenceLinePanel = ( ); const setConfig = useCallback( - (yConfig: Partial | undefined) => { + (yConfig: Partial | undefined) => { if (yConfig == null) { return; } @@ -75,7 +76,11 @@ export const ReferenceLinePanel = ( return ( <> - + | undefined) => void; + currentConfig?: ExtendedYConfig; + setConfig: (yConfig: Partial | undefined) => void; isHorizontal: boolean; }) => { return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/icon_select.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/icon_select.tsx index 08034dc9c8f70..74eb23e3e8297 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/icon_select.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/icon_select.tsx @@ -6,71 +6,20 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiIcon, IconType } from '@elastic/eui'; +import { AvailableReferenceLineIcon } from '@kbn/expression-xy-plugin/common'; export function hasIcon(icon: string | undefined): icon is string { return icon != null && icon !== 'empty'; } -export type IconSet = Array<{ value: string; label: string; icon?: IconType }>; - -export const euiIconsSet = [ - { - value: 'empty', - label: i18n.translate('xpack.lens.xyChart.iconSelect.noIconLabel', { - defaultMessage: 'None', - }), - }, - { - value: 'asterisk', - label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', { - defaultMessage: 'Asterisk', - }), - }, - { - value: 'bell', - label: i18n.translate('xpack.lens.xyChart.iconSelect.bellIconLabel', { - defaultMessage: 'Bell', - }), - }, - { - value: 'bolt', - label: i18n.translate('xpack.lens.xyChart.iconSelect.boltIconLabel', { - defaultMessage: 'Bolt', - }), - }, - { - value: 'bug', - label: i18n.translate('xpack.lens.xyChart.iconSelect.bugIconLabel', { - defaultMessage: 'Bug', - }), - }, - { - value: 'editorComment', - label: i18n.translate('xpack.lens.xyChart.iconSelect.commentIconLabel', { - defaultMessage: 'Comment', - }), - }, - { - value: 'alert', - label: i18n.translate('xpack.lens.xyChart.iconSelect.alertIconLabel', { - defaultMessage: 'Alert', - }), - }, - { - value: 'flag', - label: i18n.translate('xpack.lens.xyChart.iconSelect.flagIconLabel', { - defaultMessage: 'Flag', - }), - }, - { - value: 'tag', - label: i18n.translate('xpack.lens.xyChart.iconSelect.tagIconLabel', { - defaultMessage: 'Tag', - }), - }, -]; +export type IconSet = Array<{ + value: T; + label: string; + icon?: T | IconType; + shouldRotate?: boolean; + canFill?: boolean; +}>; const IconView = (props: { value?: string; label: string; icon?: IconType }) => { if (!props.value) return null; @@ -84,17 +33,17 @@ const IconView = (props: { value?: string; label: string; icon?: IconType }) => ); }; -export const IconSelect = ({ +export function IconSelect({ value, onChange, - customIconSet = euiIconsSet, + customIconSet, defaultIcon = 'empty', }: { - value?: string; - onChange: (newIcon: string) => void; - customIconSet?: IconSet; + value?: Icon; + onChange: (newIcon: Icon) => void; + customIconSet: IconSet; defaultIcon?: string; -}) => { +}) { const selectedIcon = customIconSet.find((option) => value === option.value) || customIconSet.find((option) => option.value === defaultIcon)!; @@ -124,4 +73,4 @@ export const IconSelect = ({ } /> ); -}; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx index a52f3130029fd..64b00ef246161 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx @@ -71,22 +71,22 @@ function getIconPositionOptions({ isHorizontal, axisMode }: LabelConfigurationOp ]; } -interface MarkerDecorationConfig { +export interface MarkerDecorationConfig { axisMode?: YAxisMode; - icon?: string; + icon?: T; iconPosition?: IconPosition; textVisibility?: boolean; } -export const TextDecorationSetting = ({ +export function TextDecorationSetting({ currentConfig, setConfig, customIconSet, }: { - currentConfig?: MarkerDecorationConfig; - setConfig: (config: MarkerDecorationConfig) => void; - customIconSet?: IconSet; -}) => { + currentConfig?: MarkerDecorationConfig; + setConfig: (config: MarkerDecorationConfig) => void; + customIconSet?: IconSet; +}) { return ( ); -}; +} -export const IconSelectSetting = ({ +export function IconSelectSetting({ currentConfig, setConfig, customIconSet, defaultIcon = 'empty', }: { - currentConfig?: MarkerDecorationConfig; - setConfig: (config: MarkerDecorationConfig) => void; - customIconSet?: IconSet; + currentConfig?: MarkerDecorationConfig; + setConfig: (config: MarkerDecorationConfig) => void; + customIconSet: IconSet; defaultIcon?: string; -}) => { +}) { return ( ); -}; +} -export const MarkerDecorationPosition = ({ +export function MarkerDecorationPosition({ currentConfig, setConfig, isHorizontal, }: { - currentConfig?: MarkerDecorationConfig; - setConfig: (config: MarkerDecorationConfig) => void; + currentConfig?: MarkerDecorationConfig; + setConfig: (config: MarkerDecorationConfig) => void; isHorizontal: boolean; -}) => { +}) { return ( <> {hasIcon(currentConfig?.icon) || currentConfig?.textVisibility ? ( @@ -213,4 +213,4 @@ export const MarkerDecorationPosition = ({ ) : null} ); -}; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx index d4edb960e70f7..ba8a246043bf2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/index.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ValidLayer } from '@kbn/expression-xy-plugin/common'; import { ToolbarPopover, TooltipWrapper, ValueLabelsSettings } from '../../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { LineCurveOption } from './line_curve_option'; @@ -67,9 +66,7 @@ export const VisualOptionsPopover: React.FC = ({ ['area_stacked', 'area', 'area_percentage_stacked'].includes(seriesType) ); - const isHistogramSeries = Boolean( - hasHistogramSeries(dataLayers as ValidLayer[], datasourceLayers) - ); + const isHistogramSeries = Boolean(hasHistogramSeries(dataLayers, datasourceLayers)); const isValueLabelsEnabled = !hasNonBarSeries && hasBarNotStacked && !isHistogramSeries; const isFittingEnabled = hasNonBarSeries; diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index 0f996e428ed2c..215f080d3dbdf 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -14,6 +14,7 @@ import { import { DOC_TYPE } from '../../common'; import { commonEnhanceTableRowHeight, + commonFixValueLabelsInXY, commonLockOldMetricVisSettings, commonMakeReversePaletteAsCustom, commonRemoveTimezoneDateHistogramParam, @@ -34,7 +35,9 @@ import { LensDocShapePre712, VisState716, VisState810, + VisState820, VisStatePre715, + VisStatePre830, } from '../migrations/types'; import { extract, inject } from '../../common/embeddable_factory'; @@ -95,18 +98,26 @@ export const makeLensEmbeddableFactory = } as unknown as SerializableRecord; }, '8.2.0': (state) => { - const lensState = state as unknown as { attributes: LensDocShape810 }; + const lensState = state as unknown as { + attributes: LensDocShape810; + }; let migratedLensState = commonSetLastValueShowArrayValues(lensState.attributes); - migratedLensState = commonEnhanceTableRowHeight(migratedLensState); + migratedLensState = commonEnhanceTableRowHeight( + migratedLensState as LensDocShape810 + ); migratedLensState = commonSetIncludeEmptyRowsDateHistogram(migratedLensState); + return { ...lensState, attributes: migratedLensState, } as unknown as SerializableRecord; }, '8.3.0': (state) => { - const lensState = state as unknown as { attributes: LensDocShape810 }; - const migratedLensState = commonLockOldMetricVisSettings(lensState.attributes); + const lensState = state as unknown as { attributes: LensDocShape810 }; + let migratedLensState = commonLockOldMetricVisSettings(lensState.attributes); + migratedLensState = commonFixValueLabelsInXY( + migratedLensState as LensDocShape810 + ); return { ...lensState, attributes: migratedLensState, diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index df0ce94abca84..7cafa41f569d4 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -25,8 +25,11 @@ import { VisState716, VisState810, VisState820, + VisState830, CustomVisualizationMigrations, LensDocShape810, + LensDocShape830, + VisStatePre830, } from './types'; import { DOCUMENT_FIELD_NAME, layerTypes, MetricState } from '../../common'; import { LensDocShape } from './saved_object_migrations'; @@ -194,9 +197,7 @@ export const commonRenameFilterReferences = (attributes: LensDocShape715): LensD return newAttributes as LensDocShape810; }; -export const commonSetLastValueShowArrayValues = ( - attributes: LensDocShape810 -): LensDocShape810 => { +export const commonSetLastValueShowArrayValues = (attributes: LensDocShape810): LensDocShape810 => { const newAttributes = cloneDeep(attributes); for (const layer of Object.values(newAttributes.state.datasourceStates.indexpattern.layers)) { for (const column of Object.values(layer.columns)) { @@ -215,19 +216,19 @@ export const commonEnhanceTableRowHeight = ( attributes: LensDocShape810 ): LensDocShape810 => { if (attributes.visualizationType !== 'lnsDatatable') { - return attributes; + return attributes as LensDocShape810; } const visState810 = attributes.state.visualization as VisState810; const newAttributes = cloneDeep(attributes); const vizState = newAttributes.state.visualization as VisState820; vizState.rowHeight = visState810.fitRowToContent ? 'auto' : 'single'; vizState.rowHeightLines = visState810.fitRowToContent ? 2 : 1; - return newAttributes; + return newAttributes as LensDocShape810; }; export const commonSetIncludeEmptyRowsDateHistogram = ( attributes: LensDocShape810 -): LensDocShape810 => { +): LensDocShape810 => { const newAttributes = cloneDeep(attributes); for (const layer of Object.values(newAttributes.state.datasourceStates.indexpattern.layers)) { for (const column of Object.values(layer.columns)) { @@ -241,17 +242,17 @@ export const commonSetIncludeEmptyRowsDateHistogram = ( export const commonLockOldMetricVisSettings = ( attributes: LensDocShape810 -): LensDocShape810 => { +): LensDocShape830 => { const newAttributes = cloneDeep(attributes); if (newAttributes.visualizationType !== 'lnsMetric') { - return newAttributes; + return newAttributes as LensDocShape830; } const visState = newAttributes.state.visualization as MetricState; visState.textAlign = visState.textAlign ?? 'center'; visState.titlePosition = visState.titlePosition ?? 'bottom'; visState.size = visState.size ?? 'xl'; - return newAttributes; + return newAttributes as LensDocShape830; }; const getApplyCustomVisualizationMigrationToLens = (id: string, migration: MigrateFunction) => { @@ -343,3 +344,25 @@ export const fixLensTopValuesCustomFormatting = (attributes: LensDocShape810): L ); return newAttributes as LensDocShape810; }; + +export const commonFixValueLabelsInXY = ( + attributes: LensDocShape830 +): LensDocShape830 => { + if (attributes.visualizationType !== 'lnsXY') { + return attributes as LensDocShape830; + } + + const newAttributes: LensDocShape830 = cloneDeep(attributes); + const { visualization } = newAttributes.state; + const { valueLabels } = visualization; + return { + ...newAttributes, + state: { + ...newAttributes.state, + visualization: { + ...visualization, + valueLabels: valueLabels && valueLabels !== 'hide' ? 'show' : valueLabels, + }, + }, + }; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 7fb9c350a0fa5..af68c5020e420 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -21,6 +21,7 @@ import { VisStatePre715, VisState810, VisState820, + VisState830, } from './types'; import { layerTypes, MetricState } from '../../common'; import { Filter } from '@kbn/es-query'; @@ -2113,4 +2114,52 @@ describe('Lens migrations', () => { expect(visState.size).toBe('s'); }); }); + + describe('8.3.0 valueLabels in XY', () => { + const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + valueLabels: 'inside', + }, + }, + }, + } as unknown as SavedObjectUnsanitizedDoc; + + it('migrates valueLabels from `inside` to `show`', () => { + const result = migrations['8.3.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + const visState = result.attributes.state.visualization as VisState830; + expect(visState.valueLabels).toBe('show'); + }); + + it("doesn't migrate valueLabels with `hide` value", () => { + const result = migrations['8.3.0']( + { + ...example, + attributes: { + ...example.attributes, + state: { + ...example.attributes.state, + visualization: { + ...(example.attributes.state.visualization as Record), + valueLabels: 'hide', + }, + }, + }, + }, + context + ) as ReturnType>; + const visState = result.attributes.state.visualization as VisState830; + expect(visState.valueLabels).toBe('hide'); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index f76381406b132..00ec6c29154e3 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -28,6 +28,11 @@ import { VisState716, CustomVisualizationMigrations, LensDocShape810, + LensDocShape830, + XYVisualizationStatePre830, + XYVisualizationState830, + VisState810, + VisState820, } from './types'; import { commonRenameOperationsForFormula, @@ -42,6 +47,7 @@ import { commonSetLastValueShowArrayValues, commonEnhanceTableRowHeight, commonSetIncludeEmptyRowsDateHistogram, + commonFixValueLabelsInXY, commonLockOldMetricVisSettings, } from './common_migrations'; @@ -473,7 +479,10 @@ const setLastValueShowArrayValues: SavedObjectMigrationFn = (doc) => { +const enhanceTableRowHeight: SavedObjectMigrationFn< + LensDocShape810, + LensDocShape810 +> = (doc) => { const newDoc = cloneDeep(doc); return { ...newDoc, attributes: commonEnhanceTableRowHeight(newDoc.attributes) }; }; @@ -484,6 +493,14 @@ const setIncludeEmptyRowsDateHistogram: SavedObjectMigrationFn, + LensDocShape830 +> = (doc) => { + const newDoc = cloneDeep(doc); + return { ...newDoc, attributes: commonFixValueLabelsInXY(newDoc.attributes) }; +}; + const lockOldMetricVisSettings: SavedObjectMigrationFn = ( doc ) => ({ ...doc, attributes: commonLockOldMetricVisSettings(doc.attributes) }); @@ -507,7 +524,7 @@ const lensMigrations: SavedObjectMigrationMap = { setIncludeEmptyRowsDateHistogram, enhanceTableRowHeight ), - '8.3.0': lockOldMetricVisSettings, + '8.3.0': flow(lockOldMetricVisSettings, fixValueLabelsInXY), }; export const getAllMigrations = ( diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index 5bdc332668621..6b38bb4b4f631 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -8,7 +8,7 @@ import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring'; import type { Query, Filter } from '@kbn/es-query'; import type { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; -import type { LayerType, PersistableFilter } from '../../common'; +import type { LayerType, PersistableFilter, ValueLabelConfig } from '../../common'; export type CustomVisualizationMigrations = Record MigrateFunctionsObject>; @@ -202,7 +202,7 @@ export type LensDocShape810 = Omit< 'filters' | 'state' > & { filters: Filter[]; - state: Omit & { + state: Omit['state'], 'datasourceStates'> & { datasourceStates: { indexpattern: Omit & { layers: Record< @@ -256,3 +256,16 @@ export interface VisState820 { rowHeight: 'auto' | 'single' | 'custom'; rowHeightLines: number; } + +export type LensDocShape830 = LensDocShape810; + +export interface XYVisualizationStatePre830 extends VisState820 { + valueLabels: 'hide' | 'inside' | 'outside'; +} + +export interface XYVisualizationState830 extends VisState820 { + valueLabels: ValueLabelConfig; +} + +export type VisStatePre830 = XYVisualizationStatePre830; +export type VisState830 = XYVisualizationState830; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 072f6e42d1cd8..ad06e5b628eea 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -13,7 +13,7 @@ import type { FieldBasedIndexPatternColumn, SeriesType, OperationType, - YConfig, + ExtendedYConfig, } from '@kbn/lens-plugin/public'; import type { PersistableFilter } from '@kbn/lens-plugin/common'; @@ -71,7 +71,7 @@ export interface SeriesConfig { hasOperationType: boolean; palette?: PaletteOutput; yTitle?: string; - yConfig?: YConfig[]; + yConfig?: ExtendedYConfig[]; query?: { query: string; language: 'kuery' }; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 63a348198a160..66cae51a7b414 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -809,7 +809,6 @@ "xpack.lens.shared.axisNameLabel": "Titre de l'axe", "xpack.lens.shared.chartValueLabelVisibilityLabel": "Étiquettes", "xpack.lens.shared.curveLabel": "Options visuelles", - "xpack.lens.shared.legend.filterForValueButtonAriaLabel": "Filtre pour la valeur", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", "xpack.lens.shared.legend.filterOutValueButtonAriaLabel": "Filtrer la valeur", "xpack.lens.shared.legendAlignmentLabel": "Alignement", @@ -3579,11 +3578,11 @@ "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "Spécifie si le titre de l'axe Y de gauche est visible ou non.", "expressionXY.axisTitlesVisibilityConfig.yRight.help": "Spécifie si le titre de l'axe Y de droite est visible ou non.", "expressionXY.dataLayer.accessors.help": "Les colonnes à afficher sur l’axe y.", - "expressionXY.dataLayer.columnToLabel.help": "Paires clé-valeur JSON de l’ID de colonne pour l’étiquette", + "expressionXY.layer.columnToLabel.help": "Paires clé-valeur JSON de l’ID de colonne pour l’étiquette", "expressionXY.dataLayer.help": "Configurer un calque dans le graphique xy", "expressionXY.dataLayer.hide.help": "Afficher/masquer l’axe", "expressionXY.dataLayer.isHistogram.help": "Disposer le graphique sous forme d’histogramme ou non", - "expressionXY.dataLayer.layerId.help": "ID du calque", + "expressionXY.layers.layerId.help": "ID du calque", "expressionXY.dataLayer.palette.help": "Palette", "expressionXY.dataLayer.seriesType.help": "Type de graphique à afficher.", "expressionXY.dataLayer.splitAccessor.help": "Colonne selon laquelle effectuer la division", @@ -3614,9 +3613,7 @@ "expressionXY.legendConfig.showSingleSeries.help": "Spécifie si une légende comportant une seule entrée doit être affichée", "expressionXY.legendConfig.verticalAlignment.help": "Spécifie l'alignement vertical de la légende lorsqu'elle est affichée à l'intérieur du graphique.", "expressionXY.referenceLineLayer.accessors.help": "Les colonnes à afficher sur l’axe y.", - "expressionXY.referenceLineLayer.columnToLabel.help": "Paires clé-valeur JSON de l’ID de colonne pour l’étiquette", "expressionXY.referenceLineLayer.help": "Configurer une ligne de référence dans le graphique xy", - "expressionXY.referenceLineLayer.layerId.help": "ID du calque", "expressionXY.referenceLineLayer.yConfig.help": "Configuration supplémentaire pour les axes y", "expressionXY.tickLabelsConfig.help": "Configurer l’aspect des étiquettes de coche du graphique xy", "expressionXY.tickLabelsConfig.x.help": "Spécifie si les étiquettes de graduation de l'axe X sont visibles ou non.", @@ -3647,7 +3644,7 @@ "expressionXY.xyVis.help": "Graphique X/Y", "expressionXY.xyVis.hideEndzones.help": "Masquer les marqueurs de zone de fin pour les données partielles", "expressionXY.xyVis.labelsOrientation.help": "Définit la rotation des étiquettes des axes", - "expressionXY.xyVis.layers.help": "Calques de série visuelle", + "expressionXY.layeredXyVis.layers.help": "Calques de série visuelle", "expressionXY.xyVis.legend.help": "Configurez la légende du graphique.", "expressionXY.xyVis.logDatatable.breakDown": "Répartir par", "expressionXY.xyVis.logDatatable.metric": "Axe vertical", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 118408cc7a6d2..70b2f076d5f8c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1010,7 +1010,6 @@ "xpack.lens.xyChart.verticalRightAxisLabel": "縦右軸", "xpack.lens.xySuggestions.asPercentageTitle": "割合(%)", "xpack.lens.xySuggestions.barChartTitle": "棒グラフ", - "xpack.lens.xySuggestions.dateSuggestion": "{xTitle}の上の {yTitle}", "xpack.lens.xySuggestions.emptyAxisTitle": "(空)", "xpack.lens.xySuggestions.flipTitle": "反転", "xpack.lens.xySuggestions.lineChartTitle": "折れ線グラフ", @@ -3565,11 +3564,11 @@ "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "左y軸のタイトルを表示するかどうかを指定します。", "expressionXY.axisTitlesVisibilityConfig.yRight.help": "右y軸のタイトルを表示するかどうかを指定します。", "expressionXY.dataLayer.accessors.help": "y軸に表示する列。", - "expressionXY.dataLayer.columnToLabel.help": "ラベリングする列IDのJSONキー値のペア", + "expressionXY.layer.columnToLabel.help": "ラベリングする列IDのJSONキー値のペア", "expressionXY.dataLayer.help": "xyグラフでレイヤーを構成", "expressionXY.dataLayer.hide.help": "軸を表示/非表示", "expressionXY.dataLayer.isHistogram.help": "グラフをヒストグラムとしてレイアウトするかどうか", - "expressionXY.dataLayer.layerId.help": "レイヤーID", + "expressionXY.layers.layerId.help": "レイヤーID", "expressionXY.dataLayer.palette.help": "パレット", "expressionXY.dataLayer.seriesType.help": "表示するグラフのタイプ。", "expressionXY.dataLayer.splitAccessor.help": "分割の基準となる列", @@ -3600,9 +3599,7 @@ "expressionXY.legendConfig.showSingleSeries.help": "エントリが1件の凡例を表示するかどうかを指定します", "expressionXY.legendConfig.verticalAlignment.help": "凡例がグラフ内に表示されるときに凡例の縦の配置を指定します。", "expressionXY.referenceLineLayer.accessors.help": "y軸に表示する列。", - "expressionXY.referenceLineLayer.columnToLabel.help": "ラベリングする列IDのJSONキー値のペア", "expressionXY.referenceLineLayer.help": "xyグラフで基準線を構成", - "expressionXY.referenceLineLayer.layerId.help": "レイヤーID", "expressionXY.referenceLineLayer.yConfig.help": "y軸の詳細構成", "expressionXY.tickLabelsConfig.help": "xyグラフのティックラベルの表示を構成", "expressionXY.tickLabelsConfig.x.help": "x軸の目盛ラベルを表示するかどうかを指定します。", @@ -3633,7 +3630,7 @@ "expressionXY.xyVis.help": "X/Y チャート", "expressionXY.xyVis.hideEndzones.help": "部分データの終了ゾーンマーカーを非表示", "expressionXY.xyVis.labelsOrientation.help": "軸ラベルの回転を定義します", - "expressionXY.xyVis.layers.help": "視覚的な系列のレイヤー", + "expressionXY.layeredXyVis.layers.help": "視覚的な系列のレイヤー", "expressionXY.xyVis.legend.help": "チャートの凡例を構成します。", "expressionXY.xyVis.logDatatable.breakDown": "内訳の基準", "expressionXY.xyVis.logDatatable.metric": "縦軸", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0c7f624c259d0..e249662ac511b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3575,11 +3575,11 @@ "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "指定左侧 y 轴的标题是否可见。", "expressionXY.axisTitlesVisibilityConfig.yRight.help": "指定右侧 y 轴的标题是否可见。", "expressionXY.dataLayer.accessors.help": "要在 y 轴上显示的列。", - "expressionXY.dataLayer.columnToLabel.help": "要标记的列 ID 的 JSON 键值对", + "expressionXY.layer.columnToLabel.help": "要标记的列 ID 的 JSON 键值对", "expressionXY.dataLayer.help": "配置 xy 图表中的图层", "expressionXY.dataLayer.hide.help": "显示/隐藏轴", "expressionXY.dataLayer.isHistogram.help": "是否将图表布局为直方图", - "expressionXY.dataLayer.layerId.help": "图层 ID", + "expressionXY.layers.layerId.help": "图层 ID", "expressionXY.dataLayer.palette.help": "调色板", "expressionXY.dataLayer.seriesType.help": "要显示的图表的类型。", "expressionXY.dataLayer.splitAccessor.help": "拆分要依据的列", @@ -3610,9 +3610,7 @@ "expressionXY.legendConfig.showSingleSeries.help": "指定是否应显示只包含一个条目的图例", "expressionXY.legendConfig.verticalAlignment.help": "指定图例显示在图表内时垂直对齐。", "expressionXY.referenceLineLayer.accessors.help": "要在 y 轴上显示的列。", - "expressionXY.referenceLineLayer.columnToLabel.help": "要标记的列 ID 的 JSON 键值对", "expressionXY.referenceLineLayer.help": "配置 xy 图表中的参考线", - "expressionXY.referenceLineLayer.layerId.help": "图层 ID", "expressionXY.referenceLineLayer.yConfig.help": "y 轴的其他配置", "expressionXY.tickLabelsConfig.help": "配置 xy 图表的刻度标签外观", "expressionXY.tickLabelsConfig.x.help": "指定 x 轴的刻度标签是否可见。", @@ -3643,7 +3641,7 @@ "expressionXY.xyVis.help": "X/Y 图表", "expressionXY.xyVis.hideEndzones.help": "隐藏部分数据的末日区域标记", "expressionXY.xyVis.labelsOrientation.help": "定义轴标签的旋转", - "expressionXY.xyVis.layers.help": "可视序列的图层", + "expressionXY.layeredXyVis.layers.help": "可视序列的图层", "expressionXY.xyVis.legend.help": "配置图表图例。", "expressionXY.xyVis.logDatatable.breakDown": "细分方式", "expressionXY.xyVis.logDatatable.metric": "垂直轴",