From 072fe63c0b326f6dfd0215cda648c982f4b6bee8 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 28 Mar 2022 18:17:36 +0300 Subject: [PATCH] [XY] Expression chart. (#127150) * added xy plugin. * Added expressionXY limits. * Added xy expression functions to the expression_xy plugin. * Moved xy to a separate plugin. * Fixed bugs, caused by the refactoring process. * Fixed lens snapshots. * Removed new line. * Fixed xy_chart tests. * Added lazy loading for xy chart. * Fixed xy chart test. * Fixed broken chart selectors. * Fixed dashboard tests. * dashboard test fixed. * Fixed heatmap vis. * Smokescreen test fixed. * more fixes. * async dashboard tests fixed. * Fixed xy smokescreen tests selectors. * fixed show_underlying_data tests. * Updated snapshots. * updated limits. * Fixed more selectors * Fixed persistent context test. * Fixed some more test at ml. * Fixed types and imports * Fixed handlers.inspectorAdapters.tables.logDatatable * Fixed logDatatable * Translations fixed. * Fixed "Visualize App ... cleans filters and query" test. * Fixed "lens disable auto-apply tests" test. * Updated dashboard tests. * Fixed translations. * Expression tests fixed. * Cleaned up expression_xy. * cleaned up lens xy_visualization. * Moved XY state types to lens. * Update src/plugins/chart_expressions/expression_xy/README.md Co-authored-by: Marta Bondyra * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * Removed yConfig from *Layers types * Removed not used utils and styles. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra --- .github/CODEOWNERS | 1 + .i18nrc.json | 1 + docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + .../chart_expressions/expression_xy/README.md | 9 + .../expression_xy/common/__mocks__/index.ts | 128 + .../expression_xy/common/constants.ts | 111 + .../annotation_layer_config.ts | 49 + .../axis_extent_config.ts | 54 + .../axis_titles_visibility_config.ts | 53 + .../data_layer_config.test.ts | 33 + .../expression_functions/data_layer_config.ts | 123 + .../grid_lines_config.test.ts | 20 + .../grid_lines_config.ts | 32 +- .../common/expression_functions/index.ts | 19 + .../labels_orientation_config.test.ts | 20 + .../labels_orientation_config.ts | 40 +- .../legend_config.test.ts | 21 + .../expression_functions/legend_config.ts | 102 + .../reference_line_layer_config.ts | 62 + .../tick_labels_config.test.ts | 20 + .../tick_labels_config.ts | 32 +- .../expression_functions/xy_vis.test.ts | 21 + .../common/expression_functions/xy_vis.ts | 154 +- .../expression_functions/y_axis_config.ts | 93 + .../expression_xy/common/index.ts | 66 + .../common/types/expression_functions.ts | 239 ++ .../common/types/expression_renderers.ts | 21 + .../expression_xy/common/types/index.ts | 10 + .../expression_xy/jest.config.js | 19 + .../expression_xy/kibana.json | 15 + .../expression_xy/public/__mocks__/index.tsx | 224 ++ .../__snapshots__/xy_chart.test.tsx.snap | 22 +- .../public/components/annotations.scss | 18 + .../public/components/annotations.tsx | 157 +- .../expression_xy/public/components/index.ts | 13 + .../public/components/legend_action.test.tsx | 17 +- .../public/components/legend_action.tsx | 17 +- .../components/legend_action_popover.tsx | 108 + .../public/components/reference_lines.scss | 4 +- .../components/reference_lines.test.tsx | 30 +- .../public/components/reference_lines.tsx | 366 ++ .../public/components}/x_domain.tsx | 12 +- .../public/components/xy_chart.scss | 7 + .../public/components/xy_chart.test.tsx | 2703 ++++++++++++++ .../public/components/xy_chart.tsx | 229 +- .../expression_xy/public/definitions/index.ts | 9 + .../public/definitions/visualizations.ts | 34 + .../public/expression_renderers/index.ts | 10 + .../xy_chart_renderer.tsx | 90 + .../public/helpers/annotations.tsx | 137 +- .../public/helpers/annotations_icon_set.tsx | 101 + .../public/helpers/axes_configuration.test.ts | 334 ++ .../public/helpers/axes_configuration.ts | 146 + .../public/helpers/color_assignment.test.ts | 239 ++ .../public/helpers/color_assignment.ts | 93 + .../public/helpers}/fitting_functions.ts | 7 +- .../expression_xy/public/helpers/icon.ts | 11 + .../expression_xy/public/helpers/index.ts | 19 + .../public/helpers/interval.test.ts | 80 + .../expression_xy/public/helpers/interval.ts | 36 + .../expression_xy/public/helpers/layers.ts | 30 + .../public/helpers/reference_lines.ts | 76 + .../expression_xy/public/helpers/state.ts | 36 + .../public/helpers/visualization.ts | 43 + .../expression_xy/public/icons/area.tsx | 32 + .../public/icons/area_percentage.tsx | 32 + .../public/icons/area_stacked.tsx | 32 + .../expression_xy/public/icons/bar.tsx | 32 + .../public/icons/bar_horizontal.tsx | 32 + .../icons/bar_horizontal_percentage.tsx | 36 + .../public/icons/bar_horizontal_stacked.tsx | 36 + .../public/icons/bar_percentage.tsx | 32 + .../public/icons/bar_reference_line.tsx | 37 + .../public/icons/bar_stacked.tsx | 32 + .../expression_xy/public/icons/circle.tsx | 32 + .../expression_xy/public/icons/index.ts | 22 + .../expression_xy/public/icons/line.tsx | 32 + .../expression_xy/public/icons/mixed_xy.tsx | 36 + .../expression_xy/public/icons/triangle.tsx | 31 + .../expression_xy/public/index.ts | 17 + .../expression_xy/public/plugin.ts | 99 + .../expression_xy/public/types.ts | 116 + .../expression_xy/server/index.ts | 15 + .../expression_xy/server/plugin.ts | 47 + .../expression_xy/server/types.ts | 20 + .../expression_xy/tsconfig.json | 26 + .../static/components/empty_placeholder.tsx | 5 +- test/functional/apps/home/_sample_data.ts | 2 +- .../plugins/lens/common/expressions/index.ts | 1 - .../expressions/xy_chart/axis_config.ts | 207 -- .../lens/common/expressions/xy_chart/index.ts | 18 - .../layer_config/annotation_layer_config.ts | 67 - .../layer_config/data_layer_config.ts | 122 - .../xy_chart/layer_config/index.ts | 17 - .../reference_line_layer_config.ts | 64 - .../expressions/xy_chart/legend_config.ts | 142 - .../expressions/xy_chart/series_type.ts | 18 - .../common/expressions/xy_chart/xy_args.ts | 45 - x-pack/plugins/lens/kibana.json | 1 + x-pack/plugins/lens/public/expressions.ts | 29 - x-pack/plugins/lens/public/index.ts | 53 +- .../shared_components/axis_title_settings.tsx | 3 +- .../__snapshots__/to_expression.test.ts.snap | 21 +- .../annotations/config_panel/index.tsx | 3 +- .../annotations/expression.scss | 37 - .../xy_visualization/annotations/helpers.tsx | 7 +- .../axes_configuration.test.ts | 12 +- .../xy_visualization/axes_configuration.ts | 3 +- .../xy_visualization/color_assignment.test.ts | 10 +- .../xy_visualization/color_assignment.ts | 39 +- .../public/xy_visualization/expression.scss | 15 - .../xy_visualization/expression.test.tsx | 3167 ----------------- .../expression_reference_lines.tsx | 213 -- .../expression_thresholds.scss | 18 - .../lens/public/xy_visualization/index.ts | 17 +- .../reference_line_helpers.test.ts | 12 +- .../reference_line_helpers.tsx | 23 +- .../public/xy_visualization/state_helpers.ts | 8 +- .../xy_visualization/to_expression.test.ts | 2 +- .../public/xy_visualization/to_expression.ts | 42 +- .../lens/public/xy_visualization/types.ts | 38 +- .../xy_visualization/visualization.test.ts | 20 +- .../public/xy_visualization/visualization.tsx | 51 +- .../visualization_helpers.tsx | 25 +- .../xy_config_panel/axis_settings_popover.tsx | 7 +- .../xy_config_panel/color_picker.tsx | 7 +- .../xy_config_panel/dimension_editor.tsx | 31 +- .../xy_config_panel/index.tsx | 5 +- .../xy_config_panel/layer_header.tsx | 9 +- .../xy_config_panel/reference_line_panel.tsx | 8 +- .../shared/line_style_settings.tsx | 3 +- .../shared/marker_decoration_settings.tsx | 5 +- .../end_value_definitions.ts} | 7 +- .../fitting_function_definitions.ts} | 7 +- .../visual_options_popover/index.tsx | 2 +- .../line_curve_option.tsx | 2 +- .../missing_values_option.tsx | 8 +- .../visual_options_popover.test.tsx | 29 +- .../xy_config_panel/xy_config_panel.test.tsx | 3 +- .../xy_visualization/xy_suggestions.test.ts | 9 +- .../public/xy_visualization/xy_suggestions.ts | 24 +- .../xy_visualization/xy_visualization.ts | 1 - .../lens/server/expressions/expressions.ts | 20 - .../saved_object_migrations.test.ts | 8 +- x-pack/plugins/lens/tsconfig.json | 109 +- .../translations/translations/fr-FR.json | 86 +- .../translations/translations/ja-JP.json | 84 +- .../translations/translations/zh-CN.json | 86 +- .../embedded_lens/embedded_example.ts | 6 +- .../apps/dashboard/_async_dashboard.ts | 8 +- .../apps/dashboard/dashboard_lens_by_value.ts | 2 +- .../time_to_visualize_security.ts | 2 +- .../functional/apps/lens/add_to_dashboard.ts | 4 +- .../test/functional/apps/lens/chart_data.ts | 14 +- .../apps/lens/disable_auto_apply.ts | 2 +- .../functional/apps/lens/drag_and_drop.ts | 29 +- .../test/functional/apps/lens/epoch_millis.ts | 4 +- x-pack/test/functional/apps/lens/formula.ts | 4 +- x-pack/test/functional/apps/lens/gauge.ts | 2 +- x-pack/test/functional/apps/lens/heatmap.ts | 14 +- x-pack/test/functional/apps/lens/inspector.ts | 2 +- .../apps/lens/persistent_context.ts | 4 +- x-pack/test/functional/apps/lens/rollup.ts | 4 +- .../apps/lens/show_underlying_data.ts | 12 +- .../test/functional/apps/lens/smokescreen.ts | 20 +- .../functional/apps/lens/tsvb_open_in_lens.ts | 10 +- .../apps/maps/lens/choropleth_chart.ts | 2 +- .../data_visualizer/index_data_visualizer.ts | 10 +- .../test/functional/page_objects/lens_page.ts | 4 +- .../services/ml/data_visualizer_table.ts | 4 +- .../save_search_session_relative_time.ts | 8 +- 172 files changed, 7945 insertions(+), 5220 deletions(-) create mode 100755 src/plugins/chart_expressions/expression_xy/README.md create mode 100644 src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/constants.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.test.ts create 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/grid_lines_config.test.ts rename {x-pack/plugins/lens/common/expressions/xy_chart => src/plugins/chart_expressions/expression_xy/common/expression_functions}/grid_lines_config.ts (51%) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts rename {x-pack/plugins/lens/common/expressions/xy_chart => src/plugins/chart_expressions/expression_xy/common/expression_functions}/labels_orientation_config.ts (50%) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.test.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.ts create 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/tick_labels_config.test.ts rename {x-pack/plugins/lens/common/expressions/xy_chart => src/plugins/chart_expressions/expression_xy/common/expression_functions}/tick_labels_config.ts (51%) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts rename x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts => src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts (53%) create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts create mode 100755 src/plugins/chart_expressions/expression_xy/common/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/types/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/jest.config.js create mode 100755 src/plugins/chart_expressions/expression_xy/kibana.json create mode 100644 src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx rename x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap => src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap (97%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/annotations.scss rename x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx => src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx (66%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/index.ts rename x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx => src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx (92%) rename x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx => src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx (79%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/legend_action_popover.tsx rename x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss => src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss (79%) rename x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx => src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx (93%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx rename {x-pack/plugins/lens/public/xy_visualization => src/plugins/chart_expressions/expression_xy/public/components}/x_domain.tsx (89%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/xy_chart.scss create mode 100644 src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx rename x-pack/plugins/lens/public/xy_visualization/expression.tsx => src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx (81%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/definitions/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/expression_renderers/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx rename x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx => src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx (57%) create 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/axes_configuration.test.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts rename {x-pack/plugins/lens/public/xy_visualization => src/plugins/chart_expressions/expression_xy/public/helpers}/fitting_functions.ts (74%) create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/reference_lines.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/state.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/area.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/area_percentage.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/area_stacked.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_percentage.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_stacked.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar_percentage.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar_reference_line.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/bar_stacked.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/circle.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/index.ts create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/line.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/mixed_xy.tsx create mode 100644 src/plugins/chart_expressions/expression_xy/public/icons/triangle.tsx create mode 100755 src/plugins/chart_expressions/expression_xy/public/index.ts create mode 100755 src/plugins/chart_expressions/expression_xy/public/plugin.ts create mode 100755 src/plugins/chart_expressions/expression_xy/public/types.ts create mode 100755 src/plugins/chart_expressions/expression_xy/server/index.ts create mode 100755 src/plugins/chart_expressions/expression_xy/server/plugin.ts create mode 100755 src/plugins/chart_expressions/expression_xy/server/types.ts create mode 100644 src/plugins/chart_expressions/expression_xy/tsconfig.json delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/index.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/layer_config/data_layer_config.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/layer_config/reference_line_layer_config.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/series_type.ts delete mode 100644 x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts delete mode 100644 x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss delete mode 100644 x-pack/plugins/lens/public/xy_visualization/expression.scss delete mode 100644 x-pack/plugins/lens/public/xy_visualization/expression.test.tsx delete mode 100644 x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx delete mode 100644 x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss rename x-pack/plugins/lens/{common/expressions/xy_chart/end_value.ts => public/xy_visualization/xy_config_panel/visual_options_popover/end_value_definitions.ts} (85%) rename x-pack/plugins/lens/{common/expressions/xy_chart/fitting_function.ts => public/xy_visualization/xy_config_panel/visual_options_popover/fitting_function_definitions.ts} (88%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1ccad2a3ac36d..1d4eb2ba39cd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -41,6 +41,7 @@ /src/plugins/chart_expressions/expression_heatmap/ @elastic/kibana-vis-editors /src/plugins/chart_expressions/expression_gauge/ @elastic/kibana-vis-editors /src/plugins/chart_expressions/expression_partition_vis/ @elastic/kibana-vis-editors +/src/plugins/chart_expressions/expression_xy/ @elastic/kibana-vis-editors /src/plugins/url_forwarding/ @elastic/kibana-vis-editors /packages/kbn-tinymath/ @elastic/kibana-vis-editors /x-pack/test/functional/apps/lens @elastic/kibana-vis-editors diff --git a/.i18nrc.json b/.i18nrc.json index 71b68d2c51d85..908963ca4d991 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -26,6 +26,7 @@ "expressionMetric": "src/plugins/expression_metric", "expressionMetricVis": "src/plugins/chart_expressions/expression_metric", "expressionPartitionVis": "src/plugins/chart_expressions/expression_partition_vis", + "expressionXY": "src/plugins/chart_expressions/expression_xy", "expressionRepeatImage": "src/plugins/expression_repeat_image", "expressionRevealImage": "src/plugins/expression_reveal_image", "expressions": "src/plugins/expressions", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b6cac30c1bc88..0b9e48020c680 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -164,6 +164,10 @@ for use in their own application. |Expression Tagcloud plugin adds a tagcloud renderer and function to the expression plugin. The renderer will display the Wordcloud chart. +|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_xy/README.md[expressionXY] +|Expression XY plugin adds a xy renderer and function to the expression plugin. The renderer will display the xy chart. + + |{kib-repo}blob/{branch}/src/plugins/field_formats/README.md[fieldFormats] |Index pattern fields formatters diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 526c1ff5dad82..34c2285016086 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -124,4 +124,5 @@ pageLoadAssetSize: sessionView: 77750 cloudSecurityPosture: 19109 visTypeGauge: 24113 + expressionXY: 41392 eventAnnotation: 19334 diff --git a/src/plugins/chart_expressions/expression_xy/README.md b/src/plugins/chart_expressions/expression_xy/README.md new file mode 100755 index 0000000000000..5ad68bebd40fb --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/README.md @@ -0,0 +1,9 @@ +# expressionXY + +Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart. + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts new file mode 100644 index 0000000000000..4bafffc065835 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -0,0 +1,128 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { PaletteOutput } from 'src/plugins/charts/common'; +import { Datatable, DatatableRow } from 'src/plugins/expressions'; +import { LayerTypes } from '../constants'; +import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../types'; + +export const mockPaletteOutput: PaletteOutput = { + type: 'palette', + name: 'mock', + params: {}, +}; + +export const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable => ({ + 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: 'date', + field: 'order_date', + sourceParams: { type: 'date-histogram', params: { interval: 'auto' } }, + params: { id: 'string' }, + }, + }, + { id: 'd', name: 'ColD', meta: { type: 'string' } }, + ], + rows, +}); + +export const sampleLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, +}; + +export const createArgsWithLayers = (layers: DataLayerConfigResult[] = [sampleLayer]): XYArgs => ({ + xTitle: '', + yTitle: '', + yRightTitle: '', + legend: { + type: 'legendConfig', + isVisible: false, + position: Position.Top, + }, + valueLabels: 'hide', + valuesInLegend: false, + axisTitlesVisibilitySettings: { + type: 'axisTitlesVisibilityConfig', + x: true, + yLeft: true, + yRight: true, + }, + tickLabelsVisibilitySettings: { + type: 'tickLabelsConfig', + x: true, + yLeft: false, + yRight: false, + }, + labelsOrientation: { + type: 'labelsOrientationConfig', + x: 0, + yLeft: -90, + yRight: -45, + }, + gridlinesVisibilitySettings: { + type: 'gridlinesConfig', + x: true, + yLeft: false, + yRight: false, + }, + yLeftExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + 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(); + + return { data, args }; +} diff --git a/src/plugins/chart_expressions/expression_xy/common/constants.ts b/src/plugins/chart_expressions/expression_xy/common/constants.ts new file mode 100644 index 0000000000000..bf1e43b205843 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/constants.ts @@ -0,0 +1,111 @@ +/* + * 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 XY_VIS = 'xyVis'; +export const Y_CONFIG = 'yConfig'; +export const MULTITABLE = 'lens_multitable'; +export const DATA_LAYER = 'dataLayer'; +export const LEGEND_CONFIG = 'legendConfig'; +export const XY_VIS_RENDERER = 'xyVis'; +export const GRID_LINES_CONFIG = 'gridlinesConfig'; +export const ANNOTATION_LAYER = 'annotationLayer'; +export const TICK_LABELS_CONFIG = 'tickLabelsConfig'; +export const AXIS_EXTENT_CONFIG = 'axisExtentConfig'; +export const REFERENCE_LINE_LAYER = 'referenceLineLayer'; +export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig'; +export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig'; + +export const LayerTypes = { + DATA: 'data', + REFERENCELINE: 'referenceLine', + ANNOTATIONS: 'annotations', +} as const; + +export const FittingFunctions = { + NONE: 'None', + ZERO: 'Zero', + LINEAR: 'Linear', + CARRY: 'Carry', + LOOKAHEAD: 'Lookahead', +} as const; + +export const EndValues = { + NONE: 'None', + ZERO: 'Zero', + NEAREST: 'Nearest', +} as const; + +export const YAxisModes = { + AUTO: 'auto', + LEFT: 'left', + RIGHT: 'right', + BOTTOM: 'bottom', +} as const; + +export const AxisExtentModes = { + FULL: 'full', + CUSTOM: 'custom', + DATA_BOUNDS: 'dataBounds', +} as const; + +export const LineStyles = { + SOLID: 'solid', + DASHED: 'dashed', + DOTTED: 'dotted', +} as const; + +export const FillStyles = { + NONE: 'none', + ABOVE: 'above', + BELOW: 'below', +} as const; + +export const IconPositions = { + AUTO: 'auto', + LEFT: 'left', + RIGHT: 'right', + ABOVE: 'above', + BELOW: 'below', +} as const; + +export const SeriesTypes = { + BAR: 'bar', + LINE: 'line', + AREA: 'area', + BAR_STACKED: 'bar_stacked', + AREA_STACKED: 'area_stacked', + BAR_HORIZONTAL: 'bar_horizontal', + BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked', + BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked', + AREA_PERCENTAGE_STACKED: 'area_percentage_stacked', + BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked', +} as const; + +export const YScaleTypes = { + TIME: 'time', + LINEAR: 'linear', + LOG: 'log', + SQRT: 'sqrt', +} as const; + +export const XScaleTypes = { + TIME: 'time', + LINEAR: 'linear', + ORDINAL: 'ordinal', +} as const; + +export const XYCurveTypes = { + LINEAR: 'LINEAR', + CURVE_MONOTONE_X: 'CURVE_MONOTONE_X', +} as const; + +export const ValueLabelModes = { + HIDE: 'hide', + INSIDE: 'inside', + OUTSIDE: 'outside', +} 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_config.ts new file mode 100644 index 0000000000000..0862b69ca44f2 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.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 type { ExpressionFunctionDefinition } from '../../../../expressions/common'; +import { LayerTypes, ANNOTATION_LAYER } from '../constants'; +import { AnnotationLayerArgs, AnnotationLayerConfigResult } from '../types'; + +export function annotationLayerConfigFunction(): ExpressionFunctionDefinition< + typeof ANNOTATION_LAYER, + null, + AnnotationLayerArgs, + AnnotationLayerConfigResult +> { + return { + name: ANNOTATION_LAYER, + aliases: [], + type: ANNOTATION_LAYER, + inputTypes: ['null'], + help: 'Annotation layer in lens', + args: { + layerId: { + types: ['string'], + help: '', + }, + hide: { + types: ['boolean'], + default: false, + help: 'Show details', + }, + annotations: { + types: ['manual_event_annotation'], + help: '', + multi: true, + }, + }, + fn: (input, args) => { + return { + type: ANNOTATION_LAYER, + ...args, + 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 new file mode 100644 index 0000000000000..c5cf89a4663c9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_extent_config.ts @@ -0,0 +1,54 @@ +/* + * 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 '../../../../expressions/common'; +import { AxisExtentConfig, AxisExtentConfigResult } from '../types'; +import { AxisExtentModes, AXIS_EXTENT_CONFIG } from '../constants'; + +export const axisExtentConfigFunction: ExpressionFunctionDefinition< + typeof AXIS_EXTENT_CONFIG, + null, + AxisExtentConfig, + AxisExtentConfigResult +> = { + name: AXIS_EXTENT_CONFIG, + aliases: [], + type: AXIS_EXTENT_CONFIG, + help: i18n.translate('expressionXY.axisExtentConfig.help', { + defaultMessage: `Configure the xy chart's axis extents`, + }), + inputTypes: ['null'], + args: { + mode: { + types: ['string'], + options: [...Object.values(AxisExtentModes)], + help: i18n.translate('expressionXY.axisExtentConfig.extentMode.help', { + defaultMessage: 'The extent mode', + }), + }, + lowerBound: { + types: ['number'], + help: i18n.translate('expressionXY.axisExtentConfig.lowerBound.help', { + defaultMessage: 'Lower bound', + }), + }, + upperBound: { + types: ['number'], + help: i18n.translate('expressionXY.axisExtentConfig.upperBound.help', { + defaultMessage: 'Upper bound', + }), + }, + }, + fn(input, args) { + return { + type: AXIS_EXTENT_CONFIG, + ...args, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts new file mode 100644 index 0000000000000..50302214fc37c --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts @@ -0,0 +1,53 @@ +/* + * 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 '../../../../expressions/common'; +import { AXIS_TITLES_VISIBILITY_CONFIG } from '../constants'; +import { AxesSettingsConfig, AxisTitlesVisibilityConfigResult } from '../types'; + +export const axisTitlesVisibilityConfigFunction: ExpressionFunctionDefinition< + typeof AXIS_TITLES_VISIBILITY_CONFIG, + null, + AxesSettingsConfig, + AxisTitlesVisibilityConfigResult +> = { + name: AXIS_TITLES_VISIBILITY_CONFIG, + aliases: [], + type: AXIS_TITLES_VISIBILITY_CONFIG, + help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.help', { + defaultMessage: `Configure the xy chart's axis titles appearance`, + }), + inputTypes: ['null'], + args: { + x: { + types: ['boolean'], + help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.x.help', { + defaultMessage: 'Specifies whether or not the title of the x-axis are visible.', + }), + }, + yLeft: { + types: ['boolean'], + help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.yLeft.help', { + defaultMessage: 'Specifies whether or not the title of the left y-axis are visible.', + }), + }, + yRight: { + types: ['boolean'], + help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.yRight.help', { + defaultMessage: 'Specifies whether or not the title of the right y-axis are visible.', + }), + }, + }, + fn(inputn, args) { + return { + type: AXIS_TITLES_VISIBILITY_CONFIG, + ...args, + }; + }, +}; 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_config.test.ts new file mode 100644 index 0000000000000..ba7fafd3b3685 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.test.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 { DataLayerArgs } from '../types'; +import { dataLayerConfigFunction } from '../expression_functions'; +import { createMockExecutionContext } from '../../../../expressions/common/mocks'; +import { mockPaletteOutput } from '../__mocks__'; +import { LayerTypes } from '../constants'; + +describe('dataLayerConfig', () => { + test('produces the correct arguments', () => { + const args: DataLayerArgs = { + layerId: 'first', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + xScaleType: 'linear', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }; + + const result = dataLayerConfigFunction.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'dataLayer', layerType: LayerTypes.DATA, ...args }); + }); +}); 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 new file mode 100644 index 0000000000000..3aac992d674d9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer_config.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionFunctionDefinition } from '../../../../expressions/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: { + default: `{theme "palette" default={system_palette name="default"} }`, + help: i18n.translate('expressionXY.dataLayer.palette.help', { + defaultMessage: 'Palette', + }), + types: ['palette'], + }, + }, + fn(input, args) { + return { + type: DATA_LAYER, + ...args, + layerType: LayerTypes.DATA, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts new file mode 100644 index 0000000000000..91bfbc8fbe6f0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts @@ -0,0 +1,20 @@ +/* + * 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 { AxesSettingsConfig } from '../types'; +import { gridlinesConfigFunction } from '../expression_functions'; +import { createMockExecutionContext } from '../../../../../plugins/expressions/common/mocks'; + +describe('gridlinesConfig', () => { + test('produces the correct arguments', () => { + const args: AxesSettingsConfig = { x: true, yLeft: false, yRight: false }; + const result = gridlinesConfigFunction.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'gridlinesConfig', ...args }); + }); +}); diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/grid_lines_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts similarity index 51% rename from x-pack/plugins/lens/common/expressions/xy_chart/grid_lines_config.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts index 6338e9f039937..b94b8b5709c03 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/grid_lines_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts @@ -1,50 +1,52 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; -import type { AxesSettingsConfig } from './axis_config'; +import { GRID_LINES_CONFIG } from '../constants'; +import { AxesSettingsConfig, GridlinesConfigResult } from '../types'; -export type GridlinesConfigResult = AxesSettingsConfig & { type: 'lens_xy_gridlinesConfig' }; - -export const gridlinesConfig: ExpressionFunctionDefinition< - 'lens_xy_gridlinesConfig', +export const gridlinesConfigFunction: ExpressionFunctionDefinition< + typeof GRID_LINES_CONFIG, null, AxesSettingsConfig, GridlinesConfigResult > = { - name: 'lens_xy_gridlinesConfig', + name: GRID_LINES_CONFIG, aliases: [], - type: 'lens_xy_gridlinesConfig', - help: `Configure the xy chart's gridlines appearance`, + type: GRID_LINES_CONFIG, + help: i18n.translate('expressionXY.gridlinesConfig.help', { + defaultMessage: `Configure the xy chart's gridlines appearance`, + }), inputTypes: ['null'], args: { x: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.xAxisGridlines.help', { + help: i18n.translate('expressionXY.gridlinesConfig.x.help', { defaultMessage: 'Specifies whether or not the gridlines of the x-axis are visible.', }), }, yLeft: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yLeftAxisgridlines.help', { + help: i18n.translate('expressionXY.gridlinesConfig.yLeft.help', { defaultMessage: 'Specifies whether or not the gridlines of the left y-axis are visible.', }), }, yRight: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yRightAxisgridlines.help', { + help: i18n.translate('expressionXY.gridlinesConfig.yRight.help', { defaultMessage: 'Specifies whether or not the gridlines of the right y-axis are visible.', }), }, }, - fn: function fn(input: unknown, args: AxesSettingsConfig) { + fn(input, args) { return { - type: 'lens_xy_gridlinesConfig', + type: GRID_LINES_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 new file mode 100644 index 0000000000000..5c7e013a91332 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './xy_vis'; +export * from './legend_config'; +export * from './annotation_layer_config'; +export * from './y_axis_config'; +export * from './data_layer_config'; +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 './axis_titles_visibility_config'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts new file mode 100644 index 0000000000000..2d54a729d3e5a --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts @@ -0,0 +1,20 @@ +/* + * 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 { LabelsOrientationConfig } from '../types'; +import { labelsOrientationConfigFunction } from '../expression_functions'; +import { createMockExecutionContext } from '../../../../../plugins/expressions/common/mocks'; + +describe('labelsOrientationConfig', () => { + test('produces the correct arguments', () => { + const args: LabelsOrientationConfig = { x: 0, yLeft: -90, yRight: -45 }; + const result = labelsOrientationConfigFunction.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'labelsOrientationConfig', ...args }); + }); +}); diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/labels_orientation_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts similarity index 50% rename from x-pack/plugins/lens/common/expressions/xy_chart/labels_orientation_config.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts index 773ce61a102f9..94d726c56f3b2 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/labels_orientation_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts @@ -1,59 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ + import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; +import { LABELS_ORIENTATION_CONFIG } from '../constants'; +import { LabelsOrientationConfig, LabelsOrientationConfigResult } from '../types'; -export interface LabelsOrientationConfig { - x: number; - yLeft: number; - yRight: number; -} - -export type LabelsOrientationConfigResult = LabelsOrientationConfig & { - type: 'lens_xy_labelsOrientationConfig'; -}; - -export const labelsOrientationConfig: ExpressionFunctionDefinition< - 'lens_xy_labelsOrientationConfig', +export const labelsOrientationConfigFunction: ExpressionFunctionDefinition< + typeof LABELS_ORIENTATION_CONFIG, null, LabelsOrientationConfig, LabelsOrientationConfigResult > = { - name: 'lens_xy_labelsOrientationConfig', + name: LABELS_ORIENTATION_CONFIG, aliases: [], - type: 'lens_xy_labelsOrientationConfig', - help: `Configure the xy chart's tick labels orientation`, + type: LABELS_ORIENTATION_CONFIG, + help: i18n.translate('expressionXY.labelsOrientationConfig.help', { + defaultMessage: `Configure the xy chart's tick labels orientation`, + }), inputTypes: ['null'], args: { x: { types: ['number'], options: [0, -90, -45], - help: i18n.translate('xpack.lens.xyChart.xAxisLabelsOrientation.help', { + help: i18n.translate('expressionXY.labelsOrientationConfig.x.help', { defaultMessage: 'Specifies the labels orientation of the x-axis.', }), }, yLeft: { types: ['number'], options: [0, -90, -45], - help: i18n.translate('xpack.lens.xyChart.yLeftAxisLabelsOrientation.help', { + help: i18n.translate('expressionXY.labelsOrientationConfig.yLeft.help', { defaultMessage: 'Specifies the labels orientation of the left y-axis.', }), }, yRight: { types: ['number'], options: [0, -90, -45], - help: i18n.translate('xpack.lens.xyChart.yRightAxisLabelsOrientation.help', { + help: i18n.translate('expressionXY.labelsOrientationConfig.yRight.help', { defaultMessage: 'Specifies the labels orientation of the right y-axis.', }), }, }, - fn: function fn(input: unknown, args: LabelsOrientationConfig) { + fn(input, args) { return { - type: 'lens_xy_labelsOrientationConfig', + type: LABELS_ORIENTATION_CONFIG, ...args, }; }, 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 new file mode 100644 index 0000000000000..2c673ab990ded --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { createMockExecutionContext } from '../../../../expressions/common/mocks'; +import { LegendConfig } from '../types'; +import { legendConfigFunction } from './legend_config'; + +describe('legendConfigFunction', () => { + test('produces the correct arguments', () => { + const args: LegendConfig = { isVisible: true, position: Position.Left }; + const result = 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 new file mode 100644 index 0000000000000..384f23aee811a --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/legend_config.ts @@ -0,0 +1,102 @@ +/* + * 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 { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; +import { LEGEND_CONFIG } from '../constants'; +import { LegendConfig, LegendConfigResult } from '../types'; + +export const legendConfigFunction: ExpressionFunctionDefinition< + typeof LEGEND_CONFIG, + null, + LegendConfig, + LegendConfigResult +> = { + name: LEGEND_CONFIG, + aliases: [], + type: LEGEND_CONFIG, + help: i18n.translate('expressionXY.legendConfig.help', { + defaultMessage: `Configure the xy chart's legend`, + }), + inputTypes: ['null'], + args: { + isVisible: { + types: ['boolean'], + help: i18n.translate('expressionXY.legendConfig.isVisible.help', { + defaultMessage: 'Specifies whether or not the legend is visible.', + }), + }, + position: { + types: ['string'], + options: [Position.Top, Position.Right, Position.Bottom, Position.Left], + help: i18n.translate('expressionXY.legendConfig.position.help', { + defaultMessage: 'Specifies the legend position.', + }), + }, + showSingleSeries: { + types: ['boolean'], + help: i18n.translate('expressionXY.legendConfig.showSingleSeries.help', { + defaultMessage: 'Specifies whether a legend with just a single entry should be shown', + }), + }, + isInside: { + types: ['boolean'], + help: i18n.translate('expressionXY.legendConfig.isInside.help', { + defaultMessage: 'Specifies whether a legend is inside the chart', + }), + }, + horizontalAlignment: { + types: ['string'], + options: [HorizontalAlignment.Right, HorizontalAlignment.Left], + help: i18n.translate('expressionXY.legendConfig.horizontalAlignment.help', { + defaultMessage: + 'Specifies the horizontal alignment of the legend when it is displayed inside chart.', + }), + }, + verticalAlignment: { + types: ['string'], + options: [VerticalAlignment.Top, VerticalAlignment.Bottom], + help: i18n.translate('expressionXY.legendConfig.verticalAlignment.help', { + defaultMessage: + 'Specifies the vertical alignment of the legend when it is displayed inside chart.', + }), + }, + floatingColumns: { + types: ['number'], + help: i18n.translate('expressionXY.legendConfig.floatingColumns.help', { + defaultMessage: 'Specifies the number of columns when legend is displayed inside chart.', + }), + }, + maxLines: { + types: ['number'], + help: i18n.translate('expressionXY.legendConfig.maxLines.help', { + defaultMessage: 'Specifies the number of lines per legend item.', + }), + }, + shouldTruncate: { + types: ['boolean'], + default: true, + help: i18n.translate('expressionXY.legendConfig.shouldTruncate.help', { + defaultMessage: 'Specifies whether the legend items will be truncated or not', + }), + }, + legendSize: { + types: ['number'], + help: i18n.translate('expressionXY.legendConfig.legendSize.help', { + defaultMessage: 'Specifies the legend size in pixels.', + }), + }, + }, + fn(input, args) { + return { + type: LEGEND_CONFIG, + ...args, + }; + }, +}; 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 new file mode 100644 index 0000000000000..c5d0f17ff138d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer_config.ts @@ -0,0 +1,62 @@ +/* + * 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 '../../../../expressions/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/tick_labels_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.test.ts new file mode 100644 index 0000000000000..8b31258377dd9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.test.ts @@ -0,0 +1,20 @@ +/* + * 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 { AxesSettingsConfig } from '../types'; +import { tickLabelsConfigFunction } from '../expression_functions'; +import { createMockExecutionContext } from '../../../../../plugins/expressions/common/mocks'; + +describe('tickLabelsConfig', () => { + test('produces the correct arguments', () => { + const args: AxesSettingsConfig = { x: true, yLeft: false, yRight: false }; + const result = tickLabelsConfigFunction.fn(null, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'tickLabelsConfig', ...args }); + }); +}); diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/tick_labels_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts similarity index 51% rename from x-pack/plugins/lens/common/expressions/xy_chart/tick_labels_config.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts index 4af78d8355786..6a882094a56d2 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/tick_labels_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts @@ -1,50 +1,52 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; -import type { AxesSettingsConfig } from './axis_config'; +import { TICK_LABELS_CONFIG } from '../constants'; +import { AxesSettingsConfig, TickLabelsConfigResult } from '../types'; -export type TickLabelsConfigResult = AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' }; - -export const tickLabelsConfig: ExpressionFunctionDefinition< - 'lens_xy_tickLabelsConfig', +export const tickLabelsConfigFunction: ExpressionFunctionDefinition< + typeof TICK_LABELS_CONFIG, null, AxesSettingsConfig, TickLabelsConfigResult > = { - name: 'lens_xy_tickLabelsConfig', + name: TICK_LABELS_CONFIG, aliases: [], - type: 'lens_xy_tickLabelsConfig', - help: `Configure the xy chart's tick labels appearance`, + type: TICK_LABELS_CONFIG, + help: i18n.translate('expressionXY.tickLabelsConfig.help', { + defaultMessage: `Configure the xy chart's tick labels appearance`, + }), inputTypes: ['null'], args: { x: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.xAxisTickLabels.help', { + help: i18n.translate('expressionXY.tickLabelsConfig.x.help', { defaultMessage: 'Specifies whether or not the tick labels of the x-axis are visible.', }), }, yLeft: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yLeftAxisTickLabels.help', { + help: i18n.translate('expressionXY.tickLabelsConfig.yLeft.help', { defaultMessage: 'Specifies whether or not the tick labels of the left y-axis are visible.', }), }, yRight: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yRightAxisTickLabels.help', { + help: i18n.translate('expressionXY.tickLabelsConfig.yRight.help', { defaultMessage: 'Specifies whether or not the tick labels of the right y-axis are visible.', }), }, }, - fn: function fn(input: unknown, args: AxesSettingsConfig) { + fn(input, args) { return { - type: 'lens_xy_tickLabelsConfig', + type: TICK_LABELS_CONFIG, ...args, }; }, 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 new file mode 100644 index 0000000000000..69d0bf51b3d64 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { xyVisFunction } from '../expression_functions'; +import { createMockExecutionContext } from '../../../../../plugins/expressions/common/mocks'; +import { sampleArgs } from '../__mocks__'; +import { XY_VIS } from '../constants'; + +describe('xyVis', () => { + test('it renders with the specified data and args', () => { + const { data, args } = sampleArgs(); + const result = xyVisFunction.fn(data, args, createMockExecutionContext()); + + expect(result).toEqual({ type: 'render', as: XY_VIS, value: { data, args } }); + }); +}); diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts similarity index 53% rename from x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 3c68837defdd9..0f83aeecc7a20 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_chart.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -1,58 +1,64 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ + import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; -import type { ExpressionValueSearchContext } from '../../../../../../src/plugins/data/common'; -import type { LensMultiTable } from '../../types'; -import type { XYArgs } from './xy_args'; -import { fittingFunctionDefinitions } from './fitting_function'; +import type { ExpressionFunctionDefinition } from '../../../../expressions'; +import { LensMultiTable, XYArgs, XYRender } from '../types'; import { prepareLogTable } from '../../../../../../src/plugins/visualizations/common/utils'; -import { endValueDefinitions } from './end_value'; - -export interface XYChartProps { - data: LensMultiTable; - args: XYArgs; -} +import { + XY_VIS, + DATA_LAYER, + MULTITABLE, + XYCurveTypes, + LEGEND_CONFIG, + ValueLabelModes, + FittingFunctions, + GRID_LINES_CONFIG, + XY_VIS_RENDERER, + AXIS_EXTENT_CONFIG, + TICK_LABELS_CONFIG, + REFERENCE_LINE_LAYER, + LABELS_ORIENTATION_CONFIG, + AXIS_TITLES_VISIBILITY_CONFIG, + EndValues, + ANNOTATION_LAYER, + LayerTypes, +} from '../constants'; const strings = { getMetricHelp: () => - i18n.translate('xpack.lens.xy.logDatatable.metric', { + i18n.translate('expressionXY.xyVis.logDatatable.metric', { defaultMessage: 'Vertical axis', }), getXAxisHelp: () => - i18n.translate('xpack.lens.xy.logDatatable.x', { + i18n.translate('expressionXY.xyVis.logDatatable.x', { defaultMessage: 'Horizontal axis', }), getBreakdownHelp: () => - i18n.translate('xpack.lens.xy.logDatatable.breakDown', { + i18n.translate('expressionXY.xyVis.logDatatable.breakDown', { defaultMessage: 'Break down by', }), getReferenceLineHelp: () => - i18n.translate('xpack.lens.xy.logDatatable.breakDown', { + i18n.translate('expressionXY.xyVis.logDatatable.breakDown', { defaultMessage: 'Break down by', }), }; -export interface XYRender { - type: 'render'; - as: 'lens_xy_chart_renderer'; - value: XYChartProps; -} - -export const xyChart: ExpressionFunctionDefinition< - 'lens_xy_chart', - LensMultiTable | ExpressionValueSearchContext | null, +export const xyVisFunction: ExpressionFunctionDefinition< + typeof XY_VIS, + LensMultiTable, XYArgs, XYRender > = { - name: 'lens_xy_chart', + name: XY_VIS, type: 'render', - inputTypes: ['lens_multitable', 'kibana_context', 'null'], - help: i18n.translate('xpack.lens.xyChart.help', { + inputTypes: [MULTITABLE], + help: i18n.translate('expressionXY.xyVis.help', { defaultMessage: 'An X/Y chart', }), args: { @@ -66,51 +72,53 @@ export const xyChart: ExpressionFunctionDefinition< }, xTitle: { types: ['string'], - help: i18n.translate('xpack.lens.xyChart.xTitle.help', { + help: i18n.translate('expressionXY.xyVis.xTitle.help', { defaultMessage: 'X axis title', }), }, yTitle: { types: ['string'], - help: i18n.translate('xpack.lens.xyChart.yLeftTitle.help', { + help: i18n.translate('expressionXY.xyVis.yLeftTitle.help', { defaultMessage: 'Y left axis title', }), }, yRightTitle: { types: ['string'], - help: i18n.translate('xpack.lens.xyChart.yRightTitle.help', { + help: i18n.translate('expressionXY.xyVis.yRightTitle.help', { defaultMessage: 'Y right axis title', }), }, yLeftExtent: { - types: ['lens_xy_axisExtentConfig'], - help: i18n.translate('xpack.lens.xyChart.yLeftExtent.help', { + types: [AXIS_EXTENT_CONFIG], + help: i18n.translate('expressionXY.xyVis.yLeftExtent.help', { defaultMessage: 'Y left axis extents', }), }, yRightExtent: { - types: ['lens_xy_axisExtentConfig'], - help: i18n.translate('xpack.lens.xyChart.yRightExtent.help', { + types: [AXIS_EXTENT_CONFIG], + help: i18n.translate('expressionXY.xyVis.yRightExtent.help', { defaultMessage: 'Y right axis extents', }), }, legend: { - types: ['lens_xy_legendConfig'], - help: i18n.translate('xpack.lens.xyChart.legend.help', { + types: [LEGEND_CONFIG], + help: i18n.translate('expressionXY.xyVis.legend.help', { defaultMessage: 'Configure the chart legend.', }), }, fittingFunction: { types: ['string'], - options: [...fittingFunctionDefinitions.map(({ id }) => id)], - help: i18n.translate('xpack.lens.xyChart.fittingFunction.help', { + options: [...Object.values(FittingFunctions)], + help: i18n.translate('expressionXY.xyVis.fittingFunction.help', { defaultMessage: 'Define how missing values are treated', }), }, endValue: { types: ['string'], - options: [...endValueDefinitions.map(({ id }) => id)], - help: '', + options: [...Object.values(EndValues)], + help: i18n.translate('expressionXY.xyVis.endValue.help', { + defaultMessage: 'End value', + }), }, emphasizeFitting: { types: ['boolean'], @@ -119,85 +127,92 @@ export const xyChart: ExpressionFunctionDefinition< }, valueLabels: { types: ['string'], - options: ['hide', 'inside'], - help: '', + options: [...Object.values(ValueLabelModes)], + help: i18n.translate('expressionXY.xyVis.valueLabels.help', { + defaultMessage: 'Value labels mode', + }), }, tickLabelsVisibilitySettings: { - types: ['lens_xy_tickLabelsConfig'], - help: i18n.translate('xpack.lens.xyChart.tickLabelsSettings.help', { + types: [TICK_LABELS_CONFIG], + help: i18n.translate('expressionXY.xyVis.tickLabelsVisibilitySettings.help', { defaultMessage: 'Show x and y axes tick labels', }), }, labelsOrientation: { - types: ['lens_xy_labelsOrientationConfig'], - help: i18n.translate('xpack.lens.xyChart.labelsOrientation.help', { + types: [LABELS_ORIENTATION_CONFIG], + help: i18n.translate('expressionXY.xyVis.labelsOrientation.help', { defaultMessage: 'Defines the rotation of the axis labels', }), }, gridlinesVisibilitySettings: { - types: ['lens_xy_gridlinesConfig'], - help: i18n.translate('xpack.lens.xyChart.gridlinesSettings.help', { + types: [GRID_LINES_CONFIG], + help: i18n.translate('expressionXY.xyVis.gridlinesVisibilitySettings.help', { defaultMessage: 'Show x and y axes gridlines', }), }, axisTitlesVisibilitySettings: { - types: ['lens_xy_axisTitlesVisibilityConfig'], - help: i18n.translate('xpack.lens.xyChart.axisTitlesSettings.help', { + types: [AXIS_TITLES_VISIBILITY_CONFIG], + help: i18n.translate('expressionXY.xyVis.axisTitlesVisibilitySettings.help', { defaultMessage: 'Show x and y axes titles', }), }, layers: { - types: [ - 'lens_xy_data_layer', - 'lens_xy_referenceLine_layer', - 'lens_xy_annotation_layer', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ] as any, - help: 'Layers of visual series', + types: [DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER], + help: i18n.translate('expressionXY.xyVis.layers.help', { + defaultMessage: 'Layers of visual series', + }), multi: true, }, curveType: { types: ['string'], - options: ['LINEAR', 'CURVE_MONOTONE_X'], - help: i18n.translate('xpack.lens.xyChart.curveType.help', { + 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('xpack.lens.xyChart.fillOpacity.help', { + help: i18n.translate('expressionXY.xyVis.fillOpacity.help', { defaultMessage: 'Define the area chart fill opacity', }), }, hideEndzones: { types: ['boolean'], default: false, - help: i18n.translate('xpack.lens.xyChart.hideEndzones.help', { + help: i18n.translate('expressionXY.xyVis.hideEndzones.help', { defaultMessage: 'Hide endzone markers for partial data', }), }, valuesInLegend: { types: ['boolean'], default: false, - help: i18n.translate('xpack.lens.xyChart.valuesInLegend.help', { + help: i18n.translate('expressionXY.xyVis.valuesInLegend.help', { defaultMessage: 'Show values in legend', }), }, ariaLabel: { types: ['string'], - help: i18n.translate('xpack.lens.xyChart.ariaLabel.help', { + help: i18n.translate('expressionXY.xyVis.ariaLabel.help', { defaultMessage: 'Specifies the aria label of the xy chart', }), required: false, }, }, - fn(data: LensMultiTable, args: XYArgs, handlers) { + fn(data, args, handlers) { if (handlers?.inspectorAdapters?.tables) { args.layers.forEach((layer) => { - if (layer.layerType === 'annotations') { + if (layer.layerType === LayerTypes.ANNOTATIONS) { return; } - const { layerId, accessors, xAccessor, splitAccessor, layerType } = layer; + + 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], [ @@ -214,9 +229,10 @@ export const xyChart: ExpressionFunctionDefinition< handlers.inspectorAdapters.tables.logDatatable(layerId, logTable); }); } + return { type: 'render', - as: 'lens_xy_chart_renderer', + as: XY_VIS_RENDERER, value: { data, args: { 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 new file mode 100644 index 0000000000000..e665fc2b8cea0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts @@ -0,0 +1,93 @@ +/* + * 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 '../../../../expressions/common'; +import { FillStyles, IconPositions, LineStyles, YAxisModes, Y_CONFIG } from '../constants'; +import { YConfig, YConfigResult } from '../types'; + +export const yAxisConfigFunction: ExpressionFunctionDefinition< + typeof Y_CONFIG, + null, + YConfig, + YConfigResult +> = { + 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`, + }), + 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', + }), + }, + }, + fn(input, args) { + return { + type: Y_CONFIG, + ...args, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts new file mode 100755 index 0000000000000..68f9f946baeb0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -0,0 +1,66 @@ +/* + * 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 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, + EndValue, + XYRender, + LayerType, + YAxisMode, + LineStyle, + FillStyle, + SeriesType, + YScaleType, + XScaleType, + AxisConfig, + ValidLayer, + XYLayerArgs, + XYCurveType, + XYChartProps, + LegendConfig, + IconPosition, + YConfigResult, + DataLayerArgs, + LensMultiTable, + ValueLabelMode, + AxisExtentMode, + FittingFunction, + AxisExtentConfig, + LegendConfigResult, + AxesSettingsConfig, + AnnotationLayerArgs, + XYLayerConfigResult, + GridlinesConfigResult, + DataLayerConfigResult, + TickLabelsConfigResult, + AxisExtentConfigResult, + ReferenceLineLayerArgs, + LabelsOrientationConfig, + AnnotationLayerConfigResult, + LabelsOrientationConfigResult, + ReferenceLineLayerConfigResult, + AxisTitlesVisibilityConfigResult, +} 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 new file mode 100644 index 0000000000000..98889da771c04 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -0,0 +1,239 @@ +/* + * 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 { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; +import { $Values } from '@kbn/utility-types'; +import { Datatable } from '../../../../expressions'; +import { PaletteOutput } from '../../../../charts/common'; +import { EventAnnotationOutput } from '../../../../event_annotation/common'; +import { + AxisExtentModes, + FillStyles, + FittingFunctions, + IconPositions, + LayerTypes, + MULTITABLE, + LineStyles, + SeriesTypes, + ValueLabelModes, + XScaleTypes, + XYCurveTypes, + YAxisModes, + YScaleTypes, + REFERENCE_LINE_LAYER, + Y_CONFIG, + AXIS_TITLES_VISIBILITY_CONFIG, + LABELS_ORIENTATION_CONFIG, + TICK_LABELS_CONFIG, + GRID_LINES_CONFIG, + LEGEND_CONFIG, + DATA_LAYER, + AXIS_EXTENT_CONFIG, + ANNOTATION_LAYER, + EndValues, +} from '../constants'; + +export type EndValue = $Values; +export type LayerType = $Values; +export type YAxisMode = $Values; +export type LineStyle = $Values; +export type FillStyle = $Values; +export type SeriesType = $Values; +export type YScaleType = $Values; +export type XScaleType = $Values; +export type XYCurveType = $Values; +export type IconPosition = $Values; +export type ValueLabelMode = $Values; +export type AxisExtentMode = $Values; +export type FittingFunction = $Values; + +export interface AxesSettingsConfig { + x: boolean; + yLeft: boolean; + yRight: boolean; +} + +export interface AxisExtentConfig { + mode: AxisExtentMode; + lowerBound?: number; + upperBound?: number; +} + +export interface AxisConfig { + title: string; + hide?: boolean; +} + +export interface YConfig { + forAccessor: string; + axisMode?: YAxisMode; + color?: string; + icon?: string; + lineWidth?: number; + lineStyle?: LineStyle; + fill?: FillStyle; + iconPosition?: IconPosition; + textVisibility?: boolean; +} + +export interface ValidLayer extends DataLayerConfigResult { + xAccessor: NonNullable; +} + +export interface DataLayerArgs { + layerId: string; + accessors: string[]; + seriesType: SeriesType; + xAccessor?: string; + hide?: boolean; + splitAccessor?: string; + columnToLabel?: string; // Actually a JSON key-value pair + yScaleType: YScaleType; + xScaleType: XScaleType; + isHistogram: boolean; + // palette will always be set on the expression + palette: PaletteOutput; + yConfig?: YConfigResult[]; +} + +export interface LegendConfig { + /** + * Flag whether the legend should be shown. If there is just a single series, it will be hidden + */ + isVisible: boolean; + /** + * Position of the legend relative to the chart + */ + position: Position; + /** + * Flag whether the legend should be shown even with just a single series + */ + showSingleSeries?: boolean; + /** + * Flag whether the legend is inside the chart + */ + isInside?: boolean; + /** + * Horizontal Alignment of the legend when it is set inside chart + */ + horizontalAlignment?: HorizontalAlignment; + /** + * Vertical Alignment of the legend when it is set inside chart + */ + verticalAlignment?: VerticalAlignment; + /** + * Number of columns when legend is set inside chart + */ + floatingColumns?: number; + /** + * Maximum number of lines per legend item + */ + maxLines?: number; + + /** + * Flag whether the legend items are truncated or not + */ + shouldTruncate?: boolean; + + /** + * Exact legend width (vertical) or height (horizontal) + * Limited to max of 70% of the chart container dimension Vertical legends limited to min of 30% of computed width + */ + legendSize?: number; +} + +export interface LabelsOrientationConfig { + x: number; + yLeft: number; + yRight: number; +} + +// 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; + valueLabels: ValueLabelMode; + layers: XYLayerConfigResult[]; + endValue?: EndValue; + emphasizeFitting?: boolean; + fittingFunction?: FittingFunction; + axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; + tickLabelsVisibilitySettings?: TickLabelsConfigResult; + gridlinesVisibilitySettings?: GridlinesConfigResult; + labelsOrientation?: LabelsOrientationConfigResult; + curveType?: XYCurveType; + fillOpacity?: number; + hideEndzones?: boolean; + valuesInLegend?: boolean; + ariaLabel?: string; +} + +export interface AnnotationLayerArgs { + annotations: EventAnnotationOutput[]; + layerId: string; + hide?: boolean; +} + +export type AnnotationLayerConfigResult = AnnotationLayerArgs & { + type: typeof ANNOTATION_LAYER; + layerType: typeof LayerTypes.ANNOTATIONS; +}; + +export interface ReferenceLineLayerArgs { + layerId: string; + accessors: string[]; + columnToLabel?: string; + yConfig?: YConfigResult[]; +} + +export type XYLayerArgs = DataLayerArgs | ReferenceLineLayerArgs | AnnotationLayerArgs; + +export type XYLayerConfigResult = + | DataLayerConfigResult + | ReferenceLineLayerConfigResult + | AnnotationLayerConfigResult; + +export interface LensMultiTable { + type: typeof MULTITABLE; + tables: Record; + dateRange?: { + fromDate: Date; + toDate: Date; + }; +} + +export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { + type: typeof REFERENCE_LINE_LAYER; + layerType: typeof LayerTypes.REFERENCELINE; +}; + +export type DataLayerConfigResult = DataLayerArgs & { + type: typeof DATA_LAYER; + layerType: typeof LayerTypes.DATA; +}; + +export type YConfigResult = YConfig & { type: typeof Y_CONFIG }; + +export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & { + type: typeof AXIS_TITLES_VISIBILITY_CONFIG; +}; + +export type LabelsOrientationConfigResult = LabelsOrientationConfig & { + type: typeof LABELS_ORIENTATION_CONFIG; +}; + +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 }; 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 new file mode 100644 index 0000000000000..1acb98903d06b --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -0,0 +1,21 @@ +/* + * 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 { LensMultiTable, XYArgs } from './expression_functions'; + +export interface XYChartProps { + data: LensMultiTable; + args: XYArgs; +} + +export interface XYRender { + type: 'render'; + as: typeof XY_VIS_RENDERER; + value: XYChartProps; +} diff --git a/src/plugins/chart_expressions/expression_xy/common/types/index.ts b/src/plugins/chart_expressions/expression_xy/common/types/index.ts new file mode 100644 index 0000000000000..9c50bfab1305d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/types/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './expression_functions'; +export * from './expression_renderers'; diff --git a/src/plugins/chart_expressions/expression_xy/jest.config.js b/src/plugins/chart_expressions/expression_xy/jest.config.js new file mode 100644 index 0000000000000..6d742af9a6f3d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/jest.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../', + roots: ['/src/plugins/chart_expressions/expression_xy'], + coverageDirectory: + '/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_xy', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/chart_expressions/expression_xy/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/chart_expressions/expression_xy/kibana.json b/src/plugins/chart_expressions/expression_xy/kibana.json new file mode 100755 index 0000000000000..e9680d9b85163 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "expressionXY", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" + }, + "description": "Expression XY plugin adds a `xy` renderer and function to the expression plugin. The renderer will display the `xy` chart.", + "server": true, + "ui": true, + "requiredPlugins": ["expressions", "charts", "data", "fieldFormats", "uiActions", "eventAnnotation", "visualizations"], + "requiredBundles": ["kibanaReact"], + "optionalPlugins": [] +} diff --git a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx new file mode 100644 index 0000000000000..cc73950438f38 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx @@ -0,0 +1,224 @@ +/* + * 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 { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; +import { DataLayerConfigResult, LensMultiTable, XYArgs } from '../../common'; +import { LayerTypes } from '../../common/constants'; +import { mockPaletteOutput, sampleArgs } from '../../common/__mocks__'; + +const chartSetupContract = chartPluginMock.createSetupContract(); +const chartStartContract = chartPluginMock.createStartContract(); + +export const chartsThemeService = chartSetupContract.theme; +export const chartsActiveCursorService = chartStartContract.activeCursor; + +export const paletteService = chartPluginMock.createPaletteRegistry(); + +export const dateHistogramData: LensMultiTable = { + type: 'lens_multitable', + tables: { + timeLayer: { + type: 'datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + appliedTimeRange: { + from: '2020-04-01T16:14:16.246Z', + to: '2020-04-01T17:15:41.263Z', + }, + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + params: { id: 'date', params: { pattern: 'HH:mm' } }, + }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + params: {}, + }, + params: { id: 'number' }, + }, + }, + ], + }, + }, + dateRange: { + fromDate: new Date('2020-04-01T16:14:16.246Z'), + toDate: new Date('2020-04-01T17:15:41.263Z'), + }, +}; + +export const dateHistogramLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'timeLayer', + layerType: LayerTypes.DATA, + hide: false, + xAccessor: 'xAccessorId', + yScaleType: 'linear', + xScaleType: 'time', + isHistogram: true, + splitAccessor: 'splitAccessorId', + seriesType: 'bar_stacked', + accessors: ['yAccessorId'], + palette: mockPaletteOutput, +}; + +export function sampleArgsWithReferenceLine(value: number = 150) { + const { data, args } = sampleArgs(); + + 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 }], + }, + }, + } 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, + }; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap similarity index 97% rename from x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap rename to src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index fdde8eb6ad3f2..828a62c85cce3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`xy_expression XYChart component annotations should render basic annotation 1`] = ` +exports[`XYChart component annotations should render basic annotation 1`] = ` `; -exports[`xy_expression XYChart component annotations should render grouped annotations preserving the shared styles 1`] = ` +exports[`XYChart component annotations should render grouped annotations preserving the shared styles 1`] = ` `; -exports[`xy_expression XYChart component annotations should render grouped annotations with default styles 1`] = ` +exports[`XYChart component annotations should render grouped annotations with default styles 1`] = ` `; -exports[`xy_expression XYChart component annotations should render simplified annotation when hide is true 1`] = ` +exports[`XYChart component annotations should render simplified annotation when hide is true 1`] = ` `; -exports[`xy_expression XYChart component it renders area 1`] = ` +exports[`XYChart component it renders area 1`] = ` @@ -447,7 +447,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` `; -exports[`xy_expression XYChart component it renders bar 1`] = ` +exports[`XYChart component it renders bar 1`] = ` @@ -693,7 +693,7 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` `; -exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` +exports[`XYChart component it renders horizontal bar 1`] = ` @@ -939,7 +939,7 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` `; -exports[`xy_expression XYChart component it renders line 1`] = ` +exports[`XYChart component it renders line 1`] = ` @@ -1173,7 +1173,7 @@ exports[`xy_expression XYChart component it renders line 1`] = ` `; -exports[`xy_expression XYChart component it renders stacked area 1`] = ` +exports[`XYChart component it renders stacked area 1`] = ` @@ -1415,7 +1415,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` `; -exports[`xy_expression XYChart component it renders stacked bar 1`] = ` +exports[`XYChart component it renders stacked bar 1`] = ` @@ -1669,7 +1669,7 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` `; -exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` +exports[`XYChart component it renders stacked horizontal bar 1`] = ` diff --git a/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss new file mode 100644 index 0000000000000..88881ae718925 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.scss @@ -0,0 +1,18 @@ +.xyAnnotationNumberIcon { + border-radius: $euiSize; + min-width: $euiSize; + height: $euiSize; + background-color: currentColor; +} + +.xyAnnotationNumberIcon__text { + font-weight: 500; + font-size: 9px; + letter-spacing: -.5px; + line-height: 11px; +} + +.xyAnnotationIcon_rotate90 { + transform: rotate(45deg); + transform-origin: center; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx similarity index 66% rename from x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx rename to src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx index fa41a752cc09b..4e8fa1b95775f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/expression.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/annotations.tsx @@ -1,11 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import './expression.scss'; +import './annotations.scss'; +import './reference_lines.scss'; + import React from 'react'; import { snakeCase } from 'lodash'; import { @@ -14,20 +17,20 @@ import { LineAnnotation, Position, } from '@elastic/charts'; -import type { FieldFormat } from 'src/plugins/field_formats/common'; -import type { EventAnnotationArgs } from 'src/plugins/event_annotation/common'; import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; +import classnames from 'classnames'; +import type { EventAnnotationArgs } from '../../../../event_annotation/common'; +import type { FieldFormat } from '../../../../field_formats/common'; import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; -import type { AnnotationLayerArgs } from '../../../common/expressions'; -import { hasIcon } from '../xy_config_panel/shared/icon_select'; -import { - mapVerticalToHorizontalPlacement, - LINES_MARKER_SIZE, - MarkerBody, - Marker, - AnnotationIcon, -} from '../annotations_helpers'; +import type { + AnnotationLayerArgs, + AnnotationLayerConfigResult, + IconPosition, + YAxisMode, +} from '../../common/types'; +import { annotationsIconSet, hasIcon, isNumericalString } from '../helpers'; +import { mapVerticalToHorizontalPlacement, LINES_MARKER_SIZE } from '../helpers'; const getRoundedTimestamp = (timestamp: number, firstTimestamp?: number, minInterval?: number) => { if (!firstTimestamp || !minInterval) { @@ -124,7 +127,7 @@ const getCommonStyles = (configArr: EventAnnotationArgs[]) => { }; export const getAnnotationsGroupedByInterval = ( - layers: AnnotationLayerArgs[], + layers: AnnotationLayerConfigResult[], minInterval?: number, firstTimestamp?: number, formatter?: FieldFormat @@ -187,7 +190,7 @@ export const Annotations = ({ isHorizontal: !isHorizontal, hasReducedPadding, label: annotation.label, - rotateClassName: isHorizontal ? 'lnsXyAnnotationIcon_rotate90' : undefined, + rotateClassName: isHorizontal ? 'xyAnnotationIcon_rotate90' : undefined, }} /> ) : undefined @@ -232,3 +235,123 @@ export const Annotations = ({ ); }; + +export function MarkerBody({ + label, + isHorizontal, +}: { + label: string | undefined; + isHorizontal: boolean; +}) { + if (!label) { + return null; + } + if (isHorizontal) { + return ( +
+ {label} +
+ ); + } + return ( +
+
+ {label} +
+
+ ); +} + +function NumberIcon({ number }: { number: number }) { + return ( + + + {number < 10 ? number : `9+`} + + + ); +} + +export const AnnotationIcon = ({ + type, + rotateClassName = '', + isHorizontal, + renderedInChart, + ...rest +}: { + type: string; + rotateClassName?: string; + isHorizontal?: boolean; + renderedInChart?: boolean; +} & EuiIconProps) => { + if (isNumericalString(type)) { + return ; + } + const iconConfig = annotationsIconSet.find((i) => i.value === type); + if (!iconConfig) { + return null; + } + return ( + + ); +}; + +interface MarkerConfig { + axisMode?: YAxisMode; + icon?: string; + textVisibility?: boolean; + iconPosition?: IconPosition; +} + +export function Marker({ + config, + isHorizontal, + hasReducedPadding, + label, + rotateClassName, +}: { + config: MarkerConfig; + isHorizontal: boolean; + hasReducedPadding: boolean; + label?: string; + rotateClassName?: string; +}) { + if (hasIcon(config.icon)) { + return ( + + ); + } + + // if there's some text, check whether to show it as marker, or just show some padding for the icon + if (config.textVisibility) { + if (hasReducedPadding) { + return ; + } + return ; + } + return null; +} diff --git a/src/plugins/chart_expressions/expression_xy/public/components/index.ts b/src/plugins/chart_expressions/expression_xy/public/components/index.ts new file mode 100644 index 0000000000000..be06610fa8922 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/index.ts @@ -0,0 +1,13 @@ +/* + * 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 * from './legend_action_popover'; +export * from './reference_lines'; +export * from './legend_action'; +export * from './x_domain'; +export * from './xy_chart'; diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx similarity index 92% rename from x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx rename to src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx index faa3ecc976d9b..0f1cdebc5bf59 100644 --- a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx @@ -1,8 +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; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -11,14 +12,15 @@ import { EuiPopover } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { ComponentType, ReactWrapper } from 'enzyme'; import type { LensMultiTable } from '../../common'; -import { layerTypes } from '../../common'; -import type { DataLayerArgs } from '../../common/expressions'; -import { getLegendAction } from './get_legend_action'; -import { LegendActionPopover } from '../shared_components'; +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, + layerType: LayerTypes.DATA, seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], @@ -27,6 +29,7 @@ const sampleLayer = { xScaleType: 'ordinal', yScaleType: 'linear', isHistogram: false, + palette: mockPaletteOutput, } as DataLayerArgs; const tables = { diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx similarity index 79% rename from x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx rename to src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx index 00532314e045b..9bbdec3635fa8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.tsx @@ -1,21 +1,22 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts'; -import type { LensFilterEvent } from '../types'; -import type { LensMultiTable, FormatFactory } from '../../common'; -import type { DataLayerArgs } from '../../common/expressions'; -import { LegendActionPopover } from '../shared_components'; +import type { FilterEvent } from '../types'; +import type { LensMultiTable, DataLayerArgs } from '../../common'; +import type { FormatFactory } from '../types'; +import { LegendActionPopover } from './legend_action_popover'; export const getLegendAction = ( filteredLayers: DataLayerArgs[], tables: LensMultiTable['tables'], - onFilter: (data: LensFilterEvent['data']) => void, + onFilter: (data: FilterEvent['data']) => void, formatFactory: FormatFactory, layersAlreadyFormatted: Record ): LegendAction => @@ -55,7 +56,7 @@ export const getLegendAction = ( }, ]; - const context: LensFilterEvent['data'] = { + const context: FilterEvent['data'] = { data, }; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action_popover.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action_popover.tsx new file mode 100644 index 0000000000000..a1ab31a993edf --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action_popover.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import { useLegendAction } from '@elastic/charts'; +import type { FilterEvent } from '../types'; + +export interface LegendActionPopoverProps { + /** + * Determines the panels label + */ + label: string; + /** + * Callback on filter value + */ + onFilter: (data: FilterEvent['data']) => void; + /** + * Determines the filter event data + */ + context: FilterEvent['data']; +} + +export const LegendActionPopover: React.FunctionComponent = ({ + label, + onFilter, + context, +}) => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [ref, onClose] = useLegendAction(); + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 'main', + title: label, + items: [ + { + name: i18n.translate('expressionXY.legend.filterForValueButtonAriaLabel', { + defaultMessage: 'Filter for value', + }), + 'data-test-subj': `legend-${label}-filterIn`, + icon: , + onClick: () => { + setPopoverOpen(false); + onFilter(context); + }, + }, + { + name: i18n.translate('expressionXY.legend.filterOutValueButtonAriaLabel', { + defaultMessage: 'Filter out value', + }), + 'data-test-subj': `legend-${label}-filterOut`, + icon: , + onClick: () => { + setPopoverOpen(false); + onFilter({ ...context, negate: true }); + }, + }, + ], + }, + ]; + + const Button = ( +
setPopoverOpen(!popoverOpen)} + onClick={() => setPopoverOpen(!popoverOpen)} + > + +
+ ); + return ( + { + setPopoverOpen(false); + onClose(); + }} + panelPaddingSize="none" + anchorPosition="upLeft" + title={i18n.translate('expressionXY.legend.filterOptionsLegend', { + defaultMessage: '{legendDataLabel}, filter options', + values: { legendDataLabel: label }, + })} + > + + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss similarity index 79% rename from x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss rename to src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss index 07946b52b0000..2cd7fb9c26915 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.scss @@ -1,9 +1,9 @@ -.lnsXyDecorationRotatedWrapper { +.xyDecorationRotatedWrapper { display: inline-block; overflow: hidden; line-height: 1.5; - .lnsXyDecorationRotatedWrapper__label { + .xyDecorationRotatedWrapper__label { display: inline-block; white-space: nowrap; transform: translate(0, 100%) rotate(-90deg); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx similarity index 93% rename from x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx rename to src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx index 4ec38f31b85af..a0d6351dad7f9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.test.tsx @@ -1,23 +1,18 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { LineAnnotation, RectAnnotation } from '@elastic/charts'; import { shallow } from 'enzyme'; import React from 'react'; -import { chartPluginMock } from 'src/plugins/charts/public/mocks'; -import { FieldFormat } from 'src/plugins/field_formats/common'; +import { FieldFormat } from '../../../../field_formats/common'; import { LensMultiTable } from '../../common'; -import { ReferenceLineLayerArgs, YConfig } from '../../common/expressions'; -import { - ReferenceLineAnnotations, - ReferenceLineAnnotationsProps, -} from './expression_reference_lines'; - -const paletteService = chartPluginMock.createPaletteRegistry(); +import { ReferenceLineLayerArgs, YConfig } from '../../common/types'; +import { ReferenceLineAnnotations, ReferenceLineAnnotationsProps } from './reference_lines'; const row: Record = { xAccessorFirstId: 1, @@ -54,7 +49,6 @@ function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLin return [ { layerId: 'firstLayer', - layerType: 'referenceLine', accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), yConfig: yConfigs, }, @@ -90,8 +84,6 @@ describe('ReferenceLineAnnotations', () => { defaultProps = { formatters, - paletteService, - syncColors: false, isHorizontal: false, axesMap: { left: true, right: false }, paddingMap: {}, @@ -117,6 +109,7 @@ describe('ReferenceLineAnnotations', () => { axisMode, lineStyle: 'solid', fill, + type: 'yConfig', }, ])} /> @@ -154,6 +147,7 @@ describe('ReferenceLineAnnotations', () => { forAccessor: `${layerPrefix}FirstId`, axisMode: 'bottom', lineStyle: 'solid', + type: 'yConfig', fill, }, ])} @@ -195,12 +189,14 @@ describe('ReferenceLineAnnotations', () => { forAccessor: `${layerPrefix}FirstId`, axisMode, lineStyle: 'solid', + type: 'yConfig', fill, }, { forAccessor: `${layerPrefix}SecondId`, axisMode, lineStyle: 'solid', + type: 'yConfig', fill, }, ])} @@ -243,12 +239,14 @@ describe('ReferenceLineAnnotations', () => { forAccessor: `${layerPrefix}FirstId`, axisMode: 'bottom', lineStyle: 'solid', + type: 'yConfig', fill, }, { forAccessor: `${layerPrefix}SecondId`, axisMode: 'bottom', lineStyle: 'solid', + type: 'yConfig', fill, }, ])} @@ -291,12 +289,14 @@ describe('ReferenceLineAnnotations', () => { axisMode, lineStyle: 'solid', fill: 'above', + type: 'yConfig', }, { forAccessor: `${layerPrefix}SecondId`, axisMode, lineStyle: 'solid', fill: 'below', + type: 'yConfig', }, ])} /> @@ -339,12 +339,14 @@ describe('ReferenceLineAnnotations', () => { axisMode: 'left', lineStyle: 'solid', fill, + type: 'yConfig', }, { forAccessor: `yAccessorRightSecondId`, axisMode: 'right', lineStyle: 'solid', fill, + type: 'yConfig', }, ])} /> 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 new file mode 100644 index 0000000000000..65bc91c06efe5 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines.tsx @@ -0,0 +1,366 @@ +/* + * 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 './reference_lines.scss'; + +import React from 'react'; +import { groupBy } from 'lodash'; +import { EuiIcon } from '@elastic/eui'; +import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; +import { euiLightVars } from '@kbn/ui-theme'; +import type { FieldFormat } from '../../../../field_formats/common'; +import type { IconPosition, ReferenceLineLayerArgs, YAxisMode } from '../../common/types'; +import type { LensMultiTable } from '../../common/types'; +import { hasIcon } from '../helpers'; + +export const REFERENCE_LINE_MARKER_SIZE = 20; + +export const computeChartMargins = ( + referenceLinePaddings: Partial>, + labelVisibility: Partial>, + titleVisibility: Partial>, + axesMap: Record<'left' | 'right', unknown>, + isHorizontal: boolean +) => { + const result: Partial> = {}; + if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; + result[placement] = referenceLinePaddings.bottom; + } + if ( + referenceLinePaddings.left && + (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; + result[placement] = referenceLinePaddings.left; + } + if ( + referenceLinePaddings.right && + (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) + ) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; + result[placement] = referenceLinePaddings.right; + } + // there's no top axis, so just check if a margin has been computed + if (referenceLinePaddings.top) { + const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; + result[placement] = referenceLinePaddings.top; + } + return result; +}; + +// Note: it does not take into consideration whether the reference line is in view or not +export const getReferenceLineRequiredPaddings = ( + referenceLineLayers: ReferenceLineLayerArgs[], + axesMap: Record<'left' | 'right', unknown> +) => { + // collect all paddings for the 4 axis: if any text is detected double it. + const paddings: Partial> = {}; + const icons: Partial> = {}; + referenceLineLayers.forEach((layer) => { + layer.yConfig?.forEach(({ axisMode, icon, iconPosition, textVisibility }) => { + if (axisMode && (hasIcon(icon) || textVisibility)) { + const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap); + paddings[placement] = Math.max( + paddings[placement] || 0, + REFERENCE_LINE_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text + ); + icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0); + } + }); + }); + // post-process the padding based on the icon presence: + // if no icon is present for the placement, just reduce the padding + (Object.keys(paddings) as Position[]).forEach((placement) => { + if (!icons[placement]) { + paddings[placement] = REFERENCE_LINE_MARKER_SIZE; + } + }); + + return paddings; +}; + +function mapVerticalToHorizontalPlacement(placement: Position) { + switch (placement) { + case Position.Top: + return Position.Right; + case Position.Bottom: + return Position.Left; + case Position.Left: + return Position.Bottom; + case Position.Right: + return Position.Top; + } +} + +// if there's just one axis, put it on the other one +// otherwise use the same axis +// this function assume the chart is vertical +function getBaseIconPlacement( + iconPosition: IconPosition | undefined, + axisMode: YAxisMode | undefined, + axesMap: Record +) { + if (iconPosition === 'auto') { + if (axisMode === 'bottom') { + return Position.Top; + } + if (axisMode === 'left') { + return axesMap.right ? Position.Left : Position.Right; + } + return axesMap.left ? Position.Right : Position.Left; + } + + if (iconPosition === 'left') { + return Position.Left; + } + if (iconPosition === 'right') { + return Position.Right; + } + if (iconPosition === 'below') { + return Position.Bottom; + } + return Position.Top; +} + +function getMarkerBody(label: string | undefined, isHorizontal: boolean) { + if (!label) { + return; + } + if (isHorizontal) { + return ( +
+ {label} +
+ ); + } + return ( +
+
+ {label} +
+
+ ); +} + +interface MarkerConfig { + axisMode?: YAxisMode; + icon?: string; + textVisibility?: boolean; +} + +function getMarkerToShow( + markerConfig: MarkerConfig, + label: string | undefined, + isHorizontal: boolean, + hasReducedPadding: boolean +) { + // show an icon if present + if (hasIcon(markerConfig.icon)) { + return ; + } + // if there's some text, check whether to show it as marker, or just show some padding for the icon + if (markerConfig.textVisibility) { + if (hasReducedPadding) { + return getMarkerBody( + label, + (!isHorizontal && markerConfig.axisMode === 'bottom') || + (isHorizontal && markerConfig.axisMode !== 'bottom') + ); + } + return ; + } +} + +export interface ReferenceLineAnnotationsProps { + layers: ReferenceLineLayerArgs[]; + data: LensMultiTable; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; + paddingMap: Partial>; +} + +export const ReferenceLineAnnotations = ({ + layers, + data, + formatters, + axesMap, + isHorizontal, + paddingMap, +}: ReferenceLineAnnotationsProps) => { + return ( + <> + {layers.flatMap((layer) => { + if (!layer.yConfig) { + return []; + } + const { columnToLabel, yConfig: yConfigs, layerId } = layer; + const columnToLabelMap: Record = columnToLabel + ? JSON.parse(columnToLabel) + : {}; + const table = data.tables[layerId]; + + const row = table.rows[0]; + + const yConfigByValue = yConfigs.sort( + ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] + ); + + const groupedByDirection = groupBy(yConfigByValue, 'fill'); + if (groupedByDirection.below) { + groupedByDirection.below.reverse(); + } + + return yConfigByValue.flatMap((yConfig, i) => { + // Find the formatter for the given axis + const groupId = + yConfig.axisMode === 'bottom' + ? undefined + : yConfig.axisMode === 'right' + ? 'right' + : 'left'; + + const formatter = formatters[groupId || 'bottom']; + + const defaultColor = euiLightVars.euiColorDarkShade; + + // get the position for vertical chart + const markerPositionVertical = getBaseIconPlacement( + yConfig.iconPosition, + yConfig.axisMode, + axesMap + ); + // the padding map is built for vertical chart + const hasReducedPadding = + paddingMap[markerPositionVertical] === REFERENCE_LINE_MARKER_SIZE; + + const props = { + groupId, + marker: getMarkerToShow( + yConfig, + columnToLabelMap[yConfig.forAccessor], + isHorizontal, + hasReducedPadding + ), + markerBody: getMarkerBody( + yConfig.textVisibility && !hasReducedPadding + ? columnToLabelMap[yConfig.forAccessor] + : undefined, + (!isHorizontal && yConfig.axisMode === 'bottom') || + (isHorizontal && yConfig.axisMode !== 'bottom') + ), + // rotate the position if required + markerPosition: isHorizontal + ? mapVerticalToHorizontalPlacement(markerPositionVertical) + : markerPositionVertical, + }; + const annotations = []; + + const dashStyle = + yConfig.lineStyle === 'dashed' + ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] + : yConfig.lineStyle === 'dotted' + ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] + : undefined; + + const sharedStyle = { + strokeWidth: yConfig.lineWidth || 1, + stroke: yConfig.color || defaultColor, + dash: dashStyle, + }; + + annotations.push( + ({ + dataValue: row[yConfig.forAccessor], + header: columnToLabelMap[yConfig.forAccessor], + details: formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], + }))} + domainType={ + yConfig.axisMode === 'bottom' + ? AnnotationDomainType.XDomain + : AnnotationDomainType.YDomain + } + style={{ + line: { + ...sharedStyle, + opacity: 1, + }, + }} + /> + ); + + if (yConfig.fill && yConfig.fill !== 'none') { + const isFillAbove = yConfig.fill === 'above'; + const indexFromSameType = groupedByDirection[yConfig.fill].findIndex( + ({ forAccessor }) => forAccessor === yConfig.forAccessor + ); + const shouldCheckNextReferenceLine = + indexFromSameType < groupedByDirection[yConfig.fill].length - 1; + annotations.push( + { + const nextValue = shouldCheckNextReferenceLine + ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] + : undefined; + if (yConfig.axisMode === 'bottom') { + return { + coordinates: { + x0: isFillAbove ? row[yConfig.forAccessor] : nextValue, + y0: undefined, + x1: isFillAbove ? nextValue : row[yConfig.forAccessor], + y1: undefined, + }, + header: columnToLabelMap[yConfig.forAccessor], + details: + formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], + }; + } + return { + coordinates: { + x0: undefined, + y0: isFillAbove ? row[yConfig.forAccessor] : nextValue, + x1: undefined, + y1: isFillAbove ? nextValue : row[yConfig.forAccessor], + }, + header: columnToLabelMap[yConfig.forAccessor], + details: + formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], + }; + })} + style={{ + ...sharedStyle, + fill: yConfig.color || defaultColor, + opacity: 0.1, + }} + /> + ); + } + return annotations; + }); + })} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx similarity index 89% rename from x-pack/plugins/lens/public/xy_visualization/x_domain.tsx rename to src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx index eb9de9a2993b4..dbc4c348cb891 100644 --- a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/x_domain.tsx @@ -1,17 +1,17 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { uniq } from 'lodash'; import React from 'react'; import moment from 'moment'; -import { Endzones } from '../../../../../src/plugins/charts/public'; -import type { LensMultiTable } from '../../common'; -import type { DataLayerArgs } from '../../common/expressions'; -import { search } from '../../../../../src/plugins/data/public'; +import { Endzones } from '../../../../../plugins/charts/public'; +import type { LensMultiTable, DataLayerArgs } from '../../common'; +import { search } from '../../../../../plugins/data/public'; export interface XDomain { min?: number; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.scss b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.scss new file mode 100644 index 0000000000000..ba998c863b6ac --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.scss @@ -0,0 +1,7 @@ +.xyChart__empty { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} 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 new file mode 100644 index 0000000000000..1a8a16c165e9e --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -0,0 +1,2703 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React 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, + BarSeries, + Fit, + GeometryValue, + HorizontalAlignment, + LayoutDirection, + LineAnnotation, + LineSeries, + Position, + ScaleType, + SeriesNameFn, + Settings, + VerticalAlignment, + XYChartSeriesIdentifier, +} from '@elastic/charts'; +import { EmptyPlaceholder } from '../../../../../plugins/charts/public'; +import { XyEndzones } from './x_domain'; +import { + chartsActiveCursorService, + chartsThemeService, + dateHistogramData, + dateHistogramLayer, + paletteService, + sampleArgsWithReferenceLine, +} from '../__mocks__'; +import { + mockPaletteOutput, + sampleArgs, + createArgsWithLayers, + createSampleDatatableWithRows, + sampleLayer, +} from '../../common/__mocks__'; +import { XYChart, XYChartRenderProps } from './xy_chart'; +import { eventAnnotationServiceMock } from '../../../../event_annotation/public/mocks'; +import { EventAnnotationOutput } from '../../../../event_annotation/common'; + +const onClickValue = jest.fn(); +const onSelectRange = jest.fn(); + +describe('XYChart component', () => { + let getFormatSpy: jest.Mock; + 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 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 getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { + return shallow(); + }; + + beforeEach(() => { + convertSpy = jest.fn((x) => x); + getFormatSpy = jest.fn(); + getFormatSpy.mockReturnValue({ convert: convertSpy }); + + defaultProps = { + formatFactory: getFormatSpy, + timeZone: 'UTC', + renderMode: 'view', + chartsThemeService, + chartsActiveCursorService, + paletteService, + minInterval: 50, + onClickValue, + onSelectRange, + syncColors: false, + useLegacyTimeAxis: false, + eventAnnotationService: eventAnnotationServiceMock, + }; + }); + + test('it renders line', () => { + const { data, 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']); + }); + + describe('date range', () => { + const timeSampleLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'time', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }; + const multiLayerArgs = createArgsWithLayers([ + timeSampleLayer, + { + ...timeSampleLayer, + layerId: 'second', + seriesType: 'bar', + xScaleType: 'time', + }, + ]); + test('it uses the full date range', () => { + const { data, args } = sampleArgs(); + + 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), + seriesType: 'line', + xScaleType: 'time', + }, + ], + }} + minInterval={undefined} + /> + ); + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); + }); + + 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 component = shallow(); + + // real auto interval is 30mins = 1800000 + expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` + Object { + "max": NaN, + "min": NaN, + "minInterval": 50, + } + `); + }); + + describe('axis time', () => { + const defaultTimeLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'time', + yScaleType: 'linear', + isHistogram: true, + palette: mockPaletteOutput, + }; + test('it should disable the new time axis for a line time layer when isHistogram is set to false', () => { + const { data } = sampleArgs(); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + 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( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + 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 = { + ...defaultTimeLayer, + seriesType: 'bar', + }; + const timeLayerArgs = createArgsWithLayers([timeLayer]); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + expect(axisStyle).toBe(0); + }); + + test('it should enable the new time axis for a stacked vertical bar with break down dimension', () => { + const { data } = sampleArgs(); + const timeLayer: DataLayerConfigResult = { + ...defaultTimeLayer, + seriesType: 'bar_stacked', + }; + const timeLayerArgs = createArgsWithLayers([timeLayer]); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + expect(axisStyle).toBe(3); + }); + }); + 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', + }, + }, + }, + } + ), + }, + }, + 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 = { + ...args, + layers: [ + { + ...(args.layers[0] as DataLayerConfigResult), + seriesType: 'line', + xScaleType: 'time', + isHistogram: true, + splitAccessor: undefined, + }, + ], + }; + + test('it extends interval if data is exceeding it', () => { + const component = shallow( + + ); + + expect(component.find(Settings).prop('xDomain')).toEqual({ + // shortened to 24th midnight (elastic-charts automatically adds one min interval) + max: new Date('2021-04-24').valueOf(), + // extended to 22nd midnight because of first bucket + min: new Date('2021-04-22').valueOf(), + minInterval: 24 * 60 * 60 * 1000, + }); + }); + + test('it renders endzone component bridging gap between domain and extended domain', () => { + const component = shallow( + + ); + + expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( + expect.objectContaining({ + domainStart: new Date('2021-04-22T12:00:00.000Z').valueOf(), + domainEnd: new Date('2021-04-24T12:00:00.000Z').valueOf(), + domainMin: new Date('2021-04-22').valueOf(), + domainMax: new Date('2021-04-24').valueOf(), + }) + ); + }); + + test('should pass enabled histogram mode and min interval to endzones component', () => { + const component = shallow( + + ); + + expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( + expect.objectContaining({ + interval: 24 * 60 * 60 * 1000, + isFullBin: false, + }) + ); + }); + + test('should pass disabled histogram mode and min interval to endzones component', () => { + const component = shallow( + + ); + + expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( + expect.objectContaining({ + interval: 24 * 60 * 60 * 1000, + isFullBin: true, + }) + ); + }); + + test('it does not render endzones if disabled via settings', () => { + const component = shallow( + + ); + + expect(component.find(XyEndzones).length).toEqual(0); + }); + }); + }); + + describe('y axis extents', () => { + test('it passes custom y axis extents to elastic-charts axis spec', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: 123, + max: 456, + }); + }); + + test('it passes fit to bounds y axis extents to elastic-charts axis spec', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: true, + min: NaN, + max: NaN, + }); + }); + + test('it does not allow fit for area chart', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: NaN, + max: NaN, + }); + }); + + test('it does not allow positive lower bound for bar', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: NaN, + max: NaN, + }); + }); + + test('it does include referenceLine values when in full extent mode', () => { + const { data, args } = sampleArgsWithReferenceLine(); + + const component = shallow(); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: 0, + max: 150, + }); + }); + + test('it should ignore referenceLine values when set to custom extents', () => { + const { data, args } = sampleArgsWithReferenceLine(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: 123, + max: 456, + }); + }); + + test('it should work for negative values in referenceLines', () => { + const { data, args } = sampleArgsWithReferenceLine(-150); + + const component = shallow(); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: -150, + max: 5, + }); + }); + }); + + test('it has xDomain undefined if the x is not a time scale or a histogram', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + const xDomain = component.find(Settings).prop('xDomain'); + expect(xDomain).toEqual(undefined); + }); + + test('it uses min interval if interval is passed in and visualization is histogram', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Settings).prop('xDomain')).toEqual({ + minInterval: 101, + min: NaN, + max: NaN, + }); + }); + + test('disabled legend extra by default', () => { + const { data, 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 component = shallow( + + ); + expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); + }); + + test('shows legend extra for histogram chart', () => { + const { args } = sampleArgs(); + const component = shallow( + + ); + expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(true); + }); + + test('it renders bar', () => { + const { data, 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']); + }); + + test('it renders area', () => { + const { data, 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']); + }); + + test('it renders horizontal bar', () => { + const { data, 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']); + 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(); + + expect(component.find(BarSeries)).toHaveLength(0); + expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined(); + }); + + test('onBrushEnd returns correct context data for date histogram data', () => { + const { args } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + wrapper.find(Settings).first().prop('onBrushEnd')!({ x: [1585757732783, 1585758880838] }); + + expect(onSelectRange).toHaveBeenCalledWith({ + column: 0, + table: dateHistogramData.tables.timeLayer, + range: [1585757732783, 1585758880838], + }); + }); + + test('onBrushEnd returns correct context data for number histogram data', () => { + const { args } = sampleArgs(); + + const numberLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'numberLayer', + layerType: LayerTypes.DATA, + hide: false, + xAccessor: 'xAccessorId', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: true, + 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'), + }, + }; + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onBrushEnd')!({ x: [5, 8] }); + + expect(onSelectRange).toHaveBeenCalledWith({ + column: 0, + table: numberHistogramData.tables.numberLayer, + range: [5, 8], + }); + }); + + test('onBrushEnd is not set on non-interactive mode', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); + }); + + test('allowBrushingLastHistogramBin is true for date histogram data', () => { + const { args } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(true); + }); + + test('onElementClick returns correct context data', () => { + const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'd', + splitAccessors: {}, + seriesKeys: [2, 'd'], + }; + + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onElementClick')!([ + [geometry, series as XYChartSeriesIdentifier], + ]); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 1, + row: 1, + table: data.tables.first, + value: 5, + }, + { + column: 1, + row: 0, + table: data.tables.first, + value: 2, + }, + ], + }); + }); + + test('onElementClick returns correct context data for date histogram', () => { + const geometry: GeometryValue = { + x: 1585758120000, + y: 1, + accessor: 'y1', + mark: null, + datum: {}, + }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'yAccessorId', + splitAccessors: {}, + seriesKeys: ['yAccessorId'], + }; + + const { args } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onElementClick')!([ + [geometry, series as XYChartSeriesIdentifier], + ]); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: dateHistogramData.tables.timeLayer, + value: 1585758120000, + }, + ], + }); + }); + + test('onElementClick returns correct context data for numeric histogram', () => { + const { args } = sampleArgs(); + + const numberLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'numberLayer', + layerType: LayerTypes.DATA, + hide: false, + xAccessor: 'xAccessorId', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: true, + 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'), + }, + }; + const geometry: GeometryValue = { + x: 5, + y: 1, + accessor: 'y1', + mark: null, + datum: {}, + }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'yAccessorId', + splitAccessors: {}, + seriesKeys: ['yAccessorId'], + }; + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onElementClick')!([ + [geometry, series as XYChartSeriesIdentifier], + ]); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: numberHistogramData.tables.numberLayer, + value: 5, + }, + ], + timeFieldName: undefined, + }); + }); + + test('returns correct original data for ordinal x axis with special formatter', () => { + const geometry: GeometryValue = { x: 'BAR', y: 1, accessor: 'y1', mark: null, datum: {} }; + const series = { + key: 'spec{d}yAccessor{d}splitAccessors{b-2}', + specId: 'd', + yAccessor: 'a', + splitAccessors: {}, + seriesKeys: ['a'], + }; + + const { args, data } = sampleArgs(); + + convertSpy.mockImplementation((x) => (typeof x === 'string' ? x.toUpperCase() : x)); + + const wrapper = mountWithIntl( + + ); + + wrapper.find(Settings).first().prop('onElementClick')!([ + [geometry, series as XYChartSeriesIdentifier], + ]); + + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 3, + row: 1, + table: data.tables.first, + value: 'Bar', + }, + ], + }); + }); + + 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 wrapper = mountWithIntl( + + ); + + expect(wrapper.find(LineSeries).at(0).prop('yScaleType')).toEqual('linear_binary'); + }); + + test('allowBrushingLastHistogramBin should be fakse for ordinal data', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(false); + }); + + test('onElementClick is not triggering event on non-interactive mode', () => { + const { args, data } = sampleArgs(); + + 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 wrapper = mountWithIntl( + + ); + + expect(wrapper.find(Settings).first().prop('legendAction')).toBeUndefined(); + }); + + test('it renders stacked bar', () => { + const { data, 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); + }); + + test('it renders stacked area', () => { + const { data, 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); + }); + + test('it renders stacked horizontal bar', () => { + const { data, 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); + expect(component.find(Settings).prop('rotation')).toEqual(90); + }); + + test('it renders stacked bar empty placeholder for no results', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + + expect(component.find(BarSeries)).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'); + }); + + test('it applies histogram mode to the series for single series', () => { + const { data, args } = sampleArgs(); + const firstLayer: DataLayerConfigResult = { + ...args.layers[0], + accessors: ['b'], + seriesType: 'bar', + isHistogram: true, + } as DataLayerConfigResult; + delete firstLayer.splitAccessor; + const component = shallow( + + ); + expect(component.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 = { + ...args.layers[0], + seriesType: 'bar', + isHistogram: true, + } as DataLayerConfigResult; + 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); + }); + + test('it applies histogram mode to more than one the series for unstacked line/area chart', () => { + const { data, args } = sampleArgs(); + const firstLayer: DataLayerConfigResult = { + ...args.layers[0], + seriesType: 'line', + isHistogram: true, + } as DataLayerConfigResult; + delete firstLayer.splitAccessor; + const secondLayer: DataLayerConfigResult = { + ...args.layers[0], + seriesType: 'line', + isHistogram: true, + } as DataLayerConfigResult; + 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); + }); + + test('it applies histogram mode to the series for stacked series', () => { + const { data, 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); + }); + + test('it does not apply histogram mode for splitted series', () => { + const { data, 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); + }); + + describe('y axes', () => { + test('single axis if possible', () => { + const args = createArgsWithLayers(); + + const component = getRenderedComponent(dataWithoutFormats, args); + const axes = component.find(Axis); + expect(axes).toHaveLength(2); + }); + + test('multiple axes because of config', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + yConfig: [ + { + forAccessor: 'a', + axisMode: 'left', + }, + { + forAccessor: 'b', + axisMode: 'right', + }, + ], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, 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')); + }); + + test('multiple axes because of incompatible formatters', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['c', 'd'], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithFormats, 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')); + }); + + test('single axis despite different formatters if enforced', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['c', 'd'], + yConfig: [ + { + forAccessor: 'c', + axisMode: 'left', + }, + { + forAccessor: 'd', + axisMode: 'left', + }, + ], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const axes = component.find(Axis); + expect(axes).toHaveLength(2); + }); + }); + + describe('y series coloring', () => { + test('color is applied to chart for multiple series', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['a', 'b'], + yConfig: [ + { + forAccessor: 'a', + color: '#550000', + }, + { + forAccessor: 'b', + color: '#FFFF00', + }, + ], + }, + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['c'], + yConfig: [ + { + forAccessor: 'c', + color: '#FEECDF', + }, + ], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + expect( + (component.find(LineSeries).at(0).prop('color') as Function)!({ + yAccessor: 'a', + seriesKeys: ['a'], + }) + ).toEqual('#550000'); + expect( + (component.find(LineSeries).at(1).prop('color') as Function)!({ + yAccessor: 'b', + seriesKeys: ['b'], + }) + ).toEqual('#FFFF00'); + expect( + (component.find(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 = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + yConfig: [ + { + forAccessor: 'a', + color: '#550000', + }, + ], + }, + { + ...args.layers[0], + splitAccessor: undefined, + accessors: ['c'], + }, + ], + } as XYArgs; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + expect( + (component.find(LineSeries).at(0).prop('color') as Function)!({ + yAccessor: 'a', + seriesKeys: ['a'], + }) + ).toEqual('blue'); + expect( + (component.find(LineSeries).at(1).prop('color') as Function)!({ + yAccessor: 'c', + seriesKeys: ['c'], + }) + ).toEqual('blue'); + }); + }); + + describe('provides correct series naming', () => { + const nameFnArgs = { + seriesKeys: [], + key: '', + specId: 'a', + yAccessor: '', + splitAccessors: new Map(), + }; + + test('simplest xy chart without human-readable name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.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(''); + }); + + test('simplest xy chart with empty name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '{"a":""}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.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(''); + }); + + test('simplest xy chart with human-readable name', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: undefined, + columnToLabel: '{"a":"Column A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Column A'); + }); + + test('multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: undefined, + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + 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; + + // 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(''); + }); + + test('split series without formatting and single y accessor', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithoutFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1'); + }); + + test('split series with formatting and single y accessor', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A"}', + }, + ], + }; + + const component = getRenderedComponent(dataWithFormats, newArgs); + const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; + + convertSpy.mockReturnValueOnce('formatted'); + expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted'); + expect(getFormatSpy).toHaveBeenCalledWith({ id: 'custom' }); + }); + + test('split series without formatting with multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A","b": "Label B"}', + }, + ], + }; + + 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; + + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( + 'split1 - Label A' + ); + expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + 'split1 - Label B' + ); + }); + + test('split series with formatting with multiple y accessors', () => { + const args = createArgsWithLayers(); + const newArgs = { + ...args, + layers: [ + { + ...args.layers[0], + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A","b": "Label B"}', + }, + ], + }; + + 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; + + convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); + expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( + 'formatted1 - Label A' + ); + expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( + 'formatted2 - Label B' + ); + }); + }); + + test('it set the scale of the x axis according to the args prop', () => { + const { data, 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); + }); + + test('it set the scale of the y axis according to the args prop', () => { + const { data, 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); + }); + + test('it gets the formatter for the x axis', () => { + const { data, args } = sampleArgs(); + + 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(); + + shallow( + + ); + expect(getFormatSpy).toHaveBeenCalledWith({ + id: 'number', + params: { pattern: '0,0.000' }, + }); + }); + + test('it should pass the formatter function to the axis', () => { + const { data, args } = sampleArgs(); + + const instance = shallow(); + + const tickFormatter = instance.find(Axis).first().prop('tickFormat'); + + if (!tickFormatter) { + throw new Error('tickFormatter prop not found'); + } + + tickFormatter('I'); + + expect(convertSpy).toHaveBeenCalledWith('I'); + }); + + test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { + x: false, + yLeft: true, + yRight: true, + type: 'tickLabelsConfig', + }; + + const instance = shallow(); + + const axisStyle = instance.find(Axis).first().prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: false, + }, + }); + }); + + test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { + x: true, + yLeft: false, + yRight: false, + type: 'tickLabelsConfig', + }; + + const instance = shallow(); + + const axisStyle = instance.find(Axis).at(1).prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: false, + }, + }); + }); + + test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { + x: true, + yLeft: true, + yRight: true, + type: 'tickLabelsConfig', + }; + + const instance = shallow(); + + const axisStyle = instance.find(Axis).first().prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: true, + }, + }); + }); + + test('it should set the tickLabel orientation on the x axis', () => { + const { data, args } = sampleArgs(); + + args.labelsOrientation = { + x: -45, + yLeft: 0, + yRight: -90, + type: 'labelsOrientationConfig', + }; + + const instance = shallow(); + + const axisStyle = instance.find(Axis).first().prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + rotation: -45, + }, + }); + }); + + test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => { + const { data, args } = sampleArgs(); + + args.tickLabelsVisibilitySettings = { + x: false, + yLeft: true, + yRight: true, + type: 'tickLabelsConfig', + }; + + const instance = shallow(); + + const axisStyle = instance.find(Axis).at(1).prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + visible: true, + }, + }); + }); + + test('it should set the tickLabel orientation on the y axis', () => { + const { data, args } = sampleArgs(); + + args.labelsOrientation = { + x: -45, + yLeft: -90, + yRight: -90, + type: 'labelsOrientationConfig', + }; + + const instance = shallow(); + + const axisStyle = instance.find(Axis).at(1).prop('style'); + + expect(axisStyle).toMatchObject({ + tickLabel: { + rotation: -90, + }, + }); + }); + + 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 args: XYArgs = { + xTitle: '', + yTitle: '', + yRightTitle: '', + legend: { type: 'legendConfig', isVisible: false, position: Position.Top }, + valueLabels: 'hide', + tickLabelsVisibilitySettings: { + type: 'tickLabelsConfig', + x: true, + yLeft: true, + yRight: true, + }, + gridlinesVisibilitySettings: { + type: 'gridlinesConfig', + x: true, + yLeft: false, + yRight: false, + }, + labelsOrientation: { + type: 'labelsOrientationConfig', + x: 0, + yLeft: 0, + yRight: 0, + }, + yLeftExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + layers: [ + { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'a', + accessors: ['c'], + splitAccessor: 'b', + columnToLabel: '', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }, + { + type: 'dataLayer', + layerId: 'second', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'a', + accessors: ['c'], + splitAccessor: 'b', + columnToLabel: '', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }, + ], + }; + + const component = shallow(); + + const series = component.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 + expect(series.prop('data')).toEqual([{ a: 1, b: 5, c: 'J', d: 'Row 2' }]); + }); + + 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 args: XYArgs = { + xTitle: '', + yTitle: '', + yRightTitle: '', + legend: { type: 'legendConfig', isVisible: false, position: Position.Top }, + valueLabels: 'hide', + tickLabelsVisibilitySettings: { + type: 'tickLabelsConfig', + x: true, + yLeft: false, + yRight: false, + }, + gridlinesVisibilitySettings: { + type: 'gridlinesConfig', + x: true, + yLeft: false, + yRight: false, + }, + labelsOrientation: { + type: 'labelsOrientationConfig', + x: 0, + yLeft: 0, + yRight: 0, + }, + yLeftExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + layers: [ + { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'a', + accessors: ['c'], + splitAccessor: 'b', + columnToLabel: '', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }, + ], + }; + + const component = shallow(); + + const series = component.find(LineSeries); + + expect(series.prop('data')).toEqual([ + { a: 0, b: 2, c: 5 }, + { a: 1, b: 0, c: 7 }, + ]); + }); + + 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 args: XYArgs = { + xTitle: '', + yTitle: '', + yRightTitle: '', + legend: { type: 'legendConfig', isVisible: true, position: Position.Top }, + valueLabels: 'hide', + tickLabelsVisibilitySettings: { + type: 'tickLabelsConfig', + x: true, + yLeft: false, + yRight: false, + }, + gridlinesVisibilitySettings: { + type: 'gridlinesConfig', + x: true, + yLeft: false, + yRight: false, + }, + labelsOrientation: { + type: 'labelsOrientationConfig', + x: 0, + yLeft: 0, + yRight: 0, + }, + yLeftExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'axisExtentConfig', + }, + layers: [ + { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'a', + accessors: ['c'], + splitAccessor: 'b', + columnToLabel: '', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }, + ], + }; + + 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 component = shallow( + + ); + + expect(component.find(Settings).prop('showLegend')).toEqual(true); + }); + + test('it should populate the correct legendPosition if isInside is set', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + + expect(component.find(Settings).prop('legendPosition')).toEqual({ + vAlign: VerticalAlignment.Top, + hAlign: HorizontalAlignment.Right, + direction: LayoutDirection.Vertical, + floating: true, + floatingColumns: 1, + }); + }); + + test('it not show legend if isVisible is set to false', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + + expect(component.find(Settings).prop('showLegend')).toEqual(false); + }); + + test('it should show legend on right side', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + + expect(component.find(Settings).prop('legendPosition')).toEqual('top'); + }); + + 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 args: XYArgs = createArgsWithLayers([ + { ...sampleLayer, accessors: ['a'] }, + { ...sampleLayer, seriesType: 'bar', accessors: ['a'] }, + { ...sampleLayer, seriesType: 'area', accessors: ['a'] }, + { ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'] }, + ]); + + 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']); + }); + + test('it should apply None fitting function if not specified', () => { + const { data, args } = sampleArgs(); + + (args.layers[0] as DataLayerConfigResult).accessors = ['a']; + + const component = shallow(); + + expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None }); + }); + + test('it should apply the xTitle if is specified', () => { + const { data, args } = sampleArgs(); + + args.xTitle = 'My custom x-axis title'; + + 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(); + + args.axisTitlesVisibilitySettings = { + x: false, + yLeft: true, + yRight: true, + type: 'axisTitlesVisibilityConfig', + }; + + const component = shallow(); + + const axisStyle = component.find(Axis).first().prop('style'); + + expect(axisStyle).toMatchObject({ + axisTitle: { + visible: false, + }, + }); + }); + + test('it should show the X axis gridlines if the setting is on', () => { + const { data, args } = sampleArgs(); + + args.gridlinesVisibilitySettings = { + x: true, + yLeft: false, + yRight: false, + type: 'gridlinesConfig', + }; + + const component = shallow(); + + expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({ + visible: true, + }); + }); + + 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 }, + ], + }, + }, + dateRange: { + fromDate: new Date('2019-01-02T05:00:00.000Z'), + toDate: new Date('2019-01-03T05:00:00.000Z'), + }, + }; + const timeSampleLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: mockPaletteOutput, + }; + 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([ + { + a: 5, + b: 2, + c: false, + }, + { + a: 19, + b: 5, + c: true, + }, + ]); + }); + + describe('annotations', () => { + const sampleStyledAnnotation: EventAnnotationOutput = { + time: '2022-03-18T08:25:00.000Z', + label: 'Event 1', + icon: 'triangle', + type: 'manual_event_annotation', + color: 'red', + lineStyle: 'dashed', + lineWidth: 3, + }; + const sampleAnnotationLayers: AnnotationLayerConfigResult[] = [ + { + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + { + time: '2022-03-18T08:25:17.140Z', + label: 'Annotation', + type: 'manual_event_annotation', + }, + ], + }, + ]; + function sampleArgsWithAnnotation(annotationLayers = sampleAnnotationLayers) { + const { args } = sampleArgs(); + return { + data: dateHistogramData, + args: { + ...args, + layers: [dateHistogramLayer, ...annotationLayers], + } as XYArgs, + }; + } + test('should render basic annotation', () => { + const { data, args } = sampleArgsWithAnnotation(); + const component = mount(); + expect(component.find('LineAnnotation')).toMatchSnapshot(); + }); + test('should render simplified annotation when hide is true', () => { + const { data, args } = sampleArgsWithAnnotation(); + (args.layers[0] as AnnotationLayerConfigResult).hide = true; + const component = mount(); + expect(component.find('LineAnnotation')).toMatchSnapshot(); + }); + + test('should render grouped annotations preserving the shared styles', () => { + const { data, args } = sampleArgsWithAnnotation([ + { + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + sampleStyledAnnotation, + { ...sampleStyledAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, + { + ...sampleStyledAnnotation, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 3', + }, + ], + }, + ]); + const component = mount(); + const groupedAnnotation = component.find(LineAnnotation); + + expect(groupedAnnotation.length).toEqual(1); + // styles are passed because they are shared, dataValues & header is rounded to the interval + expect(groupedAnnotation).toMatchSnapshot(); + // renders numeric icon for grouped annotations + const marker = mount(
{groupedAnnotation.prop('marker')}
); + const numberIcon = marker.find('NumberIcon'); + expect(numberIcon.length).toEqual(1); + expect(numberIcon.text()).toEqual('3'); + + // checking tooltip + const renderLinks = mount(
{groupedAnnotation.prop('customTooltipDetails')!()}
); + expect(renderLinks.text()).toEqual( + ' 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 annotations with default styles', () => { + const { data, args } = sampleArgsWithAnnotation([ + { + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [sampleStyledAnnotation], + }, + { + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + { + ...sampleStyledAnnotation, + icon: 'square', + color: 'blue', + lineStyle: 'dotted', + lineWidth: 10, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 2', + }, + ], + }, + ]); + const component = mount(); + const groupedAnnotation = component.find(LineAnnotation); + + expect(groupedAnnotation.length).toEqual(1); + // styles are default because they are different for both annotations + expect(groupedAnnotation).toMatchSnapshot(); + }); + test('should not render hidden annotations', () => { + const { data, args } = sampleArgsWithAnnotation([ + { + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [ + sampleStyledAnnotation, + { ...sampleStyledAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, + { + ...sampleStyledAnnotation, + time: '2022-03-18T08:35:00.001Z', + label: 'Event 3', + isHidden: true, + }, + ], + }, + ]); + const component = mount(); + const annotations = component.find(LineAnnotation); + + expect(annotations.length).toEqual(2); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx similarity index 81% rename from x-pack/plugins/lens/public/xy_visualization/expression.tsx rename to src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 105b9d24bb09b..d69e8e8b645fa 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -1,14 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import './expression.scss'; - import React, { useRef } from 'react'; -import ReactDOM from 'react-dom'; import { Chart, Settings, @@ -38,57 +36,44 @@ import { LineSeriesProps, ColorVariant, } from '@elastic/charts'; -import { I18nProvider } from '@kbn/i18n-react'; -import type { - ExpressionRenderDefinition, - Datatable, - DatatableRow, - DatatableColumn, -} from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RenderMode } from 'src/plugins/expressions'; -import { ThemeServiceStart } from 'kibana/public'; -import { FieldFormat } from 'src/plugins/field_formats/common'; -import { EventAnnotationServiceType } from '../../../../../src/plugins/event_annotation/public'; -import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; -import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; -import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types'; -import type { LensMultiTable, FormatFactory } from '../../common'; -import type { - DataLayerArgs, - SeriesType, - XYChartProps, - XYLayerArgs, -} from '../../common/expressions'; -import { visualizationTypes } from './types'; -import { VisualizationContainer } from '../visualization_container'; -import { isHorizontalChart, getSeriesColor } from './state_helpers'; -import { search } from '../../../../../src/plugins/data/public'; +import type { Datatable, DatatableRow, DatatableColumn } from '../../../../expressions/public'; +import { RenderMode } from '../../../../expressions/common'; +import { FieldFormat } from '../../../../field_formats/common'; +import { EmptyPlaceholder } from '../../../../../plugins/charts/public'; +import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; +import type { SeriesType, XYChartProps } from '../../common/types'; +import { isHorizontalChart, getSeriesColor, getAnnotationsLayers, getDataLayers } from '../helpers'; +import { EventAnnotationServiceType } from '../../../../event_annotation/public'; import { ChartsPluginSetup, ChartsPluginStart, PaletteRegistry, SeriesLayer, useActiveCursor, -} from '../../../../../src/plugins/charts/public'; -import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../src/plugins/charts/common'; -import { getFitOptions } from './fitting_functions'; -import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration'; -import { getColorAssignments } from './color_assignment'; -import { getXDomain, XyEndzones } from './x_domain'; -import { getLegendAction } from './get_legend_action'; -import { ReferenceLineAnnotations } from './expression_reference_lines'; - -import { computeChartMargins, getLinesCausedPaddings } from './annotations_helpers'; - -import { Annotations, getAnnotationsGroupedByInterval } from './annotations/expression'; -import { computeOverallDataDomain } from './reference_line_helpers'; +} from '../../../../../plugins/charts/public'; +import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../plugins/charts/common'; import { + getFilteredLayers, getReferenceLayers, - getDataLayersArgs, - getAnnotationsLayersArgs, -} from './visualization_helpers'; + isDataLayer, + getFitOptions, + getAxesConfiguration, + GroupsConfiguration, + validateExtent, + computeOverallDataDomain, + 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 { Annotations, getAnnotationsGroupedByInterval } from './annotations'; + +import './xy_chart.scss'; declare global { interface Window { @@ -110,96 +95,15 @@ export type XYChartRenderProps = XYChartProps & { useLegacyTimeAxis: boolean; minInterval: number | undefined; interactive?: boolean; - onClickValue: (data: LensFilterEvent['data']) => void; - onSelectRange: (data: LensBrushEvent['data']) => void; + onClickValue: (data: FilterEvent['data']) => void; + onSelectRange: (data: BrushEvent['data']) => void; renderMode: RenderMode; syncColors: boolean; eventAnnotationService: EventAnnotationServiceType; }; -export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { - const filteredLayers = getFilteredLayers(layers, data); - if (filteredLayers.length === 0) return; - const isTimeViz = filteredLayers.every((l) => l.xScaleType === 'time'); - const xColumn = data.tables[filteredLayers[0].layerId].columns.find( - (column) => column.id === filteredLayers[0].xAccessor - ); - - if (!xColumn) return; - if (!isTimeViz) { - const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn); - if (typeof histogramInterval === 'number') { - return histogramInterval; - } else { - return undefined; - } - } - const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval; - if (!dateInterval) return; - const intervalDuration = search.aggs.parseInterval(dateInterval); - if (!intervalDuration) return; - return intervalDuration.as('milliseconds'); -} - const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; -export const getXyChartRenderer = (dependencies: { - formatFactory: FormatFactory; - chartsThemeService: ChartsPluginStart['theme']; - chartsActiveCursorService: ChartsPluginStart['activeCursor']; - paletteService: PaletteRegistry; - timeZone: string; - useLegacyTimeAxis: boolean; - kibanaTheme: ThemeServiceStart; - eventAnnotationService: EventAnnotationServiceType; -}): ExpressionRenderDefinition => ({ - name: 'lens_xy_chart_renderer', - displayName: 'XY chart', - help: i18n.translate('xpack.lens.xyChart.renderer.help', { - defaultMessage: 'X/Y chart renderer', - }), - validate: () => undefined, - reuseDomNode: true, - render: async ( - domNode: Element, - config: XYChartProps, - handlers: ILensInterpreterRenderHandlers - ) => { - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - const onClickValue = (data: LensFilterEvent['data']) => { - handlers.event({ name: 'filter', data }); - }; - const onSelectRange = (data: LensBrushEvent['data']) => { - handlers.event({ name: 'brush', data }); - }; - - ReactDOM.render( - - - - - , - domNode, - () => handlers.done() - ); - }, -}); - function getValueLabelsStyling(isHorizontal: boolean): { displayValue: RecursivePartial; } { @@ -222,18 +126,10 @@ function getValueLabelsStyling(isHorizontal: boolean): { } function getIconForSeriesType(seriesType: SeriesType): IconType { - return visualizationTypes.find((c) => c.id === seriesType)!.icon || 'empty'; + return visualizationDefinitions.find((c) => c.id === seriesType)!.icon || 'empty'; } -const MemoizedChart = React.memo(XYChart); - -export function XYChartReportable(props: XYChartRenderProps) { - return ( - - - - ); -} +export const XYChartReportable = React.memo(XYChart); export function XYChart({ data, @@ -268,26 +164,28 @@ export function XYChart({ const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); const darkMode = chartsThemeService.useDarkMode(); const filteredLayers = getFilteredLayers(layers, data); - const layersById = filteredLayers.reduce((memo, layer) => { - memo[layer.layerId] = layer; - return memo; - }, {} as Record); + const layersById = filteredLayers.reduce>( + (hashMap, layer) => { + hashMap[layer.layerId] = layer; + return hashMap; + }, + {} + ); const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, { datatables: Object.values(data.tables), }); if (filteredLayers.length === 0) { - const icon: IconType = getIconForSeriesType( - getDataLayersArgs(layers)?.[0]?.seriesType || 'bar' - ); - return ; + const icon: IconType = getIconForSeriesType(getDataLayers(layers)?.[0]?.seriesType || '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 xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params); const layersAlreadyFormatted: Record = {}; @@ -368,7 +266,7 @@ export function XYChart({ }; const referenceLineLayers = getReferenceLayers(layers); - const annotationsLayers = getAnnotationsLayersArgs(layers); + const annotationsLayers = getAnnotationsLayers(layers); const firstTable = data.tables[filteredLayers[0].layerId]; const xColumnId = firstTable.columns.find((col) => col.id === filteredLayers[0].xAccessor)?.id; @@ -427,8 +325,12 @@ export function XYChart({ const extent = axis.groupId === 'left' ? yLeftExtent : yRightExtent; const hasBarOrArea = Boolean( axis.series.some((series) => { - const seriesType = layersById[series.layer]?.seriesType; - return seriesType?.includes('bar') || seriesType?.includes('area'); + const layer = layersById[series.layer]; + if (!(layer && isDataLayer(layer))) { + return false; + } + + return layer.seriesType.includes('bar') || layer.seriesType.includes('area'); }) ); const fit = !hasBarOrArea && extent.mode === 'dataBounds'; @@ -489,7 +391,7 @@ export function XYChart({ const valueLabelsStyling = shouldShowValueLabels && valueLabels !== 'hide' && getValueLabelsStyling(shouldRotate); - const colorAssignments = getColorAssignments(getDataLayersArgs(args.layers), data, formatFactory); + const colorAssignments = getColorAssignments(getDataLayers(args.layers), data, formatFactory); const clickHandler: ElementClickListener = ([[geometry, series]]) => { // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue @@ -548,7 +450,7 @@ export function XYChart({ value: pointValue, }); } - const context: LensFilterEvent['data'] = { + const context: FilterEvent['data'] = { data: points.map((point) => ({ row: point.row, column: point.column, @@ -572,7 +474,7 @@ export function XYChart({ const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor); - const context: LensBrushEvent['data'] = { + const context: BrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex, @@ -827,7 +729,7 @@ export function XYChart({ if (!xAccessor) { rows.forEach((row) => { - row.unifiedX = i18n.translate('xpack.lens.xyChart.emptyXLabel', { + row.unifiedX = i18n.translate('expressionXY.xyChart.emptyXLabel', { defaultMessage: '(empty)', }); }); @@ -1021,8 +923,6 @@ export function XYChart({ { - const { layerId, xAccessor, accessors, splitAccessor } = layer; - return !( - !accessors.length || - !data.tables[layerId] || - data.tables[layerId].rows.length === 0 || - (xAccessor && - data.tables[layerId].rows.every((row) => 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')) - ); - }); -} - function assertNever(x: never): never { throw new Error('Unexpected series type: ' + x); } diff --git a/src/plugins/chart_expressions/expression_xy/public/definitions/index.ts b/src/plugins/chart_expressions/expression_xy/public/definitions/index.ts new file mode 100644 index 0000000000000..53af8ec397dcb --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/definitions/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 { visualizationDefinitions } from './visualizations'; diff --git a/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts b/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts new file mode 100644 index 0000000000000..9cd1540c7bbeb --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts @@ -0,0 +1,34 @@ +/* + * 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 } from '../../common/constants'; +import { + BarIcon, + LineIcon, + AreaIcon, + BarStackedIcon, + AreaStackedIcon, + BarHorizontalIcon, + BarPercentageIcon, + AreaPercentageIcon, + BarHorizontalStackedIcon, + BarHorizontalPercentageIcon, +} from '../icons'; + +export const visualizationDefinitions = [ + { id: SeriesTypes.BAR, icon: BarIcon }, + { id: SeriesTypes.BAR_STACKED, icon: BarStackedIcon }, + { id: SeriesTypes.BAR_HORIZONTAL, icon: BarHorizontalIcon }, + { id: SeriesTypes.BAR_PERCENTAGE_STACKED, icon: BarPercentageIcon }, + { id: SeriesTypes.BAR_HORIZONTAL_STACKED, icon: BarHorizontalStackedIcon }, + { id: SeriesTypes.BAR_HORIZONTAL_PERCENTAGE_STACKED, icon: BarHorizontalPercentageIcon }, + { id: SeriesTypes.LINE, icon: LineIcon }, + { id: SeriesTypes.AREA, icon: AreaIcon }, + { id: SeriesTypes.AREA_STACKED, icon: AreaStackedIcon }, + { id: SeriesTypes.AREA_PERCENTAGE_STACKED, icon: AreaPercentageIcon }, +]; diff --git a/src/plugins/chart_expressions/expression_xy/public/expression_renderers/index.ts b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/index.ts new file mode 100644 index 0000000000000..863dbb960512d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { getXyChartRenderer } from './xy_chart_renderer'; +export type { GetStartDepsFn } from './xy_chart_renderer'; 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 new file mode 100644 index 0000000000000..70acc25330b87 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/expression_renderers/xy_chart_renderer.tsx @@ -0,0 +1,90 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n-react'; +import { ThemeServiceStart } from 'kibana/public'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ChartsPluginStart, PaletteRegistry } from '../../../../charts/public'; +import { EventAnnotationServiceType } from '../../../../event_annotation/public'; +import { ExpressionRenderDefinition } from '../../../../expressions'; +import { FormatFactory } from '../../../../field_formats/common'; +import { KibanaThemeProvider } from '../../../../kibana_react/public'; +import { XYChartProps } from '../../common'; +import { calculateMinInterval } from '../helpers'; +import { BrushEvent, FilterEvent } from '../types'; + +export type GetStartDepsFn = () => Promise<{ + formatFactory: FormatFactory; + theme: ChartsPluginStart['theme']; + activeCursor: ChartsPluginStart['activeCursor']; + paletteService: PaletteRegistry; + timeZone: string; + useLegacyTimeAxis: boolean; + kibanaTheme: ThemeServiceStart; + eventAnnotationService: EventAnnotationServiceType; +}>; + +interface XyChartRendererDeps { + getStartDeps: GetStartDepsFn; +} + +export const getXyChartRenderer = ({ + getStartDeps, +}: XyChartRendererDeps): ExpressionRenderDefinition => ({ + name: 'xyVis', + displayName: 'XY chart', + help: i18n.translate('expressionXY.xyVis.renderer.help', { + defaultMessage: 'X/Y chart renderer', + }), + validate: () => undefined, + reuseDomNode: true, + render: async (domNode: Element, config: XYChartProps, handlers) => { + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + const onClickValue = (data: FilterEvent['data']) => { + handlers.event({ name: 'filter', data }); + }; + const onSelectRange = (data: BrushEvent['data']) => { + handlers.event({ name: 'brush', data }); + }; + const deps = await getStartDeps(); + + const { XYChartReportable } = await import('../components/xy_chart'); + + ReactDOM.render( + + +
+ +
{' '} +
+
, + domNode, + () => handlers.done() + ); + }, +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx similarity index 57% rename from x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx rename to src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx index ddbdfc91f4a3e..8da38af10f5d9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations_helpers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx @@ -1,18 +1,13 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ - -import './expression_reference_lines.scss'; -import React from 'react'; -import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; import { Position } from '@elastic/charts'; -import classnames from 'classnames'; -import type { IconPosition, YAxisMode, YConfig } from '../../common/expressions'; -import { hasIcon } from './xy_config_panel/shared/icon_select'; -import { annotationsIconSet } from './annotations/config_panel/icon_set'; +import type { IconPosition, YAxisMode, YConfig } from '../../common/types'; +import { hasIcon } from './icon'; export const LINES_MARKER_SIZE = 20; @@ -130,124 +125,4 @@ export function getBaseIconPlacement( return Position.Top; } -export function MarkerBody({ - label, - isHorizontal, -}: { - label: string | undefined; - isHorizontal: boolean; -}) { - if (!label) { - return null; - } - if (isHorizontal) { - return ( -
- {label} -
- ); - } - return ( -
-
- {label} -
-
- ); -} - -const isNumericalString = (value: string) => !isNaN(Number(value)); - -function NumberIcon({ number }: { number: number }) { - return ( - - - {number < 10 ? number : `9+`} - - - ); -} - -interface MarkerConfig { - axisMode?: YAxisMode; - icon?: string; - textVisibility?: boolean; - iconPosition?: IconPosition; -} - -export const AnnotationIcon = ({ - type, - rotateClassName = '', - isHorizontal, - renderedInChart, - ...rest -}: { - type: string; - rotateClassName?: string; - isHorizontal?: boolean; - renderedInChart?: boolean; -} & EuiIconProps) => { - if (isNumericalString(type)) { - return ; - } - const iconConfig = annotationsIconSet.find((i) => i.value === type); - if (!iconConfig) { - return null; - } - return ( - - ); -}; - -export function Marker({ - config, - isHorizontal, - hasReducedPadding, - label, - rotateClassName, -}: { - config: MarkerConfig; - isHorizontal: boolean; - hasReducedPadding: boolean; - label?: string; - rotateClassName?: string; -}) { - if (hasIcon(config.icon)) { - return ( - - ); - } - - // if there's some text, check whether to show it as marker, or just show some padding for the icon - if (config.textVisibility) { - if (hasReducedPadding) { - return ; - } - return ; - } - return null; -} +export const isNumericalString = (value: string) => !isNaN(Number(value)); 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 new file mode 100644 index 0000000000000..99b4648e4d556 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations_icon_set.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 new file mode 100644 index 0000000000000..201b0198087da --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts @@ -0,0 +1,334 @@ +/* + * 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 { DataLayerConfigResult } from '../../common'; +import { LayerTypes } from '../../common/constants'; +import { Datatable } from '../../../../../plugins/expressions/public'; +import { getAxesConfiguration } from './axes_configuration'; + +describe('axes_configuration', () => { + const tables: Record = { + first: { + type: 'datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + yAccessorId2: 1, + yAccessorId3: 1, + yAccessorId4: 4, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + params: { params: { id: 'date', params: { pattern: 'HH:mm' } } }, + }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'count', + }, + params: { id: 'number' }, + }, + }, + { + id: 'yAccessorId2', + name: 'Other column', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'bytes' }, + }, + }, + { + id: 'yAccessorId3', + name: 'Other column', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'currency' }, + }, + }, + { + id: 'yAccessorId4', + name: 'Other column', + meta: { + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'currency' }, + }, + }, + ], + }, + }; + + const sampleLayer: DataLayerConfigResult = { + type: 'dataLayer', + layerId: 'first', + layerType: LayerTypes.DATA, + seriesType: 'line', + xAccessor: 'c', + accessors: ['yAccessorId'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'ordinal', + yScaleType: 'linear', + isHistogram: false, + palette: { type: 'palette', name: 'default' }, + }; + + it('should map auto series to left axis', () => { + const formatFactory = jest.fn(); + const groups = getAxesConfiguration([sampleLayer], false, tables, formatFactory); + expect(groups.length).toEqual(1); + expect(groups[0].position).toEqual('left'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].series[0].layer).toEqual('first'); + }); + + 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); + expect(groups.length).toEqual(2); + expect(groups[0].position).toEqual('left'); + expect(groups[1].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId2'); + }); + + it('should map auto series to left if left and right are already filled with non-matching series', () => { + const formatFactory = jest.fn(); + const threeSeriesLayer = { + ...sampleLayer, + accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'], + }; + const groups = getAxesConfiguration([threeSeriesLayer], false, tables, formatFactory); + expect(groups.length).toEqual(2); + expect(groups[0].position).toEqual('left'); + expect(groups[1].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].series[1].accessor).toEqual('yAccessorId3'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId2'); + }); + + it('should map right series to right axis', () => { + const formatFactory = jest.fn(); + const groups = getAxesConfiguration( + [ + { + ...sampleLayer, + yConfig: [{ type: 'yConfig', forAccessor: 'yAccessorId', axisMode: 'right' }], + }, + ], + false, + tables, + formatFactory + ); + expect(groups.length).toEqual(1); + expect(groups[0].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].series[0].layer).toEqual('first'); + }); + + it('should map series with matching formatters to same axis', () => { + const formatFactory = jest.fn(); + const groups = getAxesConfiguration( + [ + { + ...sampleLayer, + accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'], + yConfig: [{ type: 'yConfig', forAccessor: 'yAccessorId', axisMode: 'right' }], + }, + ], + false, + tables, + formatFactory + ); + expect(groups.length).toEqual(2); + expect(groups[0].position).toEqual('left'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId3'); + expect(groups[0].series[1].accessor).toEqual('yAccessorId4'); + expect(groups[1].position).toEqual('right'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId'); + expect(formatFactory).toHaveBeenCalledWith({ id: 'number' }); + expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' }); + }); + + it('should create one formatter per series group', () => { + const formatFactory = jest.fn(); + getAxesConfiguration( + [ + { + ...sampleLayer, + accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'], + yConfig: [{ type: 'yConfig', forAccessor: 'yAccessorId', axisMode: 'right' }], + }, + ], + false, + tables, + formatFactory + ); + expect(formatFactory).toHaveBeenCalledTimes(2); + expect(formatFactory).toHaveBeenCalledWith({ id: 'number' }); + expect(formatFactory).toHaveBeenCalledWith({ id: 'currency' }); + }); +}); 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 new file mode 100644 index 0000000000000..16529ecd40e90 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts @@ -0,0 +1,146 @@ +/* + * 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 { FormatFactory } from '../types'; +import { AxisExtentConfig, DataLayerConfigResult } from '../../common'; +import { Datatable } from '../../../../../plugins/expressions/public'; +import type { + IFieldFormat, + SerializedFieldFormat, +} from '../../../../../plugins/field_formats/common'; + +interface FormattedMetric { + layer: string; + accessor: string; + fieldFormat: SerializedFieldFormat; +} + +export type GroupsConfiguration = Array<{ + groupId: string; + position: 'left' | 'right' | 'bottom' | 'top'; + formatter?: IFieldFormat; + series: Array<{ layer: string; accessor: string }>; +}>; + +export function isFormatterCompatible( + formatter1: SerializedFieldFormat, + formatter2: SerializedFieldFormat +) { + return formatter1.id === formatter2.id; +} + +export function groupAxesByType( + layers: DataLayerConfigResult[], + tables?: Record +) { + const series: { + auto: FormattedMetric[]; + left: FormattedMetric[]; + right: FormattedMetric[]; + bottom: FormattedMetric[]; + } = { + auto: [], + left: [], + right: [], + bottom: [], + }; + + layers?.forEach((layer) => { + const table = tables?.[layer.layerId]; + layer.accessors.forEach((accessor) => { + const mode = + layer.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') { + formatter = { + id: 'percent', + params: { + pattern: '0.[00]%', + }, + }; + } + series[mode].push({ + layer: layer.layerId, + accessor, + fieldFormat: formatter, + }); + }); + }); + + series.auto.forEach((currentSeries) => { + if ( + series.left.length === 0 || + (tables && + series.left.every((leftSeries) => + isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) + )) + ) { + series.left.push(currentSeries); + } else if ( + series.right.length === 0 || + (tables && + series.left.every((leftSeries) => + isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) + )) + ) { + series.right.push(currentSeries); + } else if (series.right.length >= series.left.length) { + series.left.push(currentSeries); + } else { + series.right.push(currentSeries); + } + }); + return series; +} + +export function getAxesConfiguration( + layers: DataLayerConfigResult[], + shouldRotate: boolean, + tables?: Record, + formatFactory?: FormatFactory +): GroupsConfiguration { + const series = groupAxesByType(layers, tables); + + const axisGroups: GroupsConfiguration = []; + + if (series.left.length > 0) { + axisGroups.push({ + groupId: 'left', + position: shouldRotate ? 'bottom' : 'left', + formatter: formatFactory?.(series.left[0].fieldFormat), + series: series.left.map(({ fieldFormat, ...currentSeries }) => currentSeries), + }); + } + + if (series.right.length > 0) { + axisGroups.push({ + groupId: 'right', + position: shouldRotate ? 'top' : 'right', + formatter: formatFactory?.(series.right[0].fieldFormat), + series: series.right.map(({ fieldFormat, ...currentSeries }) => currentSeries), + }); + } + + return axisGroups; +} + +export function validateExtent(hasBarOrArea: boolean, extent?: AxisExtentConfig) { + const inclusiveZeroError = + extent && + hasBarOrArea && + ((extent.lowerBound !== undefined && extent.lowerBound > 0) || + (extent.upperBound !== undefined && extent.upperBound) < 0); + const boundaryError = + extent && + extent.lowerBound !== undefined && + extent.upperBound !== undefined && + extent.upperBound <= extent.lowerBound; + return { inclusiveZeroError, boundaryError }; +} 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 new file mode 100644 index 0000000000000..bd13e3217c2af --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts @@ -0,0 +1,239 @@ +/* + * 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 { getColorAssignments } from './color_assignment'; +import type { DataLayerConfigResult, LensMultiTable } from '../../common'; +import type { FormatFactory } from '../types'; +import { LayerTypes } from '../../common/constants'; + +describe('color_assignment', () => { + const layers: DataLayerConfigResult[] = [ + { + type: 'dataLayer', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: true, + seriesType: 'bar', + palette: { type: 'palette', name: 'palette1' }, + layerId: '1', + layerType: LayerTypes.DATA, + splitAccessor: 'split1', + accessors: ['y1', 'y2'], + }, + { + type: 'dataLayer', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: true, + seriesType: 'bar', + palette: { type: 'palette', name: 'palette2' }, + layerId: '2', + layerType: LayerTypes.DATA, + splitAccessor: 'split2', + accessors: ['y3', 'y4'], + }, + ]; + + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + '1': { + 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) { + return x; + }, + } as unknown)) as FormatFactory; + + describe('totalSeriesCount', () => { + it('should calculate total number of series per palette', () => { + const assignments = getColorAssignments(layers, data, formatFactory); + // two y accessors, with 3 splitted series + expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3); + expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); + }); + + 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 + expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3 + 2 * 3); + expect(assignments.palette2).toBeUndefined(); + }); + + 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 + expect(assignments.palette1.totalSeriesCount).toEqual(2 * 3 + 2); + expect(assignments.palette2).toBeUndefined(); + }); + + it('should format non-primitive values and count them correctly', () => { + const complexObject = { aProp: 123 }; + const formatMock = jest.fn((x) => 'formatted'); + const assignments = getColorAssignments( + layers, + { + ...data, + tables: { + ...data.tables, + '1': { ...data.tables['1'], rows: [{ split1: complexObject }, { split1: 'abc' }] }, + }, + }, + (() => + ({ + convert: formatMock, + } as unknown)) as FormatFactory + ); + expect(assignments.palette1.totalSeriesCount).toEqual(2 * 2); + expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); + expect(formatMock).toHaveBeenCalledWith(complexObject); + }); + + it('should handle missing tables', () => { + const assignments = getColorAssignments(layers, { ...data, tables: {} }, 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 + ); + // if the split column is missing, just assume a single split + expect(assignments.palette1.totalSeriesCount).toEqual(2); + }); + }); + + describe('getRank', () => { + it('should return the correct rank for a series key', () => { + const assignments = getColorAssignments(layers, data, 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 + expect(assignments.palette2.getRank(layers[1], '1', 'y4')).toEqual(1); + }); + + 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); + // 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 + expect(assignments.palette1.getRank(newLayers[1], '2', 'y3')).toEqual(8); + }); + + it('should return the correct rank for a series without a split', () => { + const newLayers = [ + layers[0], + { ...layers[1], palette: layers[0].palette, splitAccessor: undefined }, + ]; + const assignments = getColorAssignments(newLayers, data, 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 + expect(assignments.palette1.getRank(newLayers[1], 'Metric y4', 'y4')).toEqual(7); + }); + + it('should return the correct rank for a series with a non-primitive value', () => { + const assignments = getColorAssignments( + layers, + { + ...data, + tables: { + ...data.tables, + '1': { ...data.tables['1'], rows: [{ split1: 'abc' }, { split1: { aProp: 123 } }] }, + }, + }, + (() => + ({ + convert: () => 'formatted', + } as unknown)) as FormatFactory + ); + // 3 series in front of (complex object)/y1 - abc/y1, abc/y2 + expect(assignments.palette1.getRank(layers[0], 'formatted', 'y1')).toEqual(2); + }); + + it('should handle missing tables', () => { + const assignments = getColorAssignments(layers, { ...data, tables: {} }, 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 + ); + // 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 new file mode 100644 index 0000000000000..ef0cd36e3598e --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -0,0 +1,93 @@ +/* + * 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 { uniq, mapValues } from 'lodash'; +import { euiLightVars } from '@kbn/ui-theme'; +import type { Datatable } from '../../../../expressions'; +import { FormatFactory } from '../types'; +import { isDataLayer } from './visualization'; +import { DataLayerConfigResult, XYLayerConfigResult } from '../../common'; + +const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; + +export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade; + +export type ColorAssignments = Record< + string, + { + totalSeriesCount: number; + getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string): number; + } +>; + +export function getColorAssignments( + layers: XYLayerConfigResult[], + data: { tables: Record }, + formatFactory: FormatFactory +): ColorAssignments { + 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); + }); + + return mapValues(layersPerPalette, (paletteLayers) => { + const seriesPerLayer = paletteLayers.map((layer, layerIndex) => { + 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 columnFormatter = column && formatFactory(column.meta.params); + const splits = + !column || !data.tables[layer.layerId] + ? [] + : uniq( + data.tables[layer.layerId].rows.map((row) => { + let value = row[splitAccessor]; + if (value && !isPrimitive(value)) { + value = columnFormatter?.convert(value) ?? value; + } else { + value = String(value); + } + return value; + }) + ); + return { numberOfSeries: (splits.length || 1) * layer.accessors.length, splits }; + }); + const totalSeriesCount = seriesPerLayer.reduce( + (sum, perLayer) => sum + perLayer.numberOfSeries, + 0 + ); + return { + totalSeriesCount, + getRank(sortedLayer: DataLayerConfigResult, seriesKey: string, yAccessor: string) { + const layerIndex = paletteLayers.findIndex((l) => sortedLayer.layerId === l.layerId); + const currentSeriesPerLayer = seriesPerLayer[layerIndex]; + const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); + return ( + (layerIndex === 0 + ? 0 + : seriesPerLayer + .slice(0, layerIndex) + .reduce((sum, perLayer) => sum + perLayer.numberOfSeries, 0)) + + (sortedLayer.splitAccessor && splitRank !== -1 + ? splitRank * sortedLayer.accessors.length + : 0) + + sortedLayer.accessors.indexOf(yAccessor) + ); + }, + }; + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts similarity index 74% rename from x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts rename to src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts index 63a3b308d8ae8..43d5ad9b4c19f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/fitting_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/fitting_functions.ts @@ -1,12 +1,13 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { Fit } from '@elastic/charts'; -import { EndValue, FittingFunction } from '../../common/expressions'; +import { EndValue, FittingFunction } from '../../common'; export function getFitEnum(fittingFunction?: FittingFunction | EndValue) { if (fittingFunction) { diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts new file mode 100644 index 0000000000000..57e285a07232f --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/icon.ts @@ -0,0 +1,11 @@ +/* + * 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 function hasIcon(icon: string | undefined): icon is string { + return icon != null && icon !== 'empty'; +} diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts new file mode 100644 index 0000000000000..cb0300e47ae70 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './interval'; +export * from './layers'; +export * from './state'; +export * from './visualization'; +export * from './fitting_functions'; +export * from './axes_configuration'; +export * from './reference_lines'; +export * from './icon'; +export * from './color_assignment'; +export * from './annotations_icon_set'; +export * from './annotations'; 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 new file mode 100644 index 0000000000000..0fe979b8c3fc1 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.test.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 { DataLayerConfigResult, XYChartProps } from '../../common'; +import { sampleArgs } from '../../common/__mocks__'; +import { calculateMinInterval } from './interval'; + +describe('calculateMinInterval', () => { + let xyProps: XYChartProps; + + beforeEach(() => { + xyProps = sampleArgs(); + (xyProps.args.layers[0] as DataLayerConfigResult).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 = { + type: 'date_histogram', + params: { + used_interval: '5m', + }, + }; + 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 = { + source: 'esaggs', + type: 'number', + field: 'someField', + sourceParams: { + type: 'histogram', + params: { + interval: 'auto', + used_interval: 5, + }, + }, + }; + 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 = { + type: 'date_histogram', + params: { + used_interval: '5m', + }, + }; + const result = await calculateMinInterval(xyProps); + expect(result).toEqual(undefined); + }); + + it('should return undefined if interval can not be checked', async () => { + const result = await calculateMinInterval(xyProps); + expect(result).toEqual(undefined); + }); + + it('should return undefined if date column is not found', async () => { + xyProps.data.tables.first.columns.splice(2, 1); + 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); + 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 new file mode 100644 index 0000000000000..7e15b49c311d4 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/interval.ts @@ -0,0 +1,36 @@ +/* + * 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 { search } from '../../../../data/public'; +import { XYChartProps } from '../../common'; +import { getFilteredLayers } from './layers'; +import { isDataLayer } from './visualization'; + +export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { + const filteredLayers = getFilteredLayers(layers, data); + if (filteredLayers.length === 0) return; + const isTimeViz = filteredLayers.every((l) => isDataLayer(l) && l.xScaleType === 'time'); + const xColumn = data.tables[filteredLayers[0].layerId].columns.find( + (column) => isDataLayer(filteredLayers[0]) && column.id === filteredLayers[0].xAccessor + ); + + if (!xColumn) return; + if (!isTimeViz) { + const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn); + if (typeof histogramInterval === 'number') { + return histogramInterval; + } else { + return undefined; + } + } + const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval; + if (!dateInterval) return; + const intervalDuration = search.aggs.parseInterval(dateInterval); + if (!intervalDuration) return; + return intervalDuration.as('milliseconds'); +} diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts new file mode 100644 index 0000000000000..be1701e6b6e4b --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.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 { LensMultiTable } from '../../common'; +import { DataLayerConfigResult, XYLayerConfigResult } from '../../common/types'; +import { getDataLayers } from './visualization'; + +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 || + (xAccessor && + data.tables[layerId].rows.every((row) => 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')) + ); + } + ); +} diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/reference_lines.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/reference_lines.ts new file mode 100644 index 0000000000000..35419c3a40558 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/reference_lines.ts @@ -0,0 +1,76 @@ +/* + * 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 { partition } from 'lodash'; +import { Datatable } from '../../../../expressions'; +import type { DataLayerConfigResult } from '../../common'; +import { isStackedChart } from './state'; + +export function computeOverallDataDomain( + dataLayers: DataLayerConfigResult[], + accessorIds: string[], + activeData: Record, + allowStacking: boolean = true +) { + const accessorMap = new Set(accessorIds); + let min: number | undefined; + let max: number | undefined; + const [stacked, unstacked] = partition( + dataLayers, + ({ seriesType }) => isStackedChart(seriesType) && allowStacking + ); + for (const { layerId, accessors } of unstacked) { + const table = activeData[layerId]; + if (table) { + for (const accessor of accessors) { + if (accessorMap.has(accessor)) { + for (const row of table.rows) { + const value = row[accessor]; + if (typeof value === 'number') { + // when not stacked, do not keep the 0 + max = max != null ? Math.max(value, max) : value; + min = min != null ? Math.min(value, min) : value; + } + } + } + } + } + } + // stacked can span multiple layers, so compute an overall max/min by bucket + const stackedResults: Record = {}; + for (const { layerId, accessors, xAccessor } of stacked) { + const table = activeData[layerId]; + if (table) { + for (const accessor of accessors) { + if (accessorMap.has(accessor)) { + for (const row of table.rows) { + const value = row[accessor]; + // start with a shared bucket + let bucket = 'shared'; + // but if there's an xAccessor use it as new bucket system + if (xAccessor) { + bucket = row[xAccessor]; + } + if (typeof value === 'number') { + stackedResults[bucket] = stackedResults[bucket] ?? 0; + stackedResults[bucket] += value; + } + } + } + } + } + } + + for (const value of Object.values(stackedResults)) { + // for stacked extents keep 0 in view + max = Math.max(value, max || 0, 0); + min = Math.min(value, min || 0, 0); + } + + return { min, max }; +} diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts new file mode 100644 index 0000000000000..a5cd66f178b63 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts @@ -0,0 +1,36 @@ +/* + * 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 { SeriesType, XYLayerConfigResult, YConfig } from '../../common'; +import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization'; + +export function isHorizontalSeries(seriesType: SeriesType) { + return ( + seriesType === 'bar_horizontal' || + seriesType === 'bar_horizontal_stacked' || + seriesType === 'bar_horizontal_percentage_stacked' + ); +} + +export function isStackedChart(seriesType: SeriesType) { + return seriesType.includes('stacked'); +} + +export function isHorizontalChart(layers: XYLayerConfigResult[]) { + return getDataLayers(layers).every((l) => isHorizontalSeries(l.seriesType)); +} + +export const getSeriesColor = (layer: XYLayerConfigResult, accessor: string) => { + if ((isDataLayer(layer) && layer.splitAccessor) || isAnnotationsLayer(layer)) { + return null; + } + + return ( + layer?.yConfig?.find((yConfig: YConfig) => yConfig.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 new file mode 100644 index 0000000000000..af2e80948ffdf --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + DataLayerConfigResult, + ReferenceLineLayerConfigResult, + XYLayerConfigResult, + AnnotationLayerConfigResult, +} from '../../common/types'; +import { LayerTypes } from '../../common/constants'; + +export const isDataLayer = (layer: XYLayerConfigResult): layer is DataLayerConfigResult => + layer.layerType === LayerTypes.DATA || !layer.layerType; + +export const getDataLayers = (layers: XYLayerConfigResult[]) => + (layers || []).filter((layer): layer is DataLayerConfigResult => isDataLayer(layer)); + +export const isReferenceLayer = ( + layer: XYLayerConfigResult +): layer is ReferenceLineLayerConfigResult => layer.layerType === LayerTypes.REFERENCELINE; + +export const getReferenceLayers = (layers: XYLayerConfigResult[]) => + (layers || []).filter((layer): layer is ReferenceLineLayerConfigResult => + isReferenceLayer(layer) + ); + +const isAnnotationLayerCommon = ( + layer: XYLayerConfigResult +): layer is AnnotationLayerConfigResult => layer.layerType === LayerTypes.ANNOTATIONS; + +export const isAnnotationsLayer = ( + layer: XYLayerConfigResult +): layer is AnnotationLayerConfigResult => isAnnotationLayerCommon(layer); + +export const getAnnotationsLayers = ( + layers: XYLayerConfigResult[] +): AnnotationLayerConfigResult[] => + (layers || []).filter((layer): layer is AnnotationLayerConfigResult => isAnnotationsLayer(layer)); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/area.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/area.tsx new file mode 100644 index 0000000000000..010ffaf1fb7ec --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/area.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const AreaIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/area_percentage.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/area_percentage.tsx new file mode 100644 index 0000000000000..a51e66b68ba93 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/area_percentage.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const AreaPercentageIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/area_stacked.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/area_stacked.tsx new file mode 100644 index 0000000000000..c2b9fbe926328 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/area_stacked.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const AreaStackedIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar.tsx new file mode 100644 index 0000000000000..f134d7871bfde --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal.tsx new file mode 100644 index 0000000000000..a2fb843cb095d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarHorizontalIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_percentage.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_percentage.tsx new file mode 100644 index 0000000000000..6b2bb61a246e1 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_percentage.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarHorizontalPercentageIcon = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_stacked.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_stacked.tsx new file mode 100644 index 0000000000000..b399c47d3fc7d --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar_horizontal_stacked.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarHorizontalStackedIcon = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar_percentage.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar_percentage.tsx new file mode 100644 index 0000000000000..64514cea6c012 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar_percentage.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarPercentageIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar_reference_line.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar_reference_line.tsx new file mode 100644 index 0000000000000..95bd8e2a8d0a2 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar_reference_line.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarReferenceLineIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/bar_stacked.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/bar_stacked.tsx new file mode 100644 index 0000000000000..833f3d0e816e6 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/bar_stacked.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const BarStackedIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/circle.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/circle.tsx new file mode 100644 index 0000000000000..39bbe5cde74de --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/circle.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; +import classnames from 'classnames'; + +export const CircleIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/index.ts b/src/plugins/chart_expressions/expression_xy/public/icons/index.ts new file mode 100644 index 0000000000000..4ca0b640a3d89 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { BarHorizontalPercentageIcon } from './bar_horizontal_percentage'; +export { BarHorizontalStackedIcon } from './bar_horizontal_stacked'; +export { BarReferenceLineIcon } from './bar_reference_line'; +export { AreaPercentageIcon } from './area_percentage'; +export { BarHorizontalIcon } from './bar_horizontal'; +export { BarPercentageIcon } from './bar_percentage'; +export { AreaStackedIcon } from './area_stacked'; +export { BarStackedIcon } from './bar_stacked'; +export { TriangleIcon } from './triangle'; +export { MixedXyIcon } from './mixed_xy'; +export { CircleIcon } from './circle'; +export { AreaIcon } from './area'; +export { LineIcon } from './line'; +export { BarIcon } from './bar'; diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/line.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/line.tsx new file mode 100644 index 0000000000000..6735f58b734ec --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/line.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const LineIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/mixed_xy.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/mixed_xy.tsx new file mode 100644 index 0000000000000..e16b7f6bed76f --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/mixed_xy.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const MixedXyIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/icons/triangle.tsx b/src/plugins/chart_expressions/expression_xy/public/icons/triangle.tsx new file mode 100644 index 0000000000000..8ffb8c490d9a4 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/icons/triangle.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; +import classnames from 'classnames'; + +export const TriangleIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + +); diff --git a/src/plugins/chart_expressions/expression_xy/public/index.ts b/src/plugins/chart_expressions/expression_xy/public/index.ts new file mode 100755 index 0000000000000..d9447400aa266 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { ExpressionXyPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new ExpressionXyPlugin(); +} + +export type { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types'; diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts new file mode 100755 index 0000000000000..c43e3aae11de7 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -0,0 +1,99 @@ +/* + * 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 moment from 'moment'; +import { LEGACY_TIME_AXIS } from '../../../charts/common'; +import { DataPublicPluginStart } from '../../../data/public'; +import { FieldFormatsStart } from '../../../field_formats/public'; +import { ChartsPluginStart } from '../../../charts/public'; +import { CoreSetup, CoreStart, IUiSettingsClient } from '../../../../core/public'; +import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types'; +import { + xyVisFunction, + yAxisConfigFunction, + legendConfigFunction, + gridlinesConfigFunction, + dataLayerConfigFunction, + axisExtentConfigFunction, + tickLabelsConfigFunction, + annotationLayerConfigFunction, + labelsOrientationConfigFunction, + referenceLineLayerConfigFunction, + axisTitlesVisibilityConfigFunction, +} from '../common'; +import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers'; +import { EventAnnotationPluginSetup } from '../../../event_annotation/public'; + +export interface XYPluginStartDependencies { + data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; + charts: ChartsPluginStart; + eventAnnotation: EventAnnotationPluginSetup; +} + +export function getTimeZone(uiSettings: IUiSettingsClient) { + const configuredTimeZone = uiSettings.get('dateFormat:tz'); + if (configuredTimeZone === 'Browser') { + return moment.tz.guess(); + } + + return configuredTimeZone; +} + +export class ExpressionXyPlugin { + public setup( + core: CoreSetup, + { expressions, charts }: SetupDeps + ): ExpressionXyPluginSetup { + expressions.registerFunction(yAxisConfigFunction); + expressions.registerFunction(legendConfigFunction); + expressions.registerFunction(gridlinesConfigFunction); + expressions.registerFunction(dataLayerConfigFunction); + expressions.registerFunction(axisExtentConfigFunction); + expressions.registerFunction(tickLabelsConfigFunction); + expressions.registerFunction(annotationLayerConfigFunction); + expressions.registerFunction(labelsOrientationConfigFunction); + expressions.registerFunction(referenceLineLayerConfigFunction); + expressions.registerFunction(axisTitlesVisibilityConfigFunction); + expressions.registerFunction(xyVisFunction); + + const getStartDeps: GetStartDepsFn = async () => { + const [coreStart, deps] = await core.getStartServices(); + const { + data, + fieldFormats, + eventAnnotation, + charts: { activeCursor, theme, palettes }, + } = deps; + + const paletteService = await palettes.getPalettes(); + + const { theme: kibanaTheme } = coreStart; + const eventAnnotationService = await eventAnnotation.getService(); + const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS); + + return { + data, + formatFactory: fieldFormats.deserialize, + kibanaTheme, + theme, + activeCursor, + paletteService, + useLegacyTimeAxis, + eventAnnotationService, + timeZone: getTimeZone(core.uiSettings), + }; + }; + + expressions.registerRenderer(getXyChartRenderer({ getStartDeps })); + } + + public start(core: CoreStart): ExpressionXyPluginStart {} + + public stop() {} +} diff --git a/src/plugins/chart_expressions/expression_xy/public/types.ts b/src/plugins/chart_expressions/expression_xy/public/types.ts new file mode 100755 index 0000000000000..36b8f4c13a776 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/types.ts @@ -0,0 +1,116 @@ +/* + * 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 { IconType } from '@elastic/eui'; +import { DataPublicPluginSetup } from '../../../data/public'; +import { FieldFormatsSetup } from '../../../field_formats/public'; +import { ChartsPluginSetup } from '../../../charts/public'; +import { IFieldFormat, SerializedFieldFormat } from '../../../../plugins/field_formats/common'; +import type { RangeSelectContext, ValueClickContext } from '../../../../plugins/embeddable/public'; +import { ExpressionsServiceStart, ExpressionsSetup } from '../../../expressions/public'; + +export interface SetupDeps { + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + fieldFormats: FieldFormatsSetup; + charts: ChartsPluginSetup; +} + +export interface StartDeps { + expression: ExpressionsServiceStart; +} + +export type ExpressionXyPluginSetup = void; +export type ExpressionXyPluginStart = void; + +export interface FilterEvent { + name: 'filter'; + data: ValueClickContext['data']; +} + +export interface BrushEvent { + name: 'brush'; + data: RangeSelectContext['data']; +} + +export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; + +export interface OperationDescriptor extends Operation { + hasTimeShift: boolean; +} + +export type SortingHint = 'version'; +export type FieldOnlyDataType = 'document' | 'ip' | 'histogram' | 'geo_point' | 'geo_shape'; +export type DataType = 'string' | 'number' | 'date' | 'boolean' | FieldOnlyDataType; + +export interface OperationMetadata { + // The output of this operation will have this data type + dataType: DataType; + // A bucketed operation is grouped by duplicate values, otherwise each row is + // treated as unique + isBucketed: boolean; + /** + * ordinal: Each name is a unique value, but the names are in sorted order, like "Top values" + * interval: Histogram data, like date or number histograms + * ratio: Most number data is rendered as a ratio that includes 0 + */ + scale?: 'ordinal' | 'interval' | 'ratio'; + // Extra meta-information like cardinality, color + // TODO currently it's not possible to differentiate between a field from a raw + // document and an aggregated metric which might be handy in some cases. Once we + // introduce a raw document datasource, this should be considered here. + isStaticValue?: boolean; +} + +export interface Operation extends OperationMetadata { + // User-facing label for the operation + label: string; + sortingHint?: SortingHint; +} + +/** + * A visualization type advertised to the user in the chart switcher + */ +export interface VisualizationType { + /** + * Unique id of the visualization type within the visualization defining it + */ + id: string; + /** + * Icon used in the chart switcher + */ + icon: IconType; + /** + * Visible label used in the chart switcher and above the workspace panel in collapsed state + */ + label: string; + /** + * Optional label used in visualization type search if chart switcher is expanded and for tooltips + */ + fullLabel?: string; + /** + * The group the visualization belongs to + */ + groupLabel: string; + /** + * The priority of the visualization in the list (global priority) + * Higher number means higher priority. When omitted defaults to 0 + */ + sortPriority?: number; + /** + * Indicates if visualization is in the experimental stage. + */ + showExperimentalBadge?: boolean; +} + +export interface AccessorConfig { + columnId: string; + triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible'; + color?: string; + palette?: string[] | Array<{ color: string; stop: number }>; +} diff --git a/src/plugins/chart_expressions/expression_xy/server/index.ts b/src/plugins/chart_expressions/expression_xy/server/index.ts new file mode 100755 index 0000000000000..e529b2a15fe34 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/server/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { ExpressionXyPlugin } from './plugin'; + +export function plugin() { + return new ExpressionXyPlugin(); +} + +export type { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types'; diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts new file mode 100755 index 0000000000000..ff979fd38e1c6 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -0,0 +1,47 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin } from '../../../../core/server'; + +import { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types'; +import { + xyVisFunction, + yAxisConfigFunction, + legendConfigFunction, + gridlinesConfigFunction, + dataLayerConfigFunction, + axisExtentConfigFunction, + tickLabelsConfigFunction, + annotationLayerConfigFunction, + labelsOrientationConfigFunction, + referenceLineLayerConfigFunction, + axisTitlesVisibilityConfigFunction, +} from '../common'; +import { SetupDeps } from './types'; + +export class ExpressionXyPlugin + implements Plugin +{ + public setup(core: CoreSetup, { expressions }: SetupDeps) { + expressions.registerFunction(yAxisConfigFunction); + expressions.registerFunction(legendConfigFunction); + expressions.registerFunction(gridlinesConfigFunction); + expressions.registerFunction(dataLayerConfigFunction); + expressions.registerFunction(axisExtentConfigFunction); + expressions.registerFunction(tickLabelsConfigFunction); + expressions.registerFunction(annotationLayerConfigFunction); + expressions.registerFunction(labelsOrientationConfigFunction); + expressions.registerFunction(referenceLineLayerConfigFunction); + expressions.registerFunction(axisTitlesVisibilityConfigFunction); + expressions.registerFunction(xyVisFunction); + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/src/plugins/chart_expressions/expression_xy/server/types.ts b/src/plugins/chart_expressions/expression_xy/server/types.ts new file mode 100755 index 0000000000000..738f52b739229 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/server/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { ExpressionsServerStart, ExpressionsServerSetup } from '../../../expressions/server'; + +export type ExpressionXyPluginSetup = void; +export type ExpressionXyPluginStart = void; + +export interface SetupDeps { + expressions: ExpressionsServerSetup; +} + +export interface StartDeps { + expression: ExpressionsServerStart; +} diff --git a/src/plugins/chart_expressions/expression_xy/tsconfig.json b/src/plugins/chart_expressions/expression_xy/tsconfig.json new file mode 100644 index 0000000000000..5733914869317 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "isolatedModules": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../charts/tsconfig.json" }, + { "path": "../../../core/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../data/tsconfig.json"}, + { "path": "../../ui_actions/tsconfig.json" }, + { "path": "../../field_formats/tsconfig.json"}, + { "path": "../../kibana_utils/tsconfig.json" }, + { "path": "../../event_annotation/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + ] +} diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx index 6989ea7a7a63b..e4559a0890ff2 100644 --- a/src/plugins/charts/public/static/components/empty_placeholder.tsx +++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx @@ -10,22 +10,25 @@ import React from 'react'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import './empty_placeholder.scss'; +import classnames from 'classnames'; export const EmptyPlaceholder = ({ icon, iconColor = 'subdued', message = , dataTestSubj = 'emptyPlaceholder', + className, }: { icon: IconType; iconColor?: string; message?: JSX.Element; dataTestSubj?: string; + className?: string; }) => ( <> = { - name: 'lens_xy_axisTitlesVisibilityConfig', - aliases: [], - type: 'lens_xy_axisTitlesVisibilityConfig', - help: `Configure the xy chart's axis titles appearance`, - inputTypes: ['null'], - args: { - x: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.xAxisTitle.help', { - defaultMessage: 'Specifies whether or not the title of the x-axis are visible.', - }), - }, - yLeft: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yLeftAxisTitle.help', { - defaultMessage: 'Specifies whether or not the title of the left y-axis are visible.', - }), - }, - yRight: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yRightAxisTitle.help', { - defaultMessage: 'Specifies whether or not the title of the right y-axis are visible.', - }), - }, - }, - fn: function fn(input: unknown, args: AxesSettingsConfig) { - return { - type: 'lens_xy_axisTitlesVisibilityConfig', - ...args, - }; - }, -}; - -export type AxisExtentConfigResult = AxisExtentConfig & { type: 'lens_xy_axisExtentConfig' }; - -export const axisExtentConfig: ExpressionFunctionDefinition< - 'lens_xy_axisExtentConfig', - null, - AxisExtentConfig, - AxisExtentConfigResult -> = { - name: 'lens_xy_axisExtentConfig', - aliases: [], - type: 'lens_xy_axisExtentConfig', - help: `Configure the xy chart's axis extents`, - inputTypes: ['null'], - args: { - mode: { - types: ['string'], - options: ['full', 'dataBounds', 'custom'], - help: i18n.translate('xpack.lens.xyChart.extentMode.help', { - defaultMessage: 'The extent mode', - }), - }, - lowerBound: { - types: ['number'], - help: i18n.translate('xpack.lens.xyChart.extentMode.help', { - defaultMessage: 'The extent mode', - }), - }, - upperBound: { - types: ['number'], - help: i18n.translate('xpack.lens.xyChart.extentMode.help', { - defaultMessage: 'The extent mode', - }), - }, - }, - fn: function fn(input: unknown, args: AxisExtentConfig) { - return { - type: 'lens_xy_axisExtentConfig', - ...args, - }; - }, -}; - -export const axisConfig: { [key in keyof AxisConfig]: ArgumentType } = { - title: { - types: ['string'], - help: i18n.translate('xpack.lens.xyChart.title.help', { - defaultMessage: 'The axis title', - }), - }, - hide: { - types: ['boolean'], - default: false, - help: 'Show / hide axis', - }, -}; - -export type YConfigResult = YConfig & { type: 'lens_xy_yConfig' }; - -export const yAxisConfig: ExpressionFunctionDefinition< - 'lens_xy_yConfig', - null, - YConfig, - YConfigResult -> = { - name: 'lens_xy_yConfig', - aliases: [], - type: 'lens_xy_yConfig', - help: `Configure the behavior of a xy chart's y axis metric`, - inputTypes: ['null'], - args: { - forAccessor: { - types: ['string'], - help: 'The accessor this configuration is for', - }, - axisMode: { - types: ['string'], - options: ['auto', 'left', 'right'], - help: 'The axis mode of the metric', - }, - color: { - types: ['string'], - help: 'The color of the series', - }, - lineStyle: { - types: ['string'], - options: ['solid', 'dotted', 'dashed'], - help: 'The style of the reference line', - }, - lineWidth: { - types: ['number'], - help: 'The width of the reference line', - }, - icon: { - types: ['string'], - help: 'An optional icon used for reference lines', - }, - iconPosition: { - types: ['string'], - options: ['auto', 'above', 'below', 'left', 'right'], - help: 'The placement of the icon for the reference line', - }, - textVisibility: { - types: ['boolean'], - help: 'Visibility of the label on the reference line', - }, - fill: { - types: ['string'], - options: ['none', 'above', 'below'], - help: '', - }, - }, - fn: function fn(input: unknown, args: YConfig) { - return { - type: 'lens_xy_yConfig', - ...args, - }; - }, -}; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/index.ts b/x-pack/plugins/lens/common/expressions/xy_chart/index.ts deleted file mode 100644 index 2f66c2c61a9f1..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './axis_config'; -export * from './fitting_function'; -export * from './end_value'; -export * from './grid_lines_config'; -export * from './layer_config'; -export * from './legend_config'; -export * from './series_type'; -export * from './tick_labels_config'; -export * from './xy_args'; -export * from './xy_chart'; -export * from './labels_orientation_config'; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts deleted file mode 100644 index 45b4bf31c0cdc..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/annotation_layer_config.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EventAnnotationConfig, - EventAnnotationOutput, -} from '../../../../../../../src/plugins/event_annotation/common'; -import type { ExpressionFunctionDefinition } from '../../../../../../../src/plugins/expressions/common'; -import { layerTypes } from '../../../constants'; - -export interface XYAnnotationLayerConfig { - layerId: string; - layerType: typeof layerTypes.ANNOTATIONS; - annotations: EventAnnotationConfig[]; - hide?: boolean; -} - -export interface AnnotationLayerArgs { - annotations: EventAnnotationOutput[]; - layerId: string; - layerType: typeof layerTypes.ANNOTATIONS; - hide?: boolean; -} -export type XYAnnotationLayerArgsResult = AnnotationLayerArgs & { - type: 'lens_xy_annotation_layer'; -}; -export function annotationLayerConfig(): ExpressionFunctionDefinition< - 'lens_xy_annotation_layer', - null, - AnnotationLayerArgs, - XYAnnotationLayerArgsResult -> { - return { - name: 'lens_xy_annotation_layer', - aliases: [], - type: 'lens_xy_annotation_layer', - inputTypes: ['null'], - help: 'Annotation layer in lens', - args: { - layerId: { - types: ['string'], - help: '', - }, - layerType: { types: ['string'], options: [layerTypes.ANNOTATIONS], help: '' }, - hide: { - types: ['boolean'], - default: false, - help: 'Show details', - }, - annotations: { - types: ['manual_event_annotation'], - help: '', - multi: true, - }, - }, - fn: (input, args) => { - return { - type: 'lens_xy_annotation_layer', - ...args, - }; - }, - }; -} diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/data_layer_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/data_layer_config.ts deleted file mode 100644 index 322edccba19e3..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/data_layer_config.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { layerTypes } from '../../../constants'; -import type { PaletteOutput } from '../../../../../../../src/plugins/charts/common'; -import type { ExpressionFunctionDefinition } from '../../../../../../../src/plugins/expressions/common'; -import { axisConfig, YConfig } from '../axis_config'; -import type { SeriesType } from '../series_type'; - -export interface XYDataLayerConfig { - layerId: string; - layerType: typeof layerTypes.DATA; - accessors: string[]; - seriesType: SeriesType; - xAccessor?: string; - hide?: boolean; - yConfig?: YConfig[]; - splitAccessor?: string; - palette?: PaletteOutput; -} -export interface ValidLayer extends XYDataLayerConfig { - xAccessor: NonNullable; -} - -export type DataLayerArgs = XYDataLayerConfig & { - columnToLabel?: string; // Actually a JSON key-value pair - yScaleType: 'time' | 'linear' | 'log' | 'sqrt'; - xScaleType: 'time' | 'linear' | 'ordinal'; - isHistogram: boolean; - // palette will always be set on the expression - palette: PaletteOutput; -}; - -export type DataLayerConfigResult = DataLayerArgs & { type: 'lens_xy_data_layer' }; - -export const dataLayerConfig: ExpressionFunctionDefinition< - 'lens_xy_data_layer', - null, - DataLayerArgs, - DataLayerConfigResult -> = { - name: 'lens_xy_data_layer', - aliases: [], - type: 'lens_xy_data_layer', - help: `Configure a layer in the xy chart`, - inputTypes: ['null'], - args: { - ...axisConfig, - layerId: { - types: ['string'], - help: '', - }, - xAccessor: { - types: ['string'], - help: '', - }, - layerType: { types: ['string'], options: [layerTypes.DATA], help: '' }, - seriesType: { - types: ['string'], - options: [ - 'bar', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_percentage_stacked', - 'area_percentage_stacked', - ], - help: 'The type of chart to display.', - }, - xScaleType: { - options: ['ordinal', 'linear', 'time'], - help: 'The scale type of the x axis', - default: 'ordinal', - }, - isHistogram: { - types: ['boolean'], - default: false, - help: 'Whether to layout the chart as a histogram', - }, - yScaleType: { - options: ['log', 'sqrt', 'linear', 'time'], - help: 'The scale type of the y axes', - default: 'linear', - }, - splitAccessor: { - types: ['string'], - help: 'The column to split by', - multi: false, - }, - accessors: { - types: ['string'], - help: 'The columns to display on the y axis.', - multi: true, - }, - yConfig: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - types: ['lens_xy_yConfig' as any], - help: 'Additional configuration for y axes', - multi: true, - }, - columnToLabel: { - types: ['string'], - help: 'JSON key-value pairs of column ID to label', - }, - palette: { - default: `{theme "palette" default={system_palette name="default"} }`, - help: '', - types: ['palette'], - }, - }, - fn: function fn(input: unknown, args: DataLayerArgs) { - return { - type: 'lens_xy_data_layer', - ...args, - }; - }, -}; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts deleted file mode 100644 index df27229bdb81f..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { XYDataLayerConfig } from './data_layer_config'; -import { XYReferenceLineLayerConfig } from './reference_line_layer_config'; -import { XYAnnotationLayerConfig } from './annotation_layer_config'; -export * from './data_layer_config'; -export * from './reference_line_layer_config'; -export * from './annotation_layer_config'; - -export type XYLayerConfig = - | XYDataLayerConfig - | XYReferenceLineLayerConfig - | XYAnnotationLayerConfig; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/reference_line_layer_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/reference_line_layer_config.ts deleted file mode 100644 index 6e241f8b8db65..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/layer_config/reference_line_layer_config.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ExpressionFunctionDefinition } from '../../../../../../../src/plugins/expressions/common'; -import { layerTypes } from '../../../constants'; -import { YConfig } from '../axis_config'; - -export interface XYReferenceLineLayerConfig { - layerId: string; - layerType: typeof layerTypes.REFERENCELINE; - accessors: string[]; - yConfig?: YConfig[]; -} -export type ReferenceLineLayerArgs = XYReferenceLineLayerConfig & { - columnToLabel?: string; -}; -export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { - type: 'lens_xy_referenceLine_layer'; -}; - -export const referenceLineLayerConfig: ExpressionFunctionDefinition< - 'lens_xy_referenceLine_layer', - null, - ReferenceLineLayerArgs, - ReferenceLineLayerConfigResult -> = { - name: 'lens_xy_referenceLine_layer', - aliases: [], - type: 'lens_xy_referenceLine_layer', - help: `Configure a layer in the xy chart`, - inputTypes: ['null'], - args: { - layerId: { - types: ['string'], - help: '', - }, - layerType: { types: ['string'], options: [layerTypes.REFERENCELINE], help: '' }, - accessors: { - types: ['string'], - help: 'The columns to display on the y axis.', - multi: true, - }, - yConfig: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - types: ['lens_xy_yConfig' as any], - help: 'Additional configuration for y axes', - multi: true, - }, - columnToLabel: { - types: ['string'], - help: 'JSON key-value pairs of column ID to label', - }, - }, - fn: function fn(input: unknown, args: ReferenceLineLayerArgs) { - return { - type: 'lens_xy_referenceLine_layer', - ...args, - }; - }, -}; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts deleted file mode 100644 index bced4e284aa3c..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; - -export interface LegendConfig { - /** - * Flag whether the legend should be shown. If there is just a single series, it will be hidden - */ - isVisible: boolean; - /** - * Position of the legend relative to the chart - */ - position: Position; - /** - * Flag whether the legend should be shown even with just a single series - */ - showSingleSeries?: boolean; - /** - * Flag whether the legend is inside the chart - */ - isInside?: boolean; - /** - * Horizontal Alignment of the legend when it is set inside chart - */ - horizontalAlignment?: HorizontalAlignment; - /** - * Vertical Alignment of the legend when it is set inside chart - */ - verticalAlignment?: VerticalAlignment; - /** - * Number of columns when legend is set inside chart - */ - floatingColumns?: number; - /** - * Maximum number of lines per legend item - */ - maxLines?: number; - /** - * Flag whether the legend items are truncated or not - */ - shouldTruncate?: boolean; - /** - * Exact legend width (vertical) or height (horizontal) - * Limited to max of 70% of the chart container dimension Vertical legends limited to min of 30% of computed width - */ - legendSize?: number; -} - -export type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' }; - -export const legendConfig: ExpressionFunctionDefinition< - 'lens_xy_legendConfig', - null, - LegendConfig, - LegendConfigResult -> = { - name: 'lens_xy_legendConfig', - aliases: [], - type: 'lens_xy_legendConfig', - help: `Configure the xy chart's legend`, - inputTypes: ['null'], - args: { - isVisible: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.isVisible.help', { - defaultMessage: 'Specifies whether or not the legend is visible.', - }), - }, - position: { - types: ['string'], - options: [Position.Top, Position.Right, Position.Bottom, Position.Left], - help: i18n.translate('xpack.lens.xyChart.position.help', { - defaultMessage: 'Specifies the legend position.', - }), - }, - showSingleSeries: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.showSingleSeries.help', { - defaultMessage: 'Specifies whether a legend with just a single entry should be shown', - }), - }, - isInside: { - types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.isInside.help', { - defaultMessage: 'Specifies whether a legend is inside the chart', - }), - }, - horizontalAlignment: { - types: ['string'], - options: [HorizontalAlignment.Right, HorizontalAlignment.Left], - help: i18n.translate('xpack.lens.xyChart.horizontalAlignment.help', { - defaultMessage: - 'Specifies the horizontal alignment of the legend when it is displayed inside chart.', - }), - }, - verticalAlignment: { - types: ['string'], - options: [VerticalAlignment.Top, VerticalAlignment.Bottom], - help: i18n.translate('xpack.lens.xyChart.verticalAlignment.help', { - defaultMessage: - 'Specifies the vertical alignment of the legend when it is displayed inside chart.', - }), - }, - floatingColumns: { - types: ['number'], - help: i18n.translate('xpack.lens.xyChart.floatingColumns.help', { - defaultMessage: 'Specifies the number of columns when legend is displayed inside chart.', - }), - }, - maxLines: { - types: ['number'], - help: i18n.translate('xpack.lens.xyChart.maxLines.help', { - defaultMessage: 'Specifies the number of lines per legend item.', - }), - }, - shouldTruncate: { - types: ['boolean'], - default: true, - help: i18n.translate('xpack.lens.xyChart.shouldTruncate.help', { - defaultMessage: 'Specifies whether the legend items will be truncated or not', - }), - }, - legendSize: { - types: ['number'], - help: i18n.translate('xpack.lens.xyChart.legendSize.help', { - defaultMessage: 'Specifies the legend size in pixels.', - }), - }, - }, - fn: function fn(input: unknown, args: LegendConfig) { - return { - type: 'lens_xy_legendConfig', - ...args, - }; - }, -}; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/series_type.ts b/x-pack/plugins/lens/common/expressions/xy_chart/series_type.ts deleted file mode 100644 index f9a375b8b47a1..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/series_type.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type SeriesType = - | 'bar' - | 'bar_horizontal' - | 'line' - | 'area' - | 'bar_stacked' - | 'bar_percentage_stacked' - | 'bar_horizontal_stacked' - | 'bar_horizontal_percentage_stacked' - | 'area_stacked' - | 'area_percentage_stacked'; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts b/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts deleted file mode 100644 index 4520f0c99c3e9..0000000000000 --- a/x-pack/plugins/lens/common/expressions/xy_chart/xy_args.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AxisExtentConfigResult, AxisTitlesVisibilityConfigResult } from './axis_config'; -import type { FittingFunction } from './fitting_function'; -import type { EndValue } from './end_value'; -import type { GridlinesConfigResult } from './grid_lines_config'; -import type { AnnotationLayerArgs, DataLayerArgs } from './layer_config'; -import type { LegendConfigResult } from './legend_config'; -import type { TickLabelsConfigResult } from './tick_labels_config'; -import type { LabelsOrientationConfigResult } from './labels_orientation_config'; -import type { ValueLabelConfig } from '../../types'; - -export type XYCurveType = 'LINEAR' | 'CURVE_MONOTONE_X'; -export type XYLayerArgs = DataLayerArgs | AnnotationLayerArgs; - -// 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; - valueLabels: ValueLabelConfig; - layers: XYLayerArgs[]; - fittingFunction?: FittingFunction; - endValue?: EndValue; - emphasizeFitting?: boolean; - axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; - tickLabelsVisibilitySettings?: TickLabelsConfigResult; - gridlinesVisibilitySettings?: GridlinesConfigResult; - labelsOrientation?: LabelsOrientationConfigResult; - curveType?: XYCurveType; - fillOpacity?: number; - hideEndzones?: boolean; - valuesInLegend?: boolean; - ariaLabel?: string; -} diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 18f33adf40840..a2093e78f18f0 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -25,6 +25,7 @@ "eventAnnotation" ], "optionalPlugins": [ + "expressionXY", "usageCollection", "taskManager", "globalSearch", diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index 2bf1f49ae37c8..40fee07f5d95a 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -6,26 +6,8 @@ */ import type { ExpressionsSetup } from 'src/plugins/expressions/public'; - -import { - axisExtentConfig, - yAxisConfig, - axisTitlesVisibilityConfig, -} from '../common/expressions/xy_chart/axis_config'; -import { gridlinesConfig } from '../common/expressions/xy_chart/grid_lines_config'; -import { labelsOrientationConfig } from '../common/expressions/xy_chart/labels_orientation_config'; -import { - dataLayerConfig, - referenceLineLayerConfig, - annotationLayerConfig, -} from '../common/expressions/xy_chart/layer_config'; -import { legendConfig } from '../common/expressions/xy_chart/legend_config'; -import { tickLabelsConfig } from '../common/expressions/xy_chart/tick_labels_config'; -import { xyChart } from '../common/expressions/xy_chart/xy_chart'; - import { getDatatable } from '../common/expressions/datatable/datatable'; import { datatableColumn } from '../common/expressions/datatable/datatable_column'; - import { mergeTables } from '../common/expressions/merge_tables'; import { renameColumns } from '../common/expressions/rename_columns/rename_columns'; import { formatColumn } from '../common/expressions/format_column'; @@ -41,22 +23,11 @@ export const setupExpressions = ( [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); [ - xyChart, mergeTables, counterRate, - yAxisConfig, - dataLayerConfig, - referenceLineLayerConfig, - annotationLayerConfig, formatColumn, - legendConfig, renameColumns, - gridlinesConfig, datatableColumn, - tickLabelsConfig, - axisTitlesVisibilityConfig, - axisExtentConfig, - labelsOrientationConfig, getDatatable(formatFactory), getTimeScale(getTimeZone), ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index a86ba194bf4db..5b1501410df26 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -11,7 +11,13 @@ export type { EmbeddableComponentProps, TypedLensByValueInput, } from './embeddable/embeddable_component'; -export type { XYState } from './xy_visualization/types'; +export type { + XYState, + XYReferenceLineLayerConfig, + XYLayerConfig, + XYDataLayerConfig, + XYAnnotationLayerConfig, +} from './xy_visualization/types'; export type { DatasourcePublicAPI, DataType, @@ -21,15 +27,6 @@ export type { Visualization, VisualizationSuggestion, } from './types'; -export type { - AxesSettingsConfig, - XYLayerConfig, - LegendConfig, - SeriesType, - YAxisMode, - XYCurveType, - YConfig, -} from '../common/expressions'; export type { ValueLabelConfig, PieVisualizationState, @@ -70,6 +67,42 @@ export type { FormulaPublicApi, StaticValueIndexPatternColumn, } from './indexpattern_datasource/types'; +export type { + XYArgs, + YConfig, + XYRender, + LayerType, + YAxisMode, + LineStyle, + FillStyle, + SeriesType, + YScaleType, + XScaleType, + AxisConfig, + ValidLayer, + XYCurveType, + XYChartProps, + LegendConfig, + IconPosition, + YConfigResult, + DataLayerArgs, + LensMultiTable, + ValueLabelMode, + AxisExtentMode, + FittingFunction, + AxisExtentConfig, + LegendConfigResult, + AxesSettingsConfig, + GridlinesConfigResult, + DataLayerConfigResult, + TickLabelsConfigResult, + AxisExtentConfigResult, + ReferenceLineLayerArgs, + LabelsOrientationConfig, + LabelsOrientationConfigResult, + ReferenceLineLayerConfigResult, + AxisTitlesVisibilityConfigResult, +} from '../../../../src/plugins/chart_expressions/expression_xy/common'; export type { LensEmbeddableInput } from './embeddable'; export { layerTypes } from '../common'; diff --git a/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx b/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx index 00cba7db4d17a..ff7b058873bbc 100644 --- a/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx @@ -8,8 +8,9 @@ import React, { useCallback, useState } from 'react'; import { EuiSpacer, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AxesSettingsConfig } from '../../common/expressions'; +import { AxesSettingsConfig } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { LabelMode, useDebouncedValue, VisLabel } from './'; + type AxesSettingsConfigKeys = keyof AxesSettingsConfig; export interface AxisTitleSettingsProps { 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 a1b0431f67138..27cc383834049 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 @@ -20,7 +20,7 @@ Object { true, ], }, - "function": "lens_xy_axisTitlesVisibilityConfig", + "function": "axisTitlesVisibilityConfig", "type": "function", }, ], @@ -60,7 +60,7 @@ Object { true, ], }, - "function": "lens_xy_gridlinesConfig", + "function": "gridlinesConfig", "type": "function", }, ], @@ -85,7 +85,7 @@ Object { -45, ], }, - "function": "lens_xy_labelsOrientationConfig", + "function": "labelsOrientationConfig", "type": "function", }, ], @@ -113,9 +113,6 @@ Object { "layerId": Array [ "first", ], - "layerType": Array [ - "data", - ], "seriesType": Array [ "area", ], @@ -133,7 +130,7 @@ Object { "linear", ], }, - "function": "lens_xy_data_layer", + "function": "dataLayer", "type": "function", }, ], @@ -162,7 +159,7 @@ Object { "showSingleSeries": Array [], "verticalAlignment": Array [], }, - "function": "lens_xy_legendConfig", + "function": "legendConfig", "type": "function", }, ], @@ -184,7 +181,7 @@ Object { true, ], }, - "function": "lens_xy_tickLabelsConfig", + "function": "tickLabelsConfig", "type": "function", }, ], @@ -214,7 +211,7 @@ Object { ], "upperBound": Array [], }, - "function": "lens_xy_axisExtentConfig", + "function": "axisExtentConfig", "type": "function", }, ], @@ -236,7 +233,7 @@ Object { 456, ], }, - "function": "lens_xy_axisExtentConfig", + "function": "axisExtentConfig", "type": "function", }, ], @@ -250,7 +247,7 @@ Object { "", ], }, - "function": "lens_xy_chart", + "function": "xyVis", "type": "function", }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx index 4cdb2d6c7e0b9..c27165accb81d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/config_panel/index.tsx @@ -13,9 +13,8 @@ import type { PaletteRegistry } from 'src/plugins/charts/public'; import moment from 'moment'; import { EventAnnotationConfig } from 'src/plugins/event_annotation/common/types'; import type { VisualizationDimensionEditorProps } from '../../../types'; -import { State, XYState } from '../../types'; +import { State, XYState, XYAnnotationLayerConfig } from '../../types'; import { FormatFactory } from '../../../../common'; -import { XYAnnotationLayerConfig } from '../../../../common/expressions'; import { ColorPicker } from '../../xy_config_panel/color_picker'; import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; import { isHorizontalChart } from '../../state_helpers'; diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss b/x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss deleted file mode 100644 index fc2b1204bb1d0..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/expression.scss +++ /dev/null @@ -1,37 +0,0 @@ -.lnsXyDecorationRotatedWrapper { - display: inline-block; - overflow: hidden; - line-height: 1.5; - - .lnsXyDecorationRotatedWrapper__label { - display: inline-block; - white-space: nowrap; - transform: translate(0, 100%) rotate(-90deg); - transform-origin: 0 0; - - &::after { - content: ''; - float: left; - margin-top: 100%; - } - } -} - -.lnsXyAnnotationNumberIcon { - border-radius: $euiSize; - min-width: $euiSize; - height: $euiSize; - background-color: currentColor; -} - -.lnsXyAnnotationNumberIcon__text { - font-weight: 500; - font-size: 9px; - letter-spacing: -.5px; - line-height: 11px; -} - -.lnsXyAnnotationIcon_rotate90 { - transform: rotate(45deg); - transform-origin: center; -} 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 c82228f088e47..8f18450ba5a21 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -8,14 +8,9 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { layerTypes } from '../../../common'; -import type { - XYDataLayerConfig, - XYAnnotationLayerConfig, - XYLayerConfig, -} from '../../../common/expressions'; import type { FramePublicAPI, Visualization } from '../../types'; import { isHorizontalChart } from '../state_helpers'; -import type { XYState } from '../types'; +import type { XYState, XYDataLayerConfig, XYAnnotationLayerConfig, XYLayerConfig } from '../types'; import { checkScaleOperation, getAnnotationsLayers, 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 ac3e224663ce4..a733f83b46338 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,7 +5,7 @@ * 2.0. */ -import { DataLayerArgs } from '../../common/expressions'; +import { DataLayerConfigResult } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { layerTypes } from '../../common'; import { Datatable } from '../../../../../src/plugins/expressions/public'; import { getAxesConfiguration } from './axes_configuration'; @@ -219,7 +219,8 @@ describe('axes_configuration', () => { }, }; - const sampleLayer: DataLayerArgs = { + const sampleLayer: DataLayerConfigResult = { + type: 'dataLayer', layerId: 'first', layerType: layerTypes.DATA, seriesType: 'line', @@ -271,7 +272,12 @@ describe('axes_configuration', () => { it('should map right series to right axis', () => { const formatFactory = jest.fn(); const groups = getAxesConfiguration( - [{ ...sampleLayer, yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }] }], + [ + { + ...sampleLayer, + yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }], + }, + ], false, tables, formatFactory diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts index 7adc803f31e9f..b9b2c2ae86e42 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -6,12 +6,13 @@ */ import { FormatFactory } from '../../common'; -import { AxisExtentConfig, XYDataLayerConfig } from '../../common/expressions'; +import { AxisExtentConfig } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { Datatable } from '../../../../../src/plugins/expressions/public'; import type { IFieldFormat, SerializedFieldFormat, } from '../../../../../src/plugins/field_formats/common'; +import { XYDataLayerConfig } from './types'; interface FormattedMetric { layer: string; diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts index 9b29401d72a95..a329c12b083a5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts @@ -7,15 +7,12 @@ import { getColorAssignments } from './color_assignment'; import type { FormatFactory, LensMultiTable } from '../../common'; -import type { DataLayerArgs } from '../../common/expressions'; import { layerTypes } from '../../common'; +import { XYDataLayerConfig } from './types'; describe('color_assignment', () => { - const layers: DataLayerArgs[] = [ + const layers: XYDataLayerConfig[] = [ { - yScaleType: 'linear', - xScaleType: 'linear', - isHistogram: true, seriesType: 'bar', palette: { type: 'palette', name: 'palette1' }, layerId: '1', @@ -24,9 +21,6 @@ describe('color_assignment', () => { accessors: ['y1', 'y2'], }, { - yScaleType: 'linear', - xScaleType: 'linear', - isHistogram: true, seriesType: 'bar', palette: { type: 'palette', name: 'palette2' }, layerId: '2', diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index f8d5805279a2e..ed1b7f0244c89 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -11,46 +11,40 @@ import type { Datatable } from 'src/plugins/expressions'; import { euiLightVars } from '@kbn/ui-theme'; import type { AccessorConfig, FramePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; -import { FormatFactory, LayerType } from '../../common'; -import type { XYLayerConfig } from '../../common/expressions'; -import { isReferenceLayer, isAnnotationsLayer } from './visualization_helpers'; +import { FormatFactory } from '../../common'; +import { isDataLayer, isReferenceLayer, isAnnotationsLayer } from './visualization_helpers'; import { getAnnotationsAccessorColorConfig } from './annotations/helpers'; import { getReferenceLineAccessorColorConfig } from './reference_line_helpers'; +import { XYDataLayerConfig, XYLayerConfig } from './types'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; -interface LayerColorConfig { - palette?: PaletteOutput; - splitAccessor?: string; - accessors: string[]; - layerId: string; - layerType: LayerType; -} - export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade; export type ColorAssignments = Record< string, { totalSeriesCount: number; - getRank(sortedLayer: LayerColorConfig, seriesKey: string, yAccessor: string): number; + getRank(sortedLayer: XYDataLayerConfig, seriesKey: string, yAccessor: string): number; } >; export function getColorAssignments( - layers: LayerColorConfig[], + layers: XYLayerConfig[], data: { tables: Record }, formatFactory: FormatFactory ): ColorAssignments { - const layersPerPalette: Record = {}; + const layersPerPalette: Record = {}; - layers.forEach((layer) => { - const palette = layer.palette?.name || 'default'; - if (!layersPerPalette[palette]) { - layersPerPalette[palette] = []; - } - layersPerPalette[palette].push(layer); - }); + layers + .filter((layer): layer is XYDataLayerConfig => isDataLayer(layer)) + .forEach((layer) => { + 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) => { @@ -82,7 +76,7 @@ export function getColorAssignments( ); return { totalSeriesCount, - getRank(sortedLayer: LayerColorConfig, seriesKey: string, yAccessor: string) { + getRank(sortedLayer: XYDataLayerConfig, seriesKey: string, yAccessor: string) { const layerIndex = paletteLayers.findIndex((l) => sortedLayer.layerId === l.layerId); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); @@ -125,6 +119,7 @@ export function getAccessorColorConfig( triggerIcon: 'disabled', }; } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); const rank = colorAssignments[currentPalette.name].getRank( layer, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.scss b/x-pack/plugins/lens/public/xy_visualization/expression.scss deleted file mode 100644 index 68f5e9863d2bb..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/expression.scss +++ /dev/null @@ -1,15 +0,0 @@ -.lnsXyExpression__container { - height: 100%; - width: 100%; - // the FocusTrap is adding extra divs which are making the visualization redraw twice - // with a visible glitch. This make the chart library resilient to this extra reflow - overflow-x: hidden; -} - -.lnsChart__empty { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx deleted file mode 100644 index 36e1155750ef0..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ /dev/null @@ -1,3167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - AreaSeries, - Axis, - BarSeries, - Position, - LineSeries, - Settings, - ScaleType, - GeometryValue, - XYChartSeriesIdentifier, - SeriesNameFn, - Fit, - HorizontalAlignment, - VerticalAlignment, - LayoutDirection, - LineAnnotation, -} from '@elastic/charts'; -import { PaletteOutput } from 'src/plugins/charts/public'; -import { calculateMinInterval, XYChart, XYChartRenderProps } from './expression'; -import type { LensMultiTable } from '../../common'; -import { layerTypes } from '../../common'; -import { AnnotationLayerArgs, xyChart } from '../../common/expressions'; -import { - dataLayerConfig, - legendConfig, - tickLabelsConfig, - gridlinesConfig, - XYArgs, - LegendConfig, - DataLayerArgs, - AxesSettingsConfig, - XYChartProps, - labelsOrientationConfig, - LabelsOrientationConfig, -} from '../../common/expressions'; -import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public'; -import React from 'react'; -import { mount, shallow } from 'enzyme'; -import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; -import { XyEndzones } from './x_domain'; -import { eventAnnotationServiceMock } from '../../../../../src/plugins/event_annotation/public/mocks'; -import { EventAnnotationOutput } from 'src/plugins/event_annotation/common'; - -const onClickValue = jest.fn(); -const onSelectRange = jest.fn(); - -const chartSetupContract = chartPluginMock.createSetupContract(); -const chartStartContract = chartPluginMock.createStartContract(); - -const chartsThemeService = chartSetupContract.theme; -const chartsActiveCursorService = chartStartContract.activeCursor; - -const paletteService = chartPluginMock.createPaletteRegistry(); - -const mockPaletteOutput: PaletteOutput = { - type: 'palette', - name: 'mock', - params: {}, -}; - -const dateHistogramData: LensMultiTable = { - type: 'lens_multitable', - tables: { - timeLayer: { - type: 'datatable', - rows: [ - { - xAccessorId: 1585758120000, - splitAccessorId: "Men's Clothing", - yAccessorId: 1, - }, - { - xAccessorId: 1585758360000, - splitAccessorId: "Women's Accessories", - yAccessorId: 1, - }, - { - xAccessorId: 1585758360000, - splitAccessorId: "Women's Clothing", - yAccessorId: 1, - }, - { - xAccessorId: 1585759380000, - splitAccessorId: "Men's Clothing", - yAccessorId: 1, - }, - { - xAccessorId: 1585759380000, - splitAccessorId: "Men's Shoes", - yAccessorId: 1, - }, - { - xAccessorId: 1585759380000, - splitAccessorId: "Women's Clothing", - yAccessorId: 1, - }, - { - xAccessorId: 1585760700000, - splitAccessorId: "Men's Clothing", - yAccessorId: 1, - }, - { - xAccessorId: 1585760760000, - splitAccessorId: "Men's Clothing", - yAccessorId: 1, - }, - { - xAccessorId: 1585760760000, - splitAccessorId: "Men's Shoes", - yAccessorId: 1, - }, - { - xAccessorId: 1585761120000, - splitAccessorId: "Men's Shoes", - yAccessorId: 1, - }, - ], - columns: [ - { - id: 'xAccessorId', - name: 'order_date per minute', - meta: { - type: 'date', - field: 'order_date', - source: 'esaggs', - index: 'indexPatternId', - sourceParams: { - indexPatternId: 'indexPatternId', - type: 'date_histogram', - appliedTimeRange: { - from: '2020-04-01T16:14:16.246Z', - to: '2020-04-01T17:15:41.263Z', - }, - params: { - field: 'order_date', - timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: '1m', - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, - }, - }, - params: { id: 'date', params: { pattern: 'HH:mm' } }, - }, - }, - { - id: 'splitAccessorId', - name: 'Top values of category.keyword', - meta: { - type: 'string', - field: 'category.keyword', - source: 'esaggs', - index: 'indexPatternId', - sourceParams: { - indexPatternId: 'indexPatternId', - type: 'terms', - params: { - field: 'category.keyword', - orderBy: 'yAccessorId', - order: 'desc', - size: 3, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - params: { - id: 'terms', - params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - parsedUrl: { - origin: 'http://localhost:5601', - pathname: '/jiy/app/kibana', - basePath: '/jiy', - }, - }, - }, - }, - }, - { - id: 'yAccessorId', - name: 'Count of records', - meta: { - type: 'number', - source: 'esaggs', - index: 'indexPatternId', - sourceParams: { - indexPatternId: 'indexPatternId', - params: {}, - }, - params: { id: 'number' }, - }, - }, - ], - }, - }, - dateRange: { - fromDate: new Date('2020-04-01T16:14:16.246Z'), - toDate: new Date('2020-04-01T17:15:41.263Z'), - }, -}; - -const dateHistogramLayer: DataLayerArgs = { - layerId: 'timeLayer', - layerType: layerTypes.DATA, - hide: false, - xAccessor: 'xAccessorId', - yScaleType: 'linear', - xScaleType: 'time', - isHistogram: true, - splitAccessor: 'splitAccessorId', - seriesType: 'bar_stacked', - accessors: ['yAccessorId'], - palette: mockPaletteOutput, -}; - -const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable => ({ - 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: 'date', - field: 'order_date', - sourceParams: { type: 'date-histogram', params: { interval: 'auto' } }, - params: { id: 'string' }, - }, - }, - { id: 'd', name: 'ColD', meta: { type: 'string' } }, - ], - rows, -}); - -const sampleLayer: DataLayerArgs = { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, -}; - -const createArgsWithLayers = (layers: DataLayerArgs[] = [sampleLayer]): XYArgs => ({ - xTitle: '', - yTitle: '', - yRightTitle: '', - legend: { - type: 'lens_xy_legendConfig', - isVisible: false, - position: Position.Top, - }, - valueLabels: 'hide', - valuesInLegend: false, - axisTitlesVisibilitySettings: { - type: 'lens_xy_axisTitlesVisibilityConfig', - x: true, - yLeft: true, - yRight: true, - }, - tickLabelsVisibilitySettings: { - type: 'lens_xy_tickLabelsConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'lens_xy_labelsOrientationConfig', - x: 0, - yLeft: -90, - yRight: -45, - }, - gridlinesVisibilitySettings: { - type: 'lens_xy_gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - yLeftExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - layers, -}); - -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(); - - return { data, args }; -} - -function sampleArgsWithReferenceLine(value: number = 150) { - const { data, args } = sampleArgs(); - - 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 }], - }, - }, - } 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: 'lens_xy_yConfig' }], - }, - ], - } as XYArgs, - }; -} - -describe('xy_expression', () => { - describe('configs', () => { - test('legendConfig produces the correct arguments', () => { - const args: LegendConfig = { - isVisible: true, - position: Position.Left, - }; - - const result = legendConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_legendConfig', - ...args, - }); - }); - - test('dataLayerConfig produces the correct arguments', () => { - const args: DataLayerArgs = { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - xScaleType: 'linear', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }; - - const result = dataLayerConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_data_layer', - ...args, - }); - }); - }); - - test('tickLabelsConfig produces the correct arguments', () => { - const args: AxesSettingsConfig = { - x: true, - yLeft: false, - yRight: false, - }; - - const result = tickLabelsConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_tickLabelsConfig', - ...args, - }); - }); - - test('gridlinesConfig produces the correct arguments', () => { - const args: AxesSettingsConfig = { - x: true, - yLeft: false, - yRight: false, - }; - - const result = gridlinesConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_gridlinesConfig', - ...args, - }); - }); - - test('labelsOrientationConfig produces the correct arguments', () => { - const args: LabelsOrientationConfig = { - x: 0, - yLeft: -90, - yRight: -45, - }; - - const result = labelsOrientationConfig.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'lens_xy_labelsOrientationConfig', - ...args, - }); - }); - - describe('xyChart', () => { - test('it renders with the specified data and args', () => { - const { data, args } = sampleArgs(); - const result = xyChart.fn(data, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'render', - as: 'lens_xy_chart_renderer', - value: { data, args }, - }); - }); - }); - - describe('XYChart component', () => { - let getFormatSpy: jest.Mock; - 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 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 getRenderedComponent = (data: LensMultiTable, args: XYArgs) => { - return shallow(); - }; - - beforeEach(() => { - convertSpy = jest.fn((x) => x); - getFormatSpy = jest.fn(); - getFormatSpy.mockReturnValue({ convert: convertSpy }); - - defaultProps = { - formatFactory: getFormatSpy, - timeZone: 'UTC', - renderMode: 'view', - chartsThemeService, - chartsActiveCursorService, - paletteService, - minInterval: 50, - onClickValue, - onSelectRange, - syncColors: false, - useLegacyTimeAxis: false, - eventAnnotationService: eventAnnotationServiceMock, - }; - }); - - test('it renders line', () => { - const { data, 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']); - }); - - describe('date range', () => { - const timeSampleLayer: DataLayerArgs = { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', - xScaleType: 'time', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }; - const multiLayerArgs = createArgsWithLayers([ - timeSampleLayer, - { - ...timeSampleLayer, - layerId: 'second', - seriesType: 'bar', - xScaleType: 'time', - }, - ]); - test('it uses the full date range', () => { - const { data, args } = sampleArgs(); - - 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 DataLayerArgs), seriesType: 'line', xScaleType: 'time' }, - ], - }} - minInterval={undefined} - /> - ); - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); - }); - - 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 component = shallow(); - - // real auto interval is 30mins = 1800000 - expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": NaN, - "min": NaN, - "minInterval": 50, - } - `); - }); - - describe('axis time', () => { - const defaultTimeLayer: DataLayerArgs = { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', - xScaleType: 'time', - yScaleType: 'linear', - isHistogram: true, - palette: mockPaletteOutput, - }; - test('it should disable the new time axis for a line time layer when isHistogram is set to false', () => { - const { data } = sampleArgs(); - - const instance = shallow( - - ); - - const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); - - 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( - - ); - - const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); - - 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: DataLayerArgs = { - ...defaultTimeLayer, - seriesType: 'bar', - }; - const timeLayerArgs = createArgsWithLayers([timeLayer]); - - const instance = shallow( - - ); - - const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); - - expect(axisStyle).toBe(0); - }); - - test('it should enable the new time axis for a stacked vertical bar with break down dimension', () => { - const { data } = sampleArgs(); - const timeLayer: DataLayerArgs = { - ...defaultTimeLayer, - seriesType: 'bar_stacked', - }; - const timeLayerArgs = createArgsWithLayers([timeLayer]); - - const instance = shallow( - - ); - - const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); - - expect(axisStyle).toBe(3); - }); - }); - 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', - }, - }, - }, - } - ), - }, - }, - 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 = { - ...args, - layers: [ - { - ...(args.layers[0] as DataLayerArgs), - seriesType: 'line', - xScaleType: 'time', - isHistogram: true, - splitAccessor: undefined, - }, - ], - }; - - test('it extends interval if data is exceeding it', () => { - const component = shallow( - - ); - - expect(component.find(Settings).prop('xDomain')).toEqual({ - // shortened to 24th midnight (elastic-charts automatically adds one min interval) - max: new Date('2021-04-24').valueOf(), - // extended to 22nd midnight because of first bucket - min: new Date('2021-04-22').valueOf(), - minInterval: 24 * 60 * 60 * 1000, - }); - }); - - test('it renders endzone component bridging gap between domain and extended domain', () => { - const component = shallow( - - ); - - expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( - expect.objectContaining({ - domainStart: new Date('2021-04-22T12:00:00.000Z').valueOf(), - domainEnd: new Date('2021-04-24T12:00:00.000Z').valueOf(), - domainMin: new Date('2021-04-22').valueOf(), - domainMax: new Date('2021-04-24').valueOf(), - }) - ); - }); - - test('should pass enabled histogram mode and min interval to endzones component', () => { - const component = shallow( - - ); - - expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( - expect.objectContaining({ - interval: 24 * 60 * 60 * 1000, - isFullBin: false, - }) - ); - }); - - test('should pass disabled histogram mode and min interval to endzones component', () => { - const component = shallow( - - ); - - expect(component.find(XyEndzones).dive().find('Endzones').props()).toEqual( - expect.objectContaining({ - interval: 24 * 60 * 60 * 1000, - isFullBin: true, - }) - ); - }); - - test('it does not render endzones if disabled via settings', () => { - const component = shallow( - - ); - - expect(component.find(XyEndzones).length).toEqual(0); - }); - }); - }); - - describe('y axis extents', () => { - test('it passes custom y axis extents to elastic-charts axis spec', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: false, - min: 123, - max: 456, - }); - }); - - test('it passes fit to bounds y axis extents to elastic-charts axis spec', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: true, - min: NaN, - max: NaN, - }); - }); - - test('it does not allow fit for area chart', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: false, - min: NaN, - max: NaN, - }); - }); - - test('it does not allow positive lower bound for bar', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: false, - min: NaN, - max: NaN, - }); - }); - - test('it does include referenceLine values when in full extent mode', () => { - const { data, args } = sampleArgsWithReferenceLine(); - - const component = shallow(); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: false, - min: 0, - max: 150, - }); - }); - - test('it should ignore referenceLine values when set to custom extents', () => { - const { data, args } = sampleArgsWithReferenceLine(); - - const component = shallow( - - ); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: false, - min: 123, - max: 456, - }); - }); - - test('it should work for negative values in referenceLines', () => { - const { data, args } = sampleArgsWithReferenceLine(-150); - - const component = shallow(); - expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ - fit: false, - min: -150, - max: 5, - }); - }); - }); - - test('it has xDomain undefined if the x is not a time scale or a histogram', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - const xDomain = component.find(Settings).prop('xDomain'); - expect(xDomain).toEqual(undefined); - }); - - test('it uses min interval if interval is passed in and visualization is histogram', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - expect(component.find(Settings).prop('xDomain')).toEqual({ - minInterval: 101, - min: NaN, - max: NaN, - }); - }); - - test('disabled legend extra by default', () => { - const { data, 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 component = shallow( - - ); - expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(false); - }); - - test('shows legend extra for histogram chart', () => { - const { args } = sampleArgs(); - const component = shallow( - - ); - expect(component.find(Settings).at(0).prop('showLegendExtra')).toEqual(true); - }); - - test('it renders bar', () => { - const { data, 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']); - }); - - test('it renders area', () => { - const { data, 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']); - }); - - test('it renders horizontal bar', () => { - const { data, 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']); - 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(); - - expect(component.find(BarSeries)).toHaveLength(0); - expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined(); - }); - - test('onBrushEnd returns correct context data for date histogram data', () => { - const { args } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - wrapper.find(Settings).first().prop('onBrushEnd')!({ x: [1585757732783, 1585758880838] }); - - expect(onSelectRange).toHaveBeenCalledWith({ - column: 0, - table: dateHistogramData.tables.timeLayer, - range: [1585757732783, 1585758880838], - }); - }); - - test('onBrushEnd returns correct context data for number histogram data', () => { - const { args } = sampleArgs(); - - const numberLayer: DataLayerArgs = { - layerId: 'numberLayer', - layerType: layerTypes.DATA, - hide: false, - xAccessor: 'xAccessorId', - yScaleType: 'linear', - xScaleType: 'linear', - isHistogram: true, - 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'), - }, - }; - - const wrapper = mountWithIntl( - - ); - - wrapper.find(Settings).first().prop('onBrushEnd')!({ x: [5, 8] }); - - expect(onSelectRange).toHaveBeenCalledWith({ - column: 0, - table: numberHistogramData.tables.numberLayer, - range: [5, 8], - }); - }); - - test('onBrushEnd is not set on non-interactive mode', () => { - const { args, data } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined(); - }); - - test('allowBrushingLastHistogramBin is true for date histogram data', () => { - const { args } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(true); - }); - - test('onElementClick returns correct context data', () => { - const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} }; - const series = { - key: 'spec{d}yAccessor{d}splitAccessors{b-2}', - specId: 'd', - yAccessor: 'd', - splitAccessors: {}, - seriesKeys: [2, 'd'], - }; - - const { args, data } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - - wrapper.find(Settings).first().prop('onElementClick')!([ - [geometry, series as XYChartSeriesIdentifier], - ]); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 1, - row: 1, - table: data.tables.first, - value: 5, - }, - { - column: 1, - row: 0, - table: data.tables.first, - value: 2, - }, - ], - }); - }); - - test('onElementClick returns correct context data for date histogram', () => { - const geometry: GeometryValue = { - x: 1585758120000, - y: 1, - accessor: 'y1', - mark: null, - datum: {}, - }; - const series = { - key: 'spec{d}yAccessor{d}splitAccessors{b-2}', - specId: 'd', - yAccessor: 'yAccessorId', - splitAccessors: {}, - seriesKeys: ['yAccessorId'], - }; - - const { args } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - - wrapper.find(Settings).first().prop('onElementClick')!([ - [geometry, series as XYChartSeriesIdentifier], - ]); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 0, - row: 0, - table: dateHistogramData.tables.timeLayer, - value: 1585758120000, - }, - ], - }); - }); - - test('onElementClick returns correct context data for numeric histogram', () => { - const { args } = sampleArgs(); - - const numberLayer: DataLayerArgs = { - layerId: 'numberLayer', - layerType: layerTypes.DATA, - hide: false, - xAccessor: 'xAccessorId', - yScaleType: 'linear', - xScaleType: 'linear', - isHistogram: true, - 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'), - }, - }; - const geometry: GeometryValue = { - x: 5, - y: 1, - accessor: 'y1', - mark: null, - datum: {}, - }; - const series = { - key: 'spec{d}yAccessor{d}splitAccessors{b-2}', - specId: 'd', - yAccessor: 'yAccessorId', - splitAccessors: {}, - seriesKeys: ['yAccessorId'], - }; - - const wrapper = mountWithIntl( - - ); - - wrapper.find(Settings).first().prop('onElementClick')!([ - [geometry, series as XYChartSeriesIdentifier], - ]); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 0, - row: 0, - table: numberHistogramData.tables.numberLayer, - value: 5, - }, - ], - timeFieldName: undefined, - }); - }); - - test('returns correct original data for ordinal x axis with special formatter', () => { - const geometry: GeometryValue = { x: 'BAR', y: 1, accessor: 'y1', mark: null, datum: {} }; - const series = { - key: 'spec{d}yAccessor{d}splitAccessors{b-2}', - specId: 'd', - yAccessor: 'a', - splitAccessors: {}, - seriesKeys: ['a'], - }; - - const { args, data } = sampleArgs(); - - convertSpy.mockImplementation((x) => (typeof x === 'string' ? x.toUpperCase() : x)); - - const wrapper = mountWithIntl( - - ); - - wrapper.find(Settings).first().prop('onElementClick')!([ - [geometry, series as XYChartSeriesIdentifier], - ]); - - expect(onClickValue).toHaveBeenCalledWith({ - data: [ - { - column: 3, - row: 1, - table: data.tables.first, - value: 'Bar', - }, - ], - }); - }); - - 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 wrapper = mountWithIntl( - - ); - - expect(wrapper.find(LineSeries).at(0).prop('yScaleType')).toEqual('linear_binary'); - }); - - test('allowBrushingLastHistogramBin should be fakse for ordinal data', () => { - const { args, data } = sampleArgs(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(Settings).at(0).prop('allowBrushingLastHistogramBin')).toEqual(false); - }); - - test('onElementClick is not triggering event on non-interactive mode', () => { - const { args, data } = sampleArgs(); - - 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 wrapper = mountWithIntl( - - ); - - expect(wrapper.find(Settings).first().prop('legendAction')).toBeUndefined(); - }); - - test('it renders stacked bar', () => { - const { data, 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); - }); - - test('it renders stacked area', () => { - const { data, 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); - }); - - test('it renders stacked horizontal bar', () => { - const { data, 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); - expect(component.find(Settings).prop('rotation')).toEqual(90); - }); - - test('it renders stacked bar empty placeholder for no results', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - - expect(component.find(BarSeries)).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'); - }); - - test('it applies histogram mode to the series for single series', () => { - const { data, args } = sampleArgs(); - const firstLayer: DataLayerArgs = { - ...args.layers[0], - accessors: ['b'], - seriesType: 'bar', - isHistogram: true, - } as DataLayerArgs; - delete firstLayer.splitAccessor; - const component = shallow( - - ); - expect(component.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: DataLayerArgs = { - ...args.layers[0], - seriesType: 'bar', - isHistogram: true, - } as DataLayerArgs; - 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); - }); - - test('it applies histogram mode to more than one the series for unstacked line/area chart', () => { - const { data, args } = sampleArgs(); - const firstLayer: DataLayerArgs = { - ...args.layers[0], - seriesType: 'line', - isHistogram: true, - } as DataLayerArgs; - delete firstLayer.splitAccessor; - const secondLayer: DataLayerArgs = { - ...args.layers[0], - seriesType: 'line', - isHistogram: true, - } as DataLayerArgs; - 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); - }); - - test('it applies histogram mode to the series for stacked series', () => { - const { data, 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); - }); - - test('it does not apply histogram mode for splitted series', () => { - const { data, 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); - }); - - describe('y axes', () => { - test('single axis if possible', () => { - const args = createArgsWithLayers(); - - const component = getRenderedComponent(dataWithoutFormats, args); - const axes = component.find(Axis); - expect(axes).toHaveLength(2); - }); - - test('multiple axes because of config', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a', 'b'], - yConfig: [ - { - forAccessor: 'a', - axisMode: 'left', - }, - { - forAccessor: 'b', - axisMode: 'right', - }, - ], - }, - ], - } as XYArgs; - - const component = getRenderedComponent(dataWithoutFormats, 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') - ); - }); - - test('multiple axes because of incompatible formatters', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['c', 'd'], - }, - ], - } as XYArgs; - - const component = getRenderedComponent(dataWithFormats, 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') - ); - }); - - test('single axis despite different formatters if enforced', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['c', 'd'], - yConfig: [ - { - forAccessor: 'c', - axisMode: 'left', - }, - { - forAccessor: 'd', - axisMode: 'left', - }, - ], - }, - ], - } as XYArgs; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const axes = component.find(Axis); - expect(axes).toHaveLength(2); - }); - }); - - describe('y series coloring', () => { - test('color is applied to chart for multiple series', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - splitAccessor: undefined, - accessors: ['a', 'b'], - yConfig: [ - { - forAccessor: 'a', - color: '#550000', - }, - { - forAccessor: 'b', - color: '#FFFF00', - }, - ], - }, - { - ...args.layers[0], - splitAccessor: undefined, - accessors: ['c'], - yConfig: [ - { - forAccessor: 'c', - color: '#FEECDF', - }, - ], - }, - ], - } as XYArgs; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - expect( - (component.find(LineSeries).at(0).prop('color') as Function)!({ - yAccessor: 'a', - seriesKeys: ['a'], - }) - ).toEqual('#550000'); - expect( - (component.find(LineSeries).at(1).prop('color') as Function)!({ - yAccessor: 'b', - seriesKeys: ['b'], - }) - ).toEqual('#FFFF00'); - expect( - (component.find(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 = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a'], - yConfig: [ - { - forAccessor: 'a', - color: '#550000', - }, - ], - }, - { - ...args.layers[0], - splitAccessor: undefined, - accessors: ['c'], - }, - ], - } as XYArgs; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - expect( - (component.find(LineSeries).at(0).prop('color') as Function)!({ - yAccessor: 'a', - seriesKeys: ['a'], - }) - ).toEqual('blue'); - expect( - (component.find(LineSeries).at(1).prop('color') as Function)!({ - yAccessor: 'c', - seriesKeys: ['c'], - }) - ).toEqual('blue'); - }); - }); - - describe('provides correct series naming', () => { - const nameFnArgs = { - seriesKeys: [], - key: '', - specId: 'a', - yAccessor: '', - splitAccessors: new Map(), - }; - - test('simplest xy chart without human-readable name', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a'], - splitAccessor: undefined, - columnToLabel: '', - }, - ], - }; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.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(''); - }); - - test('simplest xy chart with empty name', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a'], - splitAccessor: undefined, - columnToLabel: '{"a":""}', - }, - ], - }; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.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(''); - }); - - test('simplest xy chart with human-readable name', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a'], - splitAccessor: undefined, - columnToLabel: '{"a":"Column A"}', - }, - ], - }; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - expect(nameFn({ ...nameFnArgs, seriesKeys: ['a'] }, false)).toEqual('Column A'); - }); - - test('multiple y accessors', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a', 'b'], - splitAccessor: undefined, - columnToLabel: '{"a": "Label A"}', - }, - ], - }; - - 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; - - // 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(''); - }); - - test('split series without formatting and single y accessor', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A"}', - }, - ], - }; - - const component = getRenderedComponent(dataWithoutFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('split1'); - }); - - test('split series with formatting and single y accessor', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A"}', - }, - ], - }; - - const component = getRenderedComponent(dataWithFormats, newArgs); - const nameFn = component.find(LineSeries).prop('name') as SeriesNameFn; - - convertSpy.mockReturnValueOnce('formatted'); - expect(nameFn({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual('formatted'); - expect(getFormatSpy).toHaveBeenCalledWith({ id: 'custom' }); - }); - - test('split series without formatting with multiple y accessors', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A","b": "Label B"}', - }, - ], - }; - - 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; - - expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( - 'split1 - Label A' - ); - expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( - 'split1 - Label B' - ); - }); - - test('split series with formatting with multiple y accessors', () => { - const args = createArgsWithLayers(); - const newArgs = { - ...args, - layers: [ - { - ...args.layers[0], - accessors: ['a', 'b'], - splitAccessor: 'd', - columnToLabel: '{"a": "Label A","b": "Label B"}', - }, - ], - }; - - 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; - - convertSpy.mockReturnValueOnce('formatted1').mockReturnValueOnce('formatted2'); - expect(nameFn1({ ...nameFnArgs, seriesKeys: ['split1', 'a'] }, false)).toEqual( - 'formatted1 - Label A' - ); - expect(nameFn2({ ...nameFnArgs, seriesKeys: ['split1', 'b'] }, false)).toEqual( - 'formatted2 - Label B' - ); - }); - }); - - test('it set the scale of the x axis according to the args prop', () => { - const { data, 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); - }); - - test('it set the scale of the y axis according to the args prop', () => { - const { data, 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); - }); - - test('it gets the formatter for the x axis', () => { - const { data, args } = sampleArgs(); - - 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(); - - shallow( - - ); - expect(getFormatSpy).toHaveBeenCalledWith({ - id: 'number', - params: { pattern: '0,0.000' }, - }); - }); - - test('it should pass the formatter function to the axis', () => { - const { data, args } = sampleArgs(); - - const instance = shallow(); - - const tickFormatter = instance.find(Axis).first().prop('tickFormat'); - - if (!tickFormatter) { - throw new Error('tickFormatter prop not found'); - } - - tickFormatter('I'); - - expect(convertSpy).toHaveBeenCalledWith('I'); - }); - - test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { - const { data, args } = sampleArgs(); - - args.tickLabelsVisibilitySettings = { - x: false, - yLeft: true, - yRight: true, - type: 'lens_xy_tickLabelsConfig', - }; - - const instance = shallow(); - - const axisStyle = instance.find(Axis).first().prop('style'); - - expect(axisStyle).toMatchObject({ - tickLabel: { - visible: false, - }, - }); - }); - - test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => { - const { data, args } = sampleArgs(); - - args.tickLabelsVisibilitySettings = { - x: true, - yLeft: false, - yRight: false, - type: 'lens_xy_tickLabelsConfig', - }; - - const instance = shallow(); - - const axisStyle = instance.find(Axis).at(1).prop('style'); - - expect(axisStyle).toMatchObject({ - tickLabel: { - visible: false, - }, - }); - }); - - test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => { - const { data, args } = sampleArgs(); - - args.tickLabelsVisibilitySettings = { - x: true, - yLeft: true, - yRight: true, - type: 'lens_xy_tickLabelsConfig', - }; - - const instance = shallow(); - - const axisStyle = instance.find(Axis).first().prop('style'); - - expect(axisStyle).toMatchObject({ - tickLabel: { - visible: true, - }, - }); - }); - - test('it should set the tickLabel orientation on the x axis', () => { - const { data, args } = sampleArgs(); - - args.labelsOrientation = { - x: -45, - yLeft: 0, - yRight: -90, - type: 'lens_xy_labelsOrientationConfig', - }; - - const instance = shallow(); - - const axisStyle = instance.find(Axis).first().prop('style'); - - expect(axisStyle).toMatchObject({ - tickLabel: { - rotation: -45, - }, - }); - }); - - test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => { - const { data, args } = sampleArgs(); - - args.tickLabelsVisibilitySettings = { - x: false, - yLeft: true, - yRight: true, - type: 'lens_xy_tickLabelsConfig', - }; - - const instance = shallow(); - - const axisStyle = instance.find(Axis).at(1).prop('style'); - - expect(axisStyle).toMatchObject({ - tickLabel: { - visible: true, - }, - }); - }); - - test('it should set the tickLabel orientation on the y axis', () => { - const { data, args } = sampleArgs(); - - args.labelsOrientation = { - x: -45, - yLeft: -90, - yRight: -90, - type: 'lens_xy_labelsOrientationConfig', - }; - - const instance = shallow(); - - const axisStyle = instance.find(Axis).at(1).prop('style'); - - expect(axisStyle).toMatchObject({ - tickLabel: { - rotation: -90, - }, - }); - }); - - 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 args: XYArgs = { - xTitle: '', - yTitle: '', - yRightTitle: '', - legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top }, - valueLabels: 'hide', - tickLabelsVisibilitySettings: { - type: 'lens_xy_tickLabelsConfig', - x: true, - yLeft: true, - yRight: true, - }, - gridlinesVisibilitySettings: { - type: 'lens_xy_gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'lens_xy_labelsOrientationConfig', - x: 0, - yLeft: 0, - yRight: 0, - }, - yLeftExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - layers: [ - { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'a', - accessors: ['c'], - splitAccessor: 'b', - columnToLabel: '', - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }, - { - layerId: 'second', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'a', - accessors: ['c'], - splitAccessor: 'b', - columnToLabel: '', - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }, - ], - }; - - const component = shallow(); - - const series = component.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 - expect(series.prop('data')).toEqual([{ a: 1, b: 5, c: 'J', d: 'Row 2' }]); - }); - - 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 args: XYArgs = { - xTitle: '', - yTitle: '', - yRightTitle: '', - legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top }, - valueLabels: 'hide', - tickLabelsVisibilitySettings: { - type: 'lens_xy_tickLabelsConfig', - x: true, - yLeft: false, - yRight: false, - }, - gridlinesVisibilitySettings: { - type: 'lens_xy_gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'lens_xy_labelsOrientationConfig', - x: 0, - yLeft: 0, - yRight: 0, - }, - yLeftExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - layers: [ - { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'a', - accessors: ['c'], - splitAccessor: 'b', - columnToLabel: '', - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }, - ], - }; - - const component = shallow(); - - const series = component.find(LineSeries); - - expect(series.prop('data')).toEqual([ - { a: 0, b: 2, c: 5 }, - { a: 1, b: 0, c: 7 }, - ]); - }); - - 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 args: XYArgs = { - xTitle: '', - yTitle: '', - yRightTitle: '', - legend: { type: 'lens_xy_legendConfig', isVisible: true, position: Position.Top }, - valueLabels: 'hide', - tickLabelsVisibilitySettings: { - type: 'lens_xy_tickLabelsConfig', - x: true, - yLeft: false, - yRight: false, - }, - gridlinesVisibilitySettings: { - type: 'lens_xy_gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'lens_xy_labelsOrientationConfig', - x: 0, - yLeft: 0, - yRight: 0, - }, - yLeftExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'lens_xy_axisExtentConfig', - }, - layers: [ - { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'a', - accessors: ['c'], - splitAccessor: 'b', - columnToLabel: '', - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }, - ], - }; - - 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 component = shallow( - - ); - - expect(component.find(Settings).prop('showLegend')).toEqual(true); - }); - - test('it should populate the correct legendPosition if isInside is set', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - - expect(component.find(Settings).prop('legendPosition')).toEqual({ - vAlign: VerticalAlignment.Top, - hAlign: HorizontalAlignment.Right, - direction: LayoutDirection.Vertical, - floating: true, - floatingColumns: 1, - }); - }); - - test('it not show legend if isVisible is set to false', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it should show legend on right side', () => { - const { data, args } = sampleArgs(); - - const component = shallow( - - ); - - expect(component.find(Settings).prop('legendPosition')).toEqual('top'); - }); - - 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 args: XYArgs = createArgsWithLayers([ - { ...sampleLayer, accessors: ['a'] }, - { ...sampleLayer, seriesType: 'bar', accessors: ['a'] }, - { ...sampleLayer, seriesType: 'area', accessors: ['a'] }, - { ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'] }, - ]); - - 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']); - }); - - test('it should apply None fitting function if not specified', () => { - const { data, args } = sampleArgs(); - - (args.layers[0] as DataLayerArgs).accessors = ['a']; - - const component = shallow( - - ); - - expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None }); - }); - - test('it should apply the xTitle if is specified', () => { - const { data, args } = sampleArgs(); - - args.xTitle = 'My custom x-axis title'; - - 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(); - - args.axisTitlesVisibilitySettings = { - x: false, - yLeft: true, - yRight: true, - type: 'lens_xy_axisTitlesVisibilityConfig', - }; - - const component = shallow( - - ); - - const axisStyle = component.find(Axis).first().prop('style'); - - expect(axisStyle).toMatchObject({ - axisTitle: { - visible: false, - }, - }); - }); - - test('it should show the X axis gridlines if the setting is on', () => { - const { data, args } = sampleArgs(); - - args.gridlinesVisibilitySettings = { - x: true, - yLeft: false, - yRight: false, - type: 'lens_xy_gridlinesConfig', - }; - - const component = shallow( - - ); - - expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({ - visible: true, - }); - }); - - 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 }, - ], - }, - }, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), - }, - }; - const timeSampleLayer: DataLayerArgs = { - layerId: 'first', - layerType: layerTypes.DATA, - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - xScaleType: 'ordinal', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }; - 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([ - { - a: 5, - b: 2, - c: false, - }, - { - a: 19, - b: 5, - c: true, - }, - ]); - }); - - describe('annotations', () => { - const sampleStyledAnnotation: EventAnnotationOutput = { - time: '2022-03-18T08:25:00.000Z', - label: 'Event 1', - icon: 'triangle', - type: 'manual_event_annotation', - color: 'red', - lineStyle: 'dashed', - lineWidth: 3, - }; - const sampleAnnotationLayers: AnnotationLayerArgs[] = [ - { - layerType: layerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - { - time: '2022-03-18T08:25:17.140Z', - label: 'Annotation', - type: 'manual_event_annotation', - }, - ], - }, - ]; - function sampleArgsWithAnnotation(annotationLayers = sampleAnnotationLayers) { - const { args } = sampleArgs(); - return { - data: dateHistogramData, - args: { - ...args, - layers: [dateHistogramLayer, ...annotationLayers], - } as XYArgs, - }; - } - test('should render basic annotation', () => { - const { data, args } = sampleArgsWithAnnotation(); - const component = mount(); - expect(component.find('LineAnnotation')).toMatchSnapshot(); - }); - test('should render simplified annotation when hide is true', () => { - const { data, args } = sampleArgsWithAnnotation(); - args.layers[0].hide = true; - const component = mount(); - expect(component.find('LineAnnotation')).toMatchSnapshot(); - }); - - test('should render grouped annotations preserving the shared styles', () => { - const { data, args } = sampleArgsWithAnnotation([ - { - layerType: layerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - sampleStyledAnnotation, - { ...sampleStyledAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, - { - ...sampleStyledAnnotation, - time: '2022-03-18T08:25:00.001Z', - label: 'Event 3', - }, - ], - }, - ]); - const component = mount(); - const groupedAnnotation = component.find(LineAnnotation); - - expect(groupedAnnotation.length).toEqual(1); - // styles are passed because they are shared, dataValues & header is rounded to the interval - expect(groupedAnnotation).toMatchSnapshot(); - // renders numeric icon for grouped annotations - const marker = mount(
{groupedAnnotation.prop('marker')}
); - const numberIcon = marker.find('NumberIcon'); - expect(numberIcon.length).toEqual(1); - expect(numberIcon.text()).toEqual('3'); - - // checking tooltip - const renderLinks = mount(
{groupedAnnotation.prop('customTooltipDetails')!()}
); - expect(renderLinks.text()).toEqual( - ' 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 annotations with default styles', () => { - const { data, args } = sampleArgsWithAnnotation([ - { - layerType: layerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [sampleStyledAnnotation], - }, - { - layerType: layerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - { - ...sampleStyledAnnotation, - icon: 'square', - color: 'blue', - lineStyle: 'dotted', - lineWidth: 10, - time: '2022-03-18T08:25:00.001Z', - label: 'Event 2', - }, - ], - }, - ]); - const component = mount(); - const groupedAnnotation = component.find(LineAnnotation); - - expect(groupedAnnotation.length).toEqual(1); - // styles are default because they are different for both annotations - expect(groupedAnnotation).toMatchSnapshot(); - }); - test('should not render hidden annotations', () => { - const { data, args } = sampleArgsWithAnnotation([ - { - layerType: layerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - sampleStyledAnnotation, - { ...sampleStyledAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, - { - ...sampleStyledAnnotation, - time: '2022-03-18T08:35:00.001Z', - label: 'Event 3', - isHidden: true, - }, - ], - }, - ]); - const component = mount(); - const annotations = component.find(LineAnnotation); - - expect(annotations.length).toEqual(2); - }); - }); - }); - - describe('calculateMinInterval', () => { - let xyProps: XYChartProps; - - beforeEach(() => { - xyProps = sampleArgs(); - (xyProps.args.layers[0] as DataLayerArgs).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 = { - type: 'date_histogram', - params: { - used_interval: '5m', - }, - }; - 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 DataLayerArgs).xScaleType = 'linear'; - xyProps.data.tables.first.columns[2].meta = { - source: 'esaggs', - type: 'number', - field: 'someField', - sourceParams: { - type: 'histogram', - params: { - interval: 'auto', - used_interval: 5, - }, - }, - }; - 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 = { - type: 'date_histogram', - params: { - used_interval: '5m', - }, - }; - const result = await calculateMinInterval(xyProps); - expect(result).toEqual(undefined); - }); - - it('should return undefined if interval can not be checked', async () => { - const result = await calculateMinInterval(xyProps); - expect(result).toEqual(undefined); - }); - - it('should return undefined if date column is not found', async () => { - xyProps.data.tables.first.columns.splice(2, 1); - 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 DataLayerArgs).xScaleType = 'ordinal'; - xyProps.data.tables.first.columns.splice(2, 1); - const result = await calculateMinInterval(xyProps); - expect(result).toEqual(undefined); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx deleted file mode 100644 index 7817db573e419..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import './expression_reference_lines.scss'; -import React from 'react'; -import { groupBy } from 'lodash'; -import { RectAnnotation, AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts'; -import type { PaletteRegistry } from 'src/plugins/charts/public'; -import type { FieldFormat } from 'src/plugins/field_formats/common'; -import type { ReferenceLineLayerArgs } from '../../common/expressions'; -import type { LensMultiTable } from '../../common/types'; -import { defaultReferenceLineColor } from './color_assignment'; -import { - MarkerBody, - Marker, - LINES_MARKER_SIZE, - mapVerticalToHorizontalPlacement, - getBaseIconPlacement, -} from './annotations_helpers'; - -export interface ReferenceLineAnnotationsProps { - layers: ReferenceLineLayerArgs[]; - data: LensMultiTable; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - paletteService: PaletteRegistry; - syncColors: boolean; - axesMap: Record<'left' | 'right', boolean>; - isHorizontal: boolean; - paddingMap: Partial>; -} - -export const ReferenceLineAnnotations = ({ - layers, - data, - formatters, - paletteService, - syncColors, - axesMap, - isHorizontal, - paddingMap, -}: ReferenceLineAnnotationsProps) => { - return ( - <> - {layers.flatMap((layer) => { - if (!layer.yConfig) { - return []; - } - const { columnToLabel, yConfig: yConfigs, layerId } = layer; - const columnToLabelMap: Record = columnToLabel - ? JSON.parse(columnToLabel) - : {}; - const table = data.tables[layerId]; - - const row = table.rows[0]; - - const yConfigByValue = yConfigs.sort( - ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] - ); - - const groupedByDirection = groupBy(yConfigByValue, 'fill'); - if (groupedByDirection.below) { - groupedByDirection.below.reverse(); - } - - return yConfigByValue.flatMap((yConfig, i) => { - // Find the formatter for the given axis - const groupId = - yConfig.axisMode === 'bottom' - ? undefined - : yConfig.axisMode === 'right' - ? 'right' - : 'left'; - - const formatter = formatters[groupId || 'bottom']; - - // get the position for vertical chart - const markerPositionVertical = getBaseIconPlacement( - yConfig.iconPosition, - axesMap, - yConfig.axisMode - ); - // the padding map is built for vertical chart - const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; - - const props = { - groupId, - marker: ( - - ), - markerBody: ( - - ), - // rotate the position if required - markerPosition: isHorizontal - ? mapVerticalToHorizontalPlacement(markerPositionVertical) - : markerPositionVertical, - }; - const annotations = []; - - const dashStyle = - yConfig.lineStyle === 'dashed' - ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] - : yConfig.lineStyle === 'dotted' - ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] - : undefined; - - const sharedStyle = { - strokeWidth: yConfig.lineWidth || 1, - stroke: yConfig.color || defaultReferenceLineColor, - dash: dashStyle, - }; - - annotations.push( - ({ - dataValue: row[yConfig.forAccessor], - header: columnToLabelMap[yConfig.forAccessor], - details: formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }))} - domainType={ - yConfig.axisMode === 'bottom' - ? AnnotationDomainType.XDomain - : AnnotationDomainType.YDomain - } - style={{ - line: { - ...sharedStyle, - opacity: 1, - }, - }} - /> - ); - - if (yConfig.fill && yConfig.fill !== 'none') { - const isFillAbove = yConfig.fill === 'above'; - const indexFromSameType = groupedByDirection[yConfig.fill].findIndex( - ({ forAccessor }) => forAccessor === yConfig.forAccessor - ); - const shouldCheckNextReferenceLine = - indexFromSameType < groupedByDirection[yConfig.fill].length - 1; - annotations.push( - { - const nextValue = shouldCheckNextReferenceLine - ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] - : undefined; - if (yConfig.axisMode === 'bottom') { - return { - coordinates: { - x0: isFillAbove ? row[yConfig.forAccessor] : nextValue, - y0: undefined, - x1: isFillAbove ? nextValue : row[yConfig.forAccessor], - y1: undefined, - }, - header: columnToLabelMap[yConfig.forAccessor], - details: - formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }; - } - return { - coordinates: { - x0: undefined, - y0: isFillAbove ? row[yConfig.forAccessor] : nextValue, - x1: undefined, - y1: isFillAbove ? nextValue : row[yConfig.forAccessor], - }, - header: columnToLabelMap[yConfig.forAccessor], - details: - formatter?.convert(row[yConfig.forAccessor]) || row[yConfig.forAccessor], - }; - })} - style={{ - ...sharedStyle, - fill: yConfig.color || defaultReferenceLineColor, - opacity: 0.1, - }} - /> - ); - } - return annotations; - }); - })} - - ); -}; diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss deleted file mode 100644 index 41b30ce40676b..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss +++ /dev/null @@ -1,18 +0,0 @@ -.lnsXyDecorationRotatedWrapper { - display: inline-block; - overflow: hidden; - line-height: $euiLineHeight; - - .lnsXyDecorationRotatedWrapper__label { - display: inline-block; - white-space: nowrap; - transform: translate(0, 100%) rotate(-90deg); - transform-origin: 0 0; - - &::after { - content: ''; - float: left; - margin-top: 100%; - } - } -} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index cfeb1387f689c..daae9a5ef56ff 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -11,7 +11,6 @@ import type { ExpressionsSetup } from '../../../../../src/plugins/expressions/pu import type { EditorFrameSetup } from '../types'; import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import type { LensPluginStartDependencies } from '../plugin'; -import { getTimeZone } from '../utils'; import type { FormatFactory } from '../../common'; import { LEGACY_TIME_AXIS } from '../../../../../src/plugins/charts/common'; @@ -26,26 +25,14 @@ export interface XyVisualizationPluginSetupPlugins { export class XyVisualization { setup( core: CoreSetup, - { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins + { editorFrame }: XyVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { - const { getXyChartRenderer, getXyVisualization } = await import('../async_services'); + const { getXyVisualization } = await import('../async_services'); const [, { charts, fieldFormats, eventAnnotation }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); const eventAnnotationService = await eventAnnotation.getService(); const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS); - expressions.registerRenderer( - getXyChartRenderer({ - formatFactory, - chartsThemeService: charts.theme, - chartsActiveCursorService: charts.activeCursor, - paletteService: palettes, - eventAnnotationService, - timeZone: getTimeZone(core.uiSettings), - useLegacyTimeAxis, - kibanaTheme: core.theme, - }) - ); return getXyVisualization({ paletteService: palettes, eventAnnotationService, diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts index 9def75615e6c8..368b213428ed7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { XYDataLayerConfig } from '../../common/expressions'; import { FramePublicAPI } from '../types'; import { computeOverallDataDomain, getStaticValue } from './reference_line_helpers'; +import { XYDataLayerConfig } from './types'; function getActiveData(json: Array<{ id: string; rows: Array> }>) { return json.reduce((memo, { id, rows }) => { @@ -277,6 +277,11 @@ describe('reference_line helpers', () => { layerType: 'data', xAccessor: 'a', accessors: [], + type: 'dataLayer', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: false, + palette: { type: 'palette', name: 'palette1' }, } as XYDataLayerConfig, ], 'x', // this is influenced by the callback @@ -300,6 +305,11 @@ describe('reference_line helpers', () => { layerType: 'data', xAccessor: 'a', accessors: [], + type: 'dataLayer', + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: false, + palette: { type: 'palette', name: 'palette1' }, } as XYDataLayerConfig, ], 'x', 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 8b6a96ce24d44..af679d1354792 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 @@ -9,15 +9,14 @@ import { groupBy, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import { layerTypes } from '../../common'; import type { - XYDataLayerConfig, - XYReferenceLineLayerConfig, + YAxisMode, YConfig, -} from '../../common/expressions'; +} from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { Datatable } from '../../../../../src/plugins/expressions/public'; import type { DatasourcePublicAPI, FramePublicAPI, Visualization } from '../types'; import { groupAxesByType } from './axes_configuration'; import { isHorizontalChart, isPercentageSeries, isStackedChart } from './state_helpers'; -import type { XYState } from './types'; +import type { XYState, XYDataLayerConfig, XYReferenceLineLayerConfig } from './types'; import { checkScaleOperation, getAxisName, @@ -49,6 +48,7 @@ export function getGroupsToShow groupsAvailable[label] || config?.length) .map((layer) => ({ ...layer, valid: groupsAvailable[layer.label] })); @@ -339,6 +339,13 @@ export const setReferenceDimension: Visualization['setDimension'] = ({ ? newLayer.yConfig?.find(({ forAccessor }) => forAccessor === previousColumn) : false; if (!hasYConfig) { + const axisMode: YAxisMode = + groupId === 'xReferenceLine' + ? 'bottom' + : groupId === 'yReferenceLineRight' + ? 'right' + : 'left'; + newLayer.yConfig = [ ...(newLayer.yConfig || []), { @@ -346,15 +353,11 @@ export const setReferenceDimension: Visualization['setDimension'] = ({ ...previousYConfig, // but keep the new group & id config forAccessor: columnId, - axisMode: - groupId === 'xReferenceLine' - ? 'bottom' - : groupId === 'yReferenceLineRight' - ? 'right' - : 'left', + axisMode, }, ]; } + return { ...prevState, layers: prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)), 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 e0984e62cb9cc..d3c8efae33836 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -9,13 +9,15 @@ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import type { FramePublicAPI, DatasourcePublicAPI } from '../types'; import type { SeriesType, - XYLayerConfig, YConfig, ValidLayer, +} from '../../../../../src/plugins/chart_expressions/expression_xy/common'; +import { + visualizationTypes, + XYLayerConfig, XYDataLayerConfig, XYReferenceLineLayerConfig, -} from '../../common/expressions'; -import { visualizationTypes } from './types'; +} from './types'; import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers'; export function isHorizontalSeries(seriesType: SeriesType) { 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 2e3db8f2f6f93..c04781198c78f 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 @@ -13,9 +13,9 @@ import { OperationDescriptor } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import { eventAnnotationServiceMock } from '../../../../../src/plugins/event_annotation/public/mocks'; import { defaultReferenceLineColor } from './color_assignment'; import { themeServiceMock } from '../../../../../src/core/public/mocks'; -import { eventAnnotationServiceMock } from 'src/plugins/event_annotation/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ 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 ade90ff98e553..ec9093a999c84 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -8,18 +8,20 @@ import { Ast } from '@kbn/interpreter'; import { ScaleType } from '@elastic/charts'; import { PaletteRegistry } from 'src/plugins/charts/public'; + import { EventAnnotationServiceType } from 'src/plugins/event_annotation/public'; -import { State } from './types'; +import { + State, + XYDataLayerConfig, + XYReferenceLineLayerConfig, + XYAnnotationLayerConfig, +} from './types'; import { OperationMetadata, DatasourcePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import type { ValidLayer, - XYAnnotationLayerConfig, - XYReferenceLineLayerConfig, YConfig, - XYDataLayerConfig, -} from '../../common/expressions'; -import { layerTypes } from '../../common'; +} from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { hasIcon } from './xy_config_panel/shared/icon_select'; import { defaultReferenceLineColor } from './color_assignment'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; @@ -31,6 +33,7 @@ import { } from './visualization_helpers'; import { defaultAnnotationLabel } from './annotations/config_panel'; import { getUniqueLabels } from './annotations/helpers'; +import { layerTypes } from '../../common'; export const getSortedAccessors = ( datasource: DatasourcePublicAPI, @@ -195,7 +198,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_chart', + function: 'xyVis', arguments: { title: [attributes?.title || ''], description: [attributes?.description || ''], @@ -208,7 +211,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_legendConfig', + function: 'legendConfig', arguments: { isVisible: [state.legend.isVisible], showSingleSeries: state.legend.showSingleSeries @@ -249,7 +252,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_axisExtentConfig', + function: 'axisExtentConfig', arguments: { mode: [state?.yLeftExtent?.mode || 'full'], lowerBound: @@ -271,7 +274,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_axisExtentConfig', + function: 'axisExtentConfig', arguments: { mode: [state?.yRightExtent?.mode || 'full'], lowerBound: @@ -293,7 +296,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_axisTitlesVisibilityConfig', + function: 'axisTitlesVisibilityConfig', arguments: { x: [state?.axisTitlesVisibilitySettings?.x ?? true], yLeft: [state?.axisTitlesVisibilitySettings?.yLeft ?? true], @@ -309,7 +312,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_tickLabelsConfig', + function: 'tickLabelsConfig', arguments: { x: [state?.tickLabelsVisibilitySettings?.x ?? true], yLeft: [state?.tickLabelsVisibilitySettings?.yLeft ?? true], @@ -325,7 +328,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_gridlinesConfig', + function: 'gridlinesConfig', arguments: { x: [state?.gridlinesVisibilitySettings?.x ?? true], yLeft: [state?.gridlinesVisibilitySettings?.yLeft ?? true], @@ -341,7 +344,7 @@ export const buildExpression = ( chain: [ { type: 'function', - function: 'lens_xy_labelsOrientationConfig', + function: 'labelsOrientationConfig', arguments: { x: [state?.labelsOrientation?.x ?? 0], yLeft: [state?.labelsOrientation?.yLeft ?? 0], @@ -388,7 +391,7 @@ const referenceLineLayerToExpression = ( chain: [ { type: 'function', - function: 'lens_xy_referenceLine_layer', + function: 'referenceLineLayer', arguments: { layerId: [layer.layerId], yConfig: layer.yConfig @@ -396,7 +399,6 @@ const referenceLineLayerToExpression = ( yConfigToExpression(yConfig, defaultReferenceLineColor) ) : [], - layerType: [layerTypes.REFERENCELINE], accessors: layer.accessors, columnToLabel: [JSON.stringify(getColumnToLabelMap(layer, datasourceLayer))], }, @@ -414,11 +416,10 @@ const annotationLayerToExpression = ( chain: [ { type: 'function', - function: 'lens_xy_annotation_layer', + function: 'annotationLayer', arguments: { hide: [Boolean(layer.hide)], layerId: [layer.layerId], - layerType: [layerTypes.ANNOTATIONS], annotations: layer.annotations ? layer.annotations.map( (ann): Ast => @@ -462,7 +463,7 @@ const dataLayerToExpression = ( chain: [ { type: 'function', - function: 'lens_xy_data_layer', + function: 'dataLayer', arguments: { layerId: [layer.layerId], hide: [Boolean(layer.hide)], @@ -477,7 +478,6 @@ const dataLayerToExpression = ( ? layer.yConfig.map((yConfig) => yConfigToExpression(yConfig)) : [], seriesType: [layer.seriesType], - layerType: [layerTypes.DATA], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], ...(layer.palette @@ -515,7 +515,7 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => { chain: [ { type: 'function', - function: 'lens_xy_yConfig', + function: 'yConfig', arguments: { forAccessor: [yConfig.forAccessor], axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 2b9d5687979be..9fcb1951d5ac2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -18,19 +18,52 @@ import { LensIconChartBarHorizontalPercentage } from '../assets/chart_bar_horizo import { LensIconChartLine } from '../assets/chart_line'; import type { VisualizationType, Suggestion } from '../types'; +import { PaletteOutput } from '../../../../../src/plugins/charts/common'; import type { SeriesType, LegendConfig, AxisExtentConfig, - XYLayerConfig, XYCurveType, AxesSettingsConfig, FittingFunction, LabelsOrientationConfig, EndValue, -} from '../../common/expressions'; + YConfig, +} from '../../../../../src/plugins/chart_expressions/expression_xy/common'; +import { EventAnnotationConfig } from '../../../../../src/plugins/event_annotation/common'; import type { ValueLabelConfig } from '../../common/types'; +export interface XYDataLayerConfig { + layerId: string; + accessors: string[]; + layerType: 'data'; + seriesType: SeriesType; + xAccessor?: string; + hide?: boolean; + yConfig?: YConfig[]; + splitAccessor?: string; + palette?: PaletteOutput; +} + +export interface XYReferenceLineLayerConfig { + layerId: string; + accessors: string[]; + yConfig?: YConfig[]; + layerType: 'referenceLine'; +} + +export interface XYAnnotationLayerConfig { + layerId: string; + layerType: 'annotations'; + annotations: EventAnnotationConfig[]; + hide?: boolean; +} + +export type XYLayerConfig = + | XYDataLayerConfig + | XYReferenceLineLayerConfig + | XYAnnotationLayerConfig; + // Persisted parts of the state export interface XYState { preferredSeriesType: SeriesType; @@ -56,6 +89,7 @@ export interface XYState { } export type State = XYState; + const groupLabelForBar = i18n.translate('xpack.lens.xyVisualization.barGroupLabel', { defaultMessage: 'Bar', }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index b5b17c4536288..18cd16c17b365 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -8,13 +8,15 @@ import { getXyVisualization } from './visualization'; import { Position } from '@elastic/charts'; import { Operation, VisualizeEditorContext, Suggestion, OperationDescriptor } from '../types'; -import type { State, XYState, XYSuggestion } from './types'; import type { - SeriesType, - XYDataLayerConfig, + State, + XYState, + XYSuggestion, XYLayerConfig, + XYDataLayerConfig, XYReferenceLineLayerConfig, -} from '../../common/expressions'; +} from './types'; +import type { SeriesType } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { layerTypes } from '../../common'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { LensIconChartBar } from '../assets/chart_bar'; @@ -45,7 +47,7 @@ const exampleAnnotation2: EventAnnotationConfig = { label: 'Annotation2', }; -function exampleState(): State { +function exampleState(): XYState { return { legend: { position: Position.Bottom, isVisible: true }, valueLabels: 'hide', @@ -58,7 +60,7 @@ function exampleState(): State { splitAccessor: 'd', xAccessor: 'a', accessors: ['b', 'c'], - } as XYDataLayerConfig, + }, ], }; } @@ -127,7 +129,7 @@ describe('xy_visualization', () => { }); describe('#getVisualizationTypeId', () => { - function mixedState(...types: SeriesType[]) { + function mixedState(...types: SeriesType[]): XYState { const state = exampleState(); return { ...state, @@ -1039,7 +1041,7 @@ describe('xy_visualization', () => { ...baseState.layers[0], accessors: ['a'], seriesType: 'bar_percentage_stacked', - } as XYDataLayerConfig, + } as XYLayerConfig, ], }, frame, @@ -1648,7 +1650,7 @@ describe('xy_visualization', () => { ...baseState.layers[0], splitAccessor: undefined, ...layerConfigOverride, - } as XYDataLayerConfig, + } as XYLayerConfig, ], }, frame, diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 78fd50f7cfece..1a6af0dc36475 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -16,14 +16,18 @@ import { ThemeServiceStart } from 'kibana/public'; import { EventAnnotationServiceType } from '../../../../../src/plugins/event_annotation/public'; import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; -import type { FillStyle, XYLayerConfig } from '../../common/expressions/xy_chart'; import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { LayerHeader } from './xy_config_panel/layer_header'; import type { Visualization, AccessorConfig, FramePublicAPI } from '../types'; -import { State, visualizationTypes, XYSuggestion } from './types'; -import { SeriesType, XYDataLayerConfig, YAxisMode } from '../../common/expressions'; +import { + FillStyle, + SeriesType, + YAxisMode, + YConfig, +} from '../../../../../src/plugins/chart_expressions/expression_xy/common'; +import { State, visualizationTypes, XYSuggestion, XYLayerConfig, XYDataLayerConfig } from './types'; import { layerTypes } from '../../common'; import { isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; @@ -61,7 +65,7 @@ import { validateLayersForDimension, } from './visualization_helpers'; import { groupAxesByType } from './axes_configuration'; -import { XYState } from '..'; +import { XYState } from './types'; import { ReferenceLinePanel } from './xy_config_panel/reference_line_panel'; import { DimensionTrigger } from '../shared_components/dimension_trigger'; import { AnnotationsPanel, defaultAnnotationLabel } from './annotations/config_panel'; @@ -202,12 +206,18 @@ export const getXyVisualization = ({ accessors: sortedAccessors, }); + if (isReferenceLayer(layer)) { + return getReferenceConfiguration({ state, frame, layer, sortedAccessors }); + } + + const dataLayer: XYDataLayerConfig = layer; + const dataLayers = getDataLayers(state.layers); const isHorizontal = isHorizontalChart(state.layers); const { left, right } = groupAxesByType([layer], frame.activeData); // Check locally if it has one accessor OR one accessor per axis const layerHasOnlyOneAccessor = Boolean( - layer.accessors.length < 2 || + dataLayer.accessors.length < 2 || (left.length && left.length < 2) || (right.length && right.length < 2) ); @@ -219,9 +229,10 @@ export const getXyVisualization = ({ // check that the other layers are compatible with this one (l) => { if ( - l.seriesType === layer.seriesType && - Boolean(l.xAccessor) === Boolean(layer.xAccessor) && - Boolean(l.splitAccessor) === Boolean(layer.splitAccessor) + isDataLayer(l) && + l.seriesType === dataLayer.seriesType && + Boolean(l.xAccessor) === Boolean(dataLayer.xAccessor) && + Boolean(l.splitAccessor) === Boolean(dataLayer.splitAccessor) ) { const { left: localLeft, right: localRight } = groupAxesByType([l], frame.activeData); // return true only if matching axis are found @@ -240,9 +251,9 @@ export const getXyVisualization = ({ { groupId: 'x', groupLabel: getAxisName('x', { isHorizontal }), - accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [], + accessors: dataLayer.xAccessor ? [{ columnId: dataLayer.xAccessor }] : [], filterOperations: isBucketed, - supportsMoreColumns: !layer.xAccessor, + supportsMoreColumns: !dataLayer.xAccessor, dataTestSubj: 'lnsXY_xDimensionPanel', }, { @@ -260,21 +271,21 @@ export const getXyVisualization = ({ groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { defaultMessage: 'Break down by', }), - accessors: layer.splitAccessor + accessors: dataLayer.splitAccessor ? [ { - columnId: layer.splitAccessor, + columnId: dataLayer.splitAccessor, triggerIcon: 'colorBy' as const, palette: paletteService - .get(layer.palette?.name || 'default') - .getCategoricalColors(10, layer.palette?.params), + .get(dataLayer.palette?.name || 'default') + .getCategoricalColors(10, dataLayer.palette?.params), }, ] : [], filterOperations: isBucketed, - supportsMoreColumns: !layer.splitAccessor, + supportsMoreColumns: !dataLayer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', - required: layer.seriesType.includes('percentage') && hasOnlyOneAccessor, + required: dataLayer.seriesType.includes('percentage') && hasOnlyOneAccessor, enableDimensionEditor: true, }, ], @@ -288,7 +299,9 @@ export const getXyVisualization = ({ setDimension(props) { const { prevState, layerId, columnId, groupId } = props; - const foundLayer = prevState.layers.find((l) => l.layerId === layerId); + const foundLayer: XYLayerConfig | undefined = prevState.layers.find( + (l) => l.layerId === layerId + ); if (!foundLayer) { return prevState; } @@ -300,7 +313,7 @@ export const getXyVisualization = ({ return setAnnotationsDimension(props); } - const newLayer = { ...foundLayer }; + const newLayer: XYDataLayerConfig = Object.assign({}, foundLayer); if (groupId === 'x') { newLayer.xAccessor = columnId; } @@ -324,7 +337,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], diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index 23c2446ca2363..d680494ef8315 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -8,18 +8,17 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import { DatasourcePublicAPI, OperationMetadata, VisualizationType } from '../types'; -import { State, visualizationTypes, XYState } from './types'; -import { isHorizontalChart } from './state_helpers'; import { - AnnotationLayerArgs, - DataLayerArgs, - SeriesType, + State, + visualizationTypes, + XYState, XYAnnotationLayerConfig, - XYDataLayerConfig, - XYLayerArgs, XYLayerConfig, + XYDataLayerConfig, XYReferenceLineLayerConfig, -} from '../../common/expressions'; +} from './types'; +import { isHorizontalChart } from './state_helpers'; +import { SeriesType } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { layerTypes } from '..'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; @@ -131,15 +130,12 @@ export function checkScaleOperation( }; } -export const isDataLayer = (layer: Pick): layer is XYDataLayerConfig => +export const isDataLayer = (layer: XYLayerConfig): layer is XYDataLayerConfig => layer.layerType === layerTypes.DATA || !layer.layerType; -export const getDataLayers = (layers: Array>) => +export const getDataLayers = (layers: XYLayerConfig[]) => (layers || []).filter((layer): layer is XYDataLayerConfig => isDataLayer(layer)); -export const getDataLayersArgs = (layers: XYLayerArgs[]) => - (layers || []).filter((layer): layer is DataLayerArgs => isDataLayer(layer)); - export const getFirstDataLayer = (layers: XYLayerConfig[]) => (layers || []).find((layer): layer is XYDataLayerConfig => isDataLayer(layer)); @@ -157,9 +153,6 @@ export const isAnnotationsLayer = ( export const getAnnotationsLayers = (layers: Array>) => (layers || []).filter((layer): layer is XYAnnotationLayerConfig => isAnnotationsLayer(layer)); -export const getAnnotationsLayersArgs = (layers: XYLayerArgs[]) => - (layers || []).filter((layer): layer is AnnotationLayerArgs => isAnnotationsLayer(layer)); - export interface LayerTypeToLayer { [layerTypes.DATA]: (layer: XYDataLayerConfig) => XYDataLayerConfig; [layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => XYReferenceLineLayerConfig; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx index ef692b59a7eb7..340a9211fcdee 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx @@ -17,7 +17,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEqual } from 'lodash'; -import { XYLayerConfig, AxesSettingsConfig, AxisExtentConfig } from '../../../common/expressions'; +import { + AxesSettingsConfig, + AxisExtentConfig, +} from '../../../../../../src/plugins/chart_expressions/expression_xy/common'; +import { XYLayerConfig } from '../types'; import { ToolbarPopover, useDebouncedValue, @@ -31,6 +35,7 @@ import { EuiIconAxisRight } from '../../assets/axis_right'; import { EuiIconAxisTop } from '../../assets/axis_top'; import { ToolbarButtonProps } from '../../../../../../src/plugins/kibana_react/public'; import { validateExtent } from '../axes_configuration'; + import './axis_settings_popover.scss'; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; 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 b448ebfbd455e..3f801fb92876d 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 @@ -11,7 +11,7 @@ import { EuiFormRow, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon } import type { PaletteRegistry } from 'src/plugins/charts/public'; import { defaultAnnotationColor } from '../../../../../../src/plugins/event_annotation/public'; import type { VisualizationDimensionEditorProps } from '../../types'; -import { State } from '../types'; +import { State, XYDataLayerConfig } from '../types'; import { FormatFactory } from '../../../common'; import { getSeriesColor } from '../state_helpers'; import { @@ -67,6 +67,7 @@ export const ColorPicker = ({ return defaultAnnotationColor; } + const dataLayer: XYDataLayerConfig = layer; const sortedAccessors: string[] = getSortedAccessors( frame.datasourceLayers[layer.layerId] ?? layer.accessors, layer @@ -81,8 +82,8 @@ export const ColorPicker = ({ colorAssignments, frame, { - ...layer, - accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), + ...dataLayer, + accessors: sortedAccessors.filter((sorted) => dataLayer.accessors.includes(sorted)), }, paletteService ); 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 1618bd4d0e540..b3e13ece50434 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 @@ -10,12 +10,18 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { VisualizationDimensionEditorProps } from '../../types'; -import { State, XYState } from '../types'; +import { State, XYState, XYDataLayerConfig } from '../types'; import { FormatFactory } from '../../../common'; -import { XYDataLayerConfig, YAxisMode, YConfig } from '../../../common/expressions'; +import { + YAxisMode, + YConfig, +} from '../../../../../../src/plugins/chart_expressions/expression_xy/common'; 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 { AnnotationsPanel } from '../annotations/config_panel'; type UnwrapArray = T extends Array ? P : T; @@ -43,16 +49,14 @@ export function DimensionEditor( ) { const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); + const layer = state.layers[index] as XYDataLayerConfig; const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ value: props.state, onChange: props.setState, }); - const localLayer = localState.layers.find((l) => l.layerId === layerId) as XYDataLayerConfig; - const localYConfig = localLayer?.yConfig?.find( - (yAxisConfig) => yAxisConfig.forAccessor === accessor - ); + const localYConfig = layer?.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor); const axisMode = localYConfig?.axisMode || 'auto'; const setConfig = useCallback( @@ -60,7 +64,7 @@ export function DimensionEditor( if (yConfig == null) { return; } - const newYConfigs = [...(localLayer.yConfig || [])]; + const newYConfigs = [...(layer.yConfig || [])]; const existingIndex = newYConfigs.findIndex( (yAxisConfig) => yAxisConfig.forAccessor === accessor ); @@ -72,11 +76,20 @@ export function DimensionEditor( ...yConfig, }); } - setLocalState(updateLayer(localState, { ...localLayer, yConfig: newYConfigs }, index)); + setLocalState(updateLayer(localState, { ...layer, yConfig: newYConfigs }, index)); }, - [accessor, index, localState, localLayer, setLocalState] + [accessor, index, localState, layer, setLocalState] ); + if (isAnnotationsLayer(layer)) { + return ; + } + + if (isReferenceLayer(layer)) { + return ; + } + + const localLayer: XYDataLayerConfig = layer; if (props.groupId === 'breakdown') { 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 1684d822b5576..42cf49c04fbd8 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 @@ -11,7 +11,10 @@ import { Position, ScaleType, VerticalAlignment, HorizontalAlignment } from '@el import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { VisualizationToolbarProps, FramePublicAPI } from '../../types'; import { State, XYState } from '../types'; -import { AxesSettingsConfig, AxisExtentConfig } from '../../../common/expressions'; +import { + AxesSettingsConfig, + AxisExtentConfig, +} from '../../../../../../src/plugins/chart_expressions/expression_xy/common'; import { isHorizontalChart } from '../state_helpers'; import { LegendSettingsPopover } from '../../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; 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 c4e5268cfb8af..2aabf255c5993 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 @@ -9,8 +9,8 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@elastic/eui'; import type { VisualizationLayerWidgetProps, VisualizationType } from '../../types'; -import { State, visualizationTypes } from '../types'; -import { SeriesType, XYDataLayerConfig } from '../../../common/expressions'; +import { State, visualizationTypes, XYDataLayerConfig } from '../types'; +import { SeriesType } from '../../../../../../src/plugins/chart_expressions/expression_xy/common'; import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { StaticHeader } from '../../shared_components'; @@ -58,8 +58,9 @@ function AnnotationsLayerHeader() { function DataLayerHeader(props: VisualizationLayerWidgetProps) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); const { state, layerId } = props; - const index = state.layers.findIndex((l) => l.layerId === layerId); - const layer = state.layers[index] as XYDataLayerConfig; + const layers = state.layers as XYDataLayerConfig[]; + const index = layers.findIndex((l) => l.layerId === layerId); + const layer = layers[index]; const currentVisType = visualizationTypes.find(({ id }) => id === layer.seriesType)!; const horizontalOnly = isHorizontalChart(state.layers); 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_panel.tsx index 78020034c3d43..ffca2c0531b7c 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_panel.tsx @@ -10,10 +10,12 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { VisualizationDimensionEditorProps } from '../../types'; -import { State, XYState } from '../types'; +import { State, XYState, XYReferenceLineLayerConfig } from '../types'; import { FormatFactory } from '../../../common'; -import { YConfig } from '../../../common/expressions'; -import { FillStyle, XYReferenceLineLayerConfig } from '../../../common/expressions/xy_chart'; +import { + FillStyle, + YConfig, +} from '../../../../../../src/plugins/chart_expressions/expression_xy/common'; import { ColorPicker } from './color_picker'; import { updateLayer } from '.'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx index 766d5462db787..8ae86c2fd5b46 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/line_style_settings.tsx @@ -14,7 +14,8 @@ import { EuiFlexItem, EuiFormRow, } from '@elastic/eui'; -import { LineStyle } from '../../../../common/expressions/xy_chart'; +import { LineStyle } from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; + import { idPrefix } from '../dimension_editor'; interface LineStyleConfig { 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 134d72b7de64b..b3736cdbf3dfe 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 @@ -8,7 +8,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; -import { IconPosition, YAxisMode } from '../../../../common/expressions/xy_chart'; +import { + IconPosition, + YAxisMode, +} from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; import { TooltipWrapper } from '../../../shared_components'; import { hasIcon, IconSelect, IconSet } from './icon_select'; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/end_value_definitions.ts similarity index 85% rename from x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/end_value_definitions.ts index 1ef664cb2e2ba..1ecd260b04a30 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/end_value_definitions.ts @@ -6,10 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import type { EndValue } from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; -export type EndValue = typeof endValueDefinitions[number]['id']; - -export const endValueDefinitions = [ +export const endValueDefinitions: Array<{ id: EndValue } & Record> = [ { id: 'None', title: i18n.translate('xpack.lens.endValue.none', { @@ -37,4 +36,4 @@ export const endValueDefinitions = [ defaultMessage: 'Extend series with the first/last value to the edge of the chart', }), }, -] as const; +]; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/fitting_function.ts b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fitting_function_definitions.ts similarity index 88% rename from x-pack/plugins/lens/common/expressions/xy_chart/fitting_function.ts rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fitting_function_definitions.ts index 0cfea62d578d7..a357f7c2f92ca 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/fitting_function.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/fitting_function_definitions.ts @@ -6,10 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import type { FittingFunction } from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; -export type FittingFunction = typeof fittingFunctionDefinitions[number]['id']; - -export const fittingFunctionDefinitions = [ +export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record> = [ { id: 'None', title: i18n.translate('xpack.lens.fittingFunctionsTitle.none', { @@ -55,4 +54,4 @@ export const fittingFunctionDefinitions = [ defaultMessage: 'Fill gaps with the next value', }), }, -] as const; +]; 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 0bdd513c1f881..00903e1107c91 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 @@ -13,7 +13,7 @@ import { LineCurveOption } from './line_curve_option'; import { FillOpacityOption } from './fill_opacity_option'; import { XYState } from '../../types'; import { hasHistogramSeries } from '../../state_helpers'; -import { ValidLayer } from '../../../../common/expressions'; +import { ValidLayer } from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; import type { FramePublicAPI } from '../../../types'; import { getDataLayers } from '../../visualization_helpers'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx index 96926412afb8a..9e69c427d99fe 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/line_curve_option.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import type { XYCurveType } from '../../../../common/expressions'; +import type { XYCurveType } from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; export interface LineCurveOptionProps { /** diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx index 9bd59cb5c4a08..645b740fc9dad 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/missing_values_option.tsx @@ -8,8 +8,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiSwitch, EuiText } from '@elastic/eui'; -import { fittingFunctionDefinitions, endValueDefinitions } from '../../../../common/expressions'; -import type { FittingFunction, EndValue } from '../../../../common/expressions'; +import { fittingFunctionDefinitions } from './fitting_function_definitions'; +import { endValueDefinitions } from './end_value_definitions'; +import type { + FittingFunction, + EndValue, +} from '../../../../../../../src/plugins/chart_expressions/expression_xy/common'; export interface MissingValuesOptionProps { fittingFunction?: FittingFunction; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx index 4e2be2f0e4749..5b91ee70c6945 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/visual_options_popover/visual_options_popover.test.tsx @@ -10,13 +10,12 @@ import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; import { Position } from '@elastic/charts'; import type { FramePublicAPI } from '../../../types'; import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks'; -import { State } from '../../types'; +import { State, XYLayerConfig } from '../../types'; import { VisualOptionsPopover } from '.'; import { ToolbarPopover, ValueLabelsSettings } from '../../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { FillOpacityOption } from './fill_opacity_option'; import { layerTypes } from '../../../../common'; -import { XYDataLayerConfig } from '../../../../common/expressions'; describe('Visual options popover', () => { let frame: FramePublicAPI; @@ -53,7 +52,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [{ ...state.layers[0], seriesType: 'bar_stacked' } as XYDataLayerConfig], + layers: [{ ...state.layers[0], seriesType: 'bar_stacked' } as XYLayerConfig], }} /> ); @@ -69,9 +68,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [ - { ...state.layers[0], seriesType: 'area_percentage_stacked' } as XYDataLayerConfig, - ], + layers: [{ ...state.layers[0], seriesType: 'area_percentage_stacked' } as XYLayerConfig], }} /> ); @@ -88,9 +85,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [ - { ...state.layers[0], seriesType: 'area_percentage_stacked' } as XYDataLayerConfig, - ], + layers: [{ ...state.layers[0], seriesType: 'area_percentage_stacked' } as XYLayerConfig], }} /> ); @@ -106,9 +101,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [ - { ...state.layers[0], seriesType: 'area_percentage_stacked' } as XYDataLayerConfig, - ], + layers: [{ ...state.layers[0], seriesType: 'area_percentage_stacked' } as XYLayerConfig], }} /> ); @@ -145,7 +138,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYDataLayerConfig], + layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYLayerConfig], fittingFunction: 'Carry', }} /> @@ -162,7 +155,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYDataLayerConfig], + layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYLayerConfig], fittingFunction: 'Carry', }} /> @@ -179,7 +172,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [{ ...state.layers[0], seriesType: 'line' } as XYDataLayerConfig], + layers: [{ ...state.layers[0], seriesType: 'line' } as XYLayerConfig], fittingFunction: 'Carry', }} /> @@ -197,7 +190,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYDataLayerConfig], + layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYLayerConfig], fittingFunction: 'Carry', }} /> @@ -214,7 +207,7 @@ describe('Visual options popover', () => { setState={jest.fn()} state={{ ...state, - layers: [{ ...state.layers[0], seriesType: 'area' } as XYDataLayerConfig], + layers: [{ ...state.layers[0], seriesType: 'area' } as XYLayerConfig], fittingFunction: 'Carry', }} /> @@ -237,7 +230,7 @@ describe('Visual options popover', () => { state={{ ...state, layers: [ - { ...state.layers[0], seriesType: 'bar' } as XYDataLayerConfig, + { ...state.layers[0], seriesType: 'bar' } as XYLayerConfig, { seriesType: 'bar', layerType: layerTypes.DATA, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx index a73d14ad15573..953828db48a72 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.test.tsx @@ -12,13 +12,12 @@ import { XyToolbar } from '.'; import { DimensionEditor } from './dimension_editor'; import { AxisSettingsPopover } from './axis_settings_popover'; import { FramePublicAPI } from '../../types'; -import { State, XYState } from '../types'; +import { State, XYState, XYDataLayerConfig } from '../types'; import { Position } from '@elastic/charts'; import { createMockFramePublicAPI, createMockDatasource } from '../../mocks'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { EuiColorPicker } from '@elastic/eui'; import { layerTypes } from '../../../common'; -import { XYDataLayerConfig } from '../../../common/expressions'; describe('XY Config panels', () => { let frame: FramePublicAPI; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 2c3483e40b955..941e50d6e5285 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -7,7 +7,13 @@ import { getSuggestions } from './xy_suggestions'; import type { TableSuggestionColumn, VisualizationSuggestion, TableSuggestion } from '../types'; -import { State, XYState, visualizationTypes } from './types'; +import { + State, + XYState, + visualizationTypes, + XYAnnotationLayerConfig, + XYDataLayerConfig, +} from './types'; import { generateId } from '../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; @@ -16,7 +22,6 @@ import { PaletteOutput } from 'src/plugins/charts/public'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { themeServiceMock } from '../../../../../src/core/public/mocks'; -import { XYAnnotationLayerConfig, XYDataLayerConfig } from '../../common/expressions'; jest.mock('../id_generator'); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index bd5a37c206c6c..610e21c1fe138 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -16,8 +16,8 @@ import { TableSuggestion, TableChangeType, } from '../types'; -import { State, XYState, visualizationTypes } from './types'; -import type { SeriesType, XYDataLayerConfig } from '../../common/expressions'; +import { State, XYState, visualizationTypes, XYLayerConfig, XYDataLayerConfig } from './types'; +import type { SeriesType } from '../../../../../src/plugins/chart_expressions/expression_xy/common'; import { layerTypes } from '../../common'; import { getIconForSeries } from './state_helpers'; import { getDataLayers, isDataLayer } from './visualization_helpers'; @@ -499,26 +499,29 @@ function buildSuggestion({ splitBy = xValue; xValue = undefined; } - const existingLayer: XYDataLayerConfig | {} = - getExistingLayer(currentState, layerId) || ({} as XYDataLayerConfig); + const existingLayer = getExistingLayer(currentState, layerId) || null; const accessors = yValues.map((col) => col.columnId); - const newLayer = { - ...existingLayer, - palette: mainPalette || ('palette' in existingLayer ? existingLayer.palette : undefined), + const newLayer: XYDataLayerConfig = { + ...(existingLayer || {}), + palette: + mainPalette || + (existingLayer && 'palette' in existingLayer + ? (existingLayer as XYDataLayerConfig).palette + : undefined), layerId, seriesType, xAccessor: xValue?.columnId, splitAccessor: splitBy?.columnId, accessors, yConfig: - 'yConfig' in existingLayer && existingLayer.yConfig + existingLayer && 'yConfig' in existingLayer && existingLayer.yConfig ? existingLayer.yConfig.filter(({ forAccessor }) => accessors.indexOf(forAccessor) !== -1) : undefined, layerType: layerTypes.DATA, }; // Maintain consistent order for any layers that were saved - const keptLayers = currentState + const keptLayers: XYLayerConfig[] = currentState ? currentState.layers // Remove layers that aren't being suggested .filter( @@ -568,7 +571,8 @@ function buildSuggestion({ yRight: true, }, preferredSeriesType: seriesType, - layers: Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer], + layers: + existingLayer && Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer], }; return { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.ts index 894b003b4b371..7b9fa15413a57 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.ts @@ -5,6 +5,5 @@ * 2.0. */ -export * from './expression'; export * from './types'; export * from './visualization'; diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts index c68fed23a7fdb..9b23c1741556a 100644 --- a/x-pack/plugins/lens/server/expressions/expressions.ts +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -7,19 +7,9 @@ import type { CoreSetup } from 'kibana/server'; import { - xyChart, counterRate, - yAxisConfig, - dataLayerConfig, - referenceLineLayerConfig, - annotationLayerConfig, formatColumn, - legendConfig, renameColumns, - gridlinesConfig, - datatableColumn, - tickLabelsConfig, - axisTitlesVisibilityConfig, getTimeScale, getDatatable, lensMultitable, @@ -36,19 +26,9 @@ export const setupExpressions = ( [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); [ - xyChart, counterRate, - yAxisConfig, - dataLayerConfig, - referenceLineLayerConfig, - annotationLayerConfig, formatColumn, - legendConfig, renameColumns, - gridlinesConfig, - datatableColumn, - tickLabelsConfig, - axisTitlesVisibilityConfig, getDatatable(getFormatFactory(core)), getTimeScale(getTimeZoneFactory(core)), ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); 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 61342af5033dd..6760dfd084b94 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 @@ -200,10 +200,10 @@ describe('Lens migrations', () => { } | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}" } - | lens_xy_chart + | xyVis xTitle="products.created_on" yTitle="Count of records" - legend={lens_xy_legendConfig isVisible=true position="right"} + legend={legendConfig isVisible=true position="right"} layers={lens_xy_layer layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c" hide=false @@ -293,7 +293,7 @@ describe('Lens migrations', () => { | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"} -| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`, +| xyVis xTitle="products.created_on" yTitle="Count of records" legend={legendConfig isVisible=true position="right"} layers={}`, }, }; const result = migrations['7.8.0'](input, context); @@ -309,7 +309,7 @@ describe('Lens migrations', () => { attributes: { description: '', expression: - 'kibana\n| kibana_context query="{\\"query\\":\\"NOT bytes > 5000\\",\\"language\\":\\"kuery\\"}" \n filters="[{\\"meta\\":{\\"index\\":\\"90943e30-9a47-11e8-b64d-95841ca0b247\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geo.src\\",\\"params\\":{\\"query\\":\\"CN\\"}},\\"query\\":{\\"match_phrase\\":{\\"geo.src\\":\\"CN\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}},{\\"meta\\":{\\"index\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geoip.country_iso_code\\",\\"params\\":{\\"query\\":\\"US\\"}},\\"query\\":{\\"match_phrase\\":{\\"geoip.country_iso_code\\":\\"US\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}]"\n| lens_merge_tables layerIds="9a27f85d-35a9-4246-81b2-48e7ee9b0707"\n layerIds="3b7791e9-326e-40d5-a787-b7594e48d906" \n tables={esaggs index="90943e30-9a47-11e8-b64d-95841ca0b247" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geo.src\\",\\"orderBy\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-96352896-c508-4fca-90d8-66e9ebfce621\\":{\\"label\\":\\"Top values of geo.src\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geo.src\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\"},\\"col-1-4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"}}"}\n tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geoip.country_iso_code\\",\\"orderBy\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-77d8383e-f66e-471e-ae50-c427feedb5ba\\":{\\"label\\":\\"Top values of geoip.country_iso_code\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geoip.country_iso_code\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\"},\\"col-1-a5c1b82d-51de-4448-a99d-6391432c3a03\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"}}"}\n| lens_xy_chart xTitle="Top values of geo.src" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} fittingFunction="None" \n layers={lens_xy_layer layerId="9a27f85d-35a9-4246-81b2-48e7ee9b0707" hide=false xAccessor="96352896-c508-4fca-90d8-66e9ebfce621" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="4ce9b4c7-2ebf-4d48-8669-0ea69d973353" columnToLabel="{\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":\\"Count of records\\"}"}\n layers={lens_xy_layer layerId="3b7791e9-326e-40d5-a787-b7594e48d906" hide=false xAccessor="77d8383e-f66e-471e-ae50-c427feedb5ba" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="a5c1b82d-51de-4448-a99d-6391432c3a03" columnToLabel="{\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\":\\"Count of records [1]\\"}"}', + 'kibana\n| kibana_context query="{\\"query\\":\\"NOT bytes > 5000\\",\\"language\\":\\"kuery\\"}" \n filters="[{\\"meta\\":{\\"index\\":\\"90943e30-9a47-11e8-b64d-95841ca0b247\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geo.src\\",\\"params\\":{\\"query\\":\\"CN\\"}},\\"query\\":{\\"match_phrase\\":{\\"geo.src\\":\\"CN\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}},{\\"meta\\":{\\"index\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geoip.country_iso_code\\",\\"params\\":{\\"query\\":\\"US\\"}},\\"query\\":{\\"match_phrase\\":{\\"geoip.country_iso_code\\":\\"US\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}]"\n| lens_merge_tables layerIds="9a27f85d-35a9-4246-81b2-48e7ee9b0707"\n layerIds="3b7791e9-326e-40d5-a787-b7594e48d906" \n tables={esaggs index="90943e30-9a47-11e8-b64d-95841ca0b247" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geo.src\\",\\"orderBy\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-96352896-c508-4fca-90d8-66e9ebfce621\\":{\\"label\\":\\"Top values of geo.src\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geo.src\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\"},\\"col-1-4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"}}"}\n tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geoip.country_iso_code\\",\\"orderBy\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-77d8383e-f66e-471e-ae50-c427feedb5ba\\":{\\"label\\":\\"Top values of geoip.country_iso_code\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geoip.country_iso_code\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\"},\\"col-1-a5c1b82d-51de-4448-a99d-6391432c3a03\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"}}"}\n| xyVis xTitle="Top values of geo.src" yTitle="Count of records" legend={legendConfig isVisible=true position="right"} fittingFunction="None" \n layers={lens_xy_layer layerId="9a27f85d-35a9-4246-81b2-48e7ee9b0707" hide=false xAccessor="96352896-c508-4fca-90d8-66e9ebfce621" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="4ce9b4c7-2ebf-4d48-8669-0ea69d973353" columnToLabel="{\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":\\"Count of records\\"}"}\n layers={lens_xy_layer layerId="3b7791e9-326e-40d5-a787-b7594e48d906" hide=false xAccessor="77d8383e-f66e-471e-ae50-c427feedb5ba" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="a5c1b82d-51de-4448-a99d-6391432c3a03" columnToLabel="{\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\":\\"Count of records [1]\\"}"}', state: { datasourceMetaData: { filterableIndexPatterns: [ diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 76e25f8b08639..eae5536012314 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -14,86 +14,33 @@ "../../../typings/**/*" ], "references": [ - { - "path": "../spaces/tsconfig.json" - }, - { - "path": "../../../src/core/tsconfig.json" - }, - { - "path": "../task_manager/tsconfig.json" - }, - { - "path": "../global_search/tsconfig.json" - }, - { - "path": "../saved_objects_tagging/tsconfig.json" - }, - { - "path": "../../../src/plugins/data/tsconfig.json" - }, - { - "path": "../../../src/plugins/data_views/tsconfig.json" - }, - { - "path": "../../../src/plugins/data_view_field_editor/tsconfig.json" - }, - { - "path": "../../../src/plugins/charts/tsconfig.json" - }, - { - "path": "../../../src/plugins/expressions/tsconfig.json" - }, - { - "path": "../../../src/plugins/navigation/tsconfig.json" - }, - { - "path": "../../../src/plugins/url_forwarding/tsconfig.json" - }, - { - "path": "../../../src/plugins/visualizations/tsconfig.json" - }, - { - "path": "../../../src/plugins/dashboard/tsconfig.json" - }, - { - "path": "../../../src/plugins/ui_actions/tsconfig.json" - }, - { - "path": "../../../src/plugins/embeddable/tsconfig.json" - }, - { - "path": "../../../src/plugins/share/tsconfig.json" - }, - { - "path": "../../../src/plugins/usage_collection/tsconfig.json" - }, - { - "path": "../../../src/plugins/saved_objects/tsconfig.json" - }, - { - "path": "../../../src/plugins/kibana_utils/tsconfig.json" - }, - { - "path": "../../../src/plugins/kibana_react/tsconfig.json" - }, - { - "path": "../../../src/plugins/embeddable/tsconfig.json" - }, - { - "path": "../../../src/plugins/presentation_util/tsconfig.json" - }, - { - "path": "../../../src/plugins/field_formats/tsconfig.json" - }, - { - "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json" - }, - { - "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json" - }, - { - "path": "../../../src/plugins/event_annotation/tsconfig.json" - } + { "path": "../spaces/tsconfig.json" }, + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../global_search/tsconfig.json"}, + { "path": "../saved_objects_tagging/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/data_views/tsconfig.json"}, + { "path": "../../../src/plugins/data_view_field_editor/tsconfig.json"}, + { "path": "../../../src/plugins/charts/tsconfig.json"}, + { "path": "../../../src/plugins/expressions/tsconfig.json"}, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json"}, + { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, + { "path": "../../../src/plugins/field_formats/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_xy/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json"}, + { "path": "../../../src/plugins/event_annotation/tsconfig.json" } ] } \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 6c186cecef8ad..9056d2e6053b3 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -642,9 +642,10 @@ "xpack.lens.section.workspaceLabel": "Espace de travail de visualisation", "xpack.lens.shared.chartValueLabelVisibilityLabel": "Étiquettes", "xpack.lens.shared.curveLabel": "Options visuelles", - "xpack.lens.shared.legend.filterForValueButtonAriaLabel": "Filtre pour la valeur", + "expressionXY.legend.filterForValueButtonAriaLabel": "Filtre pour la valeur", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", - "xpack.lens.shared.legend.filterOutValueButtonAriaLabel": "Filtrer la valeur", + "expressionXY.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", + "expressionXY.legend.filterOutValueButtonAriaLabel": "Filtrer la valeur", "xpack.lens.shared.legendAlignmentLabel": "Alignement", "xpack.lens.shared.legendInsideAlignmentLabel": "Alignement", "xpack.lens.shared.legendInsideColumnsLabel": "Nombre de colonnes", @@ -730,78 +731,77 @@ "xpack.lens.xyChart.axisSide.left": "Gauche", "xpack.lens.xyChart.axisSide.right": "Droite", "xpack.lens.xyChart.axisSide.top": "Haut", - "xpack.lens.xyChart.axisTitlesSettings.help": "Afficher les titres des axes X et Y", + "expressionXY.xyVis.axisTitlesVisibilitySettings.help": "Afficher les titres des axes X et Y", "xpack.lens.xyChart.bottomAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe du bas est activé.", "xpack.lens.xyChart.bottomAxisLabel": "Axe du bas", "xpack.lens.xyChart.boundaryError": "La limite inférieure doit être plus grande que la limite supérieure", "xpack.lens.xyChart.curveStyleLabel": "Courbes", - "xpack.lens.xyChart.curveType.help": "Définir de quelle façon le type de courbe est rendu pour un graphique linéaire", - "xpack.lens.xyChart.emptyXLabel": "(vide)", - "xpack.lens.xyChart.extentMode.help": "Mode d'extension", - "xpack.lens.xyChart.fillOpacity.help": "Définir l'opacité du remplissage du graphique en aires", + "expressionXY.xyVis.curveType.help": "Définir de quelle façon le type de courbe est rendu pour un graphique linéaire", + "expressionXY.xyChart.emptyXLabel": "(vide)", + "expressionXY.axisExtentConfig.extentMode.help": "Mode d'extension", + "expressionXY.xyVis.fillOpacity.help": "Définir l'opacité du remplissage du graphique en aires", "xpack.lens.xyChart.fillOpacityLabel": "Opacité de remplissage", - "xpack.lens.xyChart.fittingFunction.help": "Définir le mode de traitement des valeurs manquantes", - "xpack.lens.xyChart.floatingColumns.help": "Spécifie le nombre de colonnes lorsque la légende est affichée à l'intérieur du graphique.", + "expressionXY.xyVis.fittingFunction.help": "Définir le mode de traitement des valeurs manquantes", + "expressionXY.legendConfig.floatingColumns.help": "Spécifie le nombre de colonnes lorsque la légende est affichée à l'intérieur du graphique.", "xpack.lens.xyChart.Gridlines": "Quadrillage", - "xpack.lens.xyChart.gridlinesSettings.help": "Afficher le quadrillage des axes X et Y", - "xpack.lens.xyChart.help": "Graphique X/Y", - "xpack.lens.xyChart.hideEndzones.help": "Masquer les marqueurs de zone de fin pour les données partielles", - "xpack.lens.xyChart.horizontalAlignment.help": "Spécifie l'alignement horizontal de la légende lorsqu'elle est affichée à l'intérieur du graphique.", + "expressionXY.xyVis.gridlinesVisibilitySettings.help": "Afficher le quadrillage des axes X et Y", + "expressionXY.xyVis.help": "Graphique X/Y", + "expressionXY.xyVis.hideEndzones.help": "Masquer les marqueurs de zone de fin pour les données partielles", + "expressionXY.legendConfig.horizontalAlignment.help": "Spécifie l'alignement horizontal de la légende lorsqu'elle est affichée à l'intérieur du graphique.", "xpack.lens.xyChart.horizontalAxisLabel": "Axe horizontal", "xpack.lens.xyChart.inclusiveZero": "Les limites doivent inclure zéro.", - "xpack.lens.xyChart.isInside.help": "Spécifie si une légende se trouve à l'intérieur d'un graphique", - "xpack.lens.xyChart.isVisible.help": "Spécifie si la légende est visible ou non.", - "xpack.lens.xyChart.labelsOrientation.help": "Définit la rotation des étiquettes des axes", + "expressionXY.legendConfig.isInside.help": "Spécifie si une légende se trouve à l'intérieur d'un graphique", + "expressionXY.legendConfig.isVisible.help": "Spécifie si la légende est visible ou non.", + "expressionXY.xyVis.labelsOrientation.help": "Définit la rotation des étiquettes des axes", "xpack.lens.xyChart.leftAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de gauche est activé.", "xpack.lens.xyChart.leftAxisLabel": "Axe de gauche", - "xpack.lens.xyChart.legend.help": "Configurez la légende du graphique.", + "expressionXY.xyVis.legend.help": "Configurez la légende du graphique.", "xpack.lens.xyChart.legendLocation.inside": "Intérieur", "xpack.lens.xyChart.legendLocation.outside": "Extérieur", "xpack.lens.xyChart.legendVisibility.auto": "Auto", "xpack.lens.xyChart.legendVisibility.hide": "Masquer", "xpack.lens.xyChart.legendVisibility.show": "Afficher", - "xpack.lens.xyChart.maxLines.help": "Spécifie le nombre de lignes par élément de légende.", + "expressionXY.legendConfig.maxLines.help": "Spécifie le nombre de lignes par élément de légende.", "xpack.lens.xyChart.missingValuesLabel": "Valeurs manquantes", "xpack.lens.xyChart.missingValuesLabelHelpText": "Par défaut, Lens masque les blancs dans les données. Pour remplir le blanc, effectuez une sélection.", "xpack.lens.xyChart.nestUnderRoot": "Ensemble de données entier", - "xpack.lens.xyChart.position.help": "Spécifie la position de la légende.", - "xpack.lens.xyChart.renderer.help": "Outil de rendu de graphique X/Y", + "expressionXY.legendConfig.position.help": "Spécifie la position de la légende.", + "expressionXY.xyVis.renderer.help": "Outil de rendu de graphique X/Y", "xpack.lens.xyChart.rightAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de droite est activé.", "xpack.lens.xyChart.rightAxisLabel": "Axe de droite", "xpack.lens.xyChart.seriesColor.auto": "Auto", "xpack.lens.xyChart.seriesColor.label": "Couleur de la série", - "xpack.lens.xyChart.shouldTruncate.help": "Spécifie si les éléments de légende seront tronqués ou non", + "expressionXY.legendConfig.shouldTruncate.help": "Spécifie si les éléments de légende seront tronqués ou non", "xpack.lens.xyChart.showEnzones": "Afficher les marqueurs de données partielles", - "xpack.lens.xyChart.showSingleSeries.help": "Spécifie si une légende comportant une seule entrée doit être affichée", + "expressionXY.legendConfig.showSingleSeries.help": "Spécifie si une légende comportant une seule entrée doit être affichée", "xpack.lens.xyChart.splitSeries": "Répartir par", "xpack.lens.xyChart.tickLabels": "Étiquettes de graduation", - "xpack.lens.xyChart.tickLabelsSettings.help": "Afficher les étiquettes de graduation des axes X et Y", - "xpack.lens.xyChart.title.help": "Titre de l'axe", + "expressionXY.xyVis.tickLabelsVisibilitySettings.help": "Afficher les étiquettes de graduation des axes X et Y", "xpack.lens.xyChart.topAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe du haut est activé.", "xpack.lens.xyChart.topAxisLabel": "Axe du haut", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les histogrammes.", - "xpack.lens.xyChart.valuesInLegend.help": "Afficher les valeurs dans la légende", + "expressionXY.xyVis.valuesInLegend.help": "Afficher les valeurs dans la légende", "xpack.lens.xyChart.valuesPercentageDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les graphiques en aires à pourcentages.", "xpack.lens.xyChart.valuesStackedDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les graphiques empilés ou les graphiques à barres à pourcentages", - "xpack.lens.xyChart.verticalAlignment.help": "Spécifie l'alignement vertical de la légende lorsqu'elle est affichée à l'intérieur du graphique.", + "expressionXY.legendConfig.verticalAlignment.help": "Spécifie l'alignement vertical de la légende lorsqu'elle est affichée à l'intérieur du graphique.", "xpack.lens.xyChart.verticalAxisLabel": "Axe vertical", - "xpack.lens.xyChart.xAxisGridlines.help": "Spécifie si le quadrillage de l'axe X est visible ou non.", - "xpack.lens.xyChart.xAxisLabelsOrientation.help": "Spécifie l'orientation des étiquettes de l'axe X.", - "xpack.lens.xyChart.xAxisTickLabels.help": "Spécifie si les étiquettes de graduation de l'axe X sont visibles ou non.", - "xpack.lens.xyChart.xAxisTitle.help": "Spécifie si le titre de l'axe X est visible ou non.", - "xpack.lens.xyChart.xTitle.help": "Titre de l'axe X", - "xpack.lens.xyChart.yLeftAxisgridlines.help": "Spécifie si le quadrillage de l'axe Y de gauche est visible ou non.", - "xpack.lens.xyChart.yLeftAxisLabelsOrientation.help": "Spécifie l'orientation des étiquettes de l'axe Y de gauche.", - "xpack.lens.xyChart.yLeftAxisTickLabels.help": "Spécifie si les étiquettes de graduation de l'axe Y de gauche sont visibles ou non.", - "xpack.lens.xyChart.yLeftAxisTitle.help": "Spécifie si le titre de l'axe Y de gauche est visible ou non.", - "xpack.lens.xyChart.yLeftExtent.help": "Portée de l'axe Y de gauche", - "xpack.lens.xyChart.yLeftTitle.help": "Titre de l'axe Y de gauche", - "xpack.lens.xyChart.yRightAxisgridlines.help": "Spécifie si le quadrillage de l'axe Y de droite est visible ou non.", - "xpack.lens.xyChart.yRightAxisLabelsOrientation.help": "Spécifie l'orientation des étiquettes de l'axe Y de droite.", - "xpack.lens.xyChart.yRightAxisTickLabels.help": "Spécifie si les étiquettes de graduation de l'axe Y de droite sont visibles ou non.", - "xpack.lens.xyChart.yRightAxisTitle.help": "Spécifie si le titre de l'axe Y de droite est visible ou non.", - "xpack.lens.xyChart.yRightExtent.help": "Portée de l'axe Y de droite", - "xpack.lens.xyChart.yRightTitle.help": "Titre de l'axe Y de droite", + "expressionXY.gridlinesConfig.x.help": "Spécifie si le quadrillage de l'axe X est visible ou non.", + "expressionXY.labelsOrientationConfig.x.help": "Spécifie l'orientation des étiquettes de l'axe X.", + "expressionXY.tickLabelsConfig.x.help": "Spécifie si les étiquettes de graduation de l'axe X sont visibles ou non.", + "expressionXY.axisTitlesVisibilityConfig.x.help": "Spécifie si le titre de l'axe X est visible ou non.", + "expressionXY.xyVis.xTitle.help": "Titre de l'axe X", + "expressionXY.gridlinesConfig.yLeft.help": "Spécifie si le quadrillage de l'axe Y de gauche est visible ou non.", + "expressionXY.labelsOrientationConfig.yLeft.help": "Spécifie l'orientation des étiquettes de l'axe Y de gauche.", + "expressionXY.tickLabelsConfig.yLeft.help": "Spécifie si les étiquettes de graduation de l'axe Y de gauche sont visibles ou non.", + "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "Spécifie si le titre de l'axe Y de gauche est visible ou non.", + "expressionXY.xyVis.yLeftExtent.help": "Portée de l'axe Y de gauche", + "expressionXY.xyVis.yLeftTitle.help": "Titre de l'axe Y de gauche", + "expressionXY.gridlinesConfig.yRight.help": "Spécifie si le quadrillage de l'axe Y de droite est visible ou non.", + "expressionXY.labelsOrientationConfig.yRight.help": "Spécifie l'orientation des étiquettes de l'axe Y de droite.", + "expressionXY.tickLabelsConfig.yRight.help": "Spécifie si les étiquettes de graduation de l'axe Y de droite sont visibles ou non.", + "expressionXY.axisTitlesVisibilityConfig.yRight.help": "Spécifie si le titre de l'axe Y de droite est visible ou non.", + "expressionXY.xyVis.yRightExtent.help": "Portée de l'axe Y de droite", + "expressionXY.xyVis.yRightTitle.help": "Titre de l'axe Y de droite", "xpack.lens.xySuggestions.asPercentageTitle": "Pourcentage", "xpack.lens.xySuggestions.barChartTitle": "Graphique à barres", "xpack.lens.xySuggestions.dateSuggestion": "{yTitle} sur {xTitle}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ab8c2681e7a45..fc47b7ddf92a4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -770,8 +770,11 @@ "xpack.lens.shared.chartValueLabelVisibilityLabel": "ラベル", "xpack.lens.shared.curveLabel": "視覚オプション", "xpack.lens.shared.legend.filterForValueButtonAriaLabel": "値でフィルター", + "expressionXY.legend.filterForValueButtonAriaLabel": "値でフィルター", + "expressionXY.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", "xpack.lens.shared.legend.filterOutValueButtonAriaLabel": "値を除外", + "expressionXY.legend.filterOutValueButtonAriaLabel": "値を除外", "xpack.lens.shared.legendAlignmentLabel": "アラインメント", "xpack.lens.shared.legendInsideAlignmentLabel": "アラインメント", "xpack.lens.shared.legendInsideColumnsLabel": "列の数", @@ -868,34 +871,34 @@ "xpack.lens.xyChart.axisSide.left": "左", "xpack.lens.xyChart.axisSide.right": "右", "xpack.lens.xyChart.axisSide.top": "トップ", - "xpack.lens.xyChart.axisTitlesSettings.help": "xおよびy軸のタイトルを表示", + "expressionXY.xyVis.axisTitlesVisibilitySettings.help": "xおよびy軸のタイトルを表示", "xpack.lens.xyChart.bottomAxisDisabledHelpText": "この設定は、下の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.bottomAxisLabel": "下の軸", "xpack.lens.xyChart.boundaryError": "下界は上界よりも大きくなければなりません", "xpack.lens.xyChart.curveStyleLabel": "曲線", - "xpack.lens.xyChart.curveType.help": "折れ線グラフで曲線タイプをレンダリングする方法を定義します", - "xpack.lens.xyChart.emptyXLabel": "(空)", - "xpack.lens.xyChart.extentMode.help": "範囲モード", - "xpack.lens.xyChart.fillOpacity.help": "エリアグラフの塗りつぶしの透明度を定義", + "expressionXY.xyVis.curveType.help": "折れ線グラフで曲線タイプをレンダリングする方法を定義します", + "expressionXY.xyChart.emptyXLabel": "(空)", + "expressionXY.axisExtentConfig.extentMode.help": "範囲モード", + "expressionXY.xyVis.fillOpacity.help": "エリアグラフの塗りつぶしの透明度を定義", "xpack.lens.xyChart.fillOpacityLabel": "塗りつぶしの透明度", - "xpack.lens.xyChart.fittingFunction.help": "欠測値の処理方法を定義", - "xpack.lens.xyChart.floatingColumns.help": "凡例がグラフ内に表示されるときに列数を指定します。", + "expressionXY.xyVis.fittingFunction.help": "欠測値の処理方法を定義", + "expressionXY.legendConfig.floatingColumns.help": "凡例がグラフ内に表示されるときに列数を指定します。", "xpack.lens.xyChart.Gridlines": "グリッド線", - "xpack.lens.xyChart.gridlinesSettings.help": "xおよびy軸のグリッド線を表示", - "xpack.lens.xyChart.help": "X/Y チャート", - "xpack.lens.xyChart.hideEndzones.help": "部分データの終了ゾーンマーカーを非表示", - "xpack.lens.xyChart.horizontalAlignment.help": "凡例がグラフ内に表示されるときに凡例の横の配置を指定します。", + "expressionXY.xyVis.gridlinesVisibilitySettings.help": "xおよびy軸のグリッド線を表示", + "expressionXY.xyVis.help": "X/Y チャート", + "expressionXY.xyVis.hideEndzones.help": "部分データの終了ゾーンマーカーを非表示", + "expressionXY.legendConfig.horizontalAlignment.help": "凡例がグラフ内に表示されるときに凡例の横の配置を指定します。", "xpack.lens.xyChart.horizontalAxisLabel": "横軸", "xpack.lens.xyChart.horizontalLeftAxisLabel": "横上軸", "xpack.lens.xyChart.horizontalRightAxisLabel": "横下軸", "xpack.lens.xyChart.inclusiveZero": "境界にはゼロを含める必要があります。", - "xpack.lens.xyChart.isInside.help": "凡例がグラフ内に表示されるかどうかを指定します", - "xpack.lens.xyChart.isVisible.help": "判例の表示・非表示を指定します。", - "xpack.lens.xyChart.labelsOrientation.help": "軸ラベルの回転を定義します", + "expressionXY.legendConfig.isInside.help": "凡例がグラフ内に表示されるかどうかを指定します", + "expressionXY.legendConfig.isVisible.help": "判例の表示・非表示を指定します。", + "expressionXY.xyVis.labelsOrientation.help": "軸ラベルの回転を定義します", "xpack.lens.xyChart.layerReferenceLineLabel": "基準線", "xpack.lens.xyChart.leftAxisDisabledHelpText": "この設定は、左の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.leftAxisLabel": "左の軸", - "xpack.lens.xyChart.legend.help": "チャートの凡例を構成します。", + "expressionXY.xyVis.legend.help": "チャートの凡例を構成します。", "xpack.lens.xyChart.legendLocation.inside": "内部", "xpack.lens.xyChart.legendLocation.outside": "外側", "xpack.lens.xyChart.legendVisibility.auto": "自動", @@ -905,50 +908,49 @@ "xpack.lens.xyChart.markerPosition.below": "一番下", "xpack.lens.xyChart.markerPosition.left": "左", "xpack.lens.xyChart.markerPosition.right": "右", - "xpack.lens.xyChart.maxLines.help": "凡例項目ごとの行数を指定します。", + "expressionXY.legendConfig.maxLines.help": "凡例項目ごとの行数を指定します。", "xpack.lens.xyChart.missingValuesLabel": "欠測値", "xpack.lens.xyChart.missingValuesLabelHelpText": "デフォルトでは、Lensではデータのギャップが表示されません。ギャップを埋めるには、選択します。", "xpack.lens.xyChart.nestUnderRoot": "データセット全体", - "xpack.lens.xyChart.position.help": "凡例の配置を指定します。", - "xpack.lens.xyChart.renderer.help": "X/Y チャートを再レンダリング", + "expressionXY.legendConfig.position.help": "凡例の配置を指定します。", + "expressionXY.xyVis.renderer.help": "X/Y チャートを再レンダリング", "xpack.lens.xyChart.rightAxisDisabledHelpText": "この設定は、右の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.rightAxisLabel": "右の軸", "xpack.lens.xyChart.seriesColor.auto": "自動", "xpack.lens.xyChart.seriesColor.label": "系列色", - "xpack.lens.xyChart.shouldTruncate.help": "凡例項目が切り捨てられるかどうかを指定します", + "expressionXY.legendConfig.shouldTruncate.help": "凡例項目が切り捨てられるかどうかを指定します", "xpack.lens.xyChart.showEnzones": "部分データマーカーを表示", - "xpack.lens.xyChart.showSingleSeries.help": "エントリが1件の凡例を表示するかどうかを指定します", + "expressionXY.legendConfig.showSingleSeries.help": "エントリが1件の凡例を表示するかどうかを指定します", "xpack.lens.xyChart.splitSeries": "内訳の基準", "xpack.lens.xyChart.tickLabels": "目盛ラベル", - "xpack.lens.xyChart.tickLabelsSettings.help": "xおよびy軸の目盛ラベルを表示", - "xpack.lens.xyChart.title.help": "軸のタイトル", + "expressionXY.xyVis.tickLabelsVisibilitySettings.help": "xおよびy軸の目盛ラベルを表示", "xpack.lens.xyChart.topAxisDisabledHelpText": "この設定は、上の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.topAxisLabel": "上の軸", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "この設定はヒストグラムで変更できません。", - "xpack.lens.xyChart.valuesInLegend.help": "凡例に値を表示", + "expressionXY.xyVis.valuesInLegend.help": "凡例に値を表示", "xpack.lens.xyChart.valuesPercentageDisabledHelpText": "この設定は割合エリアグラフで変更できません。", "xpack.lens.xyChart.valuesStackedDisabledHelpText": "この設定は積み上げ棒グラフまたは割合棒グラフで変更できません", - "xpack.lens.xyChart.verticalAlignment.help": "凡例がグラフ内に表示されるときに凡例の縦の配置を指定します。", + "expressionXY.legendConfig.verticalAlignment.help": "凡例がグラフ内に表示されるときに凡例の縦の配置を指定します。", "xpack.lens.xyChart.verticalAxisLabel": "縦軸", "xpack.lens.xyChart.verticalLeftAxisLabel": "縦左軸", "xpack.lens.xyChart.verticalRightAxisLabel": "縦右軸", - "xpack.lens.xyChart.xAxisGridlines.help": "x 軸のグリッド線を表示するかどうかを指定します。", - "xpack.lens.xyChart.xAxisLabelsOrientation.help": "x軸のラベルの向きを指定します。", - "xpack.lens.xyChart.xAxisTickLabels.help": "x軸の目盛ラベルを表示するかどうかを指定します。", - "xpack.lens.xyChart.xAxisTitle.help": "x軸のタイトルを表示するかどうかを指定します。", - "xpack.lens.xyChart.xTitle.help": "x軸のタイトル", - "xpack.lens.xyChart.yLeftAxisgridlines.help": "左y軸のグリッド線を表示するかどうかを指定します。", - "xpack.lens.xyChart.yLeftAxisLabelsOrientation.help": "左y軸のラベルの向きを指定します。", - "xpack.lens.xyChart.yLeftAxisTickLabels.help": "左y軸の目盛ラベルを表示するかどうかを指定します。", - "xpack.lens.xyChart.yLeftAxisTitle.help": "左y軸のタイトルを表示するかどうかを指定します。", - "xpack.lens.xyChart.yLeftExtent.help": "Y左軸範囲", - "xpack.lens.xyChart.yLeftTitle.help": "左y軸のタイトル", - "xpack.lens.xyChart.yRightAxisgridlines.help": "右y軸のグリッド線を表示するかどうかを指定します。", - "xpack.lens.xyChart.yRightAxisLabelsOrientation.help": "右y軸のラベルの向きを指定します。", - "xpack.lens.xyChart.yRightAxisTickLabels.help": "右y軸の目盛ラベルを表示するかどうかを指定します。", - "xpack.lens.xyChart.yRightAxisTitle.help": "右y軸のタイトルを表示するかどうかを指定します。", - "xpack.lens.xyChart.yRightExtent.help": "Y右軸範囲", - "xpack.lens.xyChart.yRightTitle.help": "右 y 軸のタイトル", + "expressionXY.gridlinesConfig.x.help": "x 軸のグリッド線を表示するかどうかを指定します。", + "expressionXY.labelsOrientationConfig.x.help": "x軸のラベルの向きを指定します。", + "expressionXY.tickLabelsConfig.x.help": "x軸の目盛ラベルを表示するかどうかを指定します。", + "expressionXY.axisTitlesVisibilityConfig.x.help": "x軸のタイトルを表示するかどうかを指定します。", + "expressionXY.xyVis.xTitle.help": "x軸のタイトル", + "expressionXY.gridlinesConfig.yLeft.help": "左y軸のグリッド線を表示するかどうかを指定します。", + "expressionXY.labelsOrientationConfig.yLeft.help": "左y軸のラベルの向きを指定します。", + "expressionXY.tickLabelsConfig.yLeft.help": "左y軸の目盛ラベルを表示するかどうかを指定します。", + "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "左y軸のタイトルを表示するかどうかを指定します。", + "expressionXY.xyVis.yLeftExtent.help": "Y左軸範囲", + "expressionXY.xyVis.yLeftTitle.help": "左y軸のタイトル", + "expressionXY.gridlinesConfig.yRight.help": "右y軸のグリッド線を表示するかどうかを指定します。", + "expressionXY.labelsOrientationConfig.yRight.help": "右y軸のラベルの向きを指定します。", + "expressionXY.tickLabelsConfig.yRight.help": "右y軸の目盛ラベルを表示するかどうかを指定します。", + "expressionXY.axisTitlesVisibilityConfig.yRight.help": "右y軸のタイトルを表示するかどうかを指定します。", + "expressionXY.xyVis.yRightExtent.help": "Y右軸範囲", + "expressionXY.xyVis.yRightTitle.help": "右 y 軸のタイトル", "xpack.lens.xySuggestions.asPercentageTitle": "割合(%)", "xpack.lens.xySuggestions.barChartTitle": "棒グラフ", "xpack.lens.xySuggestions.dateSuggestion": "{xTitle}の上の {yTitle}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d8d945fb4f06d..8ce79eb897cb5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -775,9 +775,10 @@ "xpack.lens.shared.axisNameLabel": "轴标题", "xpack.lens.shared.chartValueLabelVisibilityLabel": "标签", "xpack.lens.shared.curveLabel": "视觉选项", - "xpack.lens.shared.legend.filterForValueButtonAriaLabel": "筛留值", + "expressionXY.legend.filterForValueButtonAriaLabel": "筛留值", + "expressionXY.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", "xpack.lens.shared.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", - "xpack.lens.shared.legend.filterOutValueButtonAriaLabel": "筛除值", + "expressionXY.legend.filterOutValueButtonAriaLabel": "筛除值", "xpack.lens.shared.legendAlignmentLabel": "对齐方式", "xpack.lens.shared.legendInsideAlignmentLabel": "对齐方式", "xpack.lens.shared.legendInsideColumnsLabel": "列数目", @@ -874,34 +875,34 @@ "xpack.lens.xyChart.axisSide.left": "左", "xpack.lens.xyChart.axisSide.right": "右", "xpack.lens.xyChart.axisSide.top": "顶部", - "xpack.lens.xyChart.axisTitlesSettings.help": "显示 x 和 y 轴标题", + "expressionXY.xyVis.axisTitlesVisibilitySettings.help": "显示 x 和 y 轴标题", "xpack.lens.xyChart.bottomAxisDisabledHelpText": "此设置仅在启用底轴时应用。", "xpack.lens.xyChart.bottomAxisLabel": "底轴", "xpack.lens.xyChart.boundaryError": "下边界必须大于上边界", "xpack.lens.xyChart.curveStyleLabel": "曲线", - "xpack.lens.xyChart.curveType.help": "定义为折线图渲染曲线类型的方式", - "xpack.lens.xyChart.emptyXLabel": "(空)", - "xpack.lens.xyChart.extentMode.help": "范围模式", - "xpack.lens.xyChart.fillOpacity.help": "定义面积图填充透明度", + "expressionXY.xyVis.curveType.help": "定义为折线图渲染曲线类型的方式", + "expressionXY.xyChart.emptyXLabel": "(空)", + "expressionXY.axisExtentConfig.extentMode.help": "范围模式", + "expressionXY.xyVis.fillOpacity.help": "定义面积图填充透明度", "xpack.lens.xyChart.fillOpacityLabel": "填充透明度", - "xpack.lens.xyChart.fittingFunction.help": "定义处理缺失值的方式", - "xpack.lens.xyChart.floatingColumns.help": "指定图例显示在图表内时的列数。", + "expressionXY.xyVis.fittingFunction.help": "定义处理缺失值的方式", + "expressionXY.legendConfig.floatingColumns.help": "指定图例显示在图表内时的列数。", "xpack.lens.xyChart.Gridlines": "网格线", - "xpack.lens.xyChart.gridlinesSettings.help": "显示 x 和 y 轴网格线", - "xpack.lens.xyChart.help": "X/Y 图表", - "xpack.lens.xyChart.hideEndzones.help": "隐藏部分数据的末日区域标记", - "xpack.lens.xyChart.horizontalAlignment.help": "指定图例显示在图表内时水平对齐。", + "expressionXY.xyVis.gridlinesVisibilitySettings.help": "显示 x 和 y 轴网格线", + "expressionXY.xyVis.help": "X/Y 图表", + "expressionXY.xyVis.hideEndzones.help": "隐藏部分数据的末日区域标记", + "expressionXY.legendConfig.horizontalAlignment.help": "指定图例显示在图表内时水平对齐。", "xpack.lens.xyChart.horizontalAxisLabel": "水平轴", "xpack.lens.xyChart.horizontalLeftAxisLabel": "水平顶轴", "xpack.lens.xyChart.horizontalRightAxisLabel": "水平底轴", "xpack.lens.xyChart.inclusiveZero": "边界必须包括零。", - "xpack.lens.xyChart.isInside.help": "指定图例是否在图表内", - "xpack.lens.xyChart.isVisible.help": "指定图例是否可见。", - "xpack.lens.xyChart.labelsOrientation.help": "定义轴标签的旋转", + "expressionXY.legendConfig.isInside.help": "指定图例是否在图表内", + "expressionXY.legendConfig.isVisible.help": "指定图例是否可见。", + "expressionXY.xyVis.labelsOrientation.help": "定义轴标签的旋转", "xpack.lens.xyChart.layerReferenceLineLabel": "参考线", "xpack.lens.xyChart.leftAxisDisabledHelpText": "此设置仅在启用左轴时应用。", "xpack.lens.xyChart.leftAxisLabel": "左轴", - "xpack.lens.xyChart.legend.help": "配置图表图例。", + "expressionXY.xyVis.legend.help": "配置图表图例。", "xpack.lens.xyChart.legendLocation.inside": "内部", "xpack.lens.xyChart.legendLocation.outside": "外部", "xpack.lens.xyChart.legendVisibility.auto": "自动", @@ -911,50 +912,49 @@ "xpack.lens.xyChart.markerPosition.below": "底部", "xpack.lens.xyChart.markerPosition.left": "左", "xpack.lens.xyChart.markerPosition.right": "右", - "xpack.lens.xyChart.maxLines.help": "指定每个图例项的行数。", + "expressionXY.legendConfig.maxLines.help": "指定每个图例项的行数。", "xpack.lens.xyChart.missingValuesLabel": "缺少的值", "xpack.lens.xyChart.missingValuesLabelHelpText": "默认情况下,Lens 隐藏数据中的缺口。要填充缺口,请进行选择。", "xpack.lens.xyChart.nestUnderRoot": "整个数据集", - "xpack.lens.xyChart.position.help": "指定图例位置。", - "xpack.lens.xyChart.renderer.help": "X/Y 图表呈现器", + "expressionXY.legendConfig.position.help": "指定图例位置。", + "expressionXY.xyVis.renderer.help": "X/Y 图表呈现器", "xpack.lens.xyChart.rightAxisDisabledHelpText": "此设置仅在启用右轴时应用。", "xpack.lens.xyChart.rightAxisLabel": "右轴", "xpack.lens.xyChart.seriesColor.auto": "自动", "xpack.lens.xyChart.seriesColor.label": "系列颜色", - "xpack.lens.xyChart.shouldTruncate.help": "指定是否将截断图例项", + "expressionXY.legendConfig.shouldTruncate.help": "指定是否将截断图例项", "xpack.lens.xyChart.showEnzones": "显示部分数据标记", - "xpack.lens.xyChart.showSingleSeries.help": "指定是否应显示只包含一个条目的图例", + "expressionXY.legendConfig.showSingleSeries.help": "指定是否应显示只包含一个条目的图例", "xpack.lens.xyChart.splitSeries": "细分方式", "xpack.lens.xyChart.tickLabels": "刻度标签", - "xpack.lens.xyChart.tickLabelsSettings.help": "显示 x 和 y 轴刻度标签", - "xpack.lens.xyChart.title.help": "轴标题", + "expressionXY.xyVis.tickLabelsVisibilitySettings.help": "显示 x 和 y 轴刻度标签", "xpack.lens.xyChart.topAxisDisabledHelpText": "此设置仅在启用顶轴时应用。", "xpack.lens.xyChart.topAxisLabel": "顶轴", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "不能在直方图上更改此设置。", - "xpack.lens.xyChart.valuesInLegend.help": "在图例中显示值", + "expressionXY.xyVis.valuesInLegend.help": "在图例中显示值", "xpack.lens.xyChart.valuesPercentageDisabledHelpText": "不能在百分比面积图上更改此设置。", "xpack.lens.xyChart.valuesStackedDisabledHelpText": "不能在堆积图或百分比条形图上更改此设置", - "xpack.lens.xyChart.verticalAlignment.help": "指定图例显示在图表内时垂直对齐。", + "expressionXY.legendConfig.verticalAlignment.help": "指定图例显示在图表内时垂直对齐。", "xpack.lens.xyChart.verticalAxisLabel": "垂直轴", "xpack.lens.xyChart.verticalLeftAxisLabel": "垂直左轴", "xpack.lens.xyChart.verticalRightAxisLabel": "垂直右轴", - "xpack.lens.xyChart.xAxisGridlines.help": "指定 x 轴的网格线是否可见。", - "xpack.lens.xyChart.xAxisLabelsOrientation.help": "指定 x 轴的标签方向。", - "xpack.lens.xyChart.xAxisTickLabels.help": "指定 x 轴的刻度标签是否可见。", - "xpack.lens.xyChart.xAxisTitle.help": "指定 x 轴的标题是否可见。", - "xpack.lens.xyChart.xTitle.help": "X 轴标题", - "xpack.lens.xyChart.yLeftAxisgridlines.help": "指定左侧 y 轴的网格线是否可见。", - "xpack.lens.xyChart.yLeftAxisLabelsOrientation.help": "指定左 y 轴的标签方向。", - "xpack.lens.xyChart.yLeftAxisTickLabels.help": "指定左侧 y 轴的刻度标签是否可见。", - "xpack.lens.xyChart.yLeftAxisTitle.help": "指定左侧 y 轴的标题是否可见。", - "xpack.lens.xyChart.yLeftExtent.help": "左侧 Y 轴范围", - "xpack.lens.xyChart.yLeftTitle.help": "左侧 Y 轴标题", - "xpack.lens.xyChart.yRightAxisgridlines.help": "指定右侧 y 轴的网格线是否可见。", - "xpack.lens.xyChart.yRightAxisLabelsOrientation.help": "指定右 y 轴的标签方向。", - "xpack.lens.xyChart.yRightAxisTickLabels.help": "指定右侧 y 轴的刻度标签是否可见。", - "xpack.lens.xyChart.yRightAxisTitle.help": "指定右侧 y 轴的标题是否可见。", - "xpack.lens.xyChart.yRightExtent.help": "右侧 Y 轴范围", - "xpack.lens.xyChart.yRightTitle.help": "右侧 Y 轴标题", + "expressionXY.gridlinesConfig.x.help": "指定 x 轴的网格线是否可见。", + "expressionXY.labelsOrientationConfig.x.help": "指定 x 轴的标签方向。", + "expressionXY.tickLabelsConfig.x.help": "指定 x 轴的刻度标签是否可见。", + "expressionXY.axisTitlesVisibilityConfig.x.help": "指定 x 轴的标题是否可见。", + "expressionXY.xyVis.xTitle.help": "X 轴标题", + "expressionXY.gridlinesConfig.yLeft.help": "指定左侧 y 轴的网格线是否可见。", + "expressionXY.labelsOrientationConfig.yLeft.help": "指定左 y 轴的标签方向。", + "expressionXY.tickLabelsConfig.yLeft.help": "指定左侧 y 轴的刻度标签是否可见。", + "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "指定左侧 y 轴的标题是否可见。", + "expressionXY.xyVis.yLeftExtent.help": "左侧 Y 轴范围", + "expressionXY.xyVis.yLeftTitle.help": "左侧 Y 轴标题", + "expressionXY.gridlinesConfig.yRight.help": "指定右侧 y 轴的网格线是否可见。", + "expressionXY.labelsOrientationConfig.yRight.help": "指定右 y 轴的标签方向。", + "expressionXY.tickLabelsConfig.yRight.help": "指定右侧 y 轴的刻度标签是否可见。", + "expressionXY.axisTitlesVisibilityConfig.yRight.help": "指定右侧 y 轴的标题是否可见。", + "expressionXY.xyVis.yRightExtent.help": "右侧 Y 轴范围", + "expressionXY.xyVis.yRightTitle.help": "右侧 Y 轴标题", "xpack.lens.xySuggestions.asPercentageTitle": "百分比", "xpack.lens.xySuggestions.barChartTitle": "条形图", "xpack.lens.xySuggestions.dateSuggestion": "{yTitle} / {xTitle}", diff --git a/x-pack/test/examples/embedded_lens/embedded_example.ts b/x-pack/test/examples/embedded_lens/embedded_example.ts index d11495f0450b4..bdd881b3ea318 100644 --- a/x-pack/test/examples/embedded_lens/embedded_example.ts +++ b/x-pack/test/examples/embedded_lens/embedded_example.ts @@ -27,12 +27,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('embedded_lens_example'); await elasticChart.setNewChartUiDebugFlag(true); await testSubjects.click('lns-example-change-time-range'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); }); it('should show chart', async () => { await testSubjects.click('lns-example-change-color'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await checkData(); }); @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should load Lens editor', async () => { await testSubjects.click('lns-example-open-editor'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await checkData(); }); }); diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 92cdc72ffc81a..71ef909ffa24a 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -137,7 +137,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // check at least one visualization await renderable.waitForRender(); log.debug('Checking charts rendered'); - await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); + await elasticChart.waitForRenderComplete('xyVisChart'); await appMenu.clickLink('Discover'); await retry.try(async function () { @@ -148,7 +148,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking charts rendered'); - await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); + await elasticChart.waitForRenderComplete('xyVisChart'); }); it('toggle from Discover to Dashboard attempt 1', async () => { @@ -161,7 +161,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking charts rendered'); - await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); + await elasticChart.waitForRenderComplete('xyVisChart'); }); it('toggle from Discover to Dashboard attempt 2', async () => { @@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking charts rendered'); - await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); + await elasticChart.waitForRenderComplete('xyVisChart'); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(10); diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts index 382449e5e2586..9e4c2554100b9 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts @@ -92,7 +92,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { operation: 'average', field: 'bytes', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await PageObjects.lens.notLinkedToOriginatingApp(); // return to origin should not be present in save modal diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts index 1d2d3f6862e43..9eeb49f5eb0d2 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/add_to_dashboard.ts index d9bdfbd1ffb6a..5fbfdd0a5806e 100644 --- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/add_to_dashboard.ts @@ -254,11 +254,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await PageObjects.lens.waitForVisualization(); - await PageObjects.lens.switchToVisualization('heatmap', 'heat'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); await PageObjects.lens.openDimensionEditor('lnsHeatmap_cellPanel > lns-dimensionTrigger'); await PageObjects.lens.openPalettePanel('lnsHeatmap'); await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); diff --git a/x-pack/test/functional/apps/lens/chart_data.ts b/x-pack/test/functional/apps/lens/chart_data.ts index 8a43ff909fea5..ff2f5b339fe53 100644 --- a/x-pack/test/functional/apps/lens/chart_data.ts +++ b/x-pack/test/functional/apps/lens/chart_data.ts @@ -32,8 +32,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'average', field: 'bytes', }); - - await PageObjects.lens.waitForVisualization(); }); const expectedData = [ @@ -75,34 +73,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } it('should render xy chart', async () => { + await PageObjects.lens.waitForVisualization('xyVisChart'); + const data = await PageObjects.lens.getCurrentChartDebugState(); assertMatchesExpectedData(data!); }); it('should render pie chart', async () => { await PageObjects.lens.switchToVisualization('pie'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('partitionVisChart'); const data = await PageObjects.lens.getCurrentChartDebugState(); assertMatchesExpectedPieData(data!); }); it('should render donut chart', async () => { await PageObjects.lens.switchToVisualization('donut'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('partitionVisChart'); const data = await PageObjects.lens.getCurrentChartDebugState(); assertMatchesExpectedPieData(data!); }); it('should render treemap chart', async () => { await PageObjects.lens.switchToVisualization('treemap', 'treemap'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('partitionVisChart'); const data = await PageObjects.lens.getCurrentChartDebugState(); assertMatchesExpectedPieData(data!); }); it('should render heatmap chart', async () => { await PageObjects.lens.switchToVisualization('heatmap', 'heat'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); if (!debugState) { @@ -150,7 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render metric', async () => { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); }); }); diff --git a/x-pack/test/functional/apps/lens/disable_auto_apply.ts b/x-pack/test/functional/apps/lens/disable_auto_apply.ts index 3660de10ecd47..e280bbd148493 100644 --- a/x-pack/test/functional/apps/lens/disable_auto_apply.ts +++ b/x-pack/test/functional/apps/lens/disable_auto_apply.ts @@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.applyChanges(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); }); it('should hide suggestions when a change is made', async () => { diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index 1a7b8e96d6802..d5b929481e6dc 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const xyChartContainer = 'xyVisChart'; describe('lens drag and drop tests', () => { describe('basic drag and drop', () => { @@ -18,7 +19,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', xyChartContainer); expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( '@timestamp' @@ -136,7 +137,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { it('Should duplicate and swap elements when dragging over secondary drop targets', async () => { await PageObjects.lens.removeLayer(); await PageObjects.lens.switchToVisualization('bar'); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', xyChartContainer); await PageObjects.lens.dragDimensionToExtraDropType( 'lnsXY_xDimensionPanel > lns-dimensionTrigger', @@ -165,8 +166,8 @@ export default function ({ getPageObjects }: FtrProviderContext) { it('should combine breakdown dimension with the horizontal one', async () => { await PageObjects.lens.removeLayer(); - await PageObjects.lens.dragFieldToWorkspace('clientip'); - await PageObjects.lens.dragFieldToWorkspace('@message.raw'); + await PageObjects.lens.dragFieldToWorkspace('clientip', xyChartContainer); + await PageObjects.lens.dragFieldToWorkspace('@message.raw', xyChartContainer); await PageObjects.lens.dragDimensionToExtraDropType( 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', @@ -180,7 +181,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { it('should combine field to existing horizontal dimension', async () => { await PageObjects.lens.removeLayer(); - await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.lens.dragFieldToWorkspace('clientip', xyChartContainer); await PageObjects.lens.dragFieldToExtraDropType( '@message.raw', @@ -194,7 +195,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { it('should combine two multi terms dimensions', async () => { await PageObjects.lens.removeLayer(); - await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.lens.dragFieldToWorkspace('clientip', xyChartContainer); await PageObjects.lens.dragFieldToExtraDropType( '@message.raw', @@ -313,10 +314,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); - await PageObjects.lens.waitForVisualization(); - await PageObjects.lens.dragFieldToWorkspace('clientip'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', xyChartContainer); + await PageObjects.lens.waitForVisualization(xyChartContainer); + await PageObjects.lens.dragFieldToWorkspace('clientip', xyChartContainer); + await PageObjects.lens.waitForVisualization(xyChartContainer); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top 3 values of clientip']); @@ -329,11 +330,11 @@ export default function ({ getPageObjects }: FtrProviderContext) { it('overwrite existing time dimension if one exists already', async () => { await PageObjects.lens.searchField('utc'); - await PageObjects.lens.dragFieldToWorkspace('utc_time'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.dragFieldToWorkspace('utc_time', xyChartContainer); + await PageObjects.lens.waitForVisualization(xyChartContainer); await PageObjects.lens.searchField('client'); - await PageObjects.lens.dragFieldToWorkspace('clientip'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.dragFieldToWorkspace('clientip', xyChartContainer); + await PageObjects.lens.waitForVisualization(xyChartContainer); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ 'utc_time', ]); diff --git a/x-pack/test/functional/apps/lens/epoch_millis.ts b/x-pack/test/functional/apps/lens/epoch_millis.ts index deaa3e720101e..d882d69ddd1fd 100644 --- a/x-pack/test/functional/apps/lens/epoch_millis.ts +++ b/x-pack/test/functional/apps/lens/epoch_millis.ts @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'count', field: 'Records', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('1'); }); @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.enableTimeShift(); await PageObjects.lens.setTimeShift('3d'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2'); }); }); diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index c2e42328dc943..02d4fda0e96a6 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.switchToFormula(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // .echLegendItem__title is the only viable way of getting the xy chart's // legend item(s), so we're using a class selector here. // 4th item is the other bucket @@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'formula', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); expect(await PageObjects.lens.getErrorCount()).to.eql(0); }); diff --git a/x-pack/test/functional/apps/lens/gauge.ts b/x-pack/test/functional/apps/lens/gauge.ts index cce05d7b9baba..c21ddf5b70791 100644 --- a/x-pack/test/functional/apps/lens/gauge.ts +++ b/x-pack/test/functional/apps/lens/gauge.ts @@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'average', field: 'bytes', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); }); it('should switch to gauge and render a gauge with default values', async () => { diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts index 946de0c9c8e93..1386e1beea899 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/heatmap.ts @@ -33,12 +33,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); }); it('should render heatmap chart with the temperature palette', async () => { await PageObjects.lens.switchToVisualization('heatmap', 'heat'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); if (!debugState) { @@ -78,7 +78,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { typeCharByChar: true, }); }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); @@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should not change when passing from percentage to number', async () => { await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); @@ -124,7 +124,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_0', '0', { clearWithKeyboard: true, }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); @@ -144,7 +144,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should reset stop numbers when changing palette', async () => { await PageObjects.lens.changePaletteTo('status'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); @@ -164,7 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should not change when passing from number to percent', async () => { await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_percent'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('heatmapChart'); const debugState = await PageObjects.lens.getCurrentChartDebugState(); diff --git a/x-pack/test/functional/apps/lens/inspector.ts b/x-pack/test/functional/apps/lens/inspector.ts index 9db804d324936..d94d3413c07b0 100644 --- a/x-pack/test/functional/apps/lens/inspector.ts +++ b/x-pack/test/functional/apps/lens/inspector.ts @@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await inspector.open('lnsApp_inspectButton'); }); diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index ff677440e8fe0..445caa1abbec2 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.navigationalSearch.clickOnOption(0); await PageObjects.lens.waitForEmptyWorkspace(); await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'mtrVis'); }); it('preserves time range', async () => { // fill the navigation search and select empty @@ -120,7 +120,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.waitForEmptyWorkspace(); await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'mtrVis'); const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); expect(timePickerValues.start).to.eql(PageObjects.timePicker.defaultStartTime); diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/rollup.ts index 7de0d7e76c95c..25ab766d04bbd 100644 --- a/x-pack/test/functional/apps/lens/rollup.ts +++ b/x-pack/test/functional/apps/lens/rollup.ts @@ -86,12 +86,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'sum', field: 'bytes', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); await PageObjects.lens.assertMetric('Sum of bytes', '16,788'); await PageObjects.lens.switchFirstLayerIndexPattern('lens_rolled_up_data'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('mtrVis'); await PageObjects.lens.assertMetric('Sum of bytes', '16,788'); }); diff --git a/x-pack/test/functional/apps/lens/show_underlying_data.ts b/x-pack/test/functional/apps/lens/show_underlying_data.ts index aa2bc03ec3069..4bc8be22eb8f4 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/show_underlying_data.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); await PageObjects.lens.goToTimeRange(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'extension.raw', }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // expect the button is shown and enabled @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // expect the button is shown and enabled await testSubjects.clickWhenNotDisabled(`lnsApp_openInDiscover`); @@ -96,7 +96,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await testSubjects.clickWhenNotDisabled(`lnsApp_openInDiscover`); @@ -132,7 +132,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // expect the button is shown and enabled await testSubjects.clickWhenNotDisabled(`lnsApp_openInDiscover`); @@ -167,7 +167,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.setFilterBy('bytes > 4000'); await PageObjects.common.sleep(1000); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // expect the button is shown and enabled await testSubjects.clickWhenNotDisabled(`lnsApp_openInDiscover`); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index f0cc3b0da7201..c2a98d2d5dedc 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await listingTable.searchForItemWithName('Afancilenstest'); await PageObjects.lens.clickVisualizeListItemTitle('Afancilenstest'); await PageObjects.lens.goToTimeRange(); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); expect(await PageObjects.lens.getTitle()).to.eql('Afancilenstest'); @@ -82,7 +82,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { keepOpen: true, }); await PageObjects.lens.addFilterToAgg(`geo.src : CN`); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // Verify that the field was persisted from the transition expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); @@ -199,7 +199,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const longLabel = 'Veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryvery long label wrapping multiple lines'; await PageObjects.lens.editDimensionLabel(longLabel); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( @@ -239,19 +239,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.changeAxisSide('right'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); let data = await PageObjects.lens.getCurrentChartDebugState(); expect(data?.axes?.y.length).to.eql(2); expect(data?.axes?.y.some(({ position }) => position === 'right')).to.eql(true); await PageObjects.lens.changeAxisSide('left'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); data = await PageObjects.lens.getCurrentChartDebugState(); expect(data?.axes?.y.length).to.eql(1); expect(data?.axes?.y.some(({ position }) => position === 'right')).to.eql(false); await PageObjects.lens.changeAxisSide('right'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); await PageObjects.lens.closeDimensionEditor(); }); @@ -261,7 +261,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.openVisualOptions(); await testSubjects.click('lns_valueLabels_inside'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // check for value labels let data = await PageObjects.lens.getCurrentChartDebugState(); @@ -269,7 +269,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // switch to stacked bar chart await PageObjects.lens.switchToVisualization('bar_stacked'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); // check for value labels data = await PageObjects.lens.getCurrentChartDebugState(); @@ -282,14 +282,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.setValue('lnsyLeftAxisTitle', axisTitle, { clearWithKeyboard: true, }); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); let data = await PageObjects.lens.getCurrentChartDebugState(); expect(data?.axes?.y?.[0].title).to.eql(axisTitle); // hide the gridlines await testSubjects.click('lnsshowyLeftAxisGridlines'); - await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.waitForVisualization('xyVisChart'); data = await PageObjects.lens.getCurrentChartDebugState(); expect(data?.axes?.y?.[0].gridlines.length).to.eql(0); diff --git a/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts b/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts index 0856fbb4ff1ec..0315d20e5fc91 100644 --- a/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts +++ b/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts @@ -48,7 +48,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('visualizes field to Lens and loads fields to the dimesion editor', async () => { const button = await testSubjects.find('visualizeEditInLensButton'); await button.click(); - await lens.waitForVisualization(); + await lens.waitForVisualization('xyVisChart'); await retry.try(async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); expect(dimensions).to.have.length(2); @@ -72,7 +72,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await header.waitUntilLoadingHasFinished(); const button = await testSubjects.find('visualizeEditInLensButton'); await button.click(); - await lens.waitForVisualization(); + await lens.waitForVisualization('xyVisChart'); expect(await filterBar.hasFilter('extension', 'css')).to.be(true); }); @@ -86,7 +86,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await header.waitUntilLoadingHasFinished(); const button = await testSubjects.find('visualizeEditInLensButton'); await button.click(); - await lens.waitForVisualization(); + await lens.waitForVisualization('xyVisChart'); expect(await queryBar.getQueryString()).to.equal('machine.os : ios'); }); @@ -128,7 +128,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const button = await testSubjects.find('visualizeEditInLensButton'); await button.click(); - await lens.waitForVisualization(); + await lens.waitForVisualization('xyVisChart'); await retry.try(async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); expect(await dimensions[1].getVisibleText()).to.be('Count of records'); @@ -157,7 +157,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const button = await testSubjects.find('visualizeEditInLensButton'); await button.click(); - await lens.waitForVisualization(); + await lens.waitForVisualization('mtrVis'); await retry.try(async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); expect(await dimensions[1].getVisibleText()).to.be('Count of records'); diff --git a/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts b/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts index daa490f8ef051..420f895fe6aa6 100644 --- a/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts +++ b/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); - await PageObjects.lens.dragFieldToWorkspace('geo.dest'); + await PageObjects.lens.dragFieldToWorkspace('geo.dest', 'xyVisChart'); // add filter to force data fetch to set activeData await filterBar.addFilter('bytes', 'is between', '200', '10000'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index a20962e607af2..a65468e0ca3ec 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -254,7 +254,10 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { const lensMetricField = testData.expected.metricFields![0]; if (lensMetricField) { - await ml.dataVisualizerTable.assertLensActionShowChart(lensMetricField.fieldName); + await ml.dataVisualizerTable.assertLensActionShowChart( + lensMetricField.fieldName, + 'mtrVis' + ); await ml.navigation.browserBackTo('dataVisualizerTable'); } const lensNonMetricField = testData.expected.nonMetricFields?.find( @@ -262,7 +265,10 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); if (lensNonMetricField) { - await ml.dataVisualizerTable.assertLensActionShowChart(lensNonMetricField.fieldName); + await ml.dataVisualizerTable.assertLensActionShowChart( + lensNonMetricField.fieldName, + 'mtrVis' + ); await ml.navigation.browserBackTo('dataVisualizerTable'); } }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index a4f6493d513cb..fa46052705c91 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -201,7 +201,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * * @param field - the desired field for the dimension * */ - async dragFieldToWorkspace(field: string) { + async dragFieldToWorkspace(field: string, visualizationTestSubj?: string) { const from = `lnsFieldListPanelField-${field}`; await find.existsByCssSelector(from); await browser.html5DragAndDrop( @@ -209,7 +209,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont testSubjects.getCssSelector('lnsWorkspace') ); await this.waitForLensDragDropToFinish(); - await this.waitForVisualization(); + await this.waitForVisualization(visualizationTestSubj); }, /** diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index cf9b1f8fa35a5..e5c0dafbd00f7 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -565,12 +565,12 @@ export function MachineLearningDataVisualizerTableProvider( } } - public async assertLensActionShowChart(fieldName: string) { + public async assertLensActionShowChart(fieldName: string, visualizationContainer?: string) { await retry.tryForTime(30 * 1000, async () => { await testSubjects.clickWhenNotDisabled( this.rowSelector(fieldName, 'dataVisualizerActionViewInLensButton') ); - await testSubjects.existOrFail('lnsVisualizationContainer', { + await testSubjects.existOrFail(visualizationContainer ?? 'lnsVisualizationContainer', { timeout: 15 * 1000, }); }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts index 71bf03365e66d..d257a2fb560dd 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session_relative_time.ts @@ -63,7 +63,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await searchSessions.save(); await searchSessions.expectState('backgroundCompleted'); - await checkSampleDashboardLoaded(); + await checkSampleDashboardLoaded('xyVisChart'); // load URL to restore a saved session await PageObjects.searchSessionsManagement.goTo(); @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); - await checkSampleDashboardLoaded(); + await checkSampleDashboardLoaded('xyVisChart'); // Check that session is restored await searchSessions.expectState('restored'); @@ -83,11 +83,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // HELPERS - async function checkSampleDashboardLoaded() { + async function checkSampleDashboardLoaded(visualizationContainer?: string) { log.debug('Checking no error labels'); await testSubjects.missingOrFail('embeddableErrorLabel'); log.debug('Checking charts rendered'); - await elasticChart.waitForRenderComplete('lnsVisualizationContainer'); + await elasticChart.waitForRenderComplete(visualizationContainer ?? 'lnsVisualizationContainer'); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(11); log.debug('Checking input controls rendered');