From d9aa72c7f8b956546cc4d1f8fb185871eb695378 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 4 Feb 2022 20:14:34 +0200 Subject: [PATCH] [PieVis] Lens adaptation. (#122420) * Added config for mosaic/pie/donut/treemap/waffle. * Added sortPredicate functionality for waffle/mosaic/treemap/pie/donut * Added Donut handling. * Refactored get_color. * Merged color computation for lens and vis_types. * Added isFlatLegend support. * Added showValuesInLegend for waffle and fixed tests. * Removed not used position, which is equivalent to labels.show = false. * legendDisplay added. * Added migrations for pieVis addLegend argument. * Added startFromSecondLargestSlice and support of correct formatters. * Updated docs. * Added functionality for truncate. * Added unit tests for pie and partial for donut/waffle. * Addressed issue with label truncation by default. * Addressed issue with formatters. * Added tests for accessor.test.ts * Added support of formatter by meta data from columns at splitChartAccessors. * Added tests for filterOutConfig. * Added tests for getFormatters. * Added tests for getAvailableFormatter. * Added tests for getFormatter. * Added tests for get_split_dimension_accessor. * Add is legend scenario. * Added tests for legend. * Replaced sortPredicate, relying on the internal terms params, with the mosaic one. * Fixed pie snapshot and added new snapshot for treemap. * Added snapshots for mosaicVis. * Added snapshot to waffleVis. * Updated unit tests for *_vis_function's. * Added storybook. * Added snapshots for partition vis component. * Added expression error on providing both, splitColumn && splitRow. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- .i18nrc.json | 2 +- docs/developer/plugin-list.asciidoc | 4 +- packages/kbn-optimizer/limits.yml | 2 +- src/dev/storybook/aliases.ts | 2 +- .../.storybook/main.js | 0 .../expression_partition_vis/README.md | 9 + .../common/constants.ts | 20 + .../mosaic_vis_function.test.ts.snap | 188 ++ .../pie_vis_function.test.ts.snap | 288 +++ .../treemap_vis_function.test.ts.snap | 188 ++ .../waffle_vis_function.test.ts.snap | 168 ++ .../common/expression_functions/i18n.ts | 126 ++ .../common/expression_functions/index.ts | 13 + .../mosaic_vis_function.test.ts | 145 ++ .../mosaic_vis_function.ts | 131 ++ .../partition_labels_function.ts | 105 + .../pie_vis_function.test.ts | 69 +- .../expression_functions/pie_vis_function.ts | 153 ++ .../treemap_vis_function.test.ts | 145 ++ .../treemap_vis_function.ts | 131 ++ .../waffle_vis_function.test.ts | 116 + .../waffle_vis_function.ts | 126 ++ .../expression_partition_vis/common/index.ts | 52 + .../common/types/expression_functions.ts | 93 + .../common/types/expression_renderers.ts | 77 +- .../common/types/index.ts | 0 .../jest.config.js | 6 +- .../kibana.json | 4 +- .../public/__mocks__/format_service.ts | 2 +- .../public/__mocks__/index.ts | 0 .../public/__mocks__/palettes.ts | 7 +- .../public/__mocks__/start_deps.ts | 0 .../public/__mocks__/theme.ts | 6 +- .../public/__mocks__/ui_settings.ts | 31 + .../mosaic_vis_renderer.stories.tsx | 51 + .../__stories__/pie_vis_renderer.stories.tsx | 51 + .../public/__stories__/shared/arg_types.ts | 216 ++ .../public/__stories__/shared/config.ts | 129 ++ .../public/__stories__/shared/data.ts | 207 ++ .../public/__stories__/shared/index.ts | 17 + .../treemap_vis_renderer.stories.tsx | 51 + .../waffle_vis_renderer.stories.tsx | 51 + .../partition_vis_component.test.tsx.snap | 1993 +++++++++++++++++ .../public/components/chart_split.tsx | 0 .../public/components}/index.ts | 2 +- .../partition_vis_component.styles.ts} | 7 +- .../partition_vis_component.test.tsx} | 113 +- .../components/partition_vis_component.tsx} | 172 +- .../components/visualization_noresults.tsx | 4 +- .../public/expression_renderers}/index.ts | 3 +- .../partition_vis_renderer.tsx} | 19 +- .../public}/index.ts | 6 +- .../public/mocks.ts | 89 +- .../public/plugin.ts | 30 +- .../public/types.ts | 4 +- .../public/utils/accessor.test.ts | 50 + .../public/utils/accessor.ts | 0 .../public/utils/filter_helpers.test.ts | 0 .../public/utils/filter_helpers.ts | 0 .../public/utils/filter_out_config.test.ts | 47 + .../public/utils/filter_out_config.ts | 22 + .../public/utils/formatters.test.ts | 186 ++ .../public/utils/formatters.ts | 52 + .../public/utils/get_color_picker.test.tsx | 0 .../public/utils/get_color_picker.tsx | 0 .../public/utils/get_columns.test.ts | 57 +- .../public/utils/get_columns.ts | 40 +- .../public/utils/get_distinct_series.test.ts | 0 .../public/utils/get_distinct_series.ts | 15 +- .../public/utils/get_legend_actions.tsx | 10 +- .../public/utils/get_partition_theme.test.ts | 496 ++++ .../public/utils/get_partition_theme.ts | 165 ++ .../public/utils/get_partition_type.ts | 19 + .../get_split_dimension_accessor.test.ts | 166 ++ .../utils/get_split_dimension_accessor.ts | 36 + .../public/utils/index.ts | 5 +- .../public/utils/layers/get_color.ts | 237 ++ .../public/utils/layers}/get_layers.test.ts | 35 +- .../public/utils/layers/get_layers.ts | 84 + .../public/utils/layers/get_node_labels.ts | 25 + .../public/utils/layers}/index.ts | 2 +- .../public/utils/layers/sort_predicate.ts | 77 + .../public/utils/legend.test.ts | 140 ++ .../public/utils/legend.ts | 45 + .../server}/index.ts | 6 +- .../expression_partition_vis/server/plugin.ts | 43 + .../server/types.ts | 4 +- .../tsconfig.json | 0 .../expression_pie/README.md | 9 - .../expression_pie/common/constants.ts | 16 - .../pie_vis_function.test.ts.snap | 98 - .../pie_labels_function.ts | 88 - .../expression_functions/pie_vis_function.ts | 183 -- .../expression_pie/common/index.ts | 32 - .../common/types/expression_functions.ts | 46 - .../__stories__/pie_renderer.stories.tsx | 115 - .../expression_pie/public/utils/get_layers.ts | 201 -- .../public/utils/get_partition_theme.test.ts | 72 - .../public/utils/get_partition_theme.ts | 85 - .../utils/get_split_dimension_accessor.ts | 31 - .../expression_pie/server/plugin.ts | 23 - src/plugins/vis_types/pie/kibana.json | 2 +- .../public/__snapshots__/to_ast.test.ts.snap | 18 +- .../pie/public/editor/collections.ts | 2 +- .../pie/public/editor/components/index.tsx | 4 +- .../pie/public/editor/components/pie.tsx | 46 +- .../pie/public/sample_vis.test.mocks.ts | 6 +- .../vis_types/pie/public/to_ast.test.ts | 4 +- src/plugins/vis_types/pie/public/to_ast.ts | 19 +- .../vis_types/pie/public/to_ast_esaggs.ts | 4 +- .../vis_types/pie/public/vis_type/pie.ts | 9 +- src/plugins/vis_types/pie/tsconfig.json | 2 +- .../public/__snapshots__/pie_fn.test.ts.snap | 2 +- .../__snapshots__/to_ast_pie.test.ts.snap | 2 +- src/plugins/vis_types/vislib/public/pie.ts | 11 +- .../vis_types/vislib/public/pie_fn.test.ts | 2 +- .../vislib/public/vis_controller.tsx | 22 +- .../vislib/visualizations/pie_chart.test.js | 4 +- .../make_visualize_embeddable_factory.ts | 7 + .../visualization_common_migrations.ts | 16 + ...ualization_saved_object_migrations.test.ts | 70 + .../visualization_saved_object_migrations.ts | 26 + .../translations/translations/ja-JP.json | 30 +- .../translations/translations/zh-CN.json | 277 +-- 125 files changed, 7718 insertions(+), 1556 deletions(-) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/.storybook/main.js (100%) create mode 100755 src/plugins/chart_expressions/expression_partition_vis/README.md create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/constants.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/index.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/common/expression_functions/pie_vis_function.test.ts (56%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts create mode 100755 src/plugins/chart_expressions/expression_partition_vis/common/index.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/common/types/expression_renderers.ts (59%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/common/types/index.ts (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/jest.config.js (72%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/kibana.json (55%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/__mocks__/format_service.ts (88%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/__mocks__/index.ts (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/__mocks__/palettes.ts (87%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/__mocks__/start_deps.ts (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/__mocks__/theme.ts (79%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/ui_settings.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/index.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/components/chart_split.tsx (100%) rename src/plugins/chart_expressions/{expression_pie/public/expression_renderers => expression_partition_vis/public/components}/index.ts (86%) rename src/plugins/chart_expressions/{expression_pie/public/components/pie_vis_component.styles.ts => expression_partition_vis/public/components/partition_vis_component.styles.ts} (78%) rename src/plugins/chart_expressions/{expression_pie/public/components/pie_vis_component.test.tsx => expression_partition_vis/public/components/partition_vis_component.test.tsx} (65%) rename src/plugins/chart_expressions/{expression_pie/public/components/pie_vis_component.tsx => expression_partition_vis/public/components/partition_vis_component.tsx} (71%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/components/visualization_noresults.tsx (86%) rename src/plugins/chart_expressions/{expression_pie/common/expression_functions => expression_partition_vis/public/expression_renderers}/index.ts (76%) rename src/plugins/chart_expressions/{expression_pie/public/expression_renderers/pie_vis_renderer.tsx => expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx} (79%) rename src/plugins/chart_expressions/{expression_pie/server => expression_partition_vis/public}/index.ts (65%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/mocks.ts (84%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/plugin.ts (64%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/types.ts (86%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/accessor.test.ts rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/accessor.ts (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/filter_helpers.test.ts (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/filter_helpers.ts (100%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_color_picker.test.tsx (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_color_picker.tsx (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_columns.test.ts (85%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_columns.ts (50%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_distinct_series.test.ts (100%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_distinct_series.ts (78%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/get_legend_actions.tsx (91%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_type.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.ts rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/public/utils/index.ts (78%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts rename src/plugins/chart_expressions/{expression_pie/public/utils => expression_partition_vis/public/utils/layers}/get_layers.test.ts (84%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_node_labels.ts rename src/plugins/chart_expressions/{expression_pie/public/components => expression_partition_vis/public/utils/layers}/index.ts (89%) create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.test.ts create mode 100644 src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.ts rename src/plugins/chart_expressions/{expression_pie/public => expression_partition_vis/server}/index.ts (65%) create mode 100755 src/plugins/chart_expressions/expression_partition_vis/server/plugin.ts rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/server/types.ts (84%) rename src/plugins/chart_expressions/{expression_pie => expression_partition_vis}/tsconfig.json (100%) delete mode 100755 src/plugins/chart_expressions/expression_pie/README.md delete mode 100644 src/plugins/chart_expressions/expression_pie/common/constants.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap delete mode 100644 src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_labels_function.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.ts delete mode 100755 src/plugins/chart_expressions/expression_pie/common/index.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/common/types/expression_functions.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/public/__stories__/pie_renderer.stories.tsx delete mode 100644 src/plugins/chart_expressions/expression_pie/public/utils/get_layers.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.test.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.ts delete mode 100644 src/plugins/chart_expressions/expression_pie/public/utils/get_split_dimension_accessor.ts delete mode 100755 src/plugins/chart_expressions/expression_pie/server/plugin.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7035660b1b46..604179ec7570 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -40,7 +40,7 @@ /src/plugins/chart_expressions/expression_metric/ @elastic/kibana-vis-editors /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_pie/ @elastic/kibana-vis-editors +/src/plugins/chart_expressions/expression_partition_vis/ @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 043e1e28a0e9..5c362908a187 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -25,7 +25,7 @@ "expressionImage": "src/plugins/expression_image", "expressionMetric": "src/plugins/expression_metric", "expressionMetricVis": "src/plugins/chart_expressions/expression_metric", - "expressionPie": "src/plugins/chart_expressions/expression_pie", + "expressionPartitionVis": "src/plugins/chart_expressions/expression_partition_vis", "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 d38775fc608d..ea7ea2469088 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -118,8 +118,8 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |Expression MetricVis plugin adds a metric renderer and function to the expression plugin. The renderer will display the metric chart. -|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_pie/README.md[expressionPie] -|Expression Pie plugin adds a pie renderer and function to the expression plugin. The renderer will display the Pie chart. +|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_partition_vis/README.md[expressionPartitionVis] +|Expression Partition Visualization plugin adds a partitionVis renderer and pieVis, mosaicVis, treemapVis, waffleVis functions to the expression plugin. The renderer will display the pie, waffle, treemap and mosaic charts. |{kib-repo}blob/{branch}/src/plugins/expression_repeat_image/README.md[expressionRepeatImage] diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index bbd7f25ca9c0..3a7c50feb38b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -119,6 +119,6 @@ pageLoadAssetSize: screenshotting: 17017 expressionGauge: 25000 controls: 34788 - expressionPie: 26338 + expressionPartitionVis: 26338 sharedUX: 16225 ux: 20784 diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index d6526781e737..542acf7b0fa8 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -24,7 +24,7 @@ export const storybookAliases = { expression_image: 'src/plugins/expression_image/.storybook', expression_metric_vis: 'src/plugins/chart_expressions/expression_metric/.storybook', expression_metric: 'src/plugins/expression_metric/.storybook', - expression_pie: 'src/plugins/chart_expressions/expression_pie/.storybook', + expression_partition_vis: 'src/plugins/chart_expressions/expression_partition_vis/.storybook', expression_repeat_image: 'src/plugins/expression_repeat_image/.storybook', expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', expression_shape: 'src/plugins/expression_shape/.storybook', diff --git a/src/plugins/chart_expressions/expression_pie/.storybook/main.js b/src/plugins/chart_expressions/expression_partition_vis/.storybook/main.js similarity index 100% rename from src/plugins/chart_expressions/expression_pie/.storybook/main.js rename to src/plugins/chart_expressions/expression_partition_vis/.storybook/main.js diff --git a/src/plugins/chart_expressions/expression_partition_vis/README.md b/src/plugins/chart_expressions/expression_partition_vis/README.md new file mode 100755 index 000000000000..9a499280236e --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/README.md @@ -0,0 +1,9 @@ +# expressionPartitionVis + +Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts. + +--- + +## 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_partition_vis/common/constants.ts b/src/plugins/chart_expressions/expression_partition_vis/common/constants.ts new file mode 100644 index 000000000000..ffa549f7b767 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/constants.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. + */ + +export const PLUGIN_ID = 'expressionPartitionVis'; +export const PLUGIN_NAME = 'expressionPartitionVis'; + +export const PIE_VIS_EXPRESSION_NAME = 'pieVis'; +export const TREEMAP_VIS_EXPRESSION_NAME = 'treemapVis'; +export const MOSAIC_VIS_EXPRESSION_NAME = 'mosaicVis'; +export const WAFFLE_VIS_EXPRESSION_NAME = 'waffleVis'; +export const PARTITION_VIS_RENDERER_NAME = 'partitionVis'; +export const PARTITION_LABELS_VALUE = 'partitionLabelsValue'; +export const PARTITION_LABELS_FUNCTION = 'partitionLabels'; + +export const DEFAULT_PERCENT_DECIMALS = 2; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap new file mode 100644 index 000000000000..de064a44058c --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`interpreter/functions#mosaicVis logs correct datatable to inspector 1`] = ` +Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + Object { + "id": "col-0-3", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 3", + }, + Object { + "id": "col-0-4", + "meta": Object { + "dimensionName": undefined, + "type": "number", + }, + "name": "Field 4", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", +} +`; + +exports[`interpreter/functions#mosaicVis returns an object with the correct structure 1`] = ` +Object { + "as": "partitionVis", + "type": "render", + "value": Object { + "params": Object { + "listenOnChange": true, + }, + "syncColors": false, + "visConfig": Object { + "addTooltip": true, + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "dimensions": Object { + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "splitColumn": undefined, + "splitRow": undefined, + }, + "labels": Object { + "last_level": false, + "percentDecimals": 2, + "position": "default", + "show": false, + "truncate": 100, + "type": "partitionLabelsValue", + "values": true, + "valuesFormat": "percent", + }, + "legendDisplay": "show", + "legendPosition": "right", + "maxLegendLines": 2, + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "nestedLegend": true, + "palette": Object { + "name": "kibana_palette", + "type": "system_palette", + }, + "splitColumn": undefined, + "splitRow": undefined, + "truncateLegend": true, + }, + "visData": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "type": "number", + }, + "name": "Field 2", + }, + Object { + "id": "col-0-3", + "meta": Object { + "type": "number", + }, + "name": "Field 3", + }, + Object { + "id": "col-0-4", + "meta": Object { + "type": "number", + }, + "name": "Field 4", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + "visType": "mosaic", + }, +} +`; + +exports[`interpreter/functions#mosaicVis throws error if provided more than 2 buckets 1`] = `"More than 2 buckets are not supported"`; + +exports[`interpreter/functions#mosaicVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap new file mode 100644 index 000000000000..95b8df13882d --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap @@ -0,0 +1,288 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`interpreter/functions#pieVis logs correct datatable to inspector 1`] = ` +Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Count", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + }, + ], + "type": "datatable", +} +`; + +exports[`interpreter/functions#pieVis returns an object with the correct structure for donut 1`] = ` +Object { + "as": "partitionVis", + "type": "render", + "value": Object { + "params": Object { + "listenOnChange": true, + }, + "syncColors": false, + "visConfig": Object { + "addTooltip": true, + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 3, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "dimensions": Object { + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 3, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "splitColumn": undefined, + "splitRow": undefined, + }, + "distinctColors": false, + "emptySizeRatio": 0.3, + "isDonut": true, + "labels": Object { + "last_level": false, + "percentDecimals": 2, + "position": "default", + "show": false, + "truncate": 100, + "type": "partitionLabelsValue", + "values": true, + "valuesFormat": "percent", + }, + "legendDisplay": "show", + "legendPosition": "right", + "maxLegendLines": 2, + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "nestedLegend": true, + "palette": Object { + "name": "kibana_palette", + "type": "system_palette", + }, + "respectSourceOrder": true, + "splitColumn": undefined, + "splitRow": undefined, + "startFromSecondLargestSlice": true, + "truncateLegend": true, + }, + "visData": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "type": "number", + }, + "name": "Count", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + }, + ], + "type": "datatable", + }, + "visType": "donut", + }, +} +`; + +exports[`interpreter/functions#pieVis returns an object with the correct structure for pie 1`] = ` +Object { + "as": "partitionVis", + "type": "render", + "value": Object { + "params": Object { + "listenOnChange": true, + }, + "syncColors": false, + "visConfig": Object { + "addTooltip": true, + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 3, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "dimensions": Object { + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 3, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "splitColumn": undefined, + "splitRow": undefined, + }, + "distinctColors": false, + "emptySizeRatio": 0.3, + "isDonut": false, + "labels": Object { + "last_level": false, + "percentDecimals": 2, + "position": "default", + "show": false, + "truncate": 100, + "type": "partitionLabelsValue", + "values": true, + "valuesFormat": "percent", + }, + "legendDisplay": "show", + "legendPosition": "right", + "maxLegendLines": 2, + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "nestedLegend": true, + "palette": Object { + "name": "kibana_palette", + "type": "system_palette", + }, + "respectSourceOrder": true, + "splitColumn": undefined, + "splitRow": undefined, + "startFromSecondLargestSlice": true, + "truncateLegend": true, + }, + "visData": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "type": "number", + }, + "name": "Count", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + }, + ], + "type": "datatable", + }, + "visType": "pie", + }, +} +`; + +exports[`interpreter/functions#pieVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap new file mode 100644 index 000000000000..d18dca573606 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`interpreter/functions#treemapVis logs correct datatable to inspector 1`] = ` +Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + Object { + "id": "col-0-3", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 3", + }, + Object { + "id": "col-0-4", + "meta": Object { + "dimensionName": undefined, + "type": "number", + }, + "name": "Field 4", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", +} +`; + +exports[`interpreter/functions#treemapVis returns an object with the correct structure 1`] = ` +Object { + "as": "partitionVis", + "type": "render", + "value": Object { + "params": Object { + "listenOnChange": true, + }, + "syncColors": false, + "visConfig": Object { + "addTooltip": true, + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "dimensions": Object { + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + Object { + "accessor": 2, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "splitColumn": undefined, + "splitRow": undefined, + }, + "labels": Object { + "last_level": false, + "percentDecimals": 2, + "position": "default", + "show": false, + "truncate": 100, + "type": "partitionLabelsValue", + "values": true, + "valuesFormat": "percent", + }, + "legendDisplay": "show", + "legendPosition": "right", + "maxLegendLines": 2, + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "nestedLegend": true, + "palette": Object { + "name": "kibana_palette", + "type": "system_palette", + }, + "splitColumn": undefined, + "splitRow": undefined, + "truncateLegend": true, + }, + "visData": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "type": "number", + }, + "name": "Field 2", + }, + Object { + "id": "col-0-3", + "meta": Object { + "type": "number", + }, + "name": "Field 3", + }, + Object { + "id": "col-0-4", + "meta": Object { + "type": "number", + }, + "name": "Field 4", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + "visType": "treemap", + }, +} +`; + +exports[`interpreter/functions#treemapVis throws error if provided more than 2 buckets 1`] = `"More than 2 buckets are not supported"`; + +exports[`interpreter/functions#treemapVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap new file mode 100644 index 000000000000..54ead941c754 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`interpreter/functions#waffleVis logs correct datatable to inspector 1`] = ` +Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + Object { + "id": "col-0-3", + "meta": Object { + "dimensionName": undefined, + "type": "number", + }, + "name": "Field 3", + }, + Object { + "id": "col-0-4", + "meta": Object { + "dimensionName": undefined, + "type": "number", + }, + "name": "Field 4", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", +} +`; + +exports[`interpreter/functions#waffleVis returns an object with the correct structure 1`] = ` +Object { + "as": "partitionVis", + "type": "render", + "value": Object { + "params": Object { + "listenOnChange": true, + }, + "syncColors": false, + "visConfig": Object { + "addTooltip": true, + "bucket": Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "dimensions": Object { + "buckets": Array [ + Object { + "accessor": 1, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + ], + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "splitColumn": undefined, + "splitRow": undefined, + }, + "labels": Object { + "last_level": false, + "percentDecimals": 2, + "position": "default", + "show": false, + "truncate": 100, + "type": "partitionLabelsValue", + "values": true, + "valuesFormat": "percent", + }, + "legendDisplay": "show", + "legendPosition": "right", + "maxLegendLines": 2, + "metric": Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", + }, + "palette": Object { + "name": "kibana_palette", + "type": "system_palette", + }, + "showValuesInLegend": true, + "splitColumn": undefined, + "splitRow": undefined, + "truncateLegend": true, + }, + "visData": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "type": "number", + }, + "name": "Field 2", + }, + Object { + "id": "col-0-3", + "meta": Object { + "type": "number", + }, + "name": "Field 3", + }, + Object { + "id": "col-0-4", + "meta": Object { + "type": "number", + }, + "name": "Field 4", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + "visType": "waffle", + }, +} +`; + +exports[`interpreter/functions#waffleVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts new file mode 100644 index 000000000000..0be470121ecb --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const strings = { + getPieVisFunctionName: () => + i18n.translate('expressionPartitionVis.pieVis.function.help', { + defaultMessage: 'Pie visualization', + }), + getMetricArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.metricHelpText', { + defaultMessage: 'Metric dimensions config', + }), + getBucketsArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.bucketsHelpText', { + defaultMessage: 'Buckets dimensions config', + }), + getBucketArgHelp: () => + i18n.translate('expressionPartitionVis.waffle.function.args.bucketHelpText', { + defaultMessage: 'Bucket dimensions config', + }), + getSplitColumnArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.splitColumnHelpText', { + defaultMessage: 'Split by column dimension config', + }), + getSplitRowArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.splitRowHelpText', { + defaultMessage: 'Split by row dimension config', + }), + getAddTooltipArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.addTooltipHelpText', { + defaultMessage: 'Show tooltip on slice hover', + }), + getLegendDisplayArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.legendDisplayHelpText', { + defaultMessage: 'Show legend chart legend', + }), + getLegendPositionArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.legendPositionHelpText', { + defaultMessage: 'Position the legend on top, bottom, left, right of the chart', + }), + getNestedLegendArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.nestedLegendHelpText', { + defaultMessage: 'Show a more detailed legend', + }), + getTruncateLegendArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.truncateLegendHelpText', { + defaultMessage: 'Defines if the legend items will be truncated or not', + }), + getMaxLegendLinesArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.maxLegendLinesHelpText', { + defaultMessage: 'Defines the number of lines per legend item', + }), + getDistinctColorsArgHelp: () => + i18n.translate('expressionPartitionVis.pieVis.function.args.distinctColorsHelpText', { + defaultMessage: + 'Maps different color per slice. Slices with the same value have the same color', + }), + getIsDonutArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.isDonutHelpText', { + defaultMessage: 'Displays the pie chart as donut', + }), + getRespectSourceOrderArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.respectSourceOrderHelpText', { + defaultMessage: 'Keeps an order of the elements, returned from the datasource', + }), + getStartFromSecondLargestSliceArgHelp: () => + i18n.translate( + 'expressionPartitionVis.reusable.function.args.startPlacementWithSecondLargestSliceHelpText', + { + defaultMessage: 'Starts placement with the second largest slice', + } + ), + getEmptySizeRatioArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.emptySizeRatioHelpText', { + defaultMessage: 'Defines donut inner empty area size', + }), + getPaletteArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.paletteHelpText', { + defaultMessage: 'Defines the chart palette name', + }), + getLabelsArgHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.args.labelsHelpText', { + defaultMessage: 'Pie labels config', + }), + getShowValuesInLegendArgHelp: () => + i18n.translate('expressionPartitionVis.waffle.function.args.showValuesInLegendHelpText', { + defaultMessage: 'Show values in legend', + }), + + getSliceSizeHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.dimension.metric', { + defaultMessage: 'Slice size', + }), + getSliceHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.dimension.buckets', { + defaultMessage: 'Slice', + }), + getColumnSplitHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.dimension.splitcolumn', { + defaultMessage: 'Column split', + }), + getRowSplitHelp: () => + i18n.translate('expressionPartitionVis.reusable.function.dimension.splitrow', { + defaultMessage: 'Row split', + }), +}; + +export const errors = { + moreThanNBucketsAreNotSupportedError: (maxLength: number) => + i18n.translate('expressionPartitionVis.reusable.function.errors.moreThenNumberBuckets', { + defaultMessage: 'More than {maxLength} buckets are not supported', + values: { maxLength }, + }), + splitRowAndSplitColumnAreSpecifiedError: () => + i18n.translate('expressionPartitionVis.reusable.function.errors.splitRowAndColumnSpecified', { + defaultMessage: + 'A split row and column are specified. Expression is supporting only one of them at once.', + }), +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/index.ts new file mode 100644 index 000000000000..6117b53a8b2a --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/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 { pieVisFunction } from './pie_vis_function'; +export { treemapVisFunction } from './treemap_vis_function'; +export { mosaicVisFunction } from './mosaic_vis_function'; +export { waffleVisFunction } from './waffle_vis_function'; +export { partitionLabelsFunction } from './partition_labels_function'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts new file mode 100644 index 000000000000..58f765899f5e --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils'; +import { + MosaicVisConfig, + LabelPositions, + ValueFormats, + LegendDisplay, +} from '../types/expression_renderers'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; +import { Datatable } from '../../../../expressions/common/expression_types/specs'; +import { mosaicVisFunction } from './mosaic_vis_function'; +import { PARTITION_LABELS_VALUE } from '../constants'; + +describe('interpreter/functions#mosaicVis', () => { + const fn = functionWrapper(mosaicVisFunction()); + const context: Datatable = { + type: 'datatable', + rows: [{ 'col-0-1': 0, 'col-0-2': 0, 'col-0-3': 0, 'col-0-4': 0 }], + columns: [ + { id: 'col-0-1', name: 'Field 1', meta: { type: 'number' } }, + { id: 'col-0-2', name: 'Field 2', meta: { type: 'number' } }, + { id: 'col-0-3', name: 'Field 3', meta: { type: 'number' } }, + { id: 'col-0-4', name: 'Field 4', meta: { type: 'number' } }, + ], + }; + + const visConfig: MosaicVisConfig = { + addTooltip: true, + legendDisplay: LegendDisplay.SHOW, + legendPosition: 'right', + nestedLegend: true, + truncateLegend: true, + maxLegendLines: 2, + palette: { + type: 'system_palette', + name: 'kibana_palette', + }, + labels: { + type: PARTITION_LABELS_VALUE, + show: false, + values: true, + position: LabelPositions.DEFAULT, + valuesFormat: ValueFormats.PERCENT, + percentDecimals: 2, + truncate: 100, + last_level: false, + }, + metric: { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: {}, + }, + }, + buckets: [ + { + type: 'vis_dimension', + accessor: 1, + format: { + id: 'number', + params: {}, + }, + }, + { + type: 'vis_dimension', + accessor: 2, + format: { + id: 'number', + params: {}, + }, + }, + ], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns an object with the correct structure', async () => { + const actual = await fn(context, visConfig); + expect(actual).toMatchSnapshot(); + }); + + it('throws error if provided more than 2 buckets', async () => { + expect(() => + fn(context, { + ...visConfig, + buckets: [ + ...(visConfig.buckets ?? []), + { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }, + ], + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it('throws error if provided split row and split column at once', async () => { + const splitDimension: ExpressionValueVisDimension = { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }; + + expect(() => + fn(context, { + ...visConfig, + splitColumn: [splitDimension], + splitRow: [splitDimension], + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it('logs correct datatable to inspector', async () => { + let loggedTable: Datatable; + const handlers = { + inspectorAdapters: { + tables: { + logDatatable: (name: string, datatable: Datatable) => { + loggedTable = datatable; + }, + }, + }, + }; + await fn(context, visConfig, handlers as any); + + expect(loggedTable!).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts new file mode 100644 index 000000000000..388b0741d23d --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts @@ -0,0 +1,131 @@ +/* + * 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 { LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; +import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table'; +import { ChartTypes, MosaicVisExpressionFunctionDefinition } from '../types'; +import { + PARTITION_LABELS_FUNCTION, + PARTITION_LABELS_VALUE, + PARTITION_VIS_RENDERER_NAME, + MOSAIC_VIS_EXPRESSION_NAME, +} from '../constants'; +import { errors, strings } from './i18n'; + +export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ + name: MOSAIC_VIS_EXPRESSION_NAME, + type: 'render', + inputTypes: ['datatable'], + help: strings.getPieVisFunctionName(), + args: { + metric: { + types: ['vis_dimension'], + help: strings.getMetricArgHelp(), + required: true, + }, + buckets: { + types: ['vis_dimension'], + help: strings.getBucketsArgHelp(), + multi: true, + }, + splitColumn: { + types: ['vis_dimension'], + help: strings.getSplitColumnArgHelp(), + multi: true, + }, + splitRow: { + types: ['vis_dimension'], + help: strings.getSplitRowArgHelp(), + multi: true, + }, + addTooltip: { + types: ['boolean'], + help: strings.getAddTooltipArgHelp(), + default: true, + }, + legendDisplay: { + types: ['string'], + help: strings.getLegendDisplayArgHelp(), + options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT], + default: LegendDisplay.HIDE, + }, + legendPosition: { + types: ['string'], + help: strings.getLegendPositionArgHelp(), + }, + nestedLegend: { + types: ['boolean'], + help: strings.getNestedLegendArgHelp(), + default: false, + }, + truncateLegend: { + types: ['boolean'], + help: strings.getTruncateLegendArgHelp(), + default: true, + }, + maxLegendLines: { + types: ['number'], + help: strings.getMaxLegendLinesArgHelp(), + }, + palette: { + types: ['palette', 'system_palette'], + help: strings.getPaletteArgHelp(), + default: '{palette}', + }, + labels: { + types: [PARTITION_LABELS_VALUE], + help: strings.getLabelsArgHelp(), + default: `{${PARTITION_LABELS_FUNCTION}}`, + }, + }, + fn(context, args, handlers) { + const maxSupportedBuckets = 2; + if ((args.buckets ?? []).length > maxSupportedBuckets) { + throw new Error(errors.moreThanNBucketsAreNotSupportedError(maxSupportedBuckets)); + } + + if (args.splitColumn && args.splitRow) { + throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); + } + + const visConfig: PartitionVisParams = { + ...args, + palette: args.palette, + dimensions: { + metric: args.metric, + buckets: args.buckets, + splitColumn: args.splitColumn, + splitRow: args.splitRow, + }, + }; + + if (handlers?.inspectorAdapters?.tables) { + const logTable = prepareLogTable(context, [ + [[args.metric], strings.getSliceSizeHelp()], + [args.buckets, strings.getSliceHelp()], + [args.splitColumn, strings.getColumnSplitHelp()], + [args.splitRow, strings.getRowSplitHelp()], + ]); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } + + return { + type: 'render', + as: PARTITION_VIS_RENDERER_NAME, + value: { + visData: context, + visConfig, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + visType: ChartTypes.MOSAIC, + params: { + listenOnChange: true, + }, + }, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts new file mode 100644 index 000000000000..900927a35983 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/partition_labels_function.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition, Datatable } from '../../../../expressions/common'; +import { PARTITION_LABELS_FUNCTION, PARTITION_LABELS_VALUE } from '../constants'; +import { + ExpressionValuePartitionLabels, + LabelPositions, + PartitionLabelsArguments, + ValueFormats, +} from '../types'; + +export const partitionLabelsFunction = (): ExpressionFunctionDefinition< + typeof PARTITION_LABELS_FUNCTION, + Datatable | null, + PartitionLabelsArguments, + ExpressionValuePartitionLabels +> => ({ + name: PARTITION_LABELS_FUNCTION, + help: i18n.translate('expressionPartitionVis.partitionLabels.function.help', { + defaultMessage: 'Generates the partition labels object', + }), + type: PARTITION_LABELS_VALUE, + args: { + show: { + types: ['boolean'], + help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.show.help', { + defaultMessage: 'Displays the partition chart labels', + }), + default: true, + }, + position: { + types: ['string'], + default: 'default', + help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.position.help', { + defaultMessage: 'Defines the label position', + }), + options: [LabelPositions.DEFAULT, LabelPositions.INSIDE], + }, + values: { + types: ['boolean'], + help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.values.help', { + defaultMessage: 'Displays the values inside the slices', + }), + default: true, + }, + percentDecimals: { + types: ['number'], + help: i18n.translate( + 'expressionPartitionVis.partitionLabels.function.args.percentDecimals.help', + { + defaultMessage: + 'Defines the number of decimals that will appear on the values as percent', + } + ), + default: 2, + }, + // Deprecated + last_level: { + types: ['boolean'], + help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.last_level.help', { + defaultMessage: 'Show top level labels only for multilayer pie/donut charts', + }), + default: false, + }, + // Deprecated + truncate: { + types: ['number', 'null'], + help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.truncate.help', { + defaultMessage: + 'Defines the number of characters that the slice value will display only for multilayer pie/donut charts', + }), + default: null, + }, + valuesFormat: { + types: ['string'], + default: 'percent', + help: i18n.translate( + 'expressionPartitionVis.partitionLabels.function.args.valuesFormat.help', + { + defaultMessage: 'Defines the format of the values', + } + ), + options: [ValueFormats.PERCENT, ValueFormats.VALUE], + }, + }, + fn: (context, args) => { + return { + type: PARTITION_LABELS_VALUE, + show: args.show, + position: args.position, + percentDecimals: args.percentDecimals, + values: args.values, + truncate: args.truncate, + valuesFormat: args.valuesFormat, + last_level: args.last_level, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts similarity index 56% rename from src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.test.ts rename to src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts index 56807b4aa6a5..366683ce80dd 100644 --- a/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts @@ -12,21 +12,24 @@ import { EmptySizeRatios, LabelPositions, ValueFormats, + LegendDisplay, } from '../types/expression_renderers'; -import { pieVisFunction } from './pie_vis_function'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; import { Datatable } from '../../../../expressions/common/expression_types/specs'; +import { pieVisFunction } from './pie_vis_function'; +import { PARTITION_LABELS_VALUE } from '../constants'; -describe('interpreter/functions#pie', () => { +describe('interpreter/functions#pieVis', () => { const fn = functionWrapper(pieVisFunction()); - const context = { + const context: Datatable = { type: 'datatable', rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - } as unknown as Datatable; + columns: [{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } }], + }; const visConfig: PieVisConfig = { addTooltip: true, - addLegend: true, + legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', isDonut: true, emptySizeRatio: EmptySizeRatios.SMALL, @@ -39,7 +42,7 @@ describe('interpreter/functions#pie', () => { name: 'kibana_palette', }, labels: { - type: 'pie_labels_value', + type: PARTITION_LABELS_VALUE, show: false, values: true, position: LabelPositions.DEFAULT, @@ -56,17 +59,67 @@ describe('interpreter/functions#pie', () => { params: {}, }, }, + buckets: [ + { + type: 'vis_dimension', + accessor: 1, + format: { + id: 'number', + params: {}, + }, + }, + { + type: 'vis_dimension', + accessor: 2, + format: { + id: 'number', + params: {}, + }, + }, + { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }, + ], }; beforeEach(() => { jest.clearAllMocks(); }); - it('returns an object with the correct structure', async () => { + it('returns an object with the correct structure for pie', async () => { + const actual = await fn(context, { ...visConfig, isDonut: false }); + expect(actual).toMatchSnapshot(); + }); + + it('returns an object with the correct structure for donut', async () => { const actual = await fn(context, visConfig); expect(actual).toMatchSnapshot(); }); + it('throws error if provided split row and split column at once', async () => { + const splitDimension: ExpressionValueVisDimension = { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }; + + expect(() => + fn(context, { + ...visConfig, + splitColumn: [splitDimension], + splitRow: [splitDimension], + }) + ).toThrowErrorMatchingSnapshot(); + }); + it('logs correct datatable to inspector', async () => { let loggedTable: Datatable; const handlers = { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts new file mode 100644 index 000000000000..c054d572538c --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -0,0 +1,153 @@ +/* + * 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 { EmptySizeRatios, LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; +import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table'; +import { ChartTypes, PieVisExpressionFunctionDefinition } from '../types'; +import { + PARTITION_LABELS_FUNCTION, + PARTITION_LABELS_VALUE, + PIE_VIS_EXPRESSION_NAME, + PARTITION_VIS_RENDERER_NAME, +} from '../constants'; +import { errors, strings } from './i18n'; + +export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ + name: PIE_VIS_EXPRESSION_NAME, + type: 'render', + inputTypes: ['datatable'], + help: strings.getPieVisFunctionName(), + args: { + metric: { + types: ['vis_dimension'], + help: strings.getMetricArgHelp(), + required: true, + }, + buckets: { + types: ['vis_dimension'], + help: strings.getBucketsArgHelp(), + multi: true, + }, + splitColumn: { + types: ['vis_dimension'], + help: strings.getSplitColumnArgHelp(), + multi: true, + }, + splitRow: { + types: ['vis_dimension'], + help: strings.getSplitRowArgHelp(), + multi: true, + }, + addTooltip: { + types: ['boolean'], + help: strings.getAddTooltipArgHelp(), + default: true, + }, + legendDisplay: { + types: ['string'], + help: strings.getLegendDisplayArgHelp(), + options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT], + default: LegendDisplay.HIDE, + }, + legendPosition: { + types: ['string'], + help: strings.getLegendPositionArgHelp(), + options: [Position.Top, Position.Right, Position.Bottom, Position.Left], + }, + nestedLegend: { + types: ['boolean'], + help: strings.getNestedLegendArgHelp(), + default: false, + }, + truncateLegend: { + types: ['boolean'], + help: strings.getTruncateLegendArgHelp(), + default: true, + }, + maxLegendLines: { + types: ['number'], + help: strings.getMaxLegendLinesArgHelp(), + }, + distinctColors: { + types: ['boolean'], + help: strings.getDistinctColorsArgHelp(), + default: false, + }, + respectSourceOrder: { + types: ['boolean'], + help: strings.getRespectSourceOrderArgHelp(), + default: true, + }, + isDonut: { + types: ['boolean'], + help: strings.getIsDonutArgHelp(), + default: false, + }, + emptySizeRatio: { + types: ['number'], + help: strings.getEmptySizeRatioArgHelp(), + default: EmptySizeRatios.SMALL, + }, + palette: { + types: ['palette', 'system_palette'], + help: strings.getPaletteArgHelp(), + default: '{palette}', + }, + labels: { + types: [PARTITION_LABELS_VALUE], + help: strings.getLabelsArgHelp(), + default: `{${PARTITION_LABELS_FUNCTION}}`, + }, + startFromSecondLargestSlice: { + types: ['boolean'], + help: strings.getStartFromSecondLargestSliceArgHelp(), + default: true, + }, + }, + fn(context, args, handlers) { + if (args.splitColumn && args.splitRow) { + throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); + } + + const visConfig: PartitionVisParams = { + ...args, + palette: args.palette, + dimensions: { + metric: args.metric, + buckets: args.buckets, + splitColumn: args.splitColumn, + splitRow: args.splitRow, + }, + }; + + if (handlers?.inspectorAdapters?.tables) { + const logTable = prepareLogTable(context, [ + [[args.metric], strings.getSliceSizeHelp()], + [args.buckets, strings.getSliceHelp()], + [args.splitColumn, strings.getColumnSplitHelp()], + [args.splitRow, strings.getRowSplitHelp()], + ]); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } + + return { + type: 'render', + as: PARTITION_VIS_RENDERER_NAME, + value: { + visData: context, + visConfig, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + visType: args.isDonut ? ChartTypes.DONUT : ChartTypes.PIE, + params: { + listenOnChange: true, + }, + }, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts new file mode 100644 index 000000000000..1d65ba35a5e0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils'; +import { + TreemapVisConfig, + LabelPositions, + ValueFormats, + LegendDisplay, +} from '../types/expression_renderers'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; +import { Datatable } from '../../../../expressions/common/expression_types/specs'; +import { treemapVisFunction } from './treemap_vis_function'; +import { PARTITION_LABELS_VALUE } from '../constants'; + +describe('interpreter/functions#treemapVis', () => { + const fn = functionWrapper(treemapVisFunction()); + const context: Datatable = { + type: 'datatable', + rows: [{ 'col-0-1': 0, 'col-0-2': 0, 'col-0-3': 0, 'col-0-4': 0 }], + columns: [ + { id: 'col-0-1', name: 'Field 1', meta: { type: 'number' } }, + { id: 'col-0-2', name: 'Field 2', meta: { type: 'number' } }, + { id: 'col-0-3', name: 'Field 3', meta: { type: 'number' } }, + { id: 'col-0-4', name: 'Field 4', meta: { type: 'number' } }, + ], + }; + + const visConfig: TreemapVisConfig = { + addTooltip: true, + legendDisplay: LegendDisplay.SHOW, + legendPosition: 'right', + nestedLegend: true, + truncateLegend: true, + maxLegendLines: 2, + palette: { + type: 'system_palette', + name: 'kibana_palette', + }, + labels: { + type: PARTITION_LABELS_VALUE, + show: false, + values: true, + position: LabelPositions.DEFAULT, + valuesFormat: ValueFormats.PERCENT, + percentDecimals: 2, + truncate: 100, + last_level: false, + }, + metric: { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: {}, + }, + }, + buckets: [ + { + type: 'vis_dimension', + accessor: 1, + format: { + id: 'number', + params: {}, + }, + }, + { + type: 'vis_dimension', + accessor: 2, + format: { + id: 'number', + params: {}, + }, + }, + ], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns an object with the correct structure', async () => { + const actual = await fn(context, visConfig); + expect(actual).toMatchSnapshot(); + }); + + it('throws error if provided more than 2 buckets', async () => { + expect(() => + fn(context, { + ...visConfig, + buckets: [ + ...(visConfig.buckets ?? []), + { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }, + ], + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it('throws error if provided split row and split column at once', async () => { + const splitDimension: ExpressionValueVisDimension = { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }; + + expect(() => + fn(context, { + ...visConfig, + splitColumn: [splitDimension], + splitRow: [splitDimension], + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it('logs correct datatable to inspector', async () => { + let loggedTable: Datatable; + const handlers = { + inspectorAdapters: { + tables: { + logDatatable: (name: string, datatable: Datatable) => { + loggedTable = datatable; + }, + }, + }, + }; + await fn(context, visConfig, handlers as any); + + expect(loggedTable!).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts new file mode 100644 index 000000000000..d0ae42b4b794 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts @@ -0,0 +1,131 @@ +/* + * 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 { LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; +import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table'; +import { ChartTypes, TreemapVisExpressionFunctionDefinition } from '../types'; +import { + PARTITION_LABELS_FUNCTION, + PARTITION_LABELS_VALUE, + PARTITION_VIS_RENDERER_NAME, + TREEMAP_VIS_EXPRESSION_NAME, +} from '../constants'; +import { errors, strings } from './i18n'; + +export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => ({ + name: TREEMAP_VIS_EXPRESSION_NAME, + type: 'render', + inputTypes: ['datatable'], + help: strings.getPieVisFunctionName(), + args: { + metric: { + types: ['vis_dimension'], + help: strings.getMetricArgHelp(), + required: true, + }, + buckets: { + types: ['vis_dimension'], + help: strings.getBucketsArgHelp(), + multi: true, + }, + splitColumn: { + types: ['vis_dimension'], + help: strings.getSplitColumnArgHelp(), + multi: true, + }, + splitRow: { + types: ['vis_dimension'], + help: strings.getSplitRowArgHelp(), + multi: true, + }, + addTooltip: { + types: ['boolean'], + help: strings.getAddTooltipArgHelp(), + default: true, + }, + legendDisplay: { + types: ['string'], + help: strings.getLegendDisplayArgHelp(), + options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT], + default: LegendDisplay.HIDE, + }, + legendPosition: { + types: ['string'], + help: strings.getLegendPositionArgHelp(), + }, + nestedLegend: { + types: ['boolean'], + help: strings.getNestedLegendArgHelp(), + default: false, + }, + truncateLegend: { + types: ['boolean'], + help: strings.getTruncateLegendArgHelp(), + default: true, + }, + maxLegendLines: { + types: ['number'], + help: strings.getMaxLegendLinesArgHelp(), + }, + palette: { + types: ['palette', 'system_palette'], + help: strings.getPaletteArgHelp(), + default: '{palette}', + }, + labels: { + types: [PARTITION_LABELS_VALUE], + help: strings.getLabelsArgHelp(), + default: `{${PARTITION_LABELS_FUNCTION}}`, + }, + }, + fn(context, args, handlers) { + const maxSupportedBuckets = 2; + if ((args.buckets ?? []).length > maxSupportedBuckets) { + throw new Error(errors.moreThanNBucketsAreNotSupportedError(maxSupportedBuckets)); + } + + if (args.splitColumn && args.splitRow) { + throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); + } + + const visConfig: PartitionVisParams = { + ...args, + palette: args.palette, + dimensions: { + metric: args.metric, + buckets: args.buckets, + splitColumn: args.splitColumn, + splitRow: args.splitRow, + }, + }; + + if (handlers?.inspectorAdapters?.tables) { + const logTable = prepareLogTable(context, [ + [[args.metric], strings.getSliceSizeHelp()], + [args.buckets, strings.getSliceHelp()], + [args.splitColumn, strings.getColumnSplitHelp()], + [args.splitRow, strings.getRowSplitHelp()], + ]); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } + + return { + type: 'render', + as: PARTITION_VIS_RENDERER_NAME, + value: { + visData: context, + visConfig, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + visType: ChartTypes.TREEMAP, + params: { + listenOnChange: true, + }, + }, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts new file mode 100644 index 000000000000..ead4d97a8f8e --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.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 { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils'; +import { + WaffleVisConfig, + LabelPositions, + ValueFormats, + LegendDisplay, +} from '../types/expression_renderers'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; +import { Datatable } from '../../../../expressions/common/expression_types/specs'; +import { waffleVisFunction } from './waffle_vis_function'; +import { PARTITION_LABELS_VALUE } from '../constants'; + +describe('interpreter/functions#waffleVis', () => { + const fn = functionWrapper(waffleVisFunction()); + const context: Datatable = { + type: 'datatable', + rows: [{ 'col-0-1': 0, 'col-0-2': 0, 'col-0-3': 0, 'col-0-4': 0 }], + columns: [ + { id: 'col-0-1', name: 'Field 1', meta: { type: 'number' } }, + { id: 'col-0-2', name: 'Field 2', meta: { type: 'number' } }, + { id: 'col-0-3', name: 'Field 3', meta: { type: 'number' } }, + { id: 'col-0-4', name: 'Field 4', meta: { type: 'number' } }, + ], + }; + + const visConfig: WaffleVisConfig = { + addTooltip: true, + showValuesInLegend: true, + legendDisplay: LegendDisplay.SHOW, + legendPosition: 'right', + truncateLegend: true, + maxLegendLines: 2, + palette: { + type: 'system_palette', + name: 'kibana_palette', + }, + labels: { + type: PARTITION_LABELS_VALUE, + show: false, + values: true, + position: LabelPositions.DEFAULT, + valuesFormat: ValueFormats.PERCENT, + percentDecimals: 2, + truncate: 100, + last_level: false, + }, + metric: { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: {}, + }, + }, + bucket: { + type: 'vis_dimension', + accessor: 1, + format: { + id: 'number', + params: {}, + }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns an object with the correct structure', async () => { + const actual = await fn(context, visConfig); + expect(actual).toMatchSnapshot(); + }); + + it('throws error if provided split row and split column at once', async () => { + const splitDimension: ExpressionValueVisDimension = { + type: 'vis_dimension', + accessor: 3, + format: { + id: 'number', + params: {}, + }, + }; + + expect(() => + fn(context, { + ...visConfig, + splitColumn: [splitDimension], + splitRow: [splitDimension], + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it('logs correct datatable to inspector', async () => { + let loggedTable: Datatable; + const handlers = { + inspectorAdapters: { + tables: { + logDatatable: (name: string, datatable: Datatable) => { + loggedTable = datatable; + }, + }, + }, + }; + await fn(context, visConfig, handlers as any); + + expect(loggedTable!).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts new file mode 100644 index 000000000000..ade524aad59c --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts @@ -0,0 +1,126 @@ +/* + * 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 { LegendDisplay, PartitionVisParams } from '../types/expression_renderers'; +import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table'; +import { ChartTypes, WaffleVisExpressionFunctionDefinition } from '../types'; +import { + PARTITION_LABELS_FUNCTION, + PARTITION_LABELS_VALUE, + PARTITION_VIS_RENDERER_NAME, + WAFFLE_VIS_EXPRESSION_NAME, +} from '../constants'; +import { errors, strings } from './i18n'; + +export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ + name: WAFFLE_VIS_EXPRESSION_NAME, + type: 'render', + inputTypes: ['datatable'], + help: strings.getPieVisFunctionName(), + args: { + metric: { + types: ['vis_dimension'], + help: strings.getMetricArgHelp(), + required: true, + }, + bucket: { + types: ['vis_dimension'], + help: strings.getBucketArgHelp(), + }, + splitColumn: { + types: ['vis_dimension'], + help: strings.getSplitColumnArgHelp(), + multi: true, + }, + splitRow: { + types: ['vis_dimension'], + help: strings.getSplitRowArgHelp(), + multi: true, + }, + addTooltip: { + types: ['boolean'], + help: strings.getAddTooltipArgHelp(), + default: true, + }, + legendDisplay: { + types: ['string'], + help: strings.getLegendDisplayArgHelp(), + options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT], + default: LegendDisplay.HIDE, + }, + legendPosition: { + types: ['string'], + help: strings.getLegendPositionArgHelp(), + }, + truncateLegend: { + types: ['boolean'], + help: strings.getTruncateLegendArgHelp(), + default: true, + }, + maxLegendLines: { + types: ['number'], + help: strings.getMaxLegendLinesArgHelp(), + }, + palette: { + types: ['palette', 'system_palette'], + help: strings.getPaletteArgHelp(), + default: '{palette}', + }, + labels: { + types: [PARTITION_LABELS_VALUE], + help: strings.getLabelsArgHelp(), + default: `{${PARTITION_LABELS_FUNCTION}}`, + }, + showValuesInLegend: { + types: ['boolean'], + help: strings.getShowValuesInLegendArgHelp(), + default: false, + }, + }, + fn(context, args, handlers) { + if (args.splitColumn && args.splitRow) { + throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); + } + + const buckets = args.bucket ? [args.bucket] : []; + const visConfig: PartitionVisParams = { + ...args, + palette: args.palette, + dimensions: { + metric: args.metric, + buckets, + splitColumn: args.splitColumn, + splitRow: args.splitRow, + }, + }; + + if (handlers?.inspectorAdapters?.tables) { + const logTable = prepareLogTable(context, [ + [[args.metric], strings.getSliceSizeHelp()], + [buckets, strings.getSliceHelp()], + [args.splitColumn, strings.getColumnSplitHelp()], + [args.splitRow, strings.getRowSplitHelp()], + ]); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } + + return { + type: 'render', + as: PARTITION_VIS_RENDERER_NAME, + value: { + visData: context, + visConfig, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + visType: ChartTypes.WAFFLE, + params: { + listenOnChange: true, + }, + }, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/index.ts b/src/plugins/chart_expressions/expression_partition_vis/common/index.ts new file mode 100755 index 000000000000..559d597caf90 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/index.ts @@ -0,0 +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 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 { + PLUGIN_ID, + PLUGIN_NAME, + PIE_VIS_EXPRESSION_NAME, + TREEMAP_VIS_EXPRESSION_NAME, + MOSAIC_VIS_EXPRESSION_NAME, + WAFFLE_VIS_EXPRESSION_NAME, + PARTITION_LABELS_VALUE, + PARTITION_LABELS_FUNCTION, +} from './constants'; + +export { + pieVisFunction, + treemapVisFunction, + waffleVisFunction, + mosaicVisFunction, + partitionLabelsFunction, +} from './expression_functions'; + +export type { + ExpressionValuePartitionLabels, + PieVisExpressionFunctionDefinition, + TreemapVisExpressionFunctionDefinition, + MosaicVisExpressionFunctionDefinition, + WaffleVisExpressionFunctionDefinition, +} from './types/expression_functions'; + +export type { + PartitionVisParams, + PieVisConfig, + TreemapVisConfig, + MosaicVisConfig, + WaffleVisConfig, + LabelsParams, + Dimension, + Dimensions, +} from './types/expression_renderers'; + +export { + ValueFormats, + LabelPositions, + EmptySizeRatios, + LegendDisplay, +} from './types/expression_renderers'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts new file mode 100644 index 000000000000..bc623fd62134 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.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 { + PARTITION_LABELS_VALUE, + PIE_VIS_EXPRESSION_NAME, + TREEMAP_VIS_EXPRESSION_NAME, + MOSAIC_VIS_EXPRESSION_NAME, + WAFFLE_VIS_EXPRESSION_NAME, +} from '../constants'; +import { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueRender, + ExpressionValueBoxed, +} from '../../../../expressions/common'; +import { + RenderValue, + PieVisConfig, + LabelPositions, + ValueFormats, + TreemapVisConfig, + MosaicVisConfig, + WaffleVisConfig, +} from './expression_renderers'; + +export interface PartitionLabelsArguments { + show: boolean; + position: LabelPositions; + values: boolean; + valuesFormat: ValueFormats; + percentDecimals: number; + /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ + truncate?: number | null; + /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ + last_level?: boolean; +} + +export type ExpressionValuePartitionLabels = ExpressionValueBoxed< + typeof PARTITION_LABELS_VALUE, + { + show: boolean; + position: LabelPositions; + values: boolean; + valuesFormat: ValueFormats; + percentDecimals: number; + /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ + truncate?: number | null; + /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ + last_level?: boolean; + } +>; + +export type PieVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof PIE_VIS_EXPRESSION_NAME, + Datatable, + PieVisConfig, + ExpressionValueRender +>; + +export type TreemapVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof TREEMAP_VIS_EXPRESSION_NAME, + Datatable, + TreemapVisConfig, + ExpressionValueRender +>; + +export type MosaicVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof MOSAIC_VIS_EXPRESSION_NAME, + Datatable, + MosaicVisConfig, + ExpressionValueRender +>; + +export type WaffleVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof WAFFLE_VIS_EXPRESSION_NAME, + Datatable, + WaffleVisConfig, + ExpressionValueRender +>; + +export enum ChartTypes { + PIE = 'pie', + DONUT = 'donut', + TREEMAP = 'treemap', + MOSAIC = 'mosaic', + WAFFLE = 'waffle', +} diff --git a/src/plugins/chart_expressions/expression_pie/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts similarity index 59% rename from src/plugins/chart_expressions/expression_pie/common/types/expression_renderers.ts rename to src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index dd7bfacc0b9c..87358d5dbe65 100644 --- a/src/plugins/chart_expressions/expression_pie/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -11,7 +11,7 @@ import { Datatable, DatatableColumn } from '../../../../expressions/common'; import { SerializedFieldFormat } from '../../../../field_formats/common'; import { ExpressionValueVisDimension } from '../../../../visualizations/common'; import { PaletteOutput } from '../../../../charts/common'; -import { ExpressionValuePieLabels } from './expression_functions'; +import { ChartTypes, ExpressionValuePartitionLabels } from './expression_functions'; export enum EmptySizeRatios { SMALL = 0.3, @@ -28,7 +28,7 @@ export interface Dimension { } export interface Dimensions { - metric: ExpressionValueVisDimension; + metric?: ExpressionValueVisDimension; buckets?: ExpressionValueVisDimension[]; splitRow?: ExpressionValueVisDimension[]; splitColumn?: ExpressionValueVisDimension[]; @@ -36,45 +36,74 @@ export interface Dimensions { export interface LabelsParams { show: boolean; - last_level: boolean; position: LabelPositions; values: boolean; - truncate: number | null; valuesFormat: ValueFormats; percentDecimals: number; + /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ + truncate?: number | null; + /** @deprecated This field is deprecated and going to be removed in the futher release versions. */ + last_level?: boolean; } -interface PieCommonParams { +interface VisCommonParams { addTooltip: boolean; - addLegend: boolean; + legendDisplay: LegendDisplay; legendPosition: Position; - nestedLegend: boolean; truncateLegend: boolean; maxLegendLines: number; - distinctColors: boolean; - isDonut: boolean; - emptySizeRatio?: EmptySizeRatios; } -export interface PieVisParams extends PieCommonParams { - dimensions: Dimensions; - labels: LabelsParams; - palette: PaletteOutput; -} - -export interface PieVisConfig extends PieCommonParams { - buckets?: ExpressionValueVisDimension[]; +interface VisCommonConfig extends VisCommonParams { metric: ExpressionValueVisDimension; splitColumn?: ExpressionValueVisDimension[]; splitRow?: ExpressionValueVisDimension[]; - labels: ExpressionValuePieLabels; + labels: ExpressionValuePartitionLabels; palette: PaletteOutput; } +export interface PartitionVisParams extends VisCommonParams { + dimensions: Dimensions; + labels: LabelsParams; + palette: PaletteOutput; + isDonut?: boolean; + showValuesInLegend?: boolean; + respectSourceOrder?: boolean; + emptySizeRatio?: EmptySizeRatios; + startFromSecondLargestSlice?: boolean; + distinctColors?: boolean; + nestedLegend?: boolean; +} + +export interface PieVisConfig extends VisCommonConfig { + buckets?: ExpressionValueVisDimension[]; + isDonut: boolean; + emptySizeRatio?: EmptySizeRatios; + respectSourceOrder?: boolean; + startFromSecondLargestSlice?: boolean; + distinctColors?: boolean; + nestedLegend: boolean; +} + +export interface TreemapVisConfig extends VisCommonConfig { + buckets?: ExpressionValueVisDimension[]; + nestedLegend: boolean; +} + +export interface MosaicVisConfig extends VisCommonConfig { + buckets?: ExpressionValueVisDimension[]; + nestedLegend: boolean; +} + +export interface WaffleVisConfig extends VisCommonConfig { + bucket?: ExpressionValueVisDimension; + showValuesInLegend: boolean; +} + export interface RenderValue { visData: Datatable; - visType: string; - visConfig: PieVisParams; + visType: ChartTypes; + visConfig: PartitionVisParams; syncColors: boolean; } @@ -88,6 +117,12 @@ export enum ValueFormats { VALUE = 'value', } +export enum LegendDisplay { + SHOW = 'show', + HIDE = 'hide', + DEFAULT = 'default', +} + export interface BucketColumns extends DatatableColumn { format?: { id?: string; diff --git a/src/plugins/chart_expressions/expression_pie/common/types/index.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/index.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/common/types/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/common/types/index.ts diff --git a/src/plugins/chart_expressions/expression_pie/jest.config.js b/src/plugins/chart_expressions/expression_partition_vis/jest.config.js similarity index 72% rename from src/plugins/chart_expressions/expression_pie/jest.config.js rename to src/plugins/chart_expressions/expression_partition_vis/jest.config.js index d8dd288fab08..c449f1e1f245 100644 --- a/src/plugins/chart_expressions/expression_pie/jest.config.js +++ b/src/plugins/chart_expressions/expression_partition_vis/jest.config.js @@ -9,11 +9,11 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../../', - roots: ['/src/plugins/chart_expressions/expression_pie'], + roots: ['/src/plugins/chart_expressions/expression_partition_vis'], coverageDirectory: - '/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_pie', + '/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_partition_vis', coverageReporters: ['text', 'html'], collectCoverageFrom: [ - '/src/plugins/chart_expressions/expression_pie/{common,public,server}/**/*.{ts,tsx}', + '/src/plugins/chart_expressions/expression_partition_vis/{common,public,server}/**/*.{ts,tsx}', ], }; diff --git a/src/plugins/chart_expressions/expression_pie/kibana.json b/src/plugins/chart_expressions/expression_partition_vis/kibana.json similarity index 55% rename from src/plugins/chart_expressions/expression_pie/kibana.json rename to src/plugins/chart_expressions/expression_partition_vis/kibana.json index a681ca1ed00a..226d1681cd3f 100755 --- a/src/plugins/chart_expressions/expression_pie/kibana.json +++ b/src/plugins/chart_expressions/expression_partition_vis/kibana.json @@ -1,12 +1,12 @@ { - "id": "expressionPie", + "id": "expressionPartitionVis", "version": "1.0.0", "kibanaVersion": "kibana", "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" }, - "description": "Expression Pie plugin adds a `pie` renderer and function to the expression plugin. The renderer will display the `pie` chart.", + "description": "Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts.", "server": true, "ui": true, "extraPublicDirs": [ diff --git a/src/plugins/chart_expressions/expression_pie/public/__mocks__/format_service.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/format_service.ts similarity index 88% rename from src/plugins/chart_expressions/expression_pie/public/__mocks__/format_service.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/format_service.ts index 77f6d8eb0bf3..84ad7f4f0f5b 100644 --- a/src/plugins/chart_expressions/expression_pie/public/__mocks__/format_service.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/format_service.ts @@ -8,6 +8,6 @@ export const getFormatService = () => ({ deserialize: (target: any) => ({ - convert: (text: string, format: string) => text, + convert: (text: string, format: string) => `${text}`, }), }); diff --git a/src/plugins/chart_expressions/expression_pie/public/__mocks__/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/index.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/__mocks__/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/index.ts diff --git a/src/plugins/chart_expressions/expression_pie/public/__mocks__/palettes.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/palettes.ts similarity index 87% rename from src/plugins/chart_expressions/expression_pie/public/__mocks__/palettes.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/palettes.ts index f418a7e56160..5637acfdbee1 100644 --- a/src/plugins/chart_expressions/expression_pie/public/__mocks__/palettes.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/palettes.ts @@ -21,10 +21,15 @@ export const getPaletteRegistry = () => { '#AA6556', '#E7664C', ]; + let counter = 0; const mockPalette: PaletteDefinition = { id: 'default', title: 'My Palette', - getCategoricalColor: (_: SeriesLayer[]) => colors[0], + getCategoricalColor: (_: SeriesLayer[]) => { + counter++; + if (counter > colors.length - 1) counter = 0; + return colors[counter]; + }, getCategoricalColors: (num: number) => colors, toExpression: () => ({ type: 'expression', diff --git a/src/plugins/chart_expressions/expression_pie/public/__mocks__/start_deps.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/start_deps.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/__mocks__/start_deps.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/start_deps.ts diff --git a/src/plugins/chart_expressions/expression_pie/public/__mocks__/theme.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts similarity index 79% rename from src/plugins/chart_expressions/expression_pie/public/__mocks__/theme.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts index b5af8dc49660..178b8db605e1 100644 --- a/src/plugins/chart_expressions/expression_pie/public/__mocks__/theme.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts @@ -8,5 +8,9 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ThemeService } from '../../../../charts/public/services'; +import { uiSettings } from './ui_settings'; -export const theme = new ThemeService(); +const theme = new ThemeService(); +theme.init(uiSettings); + +export { theme }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/ui_settings.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/ui_settings.ts new file mode 100644 index 000000000000..c5838d82867f --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/ui_settings.ts @@ -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 { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from 'kibana/public'; +import { Observable } from 'rxjs'; + +export const uiSettings: IUiSettingsClient = { + set: (key: string, value: any) => Promise.resolve(true), + remove: (key: string) => Promise.resolve(true), + isCustom: (key: string) => false, + isOverridden: (key: string) => Boolean(uiSettings.getAll()[key].isOverridden), + getUpdate$: () => + new Observable<{ + key: string; + newValue: any; + oldValue: any; + }>(), + isDeclared: (key: string) => true, + isDefault: (key: string) => true, + getUpdateErrors$: () => new Observable(), + get: (key: string, defaultOverride?: any): any => uiSettings.getAll()[key] || defaultOverride, + get$: (key: string) => new Observable(uiSettings.get(key)), + getAll: (): Readonly> => { + return {}; + }, +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx new file mode 100644 index 000000000000..bba644f72103 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { ComponentStory } from '@storybook/react'; +import { Render } from '../../../../presentation_util/public/__stories__'; +import { getPartitionVisRenderer } from '../expression_renderers'; +import { ChartTypes, RenderValue } from '../../common/types'; +import { palettes, theme, getStartDeps } from '../__mocks__'; +import { mosaicArgTypes, treemapMosaicConfig, data } from './shared'; + +const containerSize = { + width: '700px', + height: '700px', +}; + +const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps }); + +type Props = { + visType: RenderValue['visType']; + syncColors: RenderValue['syncColors']; +} & RenderValue['visConfig']; + +const PartitionVis: ComponentStory> = ({ + visType, + syncColors, + children, + ...visConfig +}) => ( + +); + +export default { + title: 'renderers/mosaicVis', + component: PartitionVis, + argTypes: mosaicArgTypes, +}; + +const Default = PartitionVis.bind({}); +Default.args = { ...treemapMosaicConfig, visType: ChartTypes.MOSAIC, syncColors: false }; + +export { Default }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx new file mode 100644 index 000000000000..b6d6e9055e69 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { ComponentStory } from '@storybook/react'; +import { Render } from '../../../../presentation_util/public/__stories__'; +import { getPartitionVisRenderer } from '../expression_renderers'; +import { ChartTypes, RenderValue } from '../../common/types'; +import { palettes, theme, getStartDeps } from '../__mocks__'; +import { pieDonutArgTypes, pieConfig, data } from './shared'; + +const containerSize = { + width: '700px', + height: '700px', +}; + +const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps }); + +type Props = { + visType: RenderValue['visType']; + syncColors: RenderValue['syncColors']; +} & RenderValue['visConfig']; + +const PartitionVis: ComponentStory> = ({ + visType, + syncColors, + children, + ...visConfig +}) => ( + +); + +export default { + title: 'renderers/pieVis', + component: PartitionVis, + argTypes: pieDonutArgTypes, +}; + +const Default = PartitionVis.bind({}); +Default.args = { ...pieConfig, visType: ChartTypes.PIE, syncColors: false }; + +export { Default }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts new file mode 100644 index 000000000000..1a18c905548d --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/arg_types.ts @@ -0,0 +1,216 @@ +/* + * 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 { ArgTypes } from '@storybook/addons'; +import { EmptySizeRatios, LegendDisplay } from '../../../common'; +import { ChartTypes } from '../../../common/types'; + +const visConfigName = 'visConfig'; + +export const argTypes: ArgTypes = { + addTooltip: { + name: `${visConfigName}.addTooltip`, + description: 'Add tooltip on hover', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: true } }, + control: { type: 'boolean' }, + }, + legendDisplay: { + name: `${visConfigName}.legendDisplay`, + description: 'Legend mode of displaying', + type: { name: 'string', required: false }, + table: { type: { summary: 'string' }, defaultValue: { summary: LegendDisplay.HIDE } }, + options: Object.values(LegendDisplay), + control: { type: 'select' }, + }, + legendPosition: { + name: `${visConfigName}.legendPosition`, + description: 'Legend position', + type: { name: 'string', required: false }, + table: { type: { summary: 'string' }, defaultValue: { summary: Position.Bottom } }, + options: Object.values(Position), + control: { type: 'select' }, + }, + truncateLegend: { + name: `${visConfigName}.truncateLegend`, + description: 'Truncate too long legend', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: true } }, + control: { type: 'boolean' }, + }, + maxLegendLines: { + name: `${visConfigName}.maxLegendLines`, + description: 'Legend maximum number of lines', + type: { name: 'number', required: false }, + table: { type: { summary: 'number' } }, + control: { type: 'number' }, + }, + palette: { + name: `${visConfigName}.palette`, + description: 'Palette', + type: { name: 'palette', required: false }, + table: { type: { summary: 'object' } }, + control: { type: 'object' }, + }, + labels: { + name: `${visConfigName}.labels`, + description: 'Labels configuration', + type: { name: 'object', required: false }, + table: { + type: { + summary: 'object', + detail: `Labels configuration consists of further fields: + - show: boolean. Default: true. + - position: string. Options: 'default', 'inside'. Default: 'default'. + - values: boolean. Default: true. + - percentDecimals: number. Default: 2. + - last_level: boolean. Default: false. DEPRECATED. + - truncate: number. Default: null. + - valuesFormat: string. Options: 'percent', 'value'. Default: percent. + `, + }, + }, + control: { type: 'object' }, + }, + dimensions: { + name: `${visConfigName}.dimensions`, + description: 'dimensions configuration', + type: { name: 'object', required: false }, + table: { + type: { + summary: 'object', + detail: `Dimensions configuration consists of two fields: + - metric: visdimension. + - buckets: visdimension[]. + `, + }, + }, + control: { type: 'object' }, + }, +}; + +export const pieDonutArgTypes: ArgTypes = { + ...argTypes, + visType: { + name: `visType`, + description: 'Type of the chart', + type: { name: 'string', required: false }, + table: { + type: { summary: 'string' }, + defaultValue: { summary: `${ChartTypes.PIE} | ${ChartTypes.DONUT}` }, + }, + control: { type: 'text', disable: true }, + }, + isDonut: { + name: `${visConfigName}.isDonut`, + description: 'Render a donut chart', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: false } }, + control: { type: 'boolean' }, + }, + emptySizeRatio: { + name: `${visConfigName}.emptySizeRatio`, + description: 'The hole size of the donut chart', + type: { name: 'number', required: false }, + table: { type: { summary: 'number' }, defaultValue: { summary: EmptySizeRatios.SMALL } }, + options: [EmptySizeRatios.SMALL, EmptySizeRatios.MEDIUM, EmptySizeRatios.LARGE], + control: { type: 'select' }, + }, + distinctColors: { + name: `${visConfigName}.distinctColors`, + description: 'Enable distinct colors', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: false } }, + control: { type: 'boolean' }, + }, + respectSourceOrder: { + name: `${visConfigName}.respectSourceOrder`, + description: 'Save default order of the incomming data', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: true } }, + control: { type: 'boolean' }, + }, + startFromSecondLargestSlice: { + name: `${visConfigName}.startFromSecondLargestSlice`, + description: 'Start placement of slices from the second largest slice', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: true } }, + control: { type: 'boolean' }, + }, + nestedLegend: { + name: `${visConfigName}.nestedLegend`, + description: 'Enable nested legend', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: false } }, + control: { type: 'boolean' }, + }, +}; + +export const treemapArgTypes: ArgTypes = { + visType: { + name: `visType`, + description: 'Type of the chart', + type: { name: 'string', required: false }, + table: { + type: { summary: 'string' }, + defaultValue: { summary: `${ChartTypes.TREEMAP}` }, + }, + control: { type: 'text', disable: true }, + }, + ...argTypes, + nestedLegend: { + name: `${visConfigName}.nestedLegend`, + description: 'Enable nested legend', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: false } }, + control: { type: 'boolean' }, + }, +}; + +export const mosaicArgTypes: ArgTypes = { + visType: { + name: `visType`, + description: 'Type of the chart', + type: { name: 'string', required: false }, + table: { + type: { summary: 'string' }, + defaultValue: { summary: `${ChartTypes.MOSAIC}` }, + }, + control: { type: 'text', disable: true }, + }, + ...argTypes, + nestedLegend: { + name: `${visConfigName}.nestedLegend`, + description: 'Enable nested legend', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: false } }, + control: { type: 'boolean' }, + }, +}; + +export const waffleArgTypes: ArgTypes = { + visType: { + name: `visType`, + description: 'Type of the chart', + type: { name: 'string', required: false }, + table: { + type: { summary: 'string' }, + defaultValue: { summary: `${ChartTypes.WAFFLE}` }, + }, + control: { type: 'text', disable: true }, + }, + ...argTypes, + showValuesInLegend: { + name: `${visConfigName}.nestedLegend`, + description: 'Enable displaying values in the legend', + type: { name: 'boolean', required: false }, + table: { type: { summary: 'boolean' }, defaultValue: { summary: false } }, + control: { type: 'boolean' }, + }, +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts new file mode 100644 index 000000000000..d16802518cce --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts @@ -0,0 +1,129 @@ +/* + * 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 { + LabelPositions, + LegendDisplay, + RenderValue, + PartitionVisParams, + ValueFormats, +} from '../../../common/types'; + +export const config: RenderValue['visConfig'] = { + addTooltip: true, + legendDisplay: LegendDisplay.HIDE, + truncateLegend: true, + respectSourceOrder: true, + legendPosition: Position.Bottom, + maxLegendLines: 1, + palette: { + type: 'palette', + name: 'system_palette', + }, + labels: { + show: true, + position: LabelPositions.DEFAULT, + percentDecimals: 2, + values: true, + truncate: 0, + valuesFormat: ValueFormats.PERCENT, + last_level: false, + }, + dimensions: { + metric: { + type: 'vis_dimension', + accessor: { + id: 'percent_uptime', + name: 'percent_uptime', + meta: { + type: 'number', + }, + }, + format: { + id: 'string', + params: {}, + }, + }, + }, +}; + +export const pieConfig: PartitionVisParams = { + ...config, + isDonut: false, + emptySizeRatio: 0, + distinctColors: false, + nestedLegend: false, + dimensions: { + ...config.dimensions, + buckets: [ + { + type: 'vis_dimension', + accessor: { + id: 'project', + name: 'project', + meta: { + type: 'string', + }, + }, + format: { + id: 'string', + params: {}, + }, + }, + ], + }, + startFromSecondLargestSlice: true, +}; + +export const treemapMosaicConfig: PartitionVisParams = { + ...config, + nestedLegend: false, + dimensions: { + ...config.dimensions, + buckets: [ + { + type: 'vis_dimension', + accessor: { + id: 'project', + name: 'project', + meta: { + type: 'string', + }, + }, + format: { + id: 'string', + params: {}, + }, + }, + ], + }, +}; + +export const waffleConfig: PartitionVisParams = { + ...config, + dimensions: { + ...config.dimensions, + buckets: [ + { + type: 'vis_dimension', + accessor: { + id: 'project', + name: 'project', + meta: { + type: 'string', + }, + }, + format: { + id: 'string', + params: {}, + }, + }, + ], + }, + showValuesInLegend: false, +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts new file mode 100644 index 000000000000..e02f090b5f7f --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts @@ -0,0 +1,207 @@ +/* + * 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 { RenderValue } from '../../../common/types'; + +export const data: RenderValue['visData'] = { + type: 'datatable', + columns: [ + { + id: '@timestamp', + name: '@timestamp', + meta: { + type: 'date', + }, + }, + { + id: 'time', + name: 'time', + meta: { + type: 'date', + }, + }, + { + id: 'cost', + name: 'cost', + meta: { + type: 'number', + }, + }, + { + id: 'username', + name: 'username', + meta: { + type: 'string', + }, + }, + { + id: 'price', + name: 'price', + meta: { + type: 'number', + }, + }, + { + id: 'age', + name: 'age', + meta: { + type: 'number', + }, + }, + { + id: 'country', + name: 'country', + meta: { + type: 'string', + }, + }, + { + id: 'state', + name: 'state', + meta: { + type: 'string', + }, + }, + { + id: 'project', + name: 'project', + meta: { + type: 'string', + }, + }, + { + id: 'percent_uptime', + name: 'percent_uptime', + meta: { + type: 'number', + }, + }, + ], + rows: [ + { + age: 63, + cost: 32.15, + country: 'US', + price: 53, + project: 'elasticsearch', + state: 'running', + time: 1546334211208, + '@timestamp': 1546334211208, + username: 'aevans2e', + percent_uptime: 0.83, + }, + { + age: 68, + cost: 20.52, + country: 'JP', + price: 33, + project: 'beats', + state: 'done', + time: 1546351551031, + '@timestamp': 1546351551031, + username: 'aking2c', + percent_uptime: 0.9, + }, + { + age: 57, + cost: 21.15, + country: 'UK', + price: 59, + project: 'apm', + state: 'running', + time: 1546352631083, + '@timestamp': 1546352631083, + username: 'mmoore2o', + percent_uptime: 0.96, + }, + { + age: 73, + cost: 35.64, + country: 'CN', + price: 71, + project: 'machine-learning', + state: 'start', + time: 1546402490956, + '@timestamp': 1546402490956, + username: 'wrodriguez1r', + percent_uptime: 0.61, + }, + { + age: 38, + cost: 27.19, + country: 'TZ', + price: 36, + project: 'kibana', + state: 'done', + time: 1546467111351, + '@timestamp': 1546467111351, + username: 'wrodriguez1r', + percent_uptime: 0.72, + }, + { + age: 61, + cost: 49.95, + country: 'NL', + price: 65, + project: 'machine-learning', + state: 'start', + time: 1546473771019, + '@timestamp': 1546473771019, + username: 'mmoore2o', + percent_uptime: 0.72, + }, + { + age: 53, + cost: 27.36, + country: 'JP', + price: 60, + project: 'x-pack', + state: 'running', + time: 1546482171310, + '@timestamp': 1546482171310, + username: 'hcrawford2h', + percent_uptime: 0.65, + }, + { + age: 31, + cost: 33.77, + country: 'AZ', + price: 77, + project: 'kibana', + state: 'start', + time: 1546493451206, + '@timestamp': 1546493451206, + username: 'aking2c', + percent_uptime: 0.92, + }, + { + age: 71, + cost: 20.2, + country: 'TZ', + price: 57, + project: 'swiftype', + state: 'running', + time: 1546494651235, + '@timestamp': 1546494651235, + username: 'jlawson2p', + percent_uptime: 0.59, + }, + { + age: 54, + cost: 36.65, + country: 'TZ', + price: 72, + project: 'apm', + state: 'done', + time: 1546498431195, + '@timestamp': 1546498431195, + username: 'aking2c', + percent_uptime: 1, + }, + ], +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/index.ts new file mode 100644 index 000000000000..10d31b77d697 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/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. + */ + +export { data } from './data'; +export { config, pieConfig, treemapMosaicConfig, waffleConfig } from './config'; +export { + argTypes, + pieDonutArgTypes, + treemapArgTypes, + mosaicArgTypes, + waffleArgTypes, +} from './arg_types'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx new file mode 100644 index 000000000000..a8f9010ade4a --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { ComponentStory } from '@storybook/react'; +import { Render } from '../../../../presentation_util/public/__stories__'; +import { getPartitionVisRenderer } from '../expression_renderers'; +import { ChartTypes, RenderValue } from '../../common/types'; +import { palettes, theme, getStartDeps } from '../__mocks__'; +import { treemapArgTypes, treemapMosaicConfig, data } from './shared'; + +const containerSize = { + width: '700px', + height: '700px', +}; + +const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps }); + +type Props = { + visType: RenderValue['visType']; + syncColors: RenderValue['syncColors']; +} & RenderValue['visConfig']; + +const PartitionVis: ComponentStory> = ({ + visType, + syncColors, + children, + ...visConfig +}) => ( + +); + +export default { + title: 'renderers/treemapVis', + component: PartitionVis, + argTypes: treemapArgTypes, +}; + +const Default = PartitionVis.bind({}); +Default.args = { ...treemapMosaicConfig, visType: ChartTypes.TREEMAP, syncColors: false }; + +export { Default }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx new file mode 100644 index 000000000000..a97efdabef89 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { ComponentStory } from '@storybook/react'; +import { Render } from '../../../../presentation_util/public/__stories__'; +import { getPartitionVisRenderer } from '../expression_renderers'; +import { ChartTypes, RenderValue } from '../../common/types'; +import { palettes, theme, getStartDeps } from '../__mocks__'; +import { waffleArgTypes, waffleConfig, data } from './shared'; + +const containerSize = { + width: '700px', + height: '700px', +}; + +const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps }); + +type Props = { + visType: RenderValue['visType']; + syncColors: RenderValue['syncColors']; +} & RenderValue['visConfig']; + +const PartitionVis: ComponentStory> = ({ + visType, + syncColors, + children, + ...visConfig +}) => ( + +); + +export default { + title: 'renderers/waffleVis', + component: PartitionVis, + argTypes: waffleArgTypes, +}; + +const Default = PartitionVis.bind({}); +Default.args = { ...waffleConfig, visType: ChartTypes.WAFFLE, syncColors: false }; + +export { Default }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap new file mode 100644 index 000000000000..4e56d2c5efa4 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap @@ -0,0 +1,1993 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PartitionVisComponent should render correct structure for donut 1`] = ` +
+
+ + + + + + + + +
+
+`; + +exports[`PartitionVisComponent should render correct structure for mosaic 1`] = ` +
+
+ + + + + + + + +
+
+`; + +exports[`PartitionVisComponent should render correct structure for pie 1`] = ` +
+
+ + + + + + + + +
+
+`; + +exports[`PartitionVisComponent should render correct structure for treemap 1`] = ` +
+
+ + + + + + + + +
+
+`; + +exports[`PartitionVisComponent should render correct structure for waffle 1`] = ` +
+
+ + + + + + + + +
+
+`; diff --git a/src/plugins/chart_expressions/expression_pie/public/components/chart_split.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/chart_split.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/components/chart_split.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/components/chart_split.tsx diff --git a/src/plugins/chart_expressions/expression_pie/public/expression_renderers/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/components/index.ts similarity index 86% rename from src/plugins/chart_expressions/expression_pie/public/expression_renderers/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/components/index.ts index 3f370b63b457..14a49bafb689 100644 --- a/src/plugins/chart_expressions/expression_pie/public/expression_renderers/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { getPieVisRenderer } from './pie_vis_renderer'; +export * from './partition_vis_component'; diff --git a/src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.styles.ts b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.styles.ts similarity index 78% rename from src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.styles.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.styles.ts index 678bdd1a73c8..b713b3b22964 100644 --- a/src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.styles.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.styles.ts @@ -9,15 +9,15 @@ import { EuiThemeComputed } from '@elastic/eui'; import { css } from '@emotion/react'; -export const pieChartWrapperStyle = css({ +export const partitionVisWrapperStyle = css({ display: 'flex', flex: '1 1 auto', minHeight: 0, minWidth: 0, }); -export const pieChartContainerStyleFactory = (theme: EuiThemeComputed) => css` - ${pieChartWrapperStyle}; +export const partitionVisContainerStyleFactory = (theme: EuiThemeComputed) => css` + ${partitionVisWrapperStyle}; position: absolute; top: 0; @@ -27,4 +27,5 @@ export const pieChartContainerStyleFactory = (theme: EuiThemeComputed) => css` padding: ${theme.size.s}; margin-left: auto; margin-right: auto; + overflow: hidden; `; diff --git a/src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx similarity index 65% rename from src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.test.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx index 0e0a26483ef6..ddade06c2c7e 100644 --- a/src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.test.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx @@ -15,8 +15,15 @@ import type { Datatable } from '../../../../expressions/public'; import { shallow, mount } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { act } from 'react-dom/test-utils'; -import PieComponent, { PieComponentProps } from './pie_vis_component'; -import { createMockPieParams, createMockVisData } from '../mocks'; +import PartitionVisComponent, { PartitionVisComponentProps } from './partition_vis_component'; +import { + createMockDonutParams, + createMockPieParams, + createMockTreemapMosaicParams, + createMockVisData, + createMockWaffleParams, +} from '../mocks'; +import { ChartTypes } from '../../common/types'; jest.mock('@elastic/charts', () => { const original = jest.requireActual('@elastic/charts'); @@ -42,8 +49,8 @@ const uiState = { setSilent: jest.fn(), } as any; -describe('PieComponent', function () { - let wrapperProps: PieComponentProps; +describe('PartitionVisComponent', function () { + let wrapperProps: PartitionVisComponentProps; beforeAll(() => { wrapperProps = { @@ -51,6 +58,7 @@ describe('PieComponent', function () { palettesRegistry, visParams, visData, + visType: ChartTypes.PIE, uiState, syncColors: false, fireEvent: jest.fn(), @@ -62,20 +70,81 @@ describe('PieComponent', function () { }; }); + it('should render correct structure for pie', function () { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should render correct structure for donut', function () { + const donutVisParams = createMockDonutParams(); + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render correct structure for treemap', function () { + const treemapVisParams = createMockTreemapMosaicParams(); + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render correct structure for mosaic', function () { + const mosaicVisParams = createMockTreemapMosaicParams(); + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render correct structure for waffle', function () { + const waffleVisParams = createMockWaffleParams(); + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + it('renders the legend on the correct position', () => { - const component = shallow(); + const component = shallow(); expect(component.find(Settings).prop('legendPosition')).toEqual('right'); }); it('renders the legend toggle component', async () => { - const component = mount(); + const component = mount(); await act(async () => { expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(1); }); }); it('hides the legend if the legend toggle is clicked', async () => { - const component = mount(); + const component = mount(); findTestSubject(component, 'vislibToggleLegend').simulate('click'); await act(async () => { expect(component.find(Settings).prop('showLegend')).toEqual(false); @@ -83,31 +152,31 @@ describe('PieComponent', function () { }); it('defaults on showing the legend for the inner cicle', () => { - const component = shallow(); + const component = shallow(); expect(component.find(Settings).prop('legendMaxDepth')).toBe(1); }); it('shows the nested legend when the user requests it', () => { const newParams = { ...visParams, nestedLegend: true }; const newProps = { ...wrapperProps, visParams: newParams }; - const component = shallow(); + const component = shallow(); expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined(); }); it('defaults on displaying the tooltip', () => { - const component = shallow(); + const component = shallow(); expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.Follow }); }); it('doesnt show the tooltip when the user requests it', () => { const newParams = { ...visParams, addTooltip: false }; const newProps = { ...wrapperProps, visParams: newParams }; - const component = shallow(); + const component = shallow(); expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.None }); }); it('calls filter callback', () => { - const component = shallow(); + const component = shallow(); component.find(Settings).first().prop('onElementClick')!([ [ [ @@ -130,14 +199,14 @@ describe('PieComponent', function () { const newVisData = { type: 'datatable', columns: [ - { - id: 'col-1-1', - name: 'Count', - }, { id: 'col-0-2', name: 'filters', }, + { + id: 'col-1-1', + name: 'Count', + }, ], rows: [ { @@ -151,7 +220,7 @@ describe('PieComponent', function () { ], } as unknown as Datatable; const newProps = { ...wrapperProps, visData: newVisData }; - const component = mount(); + const component = mount(); expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual('No results found'); }); @@ -159,14 +228,14 @@ describe('PieComponent', function () { const newVisData = { type: 'datatable', columns: [ - { - id: 'col-1-1', - name: 'Count', - }, { id: 'col-0-2', name: 'filters', }, + { + id: 'col-1-1', + name: 'Count', + }, ], rows: [ { @@ -180,7 +249,7 @@ describe('PieComponent', function () { ], } as unknown as Datatable; const newProps = { ...wrapperProps, visData: newVisData }; - const component = mount(); + const component = mount(); expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual( "Pie/donut charts can't render with negative values." ); diff --git a/src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx similarity index 71% rename from src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index 82d0a68f3d1b..834a0c9c9547 100644 --- a/src/plugins/chart_expressions/expression_pie/public/components/pie_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -18,7 +18,6 @@ import { TooltipProps, TooltipType, SeriesIdentifier, - PartitionLayout, } from '@elastic/charts'; import { useEuiTheme } from '@elastic/eui'; import { @@ -36,7 +35,7 @@ import { import type { FieldFormat } from '../../../../field_formats/common'; import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants'; import { - PieVisParams, + PartitionVisParams, BucketColumns, ValueFormats, PieContainerDimensions, @@ -53,11 +52,21 @@ import { getColumns, getSplitDimensionAccessor, getColumnByAccessor, + isLegendFlat, + shouldShowLegend, + generateFormatters, + getFormatter, + getPartitionType, } from '../utils'; import { ChartSplit, SMALL_MULTIPLES_ID } from './chart_split'; import { VisualizationNoResults } from './visualization_noresults'; import { VisTypePiePluginStartDependencies } from '../plugin'; -import { pieChartWrapperStyle, pieChartContainerStyleFactory } from './pie_vis_component.styles'; +import { + partitionVisWrapperStyle, + partitionVisContainerStyleFactory, +} from './partition_vis_component.styles'; +import { ChartTypes } from '../../common/types'; +import { filterOutConfig } from '../utils/filter_out_config'; declare global { interface Window { @@ -67,9 +76,10 @@ declare global { _echDebugStateFlag?: boolean; } } -export interface PieComponentProps { - visParams: PieVisParams; +export interface PartitionVisComponentProps { + visParams: PartitionVisParams; visData: Datatable; + visType: ChartTypes; uiState: PersistedState; fireEvent: IInterpreterRenderHandlers['event']; renderComplete: IInterpreterRenderHandlers['done']; @@ -79,15 +89,31 @@ export interface PieComponentProps { syncColors: boolean; } -const PieComponent = (props: PieComponentProps) => { +const PartitionVisComponent = (props: PartitionVisComponentProps) => { + const { visData, visParams: preVisParams, visType, services, syncColors } = props; + const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]); + const theme = useEuiTheme(); const chartTheme = props.chartsThemeService.useChartsTheme(); const chartBaseTheme = props.chartsThemeService.useChartsBaseTheme(); - const [showLegend, setShowLegend] = useState(() => { - const bwcLegendStateDefault = - props.visParams.addLegend == null ? false : props.visParams.addLegend; - return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) ?? bwcLegendStateDefault; - }); + + const { bucketColumns, metricColumn } = useMemo( + () => getColumns(props.visParams, props.visData), + [props.visData, props.visParams] + ); + + const formatters = useMemo( + () => generateFormatters(visParams, visData, services.fieldFormats.deserialize), + [services.fieldFormats.deserialize, visData, visParams] + ); + + const showLegendDefault = useCallback(() => { + const showLegendDef = shouldShowLegend(visType, visParams.legendDisplay, bucketColumns); + return props.uiState?.get('vis.legendOpen', showLegendDef) ?? showLegendDef; + }, [bucketColumns, props.uiState, visParams.legendDisplay, visType]); + + const [showLegend, setShowLegend] = useState(() => showLegendDefault()); + const [dimensions, setDimensions] = useState(); const parentRef = useRef(null); @@ -100,6 +126,12 @@ const PieComponent = (props: PieComponentProps) => { } }, [parentRef]); + useEffect(() => { + const legendShow = showLegendDefault(); + setShowLegend(legendShow); + props.uiState?.set('vis.legendOpen', legendShow); + }, [showLegendDefault, props.uiState]); + const onRenderChange = useCallback( (isRendered) => { if (isRendered) { @@ -113,15 +145,15 @@ const PieComponent = (props: PieComponentProps) => { const handleSliceClick = useCallback( ( clickedLayers: LayerValue[], - bucketColumns: Array>, - visData: Datatable, + buckets: Array>, + vData: Datatable, splitChartDimension?: DatatableColumn, splitChartFormatter?: FieldFormat ): void => { const data = getFilterClickData( clickedLayers, - bucketColumns, - visData, + buckets, + vData, splitChartDimension, splitChartFormatter ); @@ -136,9 +168,9 @@ const PieComponent = (props: PieComponentProps) => { // handles legend action event data const getLegendActionEventData = useCallback( - (visData: Datatable) => + (vData: Datatable) => (series: SeriesIdentifier): ClickTriggerEvent | null => { - const data = getFilterEventData(visData, series); + const data = getFilterEventData(vData, series); return { name: 'filterBucket', @@ -172,11 +204,6 @@ const PieComponent = (props: PieComponentProps) => { }); }, [props.uiState]); - useEffect(() => { - setShowLegend(props.visParams.addLegend); - props.uiState?.set('vis.legendOpen', props.visParams.addLegend); - }, [props.uiState, props.visParams.addLegend]); - const setColor = useCallback( (newColor: string | null, seriesLabel: string | number) => { const colors = props.uiState?.get('vis.colors') || {}; @@ -192,22 +219,22 @@ const PieComponent = (props: PieComponentProps) => { [props.uiState] ); - const { visData, visParams, services, syncColors } = props; - - function getSliceValue(d: Datum, metricColumn: DatatableColumn) { - const value = d[metricColumn.id]; + const getSliceValue = useCallback((d: Datum, metric: DatatableColumn) => { + const value = d[metric.id]; return Number.isFinite(value) && value >= 0 ? value : 0; - } + }, []); + const defaultFormatter = services.fieldFormats.deserialize; // formatters - const metricFieldFormatter = services.fieldFormats.deserialize( - visParams.dimensions.metric.format - ); - const splitChartFormatter = visParams.dimensions.splitColumn - ? services.fieldFormats.deserialize(visParams.dimensions.splitColumn[0].format) - : visParams.dimensions.splitRow - ? services.fieldFormats.deserialize(visParams.dimensions.splitRow[0].format) + const metricFieldFormatter = getFormatter(metricColumn, formatters, defaultFormatter); + const { splitColumn, splitRow } = visParams.dimensions; + + const splitChartFormatter = splitColumn + ? getFormatter(splitColumn[0], formatters, defaultFormatter) + : splitRow + ? getFormatter(splitRow[0], formatters, defaultFormatter) : undefined; + const percentFormatter = services.fieldFormats.deserialize({ id: 'percent', params: { @@ -215,70 +242,71 @@ const PieComponent = (props: PieComponentProps) => { }, }); - const { bucketColumns, metricColumn } = useMemo( - () => getColumns(visParams, visData), - [visData, visParams] - ); - + const isDarkMode = props.chartsThemeService.useDarkMode(); const layers = useMemo( () => getLayers( + visType, bucketColumns, visParams, + visData, props.uiState?.get('vis.colors', {}), visData.rows, props.palettesRegistry, + formatters, services.fieldFormats, - syncColors + syncColors, + isDarkMode ), [ + visType, bucketColumns, visParams, + visData, props.uiState, props.palettesRegistry, - visData.rows, + formatters, services.fieldFormats, syncColors, + isDarkMode, ] ); const rescaleFactor = useMemo(() => { const overallSum = visData.rows.reduce((sum, row) => sum + row[metricColumn.id], 0); const slices = visData.rows.map((row) => row[metricColumn.id] / overallSum); - const smallSlices = slices.filter((value) => value < 0.02).length; - if (smallSlices) { + const smallSlices = slices.filter((value) => value < 0.02) ?? []; + if (smallSlices.length) { // shrink up to 20% to give some room for the linked values - return 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); + return 1 / (1 + Math.min(smallSlices.length * 0.05, 0.2)); } return 1; }, [visData.rows, metricColumn]); const themeOverrides = useMemo( - () => getPartitionTheme(visParams, chartTheme, dimensions, rescaleFactor), - [chartTheme, visParams, dimensions, rescaleFactor] + () => getPartitionTheme(visType, visParams, chartTheme, dimensions, rescaleFactor), + [visType, visParams, chartTheme, dimensions, rescaleFactor] ); + + const fixedViewPort = document.getElementById('app-fixed-viewport'); const tooltip: TooltipProps = { + ...(fixedViewPort ? { boundary: fixedViewPort } : {}), type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None, }; const legendPosition = visParams.legendPosition ?? Position.Right; - const splitChartColumnAccessor = visParams.dimensions.splitColumn - ? getSplitDimensionAccessor( - services.fieldFormats, - visData.columns - )(visParams.dimensions.splitColumn[0]) - : undefined; - const splitChartRowAccessor = visParams.dimensions.splitRow - ? getSplitDimensionAccessor( - services.fieldFormats, - visData.columns - )(visParams.dimensions.splitRow[0]) + const splitChartColumnAccessor = splitColumn + ? getSplitDimensionAccessor(visData.columns, splitColumn[0], formatters, defaultFormatter) : undefined; - const splitChartDimension = visParams.dimensions.splitColumn - ? getColumnByAccessor(visParams.dimensions.splitColumn[0].accessor, visData.columns) - : visParams.dimensions.splitRow - ? getColumnByAccessor(visParams.dimensions.splitRow[0].accessor, visData.columns) + const splitChartRowAccessor = splitRow + ? getSplitDimensionAccessor(visData.columns, splitRow[0], formatters, defaultFormatter) + : undefined; + + const splitChartDimension = splitColumn + ? getColumnByAccessor(splitColumn[0].accessor, visData.columns) + : splitRow + ? getColumnByAccessor(splitRow[0].accessor, visData.columns) : undefined; /** @@ -302,15 +330,16 @@ const PieComponent = (props: PieComponentProps) => { }), [visData.rows, metricColumn] ); - + const flatLegend = isLegendFlat(visType, splitChartDimension); const canShowPieChart = !isAllZeros && !hasNegative; + const partitionType = getPartitionType(visType); return ( -
+
{!canShowPieChart ? ( ) : ( -
+
{ palette: visParams.palette.name, data: visData.rows, uiState: props.uiState, - distinctColors: visParams.distinctColors, + distinctColors: visParams.distinctColors ?? false, }} > { legendPosition={legendPosition} legendMaxDepth={visParams.nestedLegend ? undefined : 1} legendColorPicker={props.uiState ? LegendColorPickerWrapper : undefined} - flatLegend={Boolean(splitChartDimension)} + flatLegend={flatLegend} tooltip={tooltip} + showLegendExtra={visParams.showValuesInLegend} onElementClick={(args) => { handleSliceClick( args[0][0] as LayerValue[], @@ -374,11 +404,11 @@ const PieComponent = (props: PieComponentProps) => { onRenderChange={onRenderChange} /> getSliceValue(d, metricColumn)} percentFormatter={(d: number) => percentFormatter.convert(d / 100)} valueGetter={ @@ -386,7 +416,7 @@ const PieComponent = (props: PieComponentProps) => { visParams.labels.valuesFormat === ValueFormats.VALUE || !visParams.labels.values ? undefined - : 'percent' + : ValueFormats.PERCENT } valueFormatter={(d: number) => !visParams.labels.show || !visParams.labels.values @@ -405,4 +435,4 @@ const PieComponent = (props: PieComponentProps) => { }; // eslint-disable-next-line import/no-default-export -export default memo(PieComponent); +export default memo(PartitionVisComponent); diff --git a/src/plugins/chart_expressions/expression_pie/public/components/visualization_noresults.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/visualization_noresults.tsx similarity index 86% rename from src/plugins/chart_expressions/expression_pie/public/components/visualization_noresults.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/components/visualization_noresults.tsx index 46478556f5f9..8362d17920bd 100644 --- a/src/plugins/chart_expressions/expression_pie/public/components/visualization_noresults.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/visualization_noresults.tsx @@ -19,10 +19,10 @@ export const VisualizationNoResults = ({ hasNegativeValues = false }) => { body={ {hasNegativeValues - ? i18n.translate('expressionPie.negativeValuesFound', { + ? i18n.translate('expressionPartitionVis.negativeValuesFound', { defaultMessage: "Pie/donut charts can't render with negative values.", }) - : i18n.translate('expressionPie.noResultsFoundTitle', { + : i18n.translate('expressionPartitionVis.noResultsFoundTitle', { defaultMessage: 'No results found', })} diff --git a/src/plugins/chart_expressions/expression_pie/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/index.ts similarity index 76% rename from src/plugins/chart_expressions/expression_pie/common/expression_functions/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/index.ts index ee8f0ec06d43..17a103370e9f 100644 --- a/src/plugins/chart_expressions/expression_pie/common/expression_functions/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { pieVisFunction } from './pie_vis_function'; -export { pieLabelsFunction } from './pie_labels_function'; +export { getPartitionVisRenderer } from './partition_vis_renderer'; diff --git a/src/plugins/chart_expressions/expression_pie/public/expression_renderers/pie_vis_renderer.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx similarity index 79% rename from src/plugins/chart_expressions/expression_pie/public/expression_renderers/pie_vis_renderer.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx index b65e8696f7a5..c3521c7346a8 100644 --- a/src/plugins/chart_expressions/expression_pie/public/expression_renderers/pie_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx @@ -15,23 +15,23 @@ import { VisualizationContainer } from '../../../../visualizations/public'; import type { PersistedState } from '../../../../visualizations/public'; import { KibanaThemeProvider } from '../../../../kibana_react/public'; -import { PIE_VIS_EXPRESSION_NAME } from '../../common/constants'; -import { RenderValue } from '../../common/types'; +import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants'; +import { ChartTypes, RenderValue } from '../../common/types'; import { VisTypePieDependencies } from '../plugin'; export const strings = { getDisplayName: () => - i18n.translate('expressionPie.renderer.pieVis.displayName', { + i18n.translate('expressionPartitionVis.renderer.pieVis.displayName', { defaultMessage: 'Pie visualization', }), getHelpDescription: () => - i18n.translate('expressionPie.renderer.pieVis.helpDescription', { + i18n.translate('expressionPartitionVis.renderer.pieVis.helpDescription', { defaultMessage: 'Render a pie', }), }; -const PieComponent = lazy(() => import('../components/pie_vis_component')); +const PartitionVisComponent = lazy(() => import('../components/partition_vis_component')); function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean { const rows: object[] | undefined = visData?.rows; @@ -40,14 +40,14 @@ function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean { return Boolean(isZeroHits); } -export const getPieVisRenderer: ( +export const getPartitionVisRenderer: ( deps: VisTypePieDependencies ) => ExpressionRenderDefinition = ({ theme, palettes, getStartDeps }) => ({ - name: PIE_VIS_EXPRESSION_NAME, + name: PARTITION_VIS_RENDERER_NAME, displayName: strings.getDisplayName(), help: strings.getHelpDescription(), reuseDomNode: true, - render: async (domNode, { visConfig, visData, syncColors }, handlers) => { + render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => { const showNoResult = shouldShowNoResultsMessage(visData); handlers.onDestroy(() => { @@ -61,11 +61,12 @@ export const getPieVisRenderer: ( - { return [ @@ -107,50 +108,50 @@ export const createMockVisData = (): Datatable => { rows: [ { 'col-0-2': 'Logstash Airways', - 'col-2-3': 0, 'col-1-1': 797, + 'col-2-3': 0, 'col-3-1': 689, }, { 'col-0-2': 'Logstash Airways', - 'col-2-3': 1, 'col-1-1': 797, + 'col-2-3': 1, 'col-3-1': 108, }, { 'col-0-2': 'JetBeats', - 'col-2-3': 0, 'col-1-1': 766, + 'col-2-3': 0, 'col-3-1': 654, }, { 'col-0-2': 'JetBeats', - 'col-2-3': 1, 'col-1-1': 766, + 'col-2-3': 1, 'col-3-1': 112, }, { 'col-0-2': 'ES-Air', - 'col-2-3': 0, 'col-1-1': 744, + 'col-2-3': 0, 'col-3-1': 665, }, { 'col-0-2': 'ES-Air', - 'col-2-3': 1, 'col-1-1': 744, + 'col-2-3': 1, 'col-3-1': 79, }, { 'col-0-2': 'Kibana Airlines', - 'col-2-3': 0, 'col-1-1': 731, + 'col-2-3': 0, 'col-3-1': 655, }, { 'col-0-2': 'Kibana Airlines', - 'col-2-3': 1, 'col-1-1': 731, + 'col-2-3': 1, 'col-3-1': 76, }, ], @@ -269,9 +270,9 @@ export const createMockVisData = (): Datatable => { }; }; -export const createMockPieParams = (): PieVisParams => { +export const createMockPartitionVisParams = (): PartitionVisParams => { return { - addLegend: true, + legendDisplay: LegendDisplay.SHOW, addTooltip: true, isDonut: true, labels: { @@ -291,19 +292,20 @@ export const createMockPieParams = (): PieVisParams => { name: 'default', type: 'palette', }, - type: 'pie', dimensions: { metric: { + type: 'vis_dimension', accessor: 1, format: { id: 'number', + params: { + id: 'number', + }, }, - params: {}, - label: 'Count', - aggType: 'count', }, buckets: [ { + type: 'vis_dimension', accessor: 0, format: { id: 'terms', @@ -313,10 +315,9 @@ export const createMockPieParams = (): PieVisParams => { missingBucketLabel: 'Missing', }, }, - label: 'Carrier: Descending', - aggType: 'terms', }, { + type: 'vis_dimension', accessor: 2, format: { id: 'terms', @@ -326,10 +327,56 @@ export const createMockPieParams = (): PieVisParams => { missingBucketLabel: 'Missing', }, }, - label: 'Cancelled: Descending', - aggType: 'terms', }, ], }, - } as unknown as PieVisParams; + }; +}; + +export const createMockPieParams = (): PartitionVisParams => { + return { + ...createMockPartitionVisParams(), + isDonut: false, + distinctColors: false, + }; +}; + +export const createMockDonutParams = (): PartitionVisParams => { + return { + ...createMockPartitionVisParams(), + isDonut: true, + emptySizeRatio: 0.3, + }; +}; + +export const createMockTreemapMosaicParams = (): PartitionVisParams => { + return { + ...createMockPartitionVisParams(), + nestedLegend: true, + }; +}; + +export const createMockWaffleParams = (): PartitionVisParams => { + const visParams = createMockPartitionVisParams(); + return { + ...visParams, + dimensions: { + ...visParams.dimensions, + buckets: [ + { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + }, + ], + }, + showValuesInLegend: true, + }; }; diff --git a/src/plugins/chart_expressions/expression_pie/public/plugin.ts b/src/plugins/chart_expressions/expression_partition_vis/public/plugin.ts similarity index 64% rename from src/plugins/chart_expressions/expression_pie/public/plugin.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/plugin.ts index 2c141027c65f..3bc3cdb31f9b 100755 --- a/src/plugins/chart_expressions/expression_pie/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/plugin.ts @@ -10,9 +10,20 @@ import { FieldFormatsStart } from '../../../field_formats/public'; import { CoreSetup, CoreStart, ThemeServiceStart } from '../../../../core/public'; import { ChartsPluginSetup } from '../../../charts/public'; import { DataPublicPluginStart } from '../../../data/public'; -import { pieLabelsFunction, pieVisFunction } from '../common'; -import { getPieVisRenderer } from './expression_renderers'; -import { ExpressionPiePluginSetup, ExpressionPiePluginStart, SetupDeps, StartDeps } from './types'; +import { + partitionLabelsFunction, + pieVisFunction, + treemapVisFunction, + mosaicVisFunction, + waffleVisFunction, +} from '../common'; +import { getPartitionVisRenderer } from './expression_renderers'; +import { + ExpressionPartitionVisPluginSetup, + ExpressionPartitionVisPluginStart, + SetupDeps, + StartDeps, +} from './types'; /** @internal */ export interface VisTypePieDependencies { @@ -30,13 +41,16 @@ export interface VisTypePiePluginStartDependencies { fieldFormats: FieldFormatsStart; } -export class ExpressionPiePlugin { +export class ExpressionPartitionVisPlugin { public setup( core: CoreSetup, { expressions, charts }: SetupDeps - ): ExpressionPiePluginSetup { - expressions.registerFunction(pieLabelsFunction); + ): ExpressionPartitionVisPluginSetup { + expressions.registerFunction(partitionLabelsFunction); expressions.registerFunction(pieVisFunction); + expressions.registerFunction(treemapVisFunction); + expressions.registerFunction(mosaicVisFunction); + expressions.registerFunction(waffleVisFunction); const getStartDeps = async () => { const [coreStart, deps] = await core.getStartServices(); @@ -46,11 +60,11 @@ export class ExpressionPiePlugin { }; expressions.registerRenderer( - getPieVisRenderer({ theme: charts.theme, palettes: charts.palettes, getStartDeps }) + getPartitionVisRenderer({ theme: charts.theme, palettes: charts.palettes, getStartDeps }) ); } - public start(core: CoreStart, deps: StartDeps): ExpressionPiePluginStart {} + public start(core: CoreStart, deps: StartDeps): ExpressionPartitionVisPluginStart {} public stop() {} } diff --git a/src/plugins/chart_expressions/expression_pie/public/types.ts b/src/plugins/chart_expressions/expression_partition_vis/public/types.ts similarity index 86% rename from src/plugins/chart_expressions/expression_pie/public/types.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/types.ts index 32f2e83bad51..64e132d2ddad 100755 --- a/src/plugins/chart_expressions/expression_pie/public/types.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/types.ts @@ -8,8 +8,8 @@ import { ChartsPluginSetup } from '../../../charts/public'; import { ExpressionsPublicPlugin, ExpressionsServiceStart } from '../../../expressions/public'; -export type ExpressionPiePluginSetup = void; -export type ExpressionPiePluginStart = void; +export type ExpressionPartitionVisPluginSetup = void; +export type ExpressionPartitionVisPluginStart = void; export interface SetupDeps { expressions: ReturnType; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/accessor.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/accessor.test.ts new file mode 100644 index 000000000000..f1023d478d40 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/accessor.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { ExpressionValueVisDimension } from '../../../../visualizations/common'; +import { createMockVisData } from '../mocks'; +import { getColumnByAccessor } from './accessor'; + +const visData = createMockVisData(); + +describe('getColumnByAccessor', () => { + it('returns column by the index', () => { + const index = 1; + const column = getColumnByAccessor(index, visData.columns); + expect(column).toEqual(visData.columns[index]); + }); + + it('returns undefiend if the index is higher then amount of columns', () => { + const index = visData.columns.length; + const column = getColumnByAccessor(index, visData.columns); + expect(column).toBeUndefined(); + }); + + it('returns column by id', () => { + const column = visData.columns[1]; + const accessor: ExpressionValueVisDimension['accessor'] = { + id: column.id, + name: '', + meta: { type: column.meta.type }, + }; + + const foundColumn = getColumnByAccessor(accessor, visData.columns); + expect(foundColumn).toEqual(column); + }); + + it('returns undefined for the accessor to non-existent column', () => { + const accessor: ExpressionValueVisDimension['accessor'] = { + id: 'non-existent-column', + name: '', + meta: { type: 'number' }, + }; + + const column = getColumnByAccessor(accessor, visData.columns); + expect(column).toBeUndefined(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/accessor.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/accessor.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/utils/accessor.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/accessor.ts diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/filter_helpers.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.test.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/utils/filter_helpers.test.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.test.ts diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/filter_helpers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/utils/filter_helpers.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.test.ts new file mode 100644 index 000000000000..eec33cac05e3 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.test.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 { ChartTypes } from '../../common/types'; +import { createMockDonutParams, createMockPieParams } from '../mocks'; +import { filterOutConfig } from './filter_out_config'; + +describe('filterOutConfig', () => { + const config = createMockPieParams(); + const { last_level: lastLevel, truncate, ...restLabels } = config.labels; + const configWithoutTruncateAndLastLevel = { ...config, labels: restLabels }; + + it('returns full configuration for pie visualization', () => { + const fullConfig = filterOutConfig(ChartTypes.PIE, config); + + expect(fullConfig).toEqual(config); + }); + + it('returns full configuration for donut visualization', () => { + const donutConfig = createMockDonutParams(); + const fullDonutConfig = filterOutConfig(ChartTypes.DONUT, donutConfig); + + expect(fullDonutConfig).toEqual(donutConfig); + }); + + it('excludes truncate and last_level from labels for treemap', () => { + const filteredOutConfig = filterOutConfig(ChartTypes.TREEMAP, config); + + expect(filteredOutConfig).toEqual(configWithoutTruncateAndLastLevel); + }); + + it('excludes truncate and last_level from labels for mosaic', () => { + const filteredOutConfig = filterOutConfig(ChartTypes.MOSAIC, config); + + expect(filteredOutConfig).toEqual(configWithoutTruncateAndLastLevel); + }); + + it('excludes truncate and last_level from labels for waffle', () => { + const filteredOutConfig = filterOutConfig(ChartTypes.WAFFLE, config); + + expect(filteredOutConfig).toEqual(configWithoutTruncateAndLastLevel); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.ts new file mode 100644 index 000000000000..2b118cd0903c --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_out_config.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. + */ + +import { PartitionVisParams, ChartTypes } from '../../common/types'; + +export const filterOutConfig = (visType: ChartTypes, visConfig: PartitionVisParams) => { + if ([ChartTypes.PIE, ChartTypes.DONUT].includes(visType)) { + return visConfig; + } + + const { last_level: lastLevel, truncate, ...restLabelsConfig } = visConfig.labels; + + return { + ...visConfig, + labels: restLabelsConfig, + }; +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts new file mode 100644 index 000000000000..69443dcfea5f --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts @@ -0,0 +1,186 @@ +/* + * 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 { fieldFormatsMock } from '../../../../field_formats/common/mocks'; +import { Datatable } from '../../../../expressions'; +import { createMockPieParams, createMockVisData } from '../mocks'; +import { generateFormatters, getAvailableFormatter, getFormatter } from './formatters'; +import { BucketColumns } from '../../common/types'; + +describe('generateFormatters', () => { + const visParams = createMockPieParams(); + const visData = createMockVisData(); + const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + beforeEach(() => { + defaultFormatter.mockClear(); + }); + + it('returns empty object, if labels should not be should ', () => { + const formatters = generateFormatters( + { ...visParams, labels: { ...visParams.labels, show: false } }, + visData, + defaultFormatter + ); + + expect(formatters).toEqual({}); + expect(defaultFormatter).toHaveBeenCalledTimes(0); + }); + + it('returns formatters, if columns have meta parameters', () => { + const formatters = generateFormatters(visParams, visData, defaultFormatter); + const formattingResult = fieldFormatsMock.deserialize(); + + const serializedFormatters = Object.keys(formatters).reduce( + (serialized, formatterId) => ({ + ...serialized, + [formatterId]: formatters[formatterId]?.toJSON(), + }), + {} + ); + + expect(serializedFormatters).toEqual({ + 'col-0-2': formattingResult.toJSON(), + 'col-1-1': formattingResult.toJSON(), + 'col-2-3': formattingResult.toJSON(), + 'col-3-1': formattingResult.toJSON(), + }); + + expect(defaultFormatter).toHaveBeenCalledTimes(visData.columns.length); + visData.columns.forEach((col) => { + expect(defaultFormatter).toHaveBeenCalledWith(col.meta.params); + }); + }); + + it('returns undefined formatters for columns without meta parameters', () => { + const newVisData: Datatable = { + ...visData, + columns: visData.columns.map(({ meta, ...col }) => ({ ...col, meta: { type: 'string' } })), + }; + + const formatters = generateFormatters(visParams, newVisData, defaultFormatter); + + expect(formatters).toEqual({ + 'col-0-2': undefined, + 'col-1-1': undefined, + 'col-2-3': undefined, + 'col-3-1': undefined, + }); + expect(defaultFormatter).toHaveBeenCalledTimes(0); + }); +}); + +describe('getAvailableFormatter', () => { + const visData = createMockVisData(); + + const preparedFormatter1 = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + const preparedFormatter2 = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + + beforeEach(() => { + defaultFormatter.mockClear(); + preparedFormatter1.mockClear(); + preparedFormatter2.mockClear(); + }); + + const formatters: Record = { + [visData.columns[0].id]: preparedFormatter1(), + [visData.columns[1].id]: preparedFormatter2(), + }; + + it('returns formatter from formatters, if meta.params are present ', () => { + const formatter = getAvailableFormatter(visData.columns[1], formatters, defaultFormatter); + + expect(formatter).toEqual(formatters[visData.columns[1].id]); + expect(defaultFormatter).toHaveBeenCalledTimes(0); + }); + + it('returns formatter from defaultFormatter factory, if meta.params are not present and format is present at column', () => { + const column: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + format: { + id: 'string', + params: {}, + }, + }; + const formatter = getAvailableFormatter(column, formatters, defaultFormatter); + + expect(formatter).not.toBeNull(); + expect(typeof formatter).toBe('object'); + expect(defaultFormatter).toHaveBeenCalledTimes(1); + expect(defaultFormatter).toHaveBeenCalledWith(column.format); + }); + + it('returns undefined, if meta.params and format are not present', () => { + const column: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + }; + const formatter = getAvailableFormatter(column, formatters, defaultFormatter); + + expect(formatter).toBeUndefined(); + expect(defaultFormatter).toHaveBeenCalledTimes(0); + }); +}); + +describe('getFormatter', () => { + const visData = createMockVisData(); + + const preparedFormatter1 = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + const preparedFormatter2 = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + + beforeEach(() => { + defaultFormatter.mockClear(); + preparedFormatter1.mockClear(); + preparedFormatter2.mockClear(); + }); + + const formatters: Record = { + [visData.columns[0].id]: preparedFormatter1(), + [visData.columns[1].id]: preparedFormatter2(), + }; + + it('returns formatter from formatters, if meta.params are present ', () => { + const formatter = getFormatter(visData.columns[1], formatters, defaultFormatter); + + expect(formatter).toEqual(formatters[visData.columns[1].id]); + expect(defaultFormatter).toHaveBeenCalledTimes(0); + }); + + it('returns formatter from defaultFormatter factory, if meta.params are not present and format is present at column', () => { + const column: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + format: { + id: 'string', + params: {}, + }, + }; + const formatter = getFormatter(column, formatters, defaultFormatter); + + expect(formatter).not.toBeNull(); + expect(typeof formatter).toBe('object'); + expect(defaultFormatter).toHaveBeenCalledTimes(1); + expect(defaultFormatter).toHaveBeenCalledWith(column.format); + }); + + it('returns defaultFormatter, if meta.params and format are not present', () => { + const column: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + }; + + const formatter = getFormatter(column, formatters, defaultFormatter); + + expect(formatter).not.toBeNull(); + expect(typeof formatter).toBe('object'); + expect(defaultFormatter).toHaveBeenCalledTimes(1); + expect(defaultFormatter).toHaveBeenCalledWith(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts new file mode 100644 index 000000000000..59574dd24851 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts @@ -0,0 +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 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 { FieldFormat, FormatFactory } from '../../../../field_formats/common'; +import type { Datatable } from '../../../../expressions/public'; +import { BucketColumns, PartitionVisParams } from '../../common/types'; + +export const generateFormatters = ( + visParams: PartitionVisParams, + visData: Datatable, + formatFactory: FormatFactory +) => { + if (!visParams.labels.show) { + return {}; + } + + return visData.columns.reduce | undefined>>( + (newFormatters, column) => ({ + ...newFormatters, + [column.id]: column?.meta?.params ? formatFactory(column.meta.params) : undefined, + }), + {} + ); +}; + +export const getAvailableFormatter = ( + column: Partial, + formatters: Record, + defaultFormatFactory: FormatFactory +) => { + if (column?.meta?.params) { + const formatter = column?.id ? formatters[column?.id] : undefined; + if (formatter) { + return formatter; + } + } + + if (column?.format) { + return defaultFormatFactory(column.format); + } +}; + +export const getFormatter = ( + column: Partial, + formatters: Record, + defaultFormatFactory: FormatFactory +) => getAvailableFormatter(column, formatters, defaultFormatFactory) ?? defaultFormatFactory(); diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_color_picker.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_color_picker.test.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_color_picker.test.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/get_color_picker.test.tsx diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_color_picker.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_color_picker.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_color_picker.tsx rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/get_color_picker.tsx diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_columns.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts similarity index 85% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_columns.test.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts index 57dc4367c7e1..157336599a26 100644 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_columns.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts @@ -7,7 +7,12 @@ */ import { getColumns } from './get_columns'; -import { PieVisParams } from '../../common/types'; +import { + LabelPositions, + LegendDisplay, + PartitionVisParams, + ValueFormats, +} from '../../common/types'; import { createMockPieParams, createMockVisData } from '../mocks'; const visParams = createMockPieParams(); @@ -108,7 +113,16 @@ describe('getColumns', () => { }); it('should return the correct metric column if visParams returns dimensions', () => { - const { metricColumn } = getColumns(visParams, visData); + const { metricColumn } = getColumns( + { + ...visParams, + dimensions: { + ...visParams.dimensions, + metric: undefined, + }, + }, + visData + ); expect(metricColumn).toEqual({ id: 'col-3-1', meta: { @@ -130,39 +144,38 @@ describe('getColumns', () => { }); it('should return the first data column if no buckets specified', () => { - const visParamsOnlyMetric = { - addLegend: true, + const visParamsOnlyMetric: PartitionVisParams = { + legendDisplay: LegendDisplay.SHOW, addTooltip: true, - isDonut: true, labels: { - position: 'default', + position: LabelPositions.DEFAULT, show: true, truncate: 100, values: true, - valuesFormat: 'percent', + valuesFormat: ValueFormats.PERCENT, percentDecimals: 2, + last_level: false, }, legendPosition: 'right', nestedLegend: false, maxLegendLines: 1, truncateLegend: false, + distinctColors: false, palette: { name: 'default', type: 'palette', }, - type: 'pie', dimensions: { metric: { + type: 'vis_dimension', accessor: 1, format: { id: 'number', + params: {}, }, - params: {}, - label: 'Count', - aggType: 'count', }, }, - } as unknown as PieVisParams; + }; const { metricColumn } = getColumns(visParamsOnlyMetric, visData); expect(metricColumn).toEqual({ id: 'col-1-1', @@ -187,37 +200,39 @@ describe('getColumns', () => { }); it('should return an object with the name of the metric if no buckets specified', () => { - const visParamsOnlyMetric = { - addLegend: true, + const visParamsOnlyMetric: PartitionVisParams = { + legendDisplay: LegendDisplay.SHOW, addTooltip: true, isDonut: true, labels: { - position: 'default', + position: LabelPositions.DEFAULT, show: true, truncate: 100, values: true, - valuesFormat: 'percent', + valuesFormat: ValueFormats.PERCENT, percentDecimals: 2, + last_level: false, }, + truncateLegend: false, + maxLegendLines: 100, + distinctColors: false, legendPosition: 'right', nestedLegend: false, palette: { name: 'default', type: 'palette', }, - type: 'pie', dimensions: { metric: { + type: 'vis_dimension', accessor: 1, format: { id: 'number', + params: {}, }, - params: {}, - label: 'Count', - aggType: 'count', }, }, - } as unknown as PieVisParams; + }; const { bucketColumns, metricColumn } = getColumns(visParamsOnlyMetric, visData); expect(bucketColumns).toEqual([{ name: metricColumn.name }]); }); diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_columns.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts similarity index 50% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_columns.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts index 2fce86f365ff..063315e3aab9 100644 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_columns.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts @@ -6,39 +6,43 @@ * Side Public License, v 1. */ -import { getColumnByAccessor } from './accessor'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; import { DatatableColumn, Datatable } from '../../../../expressions/public'; -import { BucketColumns, PieVisParams } from '../../common/types'; +import { BucketColumns, PartitionVisParams } from '../../common/types'; +import { getColumnByAccessor } from './accessor'; + +const getMetricColumn = ( + metricAccessor: ExpressionValueVisDimension['accessor'], + visData: Datatable +) => { + return getColumnByAccessor(metricAccessor, visData.columns); +}; export const getColumns = ( - visParams: PieVisParams, + visParams: PartitionVisParams, visData: Datatable ): { metricColumn: DatatableColumn; bucketColumns: Array>; } => { - if (visParams.dimensions.buckets && visParams.dimensions.buckets.length > 0) { - const bucketColumns: Array> = visParams.dimensions.buckets.map( - ({ accessor, format }) => ({ - ...getColumnByAccessor(accessor, visData.columns), - format, - }) - ); + const { metric, buckets } = visParams.dimensions; + if (buckets && buckets.length > 0) { + const bucketColumns: Array> = buckets.map(({ accessor, format }) => ({ + ...getColumnByAccessor(accessor, visData.columns), + format, + })); + const lastBucketId = bucketColumns[bucketColumns.length - 1].id; const matchingIndex = visData.columns.findIndex((col) => col.id === lastBucketId); + return { bucketColumns, - metricColumn: visData.columns[matchingIndex + 1], + metricColumn: getMetricColumn(metric?.accessor ?? matchingIndex + 1, visData), }; } - const metricAccessor = visParams?.dimensions?.metric.accessor ?? 0; - const metricColumn = getColumnByAccessor(metricAccessor, visData.columns); + const metricColumn = getMetricColumn(metric?.accessor ?? 0, visData); return { metricColumn, - bucketColumns: [ - { - name: metricColumn.name, - }, - ], + bucketColumns: [{ name: metricColumn.name }], }; }; diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_distinct_series.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_distinct_series.test.ts similarity index 100% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_distinct_series.test.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/get_distinct_series.test.ts diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_distinct_series.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_distinct_series.ts similarity index 78% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_distinct_series.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/get_distinct_series.ts index d5014689f331..cb432bf7b258 100644 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_distinct_series.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_distinct_series.ts @@ -8,7 +8,15 @@ import { DatatableRow } from '../../../../expressions/public'; import { BucketColumns } from '../../common/types'; -export const getDistinctSeries = (rows: DatatableRow[], buckets: Array>) => { +export interface DistinctSeries { + allSeries: string[]; + parentSeries: string[]; +} + +export const getDistinctSeries = ( + rows: DatatableRow[], + buckets: Array> +): DistinctSeries => { const parentBucketId = buckets[0].id; const parentSeries: string[] = []; const allSeries: string[] = []; @@ -24,8 +32,5 @@ export const getDistinctSeries = (rows: DatatableRow[], buckets: Array Promise, getFilterEventData: (series: SeriesIdentifier) => ClickTriggerEvent | null, onFilter: (data: ClickTriggerEvent, negate?: any) => void, - visParams: PieVisParams, + visParams: PartitionVisParams, actions: DataPublicPluginStart['actions'], formatter: FieldFormatsStart ): LegendAction => { @@ -56,7 +56,7 @@ export const getLegendActions = ( title: `${title}`, items: [ { - name: i18n.translate('expressionPie.legend.filterForValueButtonAriaLabel', { + name: i18n.translate('expressionPartitionVis.legend.filterForValueButtonAriaLabel', { defaultMessage: 'Filter for value', }), 'data-test-subj': `legend-${title}-filterIn`, @@ -67,7 +67,7 @@ export const getLegendActions = ( }, }, { - name: i18n.translate('expressionPie.legend.filterOutValueButtonAriaLabel', { + name: i18n.translate('expressionPartitionVis.legend.filterOutValueButtonAriaLabel', { defaultMessage: 'Filter out value', }), 'data-test-subj': `legend-${title}-filterOut`, @@ -114,7 +114,7 @@ export const getLegendActions = ( }} panelPaddingSize="none" anchorPosition="upLeft" - title={i18n.translate('expressionPie.legend.filterOptionsLegend', { + title={i18n.translate('expressionPartitionVis.legend.filterOptionsLegend', { defaultMessage: '{legendDataLabel}, filter options', values: { legendDataLabel: title }, })} diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts new file mode 100644 index 000000000000..11838c7ce014 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts @@ -0,0 +1,496 @@ +/* + * 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 { ExpressionValueVisDimension } from '../../../../visualizations/common'; +import { getPartitionTheme } from './get_partition_theme'; +import { createMockPieParams, createMockDonutParams, createMockPartitionVisParams } from '../mocks'; +import { ChartTypes, LabelPositions, PartitionVisParams } from '../../common/types'; +import { RecursivePartial } from '@elastic/eui'; +import { Theme } from '@elastic/charts'; + +const column: ExpressionValueVisDimension = { + type: 'vis_dimension', + accessor: { id: 'col-1-1', name: 'Count', meta: { type: 'number' } }, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, +}; + +const splitRows = [column]; +const splitColumns = [column]; +const chartTheme: RecursivePartial = { + barSeriesStyle: { displayValue: { fontFamily: 'Arial' } }, + lineSeriesStyle: { point: { fill: '#fff' } }, + axes: { axisTitle: { fill: '#000' } }, +}; + +const linkLabelWithEnoughSpace = (visParams: PartitionVisParams) => ({ + maxCount: Number.POSITIVE_INFINITY, + maximumSection: Number.POSITIVE_INFINITY, + maxTextLength: visParams.labels.truncate ?? undefined, +}); + +const linkLabelsWithoutSpaceForOuterLabels = { maxCount: 0 }; + +const linkLabelsWithoutSpaceForLabels = { + maxCount: 0, + maximumSection: Number.POSITIVE_INFINITY, +}; + +const getStaticThemePartition = ( + theme: RecursivePartial, + visParams: PartitionVisParams +) => ({ + fontFamily: theme.barSeriesStyle?.displayValue?.fontFamily, + outerSizeRatio: 1, + minFontSize: 10, + maxFontSize: 16, + emptySizeRatio: visParams.emptySizeRatio ?? 0, + sectorLineStroke: theme.lineSeriesStyle?.point?.fill, + sectorLineWidth: 1.5, + circlePadding: 4, +}); + +const getStaticThemeOptions = (theme: RecursivePartial, visParams: PartitionVisParams) => ({ + partition: getStaticThemePartition(theme, visParams), + chartMargins: { top: 0, left: 0, bottom: 0, right: 0 }, +}); + +const getDefaultLinkLabel = (visParams: PartitionVisParams, theme: RecursivePartial) => ({ + maxCount: 5, + fontSize: 11, + textColor: theme.axes?.axisTitle?.fill, + maxTextLength: visParams.labels.truncate ?? undefined, +}); + +const dimensions = undefined; + +const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: PartitionVisParams) => { + const vParamsSplitRows = { + ...visParams, + dimensions: { ...visParams.dimensions, splitRow: splitRows }, + }; + const vParamsSplitColumns = { + ...visParams, + dimensions: { ...visParams.dimensions, splitColumn: splitColumns }, + }; + + it('should return correct default theme options', () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + outerSizeRatio: undefined, + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should not return padding settings if dimensions are not specified', () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + outerSizeRatio: undefined, + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should not return padding settings if split column or row are specified', () => { + const themeForSplitColumns = getPartitionTheme( + chartType, + vParamsSplitColumns, + chartTheme, + dimensions + ); + + expect(themeForSplitColumns).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + + const themeForSplitRows = getPartitionTheme( + chartType, + vParamsSplitRows, + chartTheme, + dimensions + ); + + expect(themeForSplitRows).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitRows), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + }); + + it('should return adjusted padding settings if dimensions are specified', () => { + const specifiedDimensions = { width: 2000, height: 2000 }; + const theme = getPartitionTheme(chartType, visParams, chartTheme, specifiedDimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, + partition: { + ...getStaticThemePartition(chartTheme, visParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should return right settings for the theme related fields', () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + outerSizeRatio: undefined, + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should return undefined outerSizeRatio for split chart and show labels', () => { + const specifiedDimensions = { width: 2000, height: 2000 }; + const theme = getPartitionTheme(chartType, vParamsSplitRows, chartTheme, specifiedDimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitRows), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + + const themeForSplitColumns = getPartitionTheme( + chartType, + vParamsSplitColumns, + chartTheme, + specifiedDimensions + ); + + expect(themeForSplitColumns).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + }); + + it( + 'should return undefined outerSizeRatio for not specified dimensions, visible labels,' + + 'and default labels position and not split chart', + () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + outerSizeRatio: undefined, + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + } + ); + + it( + 'should return rescaleFactor value for outerSizeRatio if dimensions are specified,' + + ' is not split chart, labels are shown and labels position is not `inside`', + () => { + const specifiedDimensions = { width: 2000, height: 2000 }; + const rescaleFactor = 2; + const theme = getPartitionTheme( + chartType, + visParams, + chartTheme, + specifiedDimensions, + rescaleFactor + ); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, + partition: { + ...getStaticThemePartition(chartTheme, visParams), + outerSizeRatio: rescaleFactor, + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + } + ); + it( + 'should return adjusted rescaleFactor for outerSizeRatio if dimensions are specified,' + + ' is not split chart, labels position is `inside` and labels are shown', + () => { + const specifiedDimensions = { width: 2000, height: 2000 }; + const rescaleFactor = 1; + const vParams = { + ...visParams, + labels: { ...visParams.labels, position: LabelPositions.INSIDE }, + }; + + const theme = getPartitionTheme( + chartType, + vParams, + chartTheme, + specifiedDimensions, + rescaleFactor + ); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, vParams), + chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, + partition: { + ...getStaticThemePartition(chartTheme, vParams), + outerSizeRatio: 0.5, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + } + ); + it( + 'should return linkLabel with enough space if labels are shown,' + + ' labels position is `default` and need to show the last level only.', + () => { + const specifiedDimensions = { width: 2000, height: 2000 }; + const vParams = { + ...visParams, + labels: { ...visParams.labels, last_level: true }, + }; + const theme = getPartitionTheme(chartType, vParams, chartTheme, specifiedDimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, vParams), + chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, + partition: { + ...getStaticThemePartition(chartTheme, vParams), + linkLabel: linkLabelWithEnoughSpace(vParams), + }, + }); + } + ); + + it('should hide links if position is `inside` or is split chart, and labels are shown', () => { + const vParams = { + ...visParams, + labels: { ...visParams.labels, position: LabelPositions.INSIDE }, + }; + const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, vParams), + partition: { + ...getStaticThemePartition(chartTheme, vParams), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + + const themeSplitColumns = getPartitionTheme( + chartType, + vParamsSplitColumns, + chartTheme, + dimensions + ); + + expect(themeSplitColumns).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + + const themeSplitRows = getPartitionTheme(chartType, vParamsSplitRows, chartTheme, dimensions); + + expect(themeSplitRows).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitRows), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForOuterLabels, + }, + }); + }); + + it('should hide links if labels are not shown', () => { + const vParams = { ...visParams, labels: { ...visParams.labels, show: false } }; + const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, vParams), + partition: { + ...getStaticThemePartition(chartTheme, vParams), + outerSizeRatio: undefined, + linkLabel: linkLabelsWithoutSpaceForLabels, + }, + }); + }); +}; + +const runTreemapMosaicTestSuites = (chartType: ChartTypes, visParams: PartitionVisParams) => { + const vParamsSplitRows = { + ...visParams, + dimensions: { ...visParams.dimensions, splitRow: splitRows }, + }; + const vParamsSplitColumns = { + ...visParams, + dimensions: { ...visParams.dimensions, splitColumn: splitColumns }, + }; + + it('should return correct theme options', () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should return empty padding settings if dimensions are not specified', () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should return padding settings if split column or row are specified', () => { + const themeForSplitColumns = getPartitionTheme( + chartType, + vParamsSplitColumns, + chartTheme, + dimensions + ); + + expect(themeForSplitColumns).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitColumns), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitColumns), + linkLabel: getDefaultLinkLabel(vParamsSplitColumns, chartTheme), + }, + }); + + const themeForSplitRows = getPartitionTheme( + chartType, + vParamsSplitRows, + chartTheme, + dimensions + ); + + expect(themeForSplitRows).toEqual({ + ...getStaticThemeOptions(chartTheme, vParamsSplitRows), + partition: { + ...getStaticThemePartition(chartTheme, vParamsSplitRows), + linkLabel: getDefaultLinkLabel(vParamsSplitRows, chartTheme), + }, + }); + }); + + it('should return fullfilled padding settings if dimensions are specified', () => { + const specifiedDimensions = { width: 2000, height: 2000 }; + const theme = getPartitionTheme(chartType, visParams, chartTheme, specifiedDimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 }, + partition: { + ...getStaticThemePartition(chartTheme, visParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should return settings for the theme related fields', () => { + const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions); + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + partition: { + ...getStaticThemePartition(chartTheme, visParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + }, + }); + }); + + it('should make color transparent if labels are hidden', () => { + const vParams = { ...visParams, labels: { ...visParams.labels, show: false } }; + const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, vParams), + partition: { + ...getStaticThemePartition(chartTheme, vParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + fillLabel: { textColor: 'rgba(0,0,0,0)' }, + }, + }); + }); +}; + +describe('Pie getPartitionTheme', () => { + runPieDonutWaffleTestSuites(ChartTypes.PIE, createMockPieParams()); +}); + +describe('Donut getPartitionTheme', () => { + const visParams = createMockDonutParams(); + const chartType = ChartTypes.DONUT; + + runPieDonutWaffleTestSuites(chartType, visParams); + + it('should return correct empty size ratio and partitionLayout', () => { + const theme = getPartitionTheme(ChartTypes.DONUT, visParams, chartTheme, dimensions); + + expect(theme).toEqual({ + ...getStaticThemeOptions(chartTheme, visParams), + outerSizeRatio: undefined, + partition: { + ...getStaticThemePartition(chartTheme, visParams), + linkLabel: getDefaultLinkLabel(visParams, chartTheme), + outerSizeRatio: undefined, + }, + }); + }); +}); + +describe('Waffle getPartitionTheme', () => { + runPieDonutWaffleTestSuites(ChartTypes.WAFFLE, createMockPartitionVisParams()); +}); + +describe('Mosaic getPartitionTheme', () => { + runTreemapMosaicTestSuites(ChartTypes.MOSAIC, createMockPartitionVisParams()); +}); + +describe('Treemap getPartitionTheme', () => { + runTreemapMosaicTestSuites(ChartTypes.TREEMAP, createMockPartitionVisParams()); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts new file mode 100644 index 000000000000..edb1aaea64aa --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts @@ -0,0 +1,165 @@ +/* + * 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 { RecursivePartial, Theme, PartialTheme } from '@elastic/charts'; +import { + ChartTypes, + LabelPositions, + PartitionVisParams, + PieContainerDimensions, +} from '../../common/types'; + +type GetThemeByTypeFn = ( + chartType: ChartTypes, + visParams: PartitionVisParams, + dimensions?: PieContainerDimensions, + rescaleFactor?: number +) => PartialTheme; + +type GetThemeFn = ( + chartType: ChartTypes, + visParams: PartitionVisParams, + chartTheme: RecursivePartial, + dimensions?: PieContainerDimensions, + rescaleFactor?: number +) => PartialTheme; + +type GetPieDonutWaffleThemeFn = ( + visParams: PartitionVisParams, + dimensions?: PieContainerDimensions, + rescaleFactor?: number +) => PartialTheme; + +type GetTreemapMosaicThemeFn = (visParams: PartitionVisParams) => PartialTheme; + +const MAX_SIZE = 1000; + +const getPieDonutWaffleCommonTheme: GetPieDonutWaffleThemeFn = ( + visParams, + dimensions, + rescaleFactor = 1 +) => { + const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); + const preventLinksFromShowing = + (visParams.labels.position === LabelPositions.INSIDE || isSplitChart) && visParams.labels.show; + + const usingOuterSizeRatio = + dimensions && !isSplitChart + ? { + outerSizeRatio: + // Cap the ratio to 1 and then rescale + rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1), + } + : { outerSizeRatio: undefined }; + + const theme: PartialTheme = {}; + theme.partition = { ...(usingOuterSizeRatio ?? {}) }; + + if ( + visParams.labels.show && + visParams.labels.position === LabelPositions.DEFAULT && + visParams.labels.last_level + ) { + theme.partition.linkLabel = { + maxCount: Number.POSITIVE_INFINITY, + maximumSection: Number.POSITIVE_INFINITY, + maxTextLength: visParams.labels.truncate ?? undefined, + }; + } + + if (preventLinksFromShowing || !visParams.labels.show) { + // Prevent links from showing + theme.partition.linkLabel = { + maxCount: 0, + ...(!visParams.labels.show ? { maximumSection: Number.POSITIVE_INFINITY } : {}), + }; + } + + if (!preventLinksFromShowing && dimensions && !isSplitChart) { + // shrink up to 20% to give some room for the linked values + theme.partition.outerSizeRatio = rescaleFactor; + } + + return theme; +}; + +const getDonutSpecificTheme: GetPieDonutWaffleThemeFn = (visParams, ...args) => { + const { partition, ...restTheme } = getPieDonutWaffleCommonTheme(visParams, ...args); + return { ...restTheme, partition: { ...partition, emptySizeRatio: visParams.emptySizeRatio } }; +}; + +const getTreemapMosaicCommonTheme: GetTreemapMosaicThemeFn = (visParams) => { + if (!visParams.labels.show) { + return { + partition: { + fillLabel: { textColor: 'rgba(0,0,0,0)' }, + }, + }; + } + return {}; +}; + +const getSpecificTheme: GetThemeByTypeFn = (chartType, visParams, dimensions, rescaleFactor) => + ({ + [ChartTypes.PIE]: () => getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor), + [ChartTypes.DONUT]: () => getDonutSpecificTheme(visParams, dimensions, rescaleFactor), + [ChartTypes.TREEMAP]: () => getTreemapMosaicCommonTheme(visParams), + [ChartTypes.MOSAIC]: () => getTreemapMosaicCommonTheme(visParams), + [ChartTypes.WAFFLE]: () => getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor), + }[chartType]()); + +export const getPartitionTheme: GetThemeFn = ( + chartType, + visParams, + chartTheme, + dimensions, + rescaleFactor = 1 +) => { + // On small multiples we want the labels to only appear inside + const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); + const paddingProps: PartialTheme | null = + dimensions && !isSplitChart + ? { + chartPaddings: { + top: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height, + bottom: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height, + left: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height, + right: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height, + }, + } + : null; + const partition = { + fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily, + outerSizeRatio: 1, + minFontSize: 10, + maxFontSize: 16, + emptySizeRatio: 0, + sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, + sectorLineWidth: 1.5, + circlePadding: 4, + linkLabel: { + maxCount: 5, + fontSize: 11, + textColor: chartTheme.axes?.axisTitle?.fill, + maxTextLength: visParams.labels.truncate ?? undefined, + }, + }; + const { partition: specificPartition = {}, ...restSpecificTheme } = getSpecificTheme( + chartType, + visParams, + dimensions, + rescaleFactor + ); + + return { + partition: { ...partition, ...specificPartition }, + chartMargins: { top: 0, bottom: 0, left: 0, right: 0 }, + ...(paddingProps ?? {}), + ...restSpecificTheme, + }; +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_type.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_type.ts new file mode 100644 index 000000000000..842c4f49b42c --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_type.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. + */ + +import { PartitionLayout } from '@elastic/charts'; +import { ChartTypes } from '../../common/types'; + +export const getPartitionType = (chartType: ChartTypes) => + ({ + [ChartTypes.PIE]: PartitionLayout.sunburst, + [ChartTypes.DONUT]: PartitionLayout.sunburst, + [ChartTypes.TREEMAP]: PartitionLayout.treemap, + [ChartTypes.MOSAIC]: PartitionLayout.mosaic, + [ChartTypes.WAFFLE]: PartitionLayout.waffle, + }[chartType]); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.test.ts new file mode 100644 index 000000000000..5e0f58a38478 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; +import { DatatableColumn } from '../../../../expressions'; +import { createMockVisData } from '../mocks'; +import { getSplitDimensionAccessor } from './get_split_dimension_accessor'; +import { BucketColumns } from '../../common/types'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; + +describe('getSplitDimensionAccessor', () => { + const visData = createMockVisData(); + + const preparedFormatter1 = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + const preparedFormatter2 = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); + + beforeEach(() => { + defaultFormatter.mockClear(); + preparedFormatter1.mockClear(); + preparedFormatter2.mockClear(); + }); + + const formatters: Record = { + [visData.columns[0].id]: preparedFormatter1(), + [visData.columns[1].id]: preparedFormatter2(), + }; + + const splitDimension: ExpressionValueVisDimension = { + type: 'vis_dimension', + accessor: { + id: visData.columns[1].id, + name: visData.columns[1].name, + meta: visData.columns[1].meta, + }, + format: { + params: {}, + }, + }; + + it('returns accessor which is using formatter from formatters, if meta.params are present at accessing column', () => { + const accessor = getSplitDimensionAccessor( + visData.columns, + splitDimension, + formatters, + defaultFormatter + ); + const formatter = formatters[visData.columns[1].id]; + const spyOnFormatterConvert = jest.spyOn(formatter, 'convert'); + + expect(defaultFormatter).toHaveBeenCalledTimes(0); + expect(typeof accessor).toBe('function'); + accessor(visData.rows[0]); + expect(spyOnFormatterConvert).toHaveBeenCalledTimes(1); + }); + + it('returns accessor which is using default formatter, if meta.params are not present and format is present at accessing column', () => { + const column: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + format: { + id: 'string', + params: {}, + }, + }; + const columns = [visData.columns[0], column, visData.columns[2]] as DatatableColumn[]; + const defaultFormatterReturnedVal = fieldFormatsMock.deserialize(); + const spyOnDefaultFormatterConvert = jest.spyOn(defaultFormatterReturnedVal, 'convert'); + + defaultFormatter.mockReturnValueOnce(defaultFormatterReturnedVal); + + const accessor = getSplitDimensionAccessor( + columns, + splitDimension, + formatters, + defaultFormatter + ); + + expect(defaultFormatter).toHaveBeenCalledTimes(1); + expect(defaultFormatter).toHaveBeenCalledWith(column.format); + + expect(typeof accessor).toBe('function'); + accessor(visData.rows[0]); + expect(spyOnDefaultFormatterConvert).toHaveBeenCalledTimes(1); + }); + + it('returns accessor which is using default formatter, if meta.params and format are not present', () => { + const column: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + }; + const columns = [visData.columns[0], column, visData.columns[2]] as DatatableColumn[]; + const defaultFormatterReturnedVal = fieldFormatsMock.deserialize(); + const spyOnDefaultFormatterConvert = jest.spyOn(defaultFormatterReturnedVal, 'convert'); + + defaultFormatter.mockReturnValueOnce(defaultFormatterReturnedVal); + + const accessor = getSplitDimensionAccessor( + columns, + splitDimension, + formatters, + defaultFormatter + ); + + expect(defaultFormatter).toHaveBeenCalledTimes(1); + expect(defaultFormatter).toHaveBeenCalledWith(); + + expect(typeof accessor).toBe('function'); + accessor(visData.rows[0]); + expect(spyOnDefaultFormatterConvert).toHaveBeenCalledTimes(1); + }); + + it('returns accessor which returns undefined, if such column is not present', () => { + const accessor1 = getSplitDimensionAccessor( + visData.columns, + splitDimension, + formatters, + defaultFormatter + ); + + expect(typeof accessor1).toBe('function'); + const result1 = accessor1({}); + expect(result1).toBeUndefined(); + + const column2: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + }; + const columns2 = [visData.columns[0], column2, visData.columns[2]] as DatatableColumn[]; + const accessor2 = getSplitDimensionAccessor( + columns2, + splitDimension, + formatters, + defaultFormatter + ); + + expect(typeof accessor2).toBe('function'); + const result2 = accessor1({}); + expect(result2).toBeUndefined(); + + const column3: Partial = { + ...visData.columns[1], + meta: { type: 'string' }, + format: { + id: 'string', + params: {}, + }, + }; + const columns3 = [visData.columns[0], column3, visData.columns[2]] as DatatableColumn[]; + + const accessor3 = getSplitDimensionAccessor( + columns3, + splitDimension, + formatters, + defaultFormatter + ); + expect(typeof accessor3).toBe('function'); + const result3 = accessor3({}); + expect(result3).toBeUndefined(); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.ts new file mode 100644 index 000000000000..1a18a1134baf --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_split_dimension_accessor.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 { AccessorFn } from '@elastic/charts'; +import { getColumnByAccessor } from './accessor'; +import { DatatableColumn } from '../../../../expressions/public'; +import { FieldFormat, FormatFactory } from '../../../../field_formats/common'; +import { ExpressionValueVisDimension } from '../../../../visualizations/common'; +import { getFormatter } from './formatters'; + +export const getSplitDimensionAccessor = ( + columns: DatatableColumn[], + splitDimension: ExpressionValueVisDimension, + formatters: Record, + defaultFormatFactory: FormatFactory +): AccessorFn => { + const splitChartColumn = getColumnByAccessor(splitDimension.accessor, columns); + const accessor = splitChartColumn.id; + const formatter = getFormatter(splitChartColumn, formatters, defaultFormatFactory); + + const fn: AccessorFn = (d) => { + const v = d[accessor]; + if (v === undefined) { + return; + } + + const f = formatter.convert(v); + return f; + }; + + return fn; +}; diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts similarity index 78% rename from src/plugins/chart_expressions/expression_pie/public/utils/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts index 7fe499e7f4ab..afa0b82a87eb 100644 --- a/src/plugins/chart_expressions/expression_pie/public/utils/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export { getLayers } from './get_layers'; +export { getLayers } from './layers'; export { LegendColorPickerWrapper, LegendColorPickerWrapperContext } from './get_color_picker'; export { getLegendActions } from './get_legend_actions'; export { canFilter, getFilterClickData, getFilterEventData } from './filter_helpers'; @@ -15,3 +15,6 @@ export { getColumns } from './get_columns'; export { getSplitDimensionAccessor } from './get_split_dimension_accessor'; export { getDistinctSeries } from './get_distinct_series'; export { getColumnByAccessor } from './accessor'; +export { isLegendFlat, shouldShowLegend } from './legend'; +export { generateFormatters, getAvailableFormatter, getFormatter } from './formatters'; +export { getPartitionType } from './get_partition_type'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts new file mode 100644 index 000000000000..d381c9cd3f0b --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.ts @@ -0,0 +1,237 @@ +/* + * 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 { ShapeTreeNode } from '@elastic/charts'; +import { isEqual } from 'lodash'; +import type { FieldFormatsStart } from '../../../../../field_formats/public'; +import { + SeriesLayer, + PaletteRegistry, + lightenColor, + PaletteDefinition, + PaletteOutput, +} from '../../../../../charts/public'; +import type { Datatable, DatatableRow } from '../../../../../expressions/public'; +import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types'; +import { DistinctSeries, getDistinctSeries } from '../get_distinct_series'; + +const isTreemapOrMosaicChart = (shape: ChartTypes) => + [ChartTypes.MOSAIC, ChartTypes.TREEMAP].includes(shape); + +export const byDataColorPaletteMap = ( + rows: Datatable['rows'], + columnId: string, + paletteDefinition: PaletteDefinition, + { params }: PaletteOutput +) => { + const colorMap = new Map( + rows.map((item) => [String(item[columnId]), undefined]) + ); + let rankAtDepth = 0; + + return { + getColor: (item: unknown) => { + const key = String(item); + if (!colorMap.has(key)) return; + + let color = colorMap.get(key); + if (color) { + return color; + } + color = + paletteDefinition.getCategoricalColor( + [ + { + name: key, + totalSeriesAtDepth: colorMap.size, + rankAtDepth: rankAtDepth++, + }, + ], + { behindText: false }, + params + ) || undefined; + + colorMap.set(key, color); + return color; + }, + }; +}; + +const getDistinctColor = ( + d: ShapeTreeNode, + isSplitChart: boolean, + overwriteColors: { [key: string]: string } = {}, + visParams: PartitionVisParams, + palettes: PaletteRegistry | null, + syncColors: boolean, + { parentSeries, allSeries }: DistinctSeries, + name: string +) => { + let overwriteColor; + // this is for supporting old visualizations (created by vislib plugin) + // it seems that there for some aggs, the uiState saved from vislib is + // different than the es-charts handle it + if (overwriteColors.hasOwnProperty(name)) { + overwriteColor = overwriteColors[name]; + } + + if (Object.keys(overwriteColors).includes(d.dataName.toString())) { + overwriteColor = overwriteColors[d.dataName]; + } + + if (overwriteColor) { + return overwriteColor; + } + + const index = allSeries.findIndex((dataName) => isEqual(dataName, d.dataName)); + const isSplitParentLayer = isSplitChart && parentSeries.includes(d.dataName); + return palettes?.get(visParams.palette.name).getCategoricalColor( + [ + { + name: d.dataName, + rankAtDepth: isSplitParentLayer + ? parentSeries.findIndex((dataName) => dataName === d.dataName) + : index > -1 + ? index + : 0, + totalSeriesAtDepth: isSplitParentLayer ? parentSeries.length : allSeries.length || 1, + }, + ], + { + maxDepth: 1, + totalSeries: allSeries.length || 1, + behindText: visParams.labels.show, + syncColors, + }, + visParams.palette?.params ?? { colors: [] } + ); +}; + +const createSeriesLayers = ( + d: ShapeTreeNode, + parentSeries: DistinctSeries['parentSeries'], + isSplitChart: boolean +) => { + const seriesLayers: SeriesLayer[] = []; + let tempParent: typeof d | typeof d['parent'] = d; + while (tempParent.parent && tempParent.depth > 0) { + const seriesName = String(tempParent.parent.children[tempParent.sortIndex][0]); + const isSplitParentLayer = isSplitChart && parentSeries.includes(seriesName); + seriesLayers.unshift({ + name: seriesName, + rankAtDepth: isSplitParentLayer + ? parentSeries.findIndex((name) => name === seriesName) + : tempParent.sortIndex, + totalSeriesAtDepth: isSplitParentLayer + ? parentSeries.length + : tempParent.parent.children.length, + }); + tempParent = tempParent.parent; + } + return seriesLayers; +}; + +const overrideColorForOldVisualization = ( + seriesLayers: SeriesLayer[], + overwriteColors: { [key: string]: string }, + name: string +) => { + let overwriteColor; + // this is for supporting old visualizations (created by vislib plugin) + // it seems that there for some aggs, the uiState saved from vislib is + // different than the es-charts handle it + if (overwriteColors.hasOwnProperty(name)) { + overwriteColor = overwriteColors[name]; + } + + seriesLayers.forEach((layer) => { + if (Object.keys(overwriteColors).includes(layer.name)) { + overwriteColor = overwriteColors[layer.name]; + } + }); + + return overwriteColor; +}; + +export const getColor = ( + chartType: ChartTypes, + d: ShapeTreeNode, + layerIndex: number, + isSplitChart: boolean, + overwriteColors: { [key: string]: string } = {}, + columns: Array>, + rows: DatatableRow[], + visParams: PartitionVisParams, + palettes: PaletteRegistry | null, + byDataPalette: ReturnType, + syncColors: boolean, + isDarkMode: boolean, + formatter: FieldFormatsStart, + format?: BucketColumns['format'] +) => { + const distinctSeries = getDistinctSeries(rows, columns); + const { parentSeries } = distinctSeries; + const dataName = d.dataName; + + // Mind the difference here: the contrast computation for the text ignores the alpha/opacity + // therefore change it for dask mode + const defaultColor = isDarkMode ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)'; + + let name = ''; + if (format) { + name = formatter.deserialize(format).convert(dataName) ?? ''; + } + + if (visParams.distinctColors) { + return ( + getDistinctColor( + d, + isSplitChart, + overwriteColors, + visParams, + palettes, + syncColors, + distinctSeries, + name + ) || defaultColor + ); + } + + const seriesLayers = createSeriesLayers(d, parentSeries, isSplitChart); + + const overwriteColor = overrideColorForOldVisualization(seriesLayers, overwriteColors, name); + if (overwriteColor) { + return lightenColor(overwriteColor, seriesLayers.length, columns.length); + } + + if (chartType === ChartTypes.MOSAIC && byDataPalette && seriesLayers[1]) { + return byDataPalette.getColor(seriesLayers[1].name) || defaultColor; + } + + if (isTreemapOrMosaicChart(chartType)) { + if (layerIndex < columns.length - 1) { + return defaultColor; + } + // only use the top level series layer for coloring + if (seriesLayers.length > 1) { + seriesLayers.pop(); + } + } + + const outputColor = palettes?.get(visParams.palette.name).getCategoricalColor( + seriesLayers, + { + behindText: visParams.labels.show || isTreemapOrMosaicChart(chartType), + maxDepth: columns.length, + totalSeries: rows.length, + syncColors, + }, + visParams.palette?.params ?? { colors: [] } + ); + + return outputColor || defaultColor; +}; diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_layers.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts similarity index 84% rename from src/plugins/chart_expressions/expression_pie/public/utils/get_layers.test.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts index 39c3ccfc45a9..34daed61f67c 100644 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_layers.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.test.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ import { ShapeTreeNode } from '@elastic/charts'; -import { PaletteDefinition, SeriesLayer } from '../../../../charts/public'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import type { DataPublicPluginStart } from '../../../../data/public'; -import { computeColor } from './get_layers'; -import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../mocks'; +import { PaletteDefinition, SeriesLayer } from '../../../../../charts/public'; +import { dataPluginMock } from '../../../../../data/public/mocks'; +import type { DataPublicPluginStart } from '../../../../../data/public'; +import { getColor } from './get_color'; +import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../../mocks'; +import { ChartTypes } from '../../../common/types'; const visData = createMockVisData(); const buckets = createMockBucketColumns(); @@ -68,14 +69,18 @@ describe('computeColor', () => { sortIndex: 0, }, } as unknown as ShapeTreeNode; - const color = computeColor( + const color = getColor( + ChartTypes.PIE, d, + 0, false, {}, buckets, visData.rows, visParams, getPaletteRegistry(), + { getColor: () => undefined }, + false, false, dataMock.fieldFormats ); @@ -93,14 +98,18 @@ describe('computeColor', () => { sortIndex: 0, }, } as unknown as ShapeTreeNode; - const color = computeColor( + const color = getColor( + ChartTypes.PIE, d, + 0, true, {}, buckets, visData.rows, visParams, getPaletteRegistry(), + { getColor: () => undefined }, + false, false, dataMock.fieldFormats ); @@ -117,14 +126,18 @@ describe('computeColor', () => { sortIndex: 0, }, } as unknown as ShapeTreeNode; - const color = computeColor( + const color = getColor( + ChartTypes.PIE, d, + 0, true, { 'ES-Air': '#000028' }, buckets, visData.rows, visParams, getPaletteRegistry(), + { getColor: () => undefined }, + false, false, dataMock.fieldFormats ); @@ -162,14 +175,18 @@ describe('computeColor', () => { ...visParams, distinctColors: true, }; - const color = computeColor( + const color = getColor( + ChartTypes.PIE, d, + 0, true, { '≥ 1000 and < 2000': '#3F6833' }, buckets, visData.rows, visParamsNew, getPaletteRegistry(), + { getColor: () => undefined }, + false, false, dataMock.fieldFormats, { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts new file mode 100644 index 000000000000..9f27ff628cf9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts @@ -0,0 +1,84 @@ +/* + * 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 { Datum, PartitionLayer } from '@elastic/charts'; +import { FieldFormat } from '../../../../../field_formats/common'; +import type { FieldFormatsStart } from '../../../../../field_formats/public'; +import { PaletteRegistry } from '../../../../../charts/public'; +import type { Datatable, DatatableRow } from '../../../../../expressions/public'; +import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types'; +import { sortPredicateByType } from './sort_predicate'; +import { byDataColorPaletteMap, getColor } from './get_color'; +import { getNodeLabel } from './get_node_labels'; + +const EMPTY_SLICE = Symbol('empty_slice'); + +export const getLayers = ( + chartType: ChartTypes, + columns: Array>, + visParams: PartitionVisParams, + visData: Datatable, + overwriteColors: { [key: string]: string } = {}, + rows: DatatableRow[], + palettes: PaletteRegistry | null, + formatters: Record, + formatter: FieldFormatsStart, + syncColors: boolean, + isDarkMode: boolean +): PartitionLayer[] => { + const fillLabel: PartitionLayer['fillLabel'] = { + valueFont: { + fontWeight: 700, + }, + }; + + if (!visParams.labels.values) { + fillLabel.valueFormatter = () => ''; + } + + const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); + let byDataPalette: ReturnType; + if (!syncColors && columns[1]?.id && palettes && visParams.palette) { + byDataPalette = byDataColorPaletteMap( + rows, + columns[1].id, + palettes?.get(visParams.palette.name), + visParams.palette + ); + } + + const sortPredicate = sortPredicateByType(chartType, visParams, visData, columns); + return columns.map((col, layerIndex) => { + return { + groupByRollup: (d: Datum) => (col.id ? d[col.id] ?? EMPTY_SLICE : col.name), + showAccessor: (d: Datum) => d !== EMPTY_SLICE, + nodeLabel: (d: unknown) => getNodeLabel(d, col, formatters, formatter.deserialize), + fillLabel, + sortPredicate, + shape: { + fillColor: (d) => + getColor( + chartType, + d, + layerIndex, + isSplitChart, + overwriteColors, + columns, + rows, + visParams, + palettes, + byDataPalette, + syncColors, + isDarkMode, + formatter, + col.format + ), + }, + }; + }); +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_node_labels.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_node_labels.ts new file mode 100644 index 000000000000..90c271daef6a --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_node_labels.ts @@ -0,0 +1,25 @@ +/* + * 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 { FieldFormat, FormatFactory } from '../../../../../field_formats/common'; +import { BucketColumns } from '../../../common/types'; +import { getAvailableFormatter } from '../formatters'; + +export const getNodeLabel = ( + nodeName: unknown, + column: Partial, + formatters: Record, + defaultFormatFactory: FormatFactory +) => { + const formatter = getAvailableFormatter(column, formatters, defaultFormatFactory); + if (formatter) { + return formatter.convert(nodeName) ?? ''; + } + + return String(nodeName); +}; diff --git a/src/plugins/chart_expressions/expression_pie/public/components/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/index.ts similarity index 89% rename from src/plugins/chart_expressions/expression_pie/public/components/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/index.ts index ef4589dac271..84dad45cb399 100644 --- a/src/plugins/chart_expressions/expression_pie/public/components/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export * from './pie_vis_component'; +export { getLayers } from './get_layers'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.ts new file mode 100644 index 000000000000..c7eaa2c49449 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.ts @@ -0,0 +1,77 @@ +/* + * 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 { ArrayEntry } from '@elastic/charts'; +import { Datatable } from '../../../../../../../src/plugins/expressions'; +import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types'; + +type SortFn = (([name1, node1]: ArrayEntry, [name2, node2]: ArrayEntry) => number) | undefined; + +type SortPredicateDefaultFn = ( + visData: Datatable, + columns: Array> +) => SortFn; + +type SortPredicatePieDonutFn = (visParams: PartitionVisParams) => SortFn; + +type SortPredicatePureFn = () => SortFn; + +export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) => + [...new Set(dataTable.rows.map((item) => item[columnId]))].reduce( + (acc, item, index) => ({ + ...acc, + [item]: index, + }), + {} + ); + +const sortPredicateSaveSourceOrder: SortPredicatePureFn = + () => + ([, node1], [, node2]) => { + const [index1] = node1.inputIndex ?? []; + if (index1 !== undefined) { + return index1; + } + return node2.value - node1.value; + }; + +const sortPredicatePieDonut: SortPredicatePieDonutFn = (visParams) => + visParams.respectSourceOrder ? sortPredicateSaveSourceOrder() : undefined; + +const sortPredicateMosaic: SortPredicateDefaultFn = (visData, columns) => { + const sortingMap = columns[0]?.id ? extractUniqTermsMap(visData, columns[0].id) : {}; + + return ([name1, node1], [, node2]) => { + // Sorting for first group + if (columns.length === 1 || (node1.children.length && name1 in sortingMap)) { + return sortingMap[name1]; + } + + // Sorting for second group + return node2.value - node1.value; + }; +}; + +const sortPredicateWaffle: SortPredicatePureFn = + () => + ([, node1], [, node2]) => + node2.value - node1.value; + +export const sortPredicateByType = ( + chartType: ChartTypes, + visParams: PartitionVisParams, + visData: Datatable, + columns: Array> +) => + ({ + [ChartTypes.PIE]: () => sortPredicatePieDonut(visParams), + [ChartTypes.DONUT]: () => sortPredicatePieDonut(visParams), + [ChartTypes.WAFFLE]: () => sortPredicateWaffle(), + [ChartTypes.TREEMAP]: () => undefined, + [ChartTypes.MOSAIC]: () => sortPredicateMosaic(visData, columns), + }[chartType]()); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.test.ts new file mode 100644 index 000000000000..9d5e512de995 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { ChartTypes, LegendDisplay } from '../../common/types'; +import { createMockVisData } from '../mocks'; +import { isLegendFlat, shouldShowLegend } from './legend'; + +describe('isLegendFlat', () => { + const visData = createMockVisData(); + const splitChartDimension = visData.columns[0]; + + const runIsFlatCommonScenario = (chartType: ChartTypes) => { + it(`legend should be flat for ${chartType} if split dimension is specified`, () => { + const flat = isLegendFlat(chartType, splitChartDimension); + expect(flat).toBeTruthy(); + }); + + it(`legend should be not flat for ${chartType} if split dimension is not specified`, () => { + const flat = isLegendFlat(chartType, undefined); + expect(flat).toBeFalsy(); + }); + }; + + runIsFlatCommonScenario(ChartTypes.PIE); + runIsFlatCommonScenario(ChartTypes.DONUT); + runIsFlatCommonScenario(ChartTypes.TREEMAP); + runIsFlatCommonScenario(ChartTypes.MOSAIC); + + it('legend should be flat for Waffle if split dimension is specified', () => { + const flat = isLegendFlat(ChartTypes.WAFFLE, splitChartDimension); + expect(flat).toBeTruthy(); + }); + + it('legend should be flat for Waffle if split dimension is not specified', () => { + const flat = isLegendFlat(ChartTypes.WAFFLE, undefined); + expect(flat).toBeTruthy(); + }); +}); + +describe('shouldShowLegend', () => { + const visData = createMockVisData(); + + const runCommonShouldShowLegendScenario = (chartType: ChartTypes) => { + it(`should hide legend if legendDisplay = hide for ${chartType}`, () => { + const show = shouldShowLegend(chartType, LegendDisplay.HIDE); + expect(show).toBeFalsy(); + }); + + it(`should show legend if legendDisplay = show for ${chartType}`, () => { + const show = shouldShowLegend(chartType, LegendDisplay.SHOW); + expect(show).toBeTruthy(); + }); + }; + + const runShouldShowLegendDefaultBucketsScenario = (chartType: ChartTypes) => { + it(`should show legend if legendDisplay = default and multiple buckets for ${chartType}`, () => { + const show = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [ + visData.columns[0], + visData.columns[1], + ]); + + expect(show).toBeTruthy(); + }); + + it(`should hide legend if legendDisplay = default and one bucket or less for ${chartType}`, () => { + const show1 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [visData.columns[0]]); + expect(show1).toBeFalsy(); + + const show2 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, []); + expect(show2).toBeFalsy(); + + const show3 = shouldShowLegend(chartType, LegendDisplay.DEFAULT); + expect(show3).toBeFalsy(); + }); + }; + + const runShouldShowLegendDefaultAlwaysFalsyScenario = (chartType: ChartTypes) => { + it(`should hide legend if legendDisplay = default and multiple buckets for ${chartType}`, () => { + const show = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [ + visData.columns[0], + visData.columns[1], + ]); + + expect(show).toBeFalsy(); + }); + + it(`should hide legend if legendDisplay = default and one bucket or less for ${chartType}`, () => { + const show1 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [visData.columns[0]]); + expect(show1).toBeFalsy(); + + const show2 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, []); + expect(show2).toBeFalsy(); + + const show3 = shouldShowLegend(chartType, LegendDisplay.DEFAULT); + expect(show3).toBeFalsy(); + }); + }; + + const runShouldShowLegendDefaultAlwaysTruthyScenario = (chartType: ChartTypes) => { + it(`should show legend if legendDisplay = default and multiple buckets for ${chartType}`, () => { + const show = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [ + visData.columns[0], + visData.columns[1], + ]); + + expect(show).toBeTruthy(); + }); + + it(`should show legend if legendDisplay = default and one bucket or less for ${chartType}`, () => { + const show1 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [visData.columns[0]]); + expect(show1).toBeTruthy(); + + const show2 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, []); + expect(show2).toBeTruthy(); + + const show3 = shouldShowLegend(chartType, LegendDisplay.DEFAULT); + expect(show3).toBeTruthy(); + }); + }; + + runCommonShouldShowLegendScenario(ChartTypes.PIE); + runShouldShowLegendDefaultBucketsScenario(ChartTypes.PIE); + + runCommonShouldShowLegendScenario(ChartTypes.DONUT); + runShouldShowLegendDefaultBucketsScenario(ChartTypes.DONUT); + + runCommonShouldShowLegendScenario(ChartTypes.TREEMAP); + runShouldShowLegendDefaultAlwaysFalsyScenario(ChartTypes.TREEMAP); + + runCommonShouldShowLegendScenario(ChartTypes.MOSAIC); + runShouldShowLegendDefaultAlwaysFalsyScenario(ChartTypes.MOSAIC); + + runCommonShouldShowLegendScenario(ChartTypes.WAFFLE); + runShouldShowLegendDefaultAlwaysTruthyScenario(ChartTypes.WAFFLE); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.ts new file mode 100644 index 000000000000..9990c1a8e65e --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/legend.ts @@ -0,0 +1,45 @@ +/* + * 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 { DatatableColumn } from '../../../../expressions'; +import { BucketColumns, ChartTypes, LegendDisplay } from '../../common/types'; + +type GetLegendIsFlatFn = (splitChartDimension: DatatableColumn | undefined) => boolean; + +const isLegendFlatCommon: GetLegendIsFlatFn = (splitChartDimension) => Boolean(splitChartDimension); + +export const isLegendFlat = ( + visType: ChartTypes, + splitChartDimension: DatatableColumn | undefined +) => + ({ + [ChartTypes.PIE]: () => isLegendFlatCommon(splitChartDimension), + [ChartTypes.DONUT]: () => isLegendFlatCommon(splitChartDimension), + [ChartTypes.TREEMAP]: () => isLegendFlatCommon(splitChartDimension), + [ChartTypes.MOSAIC]: () => isLegendFlatCommon(splitChartDimension), + [ChartTypes.WAFFLE]: () => true, + }[visType]()); + +const showIfBuckets = (bucketColumns: Array>) => bucketColumns.length > 1; + +const showLegendDefault = (visType: ChartTypes, bucketColumns: Array>) => + ({ + [ChartTypes.PIE]: () => showIfBuckets(bucketColumns), + [ChartTypes.DONUT]: () => showIfBuckets(bucketColumns), + [ChartTypes.TREEMAP]: () => false, + [ChartTypes.MOSAIC]: () => false, + [ChartTypes.WAFFLE]: () => true, + }[visType]()); + +export const shouldShowLegend = ( + visType: ChartTypes, + legendDisplay: LegendDisplay, + bucketColumns: Array> = [] +) => + legendDisplay === LegendDisplay.SHOW || + (legendDisplay === LegendDisplay.DEFAULT && showLegendDefault(visType, bucketColumns)); diff --git a/src/plugins/chart_expressions/expression_pie/public/index.ts b/src/plugins/chart_expressions/expression_partition_vis/server/index.ts similarity index 65% rename from src/plugins/chart_expressions/expression_pie/public/index.ts rename to src/plugins/chart_expressions/expression_partition_vis/server/index.ts index 8123bc03c8e1..98395d521e23 100755 --- a/src/plugins/chart_expressions/expression_pie/public/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/server/index.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { ExpressionPiePlugin } from './plugin'; +import { ExpressionPartitionVisPlugin } from './plugin'; export function plugin() { - return new ExpressionPiePlugin(); + return new ExpressionPartitionVisPlugin(); } -export type { ExpressionPiePluginSetup, ExpressionPiePluginStart } from './types'; +export type { ExpressionPartitionVisPluginSetup, ExpressionPartitionVisPluginStart } from './types'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/server/plugin.ts b/src/plugins/chart_expressions/expression_partition_vis/server/plugin.ts new file mode 100755 index 000000000000..190e43e4c7df --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/server/plugin.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 { CoreSetup, CoreStart, Plugin } from '../../../../core/server'; +import { + partitionLabelsFunction, + pieVisFunction, + treemapVisFunction, + mosaicVisFunction, + waffleVisFunction, +} from '../common'; +import { + ExpressionPartitionVisPluginSetup, + ExpressionPartitionVisPluginStart, + SetupDeps, + StartDeps, +} from './types'; + +export class ExpressionPartitionVisPlugin + implements + Plugin< + ExpressionPartitionVisPluginSetup, + ExpressionPartitionVisPluginStart, + SetupDeps, + StartDeps + > +{ + public setup(core: CoreSetup, { expressions }: SetupDeps) { + expressions.registerFunction(partitionLabelsFunction); + expressions.registerFunction(pieVisFunction); + expressions.registerFunction(treemapVisFunction); + expressions.registerFunction(mosaicVisFunction); + expressions.registerFunction(waffleVisFunction); + } + + public start(core: CoreStart, deps: StartDeps) {} + + public stop() {} +} diff --git a/src/plugins/chart_expressions/expression_pie/server/types.ts b/src/plugins/chart_expressions/expression_partition_vis/server/types.ts similarity index 84% rename from src/plugins/chart_expressions/expression_pie/server/types.ts rename to src/plugins/chart_expressions/expression_partition_vis/server/types.ts index ff3e00bdf6da..0fdca6e6b319 100755 --- a/src/plugins/chart_expressions/expression_pie/server/types.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/server/types.ts @@ -7,8 +7,8 @@ */ import { ExpressionsServerStart, ExpressionsServerSetup } from '../../../expressions/server'; -export type ExpressionPiePluginSetup = void; -export type ExpressionPiePluginStart = void; +export type ExpressionPartitionVisPluginSetup = void; +export type ExpressionPartitionVisPluginStart = void; export interface SetupDeps { expressions: ExpressionsServerSetup; diff --git a/src/plugins/chart_expressions/expression_pie/tsconfig.json b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json similarity index 100% rename from src/plugins/chart_expressions/expression_pie/tsconfig.json rename to src/plugins/chart_expressions/expression_partition_vis/tsconfig.json diff --git a/src/plugins/chart_expressions/expression_pie/README.md b/src/plugins/chart_expressions/expression_pie/README.md deleted file mode 100755 index 95f4298aa293..000000000000 --- a/src/plugins/chart_expressions/expression_pie/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# expressionPie - -Expression Pie plugin adds a `pie` renderer and function to the expression plugin. The renderer will display the `Pie` 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_pie/common/constants.ts b/src/plugins/chart_expressions/expression_pie/common/constants.ts deleted file mode 100644 index c666692c3ea7..000000000000 --- a/src/plugins/chart_expressions/expression_pie/common/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const PLUGIN_ID = 'expressionPie'; -export const PLUGIN_NAME = 'expressionPie'; - -export const PIE_VIS_EXPRESSION_NAME = 'pie_vis'; -export const PIE_LABELS_VALUE = 'pie_labels_value'; -export const PIE_LABELS_FUNCTION = 'pie_labels'; - -export const DEFAULT_PERCENT_DECIMALS = 2; diff --git a/src/plugins/chart_expressions/expression_pie/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_pie/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap deleted file mode 100644 index e4d994c058f0..000000000000 --- a/src/plugins/chart_expressions/expression_pie/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`interpreter/functions#pie logs correct datatable to inspector 1`] = ` -Object { - "columns": Array [ - Object { - "id": "col-0-1", - "meta": Object { - "dimensionName": "Slice size", - }, - "name": "Count", - }, - ], - "rows": Array [ - Object { - "col-0-1": 0, - }, - ], - "type": "datatable", -} -`; - -exports[`interpreter/functions#pie returns an object with the correct structure 1`] = ` -Object { - "as": "pie_vis", - "type": "render", - "value": Object { - "params": Object { - "listenOnChange": true, - }, - "syncColors": false, - "visConfig": Object { - "addLegend": true, - "addTooltip": true, - "buckets": undefined, - "dimensions": Object { - "buckets": undefined, - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, - }, - "type": "vis_dimension", - }, - "splitColumn": undefined, - "splitRow": undefined, - }, - "distinctColors": false, - "emptySizeRatio": 0.3, - "isDonut": true, - "labels": Object { - "last_level": false, - "percentDecimals": 2, - "position": "default", - "show": false, - "truncate": 100, - "type": "pie_labels_value", - "values": true, - "valuesFormat": "percent", - }, - "legendPosition": "right", - "maxLegendLines": 2, - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, - }, - "type": "vis_dimension", - }, - "nestedLegend": true, - "palette": Object { - "name": "kibana_palette", - "type": "system_palette", - }, - "splitColumn": undefined, - "splitRow": undefined, - "truncateLegend": true, - }, - "visData": Object { - "columns": Array [ - Object { - "id": "col-0-1", - "name": "Count", - }, - ], - "rows": Array [ - Object { - "col-0-1": 0, - }, - ], - "type": "datatable", - }, - "visType": "pie", - }, -} -`; diff --git a/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_labels_function.ts b/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_labels_function.ts deleted file mode 100644 index 6bdb4f6b0408..000000000000 --- a/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_labels_function.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, Datatable } from '../../../../expressions/common'; -import { PIE_LABELS_FUNCTION, PIE_LABELS_VALUE } from '../constants'; -import { ExpressionValuePieLabels, PieLabelsArguments } from '../types/expression_functions'; - -export const pieLabelsFunction = (): ExpressionFunctionDefinition< - typeof PIE_LABELS_FUNCTION, - Datatable | null, - PieLabelsArguments, - ExpressionValuePieLabels -> => ({ - name: PIE_LABELS_FUNCTION, - help: i18n.translate('expressionPie.pieLabels.function.help', { - defaultMessage: 'Generates the pie labels object', - }), - type: PIE_LABELS_VALUE, - args: { - show: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieLabels.function.args.show.help', { - defaultMessage: 'Displays the pie labels', - }), - default: true, - }, - position: { - types: ['string'], - default: 'default', - help: i18n.translate('expressionPie.pieLabels.function.args.position.help', { - defaultMessage: 'Defines the label position', - }), - }, - values: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieLabels.function.args.values.help', { - defaultMessage: 'Displays the values inside the slices', - }), - default: true, - }, - percentDecimals: { - types: ['number'], - help: i18n.translate('expressionPie.pieLabels.function.args.percentDecimals.help', { - defaultMessage: 'Defines the number of decimals that will appear on the values as percent', - }), - default: 2, - }, - lastLevel: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieLabels.function.args.lastLevel.help', { - defaultMessage: 'Show top level labels only', - }), - default: true, - }, - truncate: { - types: ['number'], - help: i18n.translate('expressionPie.pieLabels.function.args.truncate.help', { - defaultMessage: 'Defines the number of characters that the slice value will display', - }), - default: null, - }, - valuesFormat: { - types: ['string'], - default: 'percent', - help: i18n.translate('expressionPie.pieLabels.function.args.valuesFormat.help', { - defaultMessage: 'Defines the format of the values', - }), - }, - }, - fn: (context, args) => { - return { - type: PIE_LABELS_VALUE, - show: args.show, - position: args.position, - percentDecimals: args.percentDecimals, - values: args.values, - truncate: args.truncate, - valuesFormat: args.valuesFormat, - last_level: args.lastLevel, - }; - }, -}); diff --git a/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.ts deleted file mode 100644 index 1e5507c81844..000000000000 --- a/src/plugins/chart_expressions/expression_pie/common/expression_functions/pie_vis_function.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { EmptySizeRatios, PieVisParams } from '../types/expression_renderers'; -import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table'; -import { PieVisExpressionFunctionDefinition } from '../types/expression_functions'; -import { PIE_LABELS_FUNCTION, PIE_LABELS_VALUE, PIE_VIS_EXPRESSION_NAME } from '../constants'; - -export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ - name: PIE_VIS_EXPRESSION_NAME, - type: 'render', - inputTypes: ['datatable'], - help: i18n.translate('expressionPie.pieVis.function.help', { - defaultMessage: 'Pie visualization', - }), - args: { - metric: { - types: ['vis_dimension'], - help: i18n.translate('expressionPie.pieVis.function.args.metricHelpText', { - defaultMessage: 'Metric dimensions config', - }), - required: true, - }, - buckets: { - types: ['vis_dimension'], - help: i18n.translate('expressionPie.pieVis.function.args.bucketsHelpText', { - defaultMessage: 'Buckets dimensions config', - }), - multi: true, - }, - splitColumn: { - types: ['vis_dimension'], - help: i18n.translate('expressionPie.pieVis.function.args.splitColumnHelpText', { - defaultMessage: 'Split by column dimension config', - }), - multi: true, - }, - splitRow: { - types: ['vis_dimension'], - help: i18n.translate('expressionPie.pieVis.function.args.splitRowHelpText', { - defaultMessage: 'Split by row dimension config', - }), - multi: true, - }, - addTooltip: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieVis.function.args.addTooltipHelpText', { - defaultMessage: 'Show tooltip on slice hover', - }), - default: true, - }, - addLegend: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieVis.function.args.addLegendHelpText', { - defaultMessage: 'Show legend chart legend', - }), - }, - legendPosition: { - types: ['string'], - help: i18n.translate('expressionPie.pieVis.function.args.legendPositionHelpText', { - defaultMessage: 'Position the legend on top, bottom, left, right of the chart', - }), - }, - nestedLegend: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieVis.function.args.nestedLegendHelpText', { - defaultMessage: 'Show a more detailed legend', - }), - default: false, - }, - truncateLegend: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieVis.function.args.truncateLegendHelpText', { - defaultMessage: 'Defines if the legend items will be truncated or not', - }), - default: true, - }, - maxLegendLines: { - types: ['number'], - help: i18n.translate('expressionPie.pieVis.function.args.maxLegendLinesHelpText', { - defaultMessage: 'Defines the number of lines per legend item', - }), - }, - distinctColors: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieVis.function.args.distinctColorsHelpText', { - defaultMessage: - 'Maps different color per slice. Slices with the same value have the same color', - }), - default: false, - }, - isDonut: { - types: ['boolean'], - help: i18n.translate('expressionPie.pieVis.function.args.isDonutHelpText', { - defaultMessage: 'Displays the pie chart as donut', - }), - default: false, - }, - emptySizeRatio: { - types: ['number'], - help: i18n.translate('expressionPie.pieVis.function.args.emptySizeRatioHelpText', { - defaultMessage: 'Defines donut inner empty area size', - }), - default: EmptySizeRatios.SMALL, - }, - palette: { - types: ['palette', 'system_palette'], - help: i18n.translate('expressionPie.pieVis.function.args.paletteHelpText', { - defaultMessage: 'Defines the chart palette name', - }), - default: '{palette}', - }, - labels: { - types: [PIE_LABELS_VALUE], - help: i18n.translate('expressionPie.pieVis.function.args.labelsHelpText', { - defaultMessage: 'Pie labels config', - }), - default: `{${PIE_LABELS_FUNCTION}}`, - }, - }, - fn(context, args, handlers) { - const visConfig: PieVisParams = { - ...args, - palette: args.palette, - dimensions: { - metric: args.metric, - buckets: args.buckets, - splitColumn: args.splitColumn, - splitRow: args.splitRow, - }, - }; - - if (handlers?.inspectorAdapters?.tables) { - const logTable = prepareLogTable(context, [ - [ - [args.metric], - i18n.translate('expressionPie.pieVis.function.dimension.metric', { - defaultMessage: 'Slice size', - }), - ], - [ - args.buckets, - i18n.translate('expressionPie.pieVis.function.dimension.buckets', { - defaultMessage: 'Slice', - }), - ], - [ - args.splitColumn, - i18n.translate('expressionPie.pieVis.function.dimension.splitcolumn', { - defaultMessage: 'Column split', - }), - ], - [ - args.splitRow, - i18n.translate('expressionPie.pieVis.function.dimension.splitrow', { - defaultMessage: 'Row split', - }), - ], - ]); - handlers.inspectorAdapters.tables.logDatatable('default', logTable); - } - - return { - type: 'render', - as: PIE_VIS_EXPRESSION_NAME, - value: { - visData: context, - visConfig, - syncColors: handlers?.isSyncColorsEnabled?.() ?? false, - visType: 'pie', - params: { - listenOnChange: true, - }, - }, - }; - }, -}); diff --git a/src/plugins/chart_expressions/expression_pie/common/index.ts b/src/plugins/chart_expressions/expression_pie/common/index.ts deleted file mode 100755 index c5943c54c0c6..000000000000 --- a/src/plugins/chart_expressions/expression_pie/common/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { - PLUGIN_ID, - PLUGIN_NAME, - PIE_VIS_EXPRESSION_NAME, - PIE_LABELS_VALUE, - PIE_LABELS_FUNCTION, -} from './constants'; - -export { pieVisFunction, pieLabelsFunction } from './expression_functions'; - -export type { - ExpressionValuePieLabels, - PieVisExpressionFunctionDefinition, -} from './types/expression_functions'; - -export type { - PieVisParams, - PieVisConfig, - LabelsParams, - Dimension, - Dimensions, -} from './types/expression_renderers'; - -export { ValueFormats, LabelPositions, EmptySizeRatios } from './types/expression_renderers'; diff --git a/src/plugins/chart_expressions/expression_pie/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_pie/common/types/expression_functions.ts deleted file mode 100644 index 39d5392c65ed..000000000000 --- a/src/plugins/chart_expressions/expression_pie/common/types/expression_functions.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PIE_LABELS_VALUE, PIE_VIS_EXPRESSION_NAME } from '../constants'; -import { - ExpressionFunctionDefinition, - Datatable, - ExpressionValueRender, - ExpressionValueBoxed, -} from '../../../../expressions/common'; -import { RenderValue, PieVisConfig, LabelPositions, ValueFormats } from './expression_renderers'; - -export interface PieLabelsArguments { - show: boolean; - position: LabelPositions; - values: boolean; - truncate: number | null; - valuesFormat: ValueFormats; - lastLevel: boolean; - percentDecimals: number; -} - -export type ExpressionValuePieLabels = ExpressionValueBoxed< - typeof PIE_LABELS_VALUE, - { - show: boolean; - position: LabelPositions; - values: boolean; - truncate: number | null; - valuesFormat: ValueFormats; - last_level: boolean; - percentDecimals: number; - } ->; - -export type PieVisExpressionFunctionDefinition = ExpressionFunctionDefinition< - typeof PIE_VIS_EXPRESSION_NAME, - Datatable, - PieVisConfig, - ExpressionValueRender ->; diff --git a/src/plugins/chart_expressions/expression_pie/public/__stories__/pie_renderer.stories.tsx b/src/plugins/chart_expressions/expression_pie/public/__stories__/pie_renderer.stories.tsx deleted file mode 100644 index 8afca1f9912f..000000000000 --- a/src/plugins/chart_expressions/expression_pie/public/__stories__/pie_renderer.stories.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { Datatable } from '../../../../expressions'; -import { Render } from '../../../../presentation_util/public/__stories__'; -import { getPieVisRenderer } from '../expression_renderers'; -import { LabelPositions, RenderValue, ValueFormats } from '../../common/types'; -import { palettes, theme, getStartDeps } from '../__mocks__'; - -const visData: Datatable = { - type: 'datatable', - columns: [ - { id: 'cost', name: 'cost', meta: { type: 'number' } }, - { id: 'age', name: 'age', meta: { type: 'number' } }, - { id: 'price', name: 'price', meta: { type: 'number' } }, - { id: 'project', name: 'project', meta: { type: 'string' } }, - { id: '@timestamp', name: '@timestamp', meta: { type: 'date' } }, - ], - rows: [ - { - cost: 32.15, - age: 63, - price: 53, - project: 'elasticsearch', - '@timestamp': 1546334211208, - }, - { - cost: 20.52, - age: 68, - price: 33, - project: 'beats', - '@timestamp': 1546351551031, - }, - { - cost: 21.15, - age: 57, - price: 59, - project: 'apm', - '@timestamp': 1546352631083, - }, - { - cost: 35.64, - age: 73, - price: 71, - project: 'machine-learning', - '@timestamp': 1546402490956, - }, - { - cost: 27.19, - age: 38, - price: 36, - project: 'kibana', - '@timestamp': 1546467111351, - }, - ], -}; - -const config: RenderValue = { - visType: 'pie_vis', - visData, - visConfig: { - dimensions: { - metric: { - type: 'vis_dimension', - accessor: { id: 'cost', name: 'cost', meta: { type: 'number' } }, - format: { id: 'number', params: {} }, - }, - buckets: [ - { - type: 'vis_dimension', - accessor: { id: 'age', name: 'age', meta: { type: 'number' } }, - format: { id: 'number', params: {} }, - }, - ], - }, - palette: { type: 'system_palette', name: 'default' }, - addTooltip: false, - addLegend: false, - legendPosition: 'right', - nestedLegend: false, - truncateLegend: false, - distinctColors: false, - isDonut: false, - emptySizeRatio: 0.37, - maxLegendLines: 1, - labels: { - show: false, - last_level: false, - position: LabelPositions.DEFAULT, - values: false, - truncate: null, - valuesFormat: ValueFormats.VALUE, - percentDecimals: 1, - }, - }, - syncColors: false, -}; - -const containerSize = { - width: '700px', - height: '700px', -}; - -const pieRenderer = getPieVisRenderer({ palettes, theme, getStartDeps }); - -storiesOf('renderers/pieVis', module).add('Default', () => { - return pieRenderer} config={config} {...containerSize} />; -}); diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_layers.ts b/src/plugins/chart_expressions/expression_pie/public/utils/get_layers.ts deleted file mode 100644 index 9268f5631e73..000000000000 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_layers.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Datum, PartitionLayer, ShapeTreeNode, ArrayEntry } from '@elastic/charts'; -import { isEqual } from 'lodash'; -import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; -import { SeriesLayer, PaletteRegistry, lightenColor } from '../../../../charts/public'; -import type { DatatableRow } from '../../../../expressions/public'; -import type { BucketColumns, PieVisParams, SplitDimensionParams } from '../../common/types'; -import { getDistinctSeries } from './get_distinct_series'; - -const EMPTY_SLICE = Symbol('empty_slice'); - -export const computeColor = ( - d: ShapeTreeNode, - isSplitChart: boolean, - overwriteColors: { [key: string]: string } = {}, - columns: Array>, - rows: DatatableRow[], - visParams: PieVisParams, - palettes: PaletteRegistry | null, - syncColors: boolean, - formatter: FieldFormatsStart, - format?: BucketColumns['format'] -) => { - const { parentSeries, allSeries } = getDistinctSeries(rows, columns); - const dataName = d.dataName; - - let formattedLabel = ''; - if (format) { - formattedLabel = formatter.deserialize(format).convert(dataName) ?? ''; - } - - if (visParams.distinctColors) { - let overwriteColor; - // this is for supporting old visualizations (created by vislib plugin) - // it seems that there for some aggs, the uiState saved from vislib is - // different than the es-charts handle it - if (overwriteColors.hasOwnProperty(formattedLabel)) { - overwriteColor = overwriteColors[formattedLabel]; - } - - if (Object.keys(overwriteColors).includes(dataName.toString())) { - overwriteColor = overwriteColors[dataName]; - } - - if (overwriteColor) { - return overwriteColor; - } - - const index = allSeries.findIndex((name) => isEqual(name, dataName)); - const isSplitParentLayer = isSplitChart && parentSeries.includes(dataName); - return palettes?.get(visParams.palette.name).getCategoricalColor( - [ - { - name: dataName, - rankAtDepth: isSplitParentLayer - ? parentSeries.findIndex((name) => name === dataName) - : index > -1 - ? index - : 0, - totalSeriesAtDepth: isSplitParentLayer ? parentSeries.length : allSeries.length || 1, - }, - ], - { - maxDepth: 1, - totalSeries: allSeries.length || 1, - behindText: visParams.labels.show, - syncColors, - }, - visParams.palette?.params ?? { colors: [] } - ); - } - const seriesLayers: SeriesLayer[] = []; - let tempParent: typeof d | typeof d['parent'] = d; - while (tempParent.parent && tempParent.depth > 0) { - const seriesName = String(tempParent.parent.children[tempParent.sortIndex][0]); - const isSplitParentLayer = isSplitChart && parentSeries.includes(seriesName); - seriesLayers.unshift({ - name: seriesName, - rankAtDepth: isSplitParentLayer - ? parentSeries.findIndex((name) => name === seriesName) - : tempParent.sortIndex, - totalSeriesAtDepth: isSplitParentLayer - ? parentSeries.length - : tempParent.parent.children.length, - }); - tempParent = tempParent.parent; - } - - let overwriteColor; - // this is for supporting old visualizations (created by vislib plugin) - // it seems that there for some aggs, the uiState saved from vislib is - // different than the es-charts handle it - if (overwriteColors.hasOwnProperty(formattedLabel)) { - overwriteColor = overwriteColors[formattedLabel]; - } - - seriesLayers.forEach((layer) => { - if (Object.keys(overwriteColors).includes(layer.name)) { - overwriteColor = overwriteColors[layer.name]; - } - }); - - if (overwriteColor) { - return lightenColor(overwriteColor, seriesLayers.length, columns.length); - } - return palettes?.get(visParams.palette.name).getCategoricalColor( - seriesLayers, - { - behindText: visParams.labels.show, - maxDepth: columns.length, - totalSeries: rows.length, - syncColors, - }, - visParams.palette?.params ?? { colors: [] } - ); -}; - -export const getLayers = ( - columns: Array>, - visParams: PieVisParams, - overwriteColors: { [key: string]: string } = {}, - rows: DatatableRow[], - palettes: PaletteRegistry | null, - formatter: FieldFormatsStart, - syncColors: boolean -): PartitionLayer[] => { - const fillLabel: PartitionLayer['fillLabel'] = { - valueFont: { - fontWeight: 700, - }, - }; - - if (!visParams.labels.values) { - fillLabel.valueFormatter = () => ''; - } - const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); - return columns.map((col) => { - return { - groupByRollup: (d: Datum) => { - return col.id ? d[col.id] : col.name; - }, - showAccessor: (d: Datum) => d !== EMPTY_SLICE, - nodeLabel: (d: unknown) => { - if (col.format) { - return formatter.deserialize(col.format).convert(d) ?? ''; - } - return String(d); - }, - sortPredicate: ([name1, node1]: ArrayEntry, [name2, node2]: ArrayEntry) => { - const params = col.meta?.sourceParams?.params as SplitDimensionParams | undefined; - const sort: string | undefined = params?.orderBy; - // unconditionally put "Other" to the end (as the "Other" slice may be larger than a regular slice, yet should be at the end) - if (name1 === '__other__' && name2 !== '__other__') return 1; - if (name2 === '__other__' && name1 !== '__other__') return -1; - // metric sorting - if (sort && sort !== '_key') { - if (params?.order === 'desc') { - return node2.value - node1.value; - } else { - return node1.value - node2.value; - } - // alphabetical sorting - } else { - if (name1 > name2) { - return params?.order === 'desc' ? -1 : 1; - } - if (name2 > name1) { - return params?.order === 'desc' ? 1 : -1; - } - } - return 0; - }, - fillLabel, - shape: { - fillColor: (d) => { - const outputColor = computeColor( - d, - isSplitChart, - overwriteColors, - columns, - rows, - visParams, - palettes, - syncColors, - formatter, - col.format - ); - - return outputColor || 'rgba(0,0,0,0)'; - }, - }, - }; - }); -}; diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.test.ts b/src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.test.ts deleted file mode 100644 index 1cccdf8a5e47..000000000000 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getPartitionTheme } from './get_partition_theme'; -import { createMockPieParams } from '../mocks'; - -const visParams = createMockPieParams(); - -describe('getConfig', () => { - it('should cap the outerSizeRatio to 1', () => { - expect( - getPartitionTheme(visParams, {}, { width: 400, height: 400 }).partition?.outerSizeRatio - ).toBe(1); - }); - - it('should not have outerSizeRatio for split chart', () => { - expect( - getPartitionTheme( - { - ...visParams, - dimensions: { - ...visParams.dimensions, - splitColumn: [ - { - type: 'vis_dimension', - accessor: 1, - format: { - id: 'number', - params: {}, - }, - }, - ], - }, - }, - {}, - { width: 400, height: 400 } - ).partition?.outerSizeRatio - ).toBeUndefined(); - - expect( - getPartitionTheme( - { - ...visParams, - dimensions: { - ...visParams.dimensions, - splitRow: [ - { - type: 'vis_dimension', - accessor: 1, - format: { - id: 'number', - params: {}, - }, - }, - ], - }, - }, - {}, - { width: 400, height: 400 } - ).partition?.outerSizeRatio - ).toBeUndefined(); - }); - - it('should not set outerSizeRatio if dimensions are not defined', () => { - expect(getPartitionTheme(visParams, {}).partition?.outerSizeRatio).toBeUndefined(); - }); -}); diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.ts b/src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.ts deleted file mode 100644 index 4daaf835fa78..000000000000 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_partition_theme.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PartialTheme } from '@elastic/charts'; -import { Required } from '@kbn/utility-types'; -import { LabelPositions, PieVisParams, PieContainerDimensions } from '../../common/types'; - -const MAX_SIZE = 1000; - -export const getPartitionTheme = ( - visParams: PieVisParams, - chartTheme: PartialTheme, - dimensions?: PieContainerDimensions, - rescaleFactor: number = 1 -): PartialTheme => { - // On small multiples we want the labels to only appear inside - const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); - const paddingProps: PartialTheme | null = - dimensions && !isSplitChart - ? { - chartPaddings: { - // TODO: simplify ratio logic to be static px units - top: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height, - bottom: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height, - left: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height, - right: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height, - }, - } - : null; - - const outerSizeRatio: PartialTheme['partition'] | null = - dimensions && !isSplitChart - ? { - outerSizeRatio: - // Cap the ratio to 1 and then rescale - rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1), - } - : null; - const theme: Required = { - chartMargins: { top: 0, bottom: 0, left: 0, right: 0 }, - ...paddingProps, - partition: { - fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily, - ...outerSizeRatio, - minFontSize: 10, - maxFontSize: 16, - linkLabel: { - maxCount: 5, - fontSize: 11, - textColor: chartTheme.axes?.axisTitle?.fill, - maxTextLength: visParams.labels.truncate ?? undefined, - }, - sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, - sectorLineWidth: 1.5, - circlePadding: 4, - emptySizeRatio: visParams.isDonut ? visParams.emptySizeRatio : 0, - }, - }; - if (!visParams.labels.show) { - // Force all labels to be linked, then prevent links from showing - theme.partition.linkLabel = { maxCount: 0, maximumSection: Number.POSITIVE_INFINITY }; - } - - if (visParams.labels.last_level && visParams.labels.show) { - theme.partition.linkLabel = { - maxCount: Number.POSITIVE_INFINITY, - maximumSection: Number.POSITIVE_INFINITY, - maxTextLength: visParams.labels.truncate ?? undefined, - }; - } - - if ( - (visParams.labels.position === LabelPositions.INSIDE || isSplitChart) && - visParams.labels.show - ) { - theme.partition.linkLabel = { maxCount: 0 }; - } - - return theme; -}; diff --git a/src/plugins/chart_expressions/expression_pie/public/utils/get_split_dimension_accessor.ts b/src/plugins/chart_expressions/expression_pie/public/utils/get_split_dimension_accessor.ts deleted file mode 100644 index ebc197990845..000000000000 --- a/src/plugins/chart_expressions/expression_pie/public/utils/get_split_dimension_accessor.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { AccessorFn } from '@elastic/charts'; -import { getColumnByAccessor } from './accessor'; -import { DatatableColumn } from '../../../../expressions/public'; -import type { FieldFormatsStart } from '../../../../field_formats/public'; -import { ExpressionValueVisDimension } from '../../../../visualizations/common'; - -export const getSplitDimensionAccessor = - (fieldFormats: FieldFormatsStart, columns: DatatableColumn[]) => - (splitDimension: ExpressionValueVisDimension): AccessorFn => { - const formatter = fieldFormats.deserialize(splitDimension.format); - const splitChartColumn = getColumnByAccessor(splitDimension.accessor, columns); - const accessor = splitChartColumn.id; - - const fn: AccessorFn = (d) => { - const v = d[accessor]; - if (v === undefined) { - return; - } - const f = formatter.convert(v); - return f; - }; - - return fn; - }; diff --git a/src/plugins/chart_expressions/expression_pie/server/plugin.ts b/src/plugins/chart_expressions/expression_pie/server/plugin.ts deleted file mode 100755 index f46983f0f882..000000000000 --- a/src/plugins/chart_expressions/expression_pie/server/plugin.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { CoreSetup, CoreStart, Plugin } from '../../../../core/server'; -import { pieLabelsFunction, pieVisFunction } from '../common'; -import { ExpressionPiePluginSetup, ExpressionPiePluginStart, SetupDeps, StartDeps } from './types'; - -export class ExpressionPiePlugin - implements Plugin -{ - public setup(core: CoreSetup, { expressions }: SetupDeps) { - expressions.registerFunction(pieLabelsFunction); - expressions.registerFunction(pieVisFunction); - } - - public start(core: CoreStart, deps: StartDeps) {} - - public stop() {} -} diff --git a/src/plugins/vis_types/pie/kibana.json b/src/plugins/vis_types/pie/kibana.json index fb310d8afd82..abed576cc673 100644 --- a/src/plugins/vis_types/pie/kibana.json +++ b/src/plugins/vis_types/pie/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "ui": true, "server": true, - "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPie"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPartitionVis"], "requiredBundles": ["visDefaultEditor"], "extraPublicDirs": ["common/index"], "owner": { diff --git a/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap index 2edf2fff72a3..904dff6ee119 100644 --- a/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/pie/public/__snapshots__/to_ast.test.ts.snap @@ -34,9 +34,6 @@ Object { }, Object { "arguments": Object { - "addLegend": Array [ - true, - ], "addTooltip": Array [ true, ], @@ -70,7 +67,7 @@ Object { "chain": Array [ Object { "arguments": Object { - "lastLevel": Array [ + "last_level": Array [ true, ], "show": Array [ @@ -83,13 +80,16 @@ Object { true, ], }, - "function": "pie_labels", + "function": "partitionLabels", "type": "function", }, ], "type": "expression", }, ], + "legendDisplay": Array [ + "show", + ], "legendPosition": Array [ "right", ], @@ -112,6 +112,9 @@ Object { "type": "expression", }, ], + "nestedLegend": Array [ + false, + ], "palette": Array [ Object { "chain": Array [ @@ -124,8 +127,11 @@ Object { "type": "expression", }, ], + "startFromSecondLargestSlice": Array [ + false, + ], }, - "function": "pie_vis", + "function": "pieVis", "type": "function", }, ], diff --git a/src/plugins/vis_types/pie/public/editor/collections.ts b/src/plugins/vis_types/pie/public/editor/collections.ts index 16e6bd937289..dd9d3fb3737b 100644 --- a/src/plugins/vis_types/pie/public/editor/collections.ts +++ b/src/plugins/vis_types/pie/public/editor/collections.ts @@ -11,7 +11,7 @@ import { LabelPositions, ValueFormats, EmptySizeRatios, -} from '../../../../chart_expressions/expression_pie/common'; +} from '../../../../chart_expressions/expression_partition_vis/common'; export const getLabelPositions = [ { diff --git a/src/plugins/vis_types/pie/public/editor/components/index.tsx b/src/plugins/vis_types/pie/public/editor/components/index.tsx index c61e2724a466..591eaba64ecc 100644 --- a/src/plugins/vis_types/pie/public/editor/components/index.tsx +++ b/src/plugins/vis_types/pie/public/editor/components/index.tsx @@ -9,13 +9,13 @@ import React, { lazy } from 'react'; import { VisEditorOptionsProps } from '../../../../../visualizations/public'; import { PieTypeProps } from '../../types'; -import { PieVisParams } from '../../../../../chart_expressions/expression_pie/common'; +import { PartitionVisParams } from '../../../../../chart_expressions/expression_partition_vis/common'; const PieOptionsLazy = lazy(() => import('./pie')); export const getPieOptions = ({ showElasticChartsOptions, palettes, trackUiMetric }: PieTypeProps) => - (props: VisEditorOptionsProps) => + (props: VisEditorOptionsProps) => ( , PieTypeProps {} +export interface PieOptionsProps extends VisEditorOptionsProps, PieTypeProps {} const emptySizeRatioLabel = i18n.translate('visTypePie.editors.pie.emptySizeRatioLabel', { defaultMessage: 'Inner area size', @@ -82,19 +83,24 @@ function DecimalSlider({ const PieOptions = (props: PieOptionsProps) => { const { stateParams, setValue, aggs } = props; - const setLabels = ( + const setLabels = ( paramName: T, - value: PieVisParams['labels'][T] + value: PartitionVisParams['labels'][T] ) => setValue('labels', { ...stateParams.labels, [paramName]: value }); const legendUiStateValue = props.uiState?.get('vis.legendOpen'); const [palettesRegistry, setPalettesRegistry] = useState(undefined); const [legendVisibility, setLegendVisibility] = useState(() => { - const bwcLegendStateDefault = stateParams.addLegend == null ? false : stateParams.addLegend; - return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean; + const bwcLegendStateDefault = stateParams.legendDisplay === LegendDisplay.SHOW; + return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault); }); const hasSplitChart = Boolean(aggs?.aggs?.find((agg) => agg.schema === 'split' && agg.enabled)); const segments = aggs?.aggs?.filter((agg) => agg.schema === 'segment' && agg.enabled) ?? []; + const getLegendDisplay = useCallback( + (isVisible: boolean) => (isVisible ? LegendDisplay.SHOW : LegendDisplay.HIDE), + [] + ); + useEffect(() => { setLegendVisibility(legendUiStateValue); }, [legendUiStateValue]); @@ -115,6 +121,21 @@ const PieOptions = (props: PieOptionsProps) => { [setValue] ); + const handleLegendDisplayChange = useCallback( + (name: keyof PartitionVisParams, show: boolean) => { + setLegendVisibility(show); + + const legendDisplay = getLegendDisplay(show); + if (legendDisplay === stateParams[name]) { + setValue(name, getLegendDisplay(!show)); + } + setValue(name, legendDisplay); + + props.uiState?.set('vis.legendOpen', show); + }, + [getLegendDisplay, props.uiState, setValue, stateParams] + ); + return ( <> @@ -180,15 +201,12 @@ const PieOptions = (props: PieOptionsProps) => { { - setLegendVisibility(value); - setValue(paramName, value); - }} + setValue={handleLegendDisplayChange} data-test-subj="visTypePieAddLegendSwitch" /> { })} paramName="nestedLegend" value={stateParams.nestedLegend} - disabled={!stateParams.addLegend} + disabled={stateParams.legendDisplay === LegendDisplay.HIDE} setValue={(paramName, value) => { if (props.trackUiMetric) { props.trackUiMetric(METRIC_TYPE.CLICK, 'nested_legend_switched'); diff --git a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts index 56f062078788..53140c822bd9 100644 --- a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { LegendDisplay } from '../../../chart_expressions/expression_partition_vis/common'; + export const samplePieVis = { type: { name: 'pie', @@ -24,7 +26,7 @@ export const samplePieVis = { defaults: { type: 'pie', addTooltip: true, - addLegend: true, + legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', isDonut: true, nestedLegend: true, @@ -138,7 +140,7 @@ export const samplePieVis = { params: { type: 'pie', addTooltip: true, - addLegend: true, + legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', isDonut: true, labels: { diff --git a/src/plugins/vis_types/pie/public/to_ast.test.ts b/src/plugins/vis_types/pie/public/to_ast.test.ts index 87e279e787b3..62421d30c2ac 100644 --- a/src/plugins/vis_types/pie/public/to_ast.test.ts +++ b/src/plugins/vis_types/pie/public/to_ast.test.ts @@ -8,12 +8,12 @@ import { Vis } from '../../../visualizations/public'; -import { PieVisParams } from '../../../chart_expressions/expression_pie/common'; +import { PartitionVisParams } from '../../../chart_expressions/expression_partition_vis/common'; import { samplePieVis } from './sample_vis.test.mocks'; import { toExpressionAst } from './to_ast'; describe('vis type pie vis toExpressionAst function', () => { - let vis: Vis; + let vis: Vis; const params = { timefilter: {}, timeRange: {}, diff --git a/src/plugins/vis_types/pie/public/to_ast.ts b/src/plugins/vis_types/pie/public/to_ast.ts index 09e00918d47d..3879980bbf85 100644 --- a/src/plugins/vis_types/pie/public/to_ast.ts +++ b/src/plugins/vis_types/pie/public/to_ast.ts @@ -11,11 +11,11 @@ import { buildExpression, buildExpressionFunction } from '../../../expressions/p import { PaletteOutput } from '../../../charts/common'; import { PIE_VIS_EXPRESSION_NAME, - PIE_LABELS_FUNCTION, + PARTITION_LABELS_FUNCTION, PieVisExpressionFunctionDefinition, - PieVisParams, + PartitionVisParams, LabelsParams, -} from '../../../chart_expressions/expression_pie/common'; +} from '../../../chart_expressions/expression_partition_vis/common'; import { getEsaggsFn } from './to_ast_esaggs'; const prepareDimension = (params: SchemaConfig) => { @@ -37,9 +37,9 @@ const preparePalette = (palette?: PaletteOutput) => { }; const prepareLabels = (params: LabelsParams) => { - const pieLabels = buildExpressionFunction(PIE_LABELS_FUNCTION, { + const pieLabels = buildExpressionFunction(PARTITION_LABELS_FUNCTION, { show: params.show, - lastLevel: params.last_level, + last_level: params.last_level, values: params.values, truncate: params.truncate, }); @@ -55,18 +55,18 @@ const prepareLabels = (params: LabelsParams) => { return buildExpression([pieLabels]); }; -export const toExpressionAst: VisToExpressionAst = async (vis, params) => { +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const schemas = getVisSchemas(vis, params); const args = { // explicitly pass each param to prevent extra values trapping addTooltip: vis.params.addTooltip, - addLegend: vis.params.addLegend, + legendDisplay: vis.params.legendDisplay, legendPosition: vis.params.legendPosition, - nestedLegend: vis.params?.nestedLegend, + nestedLegend: vis.params?.nestedLegend ?? false, truncateLegend: vis.params.truncateLegend, maxLegendLines: vis.params.maxLegendLines, distinctColors: vis.params?.distinctColors, - isDonut: vis.params.isDonut, + isDonut: vis.params.isDonut ?? false, emptySizeRatio: vis.params.emptySizeRatio, palette: preparePalette(vis.params?.palette), labels: prepareLabels(vis.params.labels), @@ -74,6 +74,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, par buckets: schemas.segment?.map(prepareDimension), splitColumn: schemas.split_column?.map(prepareDimension), splitRow: schemas.split_row?.map(prepareDimension), + startFromSecondLargestSlice: false, }; const visTypePie = buildExpressionFunction( diff --git a/src/plugins/vis_types/pie/public/to_ast_esaggs.ts b/src/plugins/vis_types/pie/public/to_ast_esaggs.ts index 41eddedd6fa2..3f41a59e3aa8 100644 --- a/src/plugins/vis_types/pie/public/to_ast_esaggs.ts +++ b/src/plugins/vis_types/pie/public/to_ast_esaggs.ts @@ -12,13 +12,13 @@ import { EsaggsExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition, } from '../../../data/public'; -import { PieVisParams } from '../../../chart_expressions/expression_pie/common'; +import { PartitionVisParams } from '../../../chart_expressions/expression_partition_vis/common'; /** * Get esaggs expressions function * @param vis */ -export function getEsaggsFn(vis: Vis) { +export function getEsaggsFn(vis: Vis) { return buildExpressionFunction('esaggs', { index: buildExpression([ buildExpressionFunction('indexPatternLoad', { diff --git a/src/plugins/vis_types/pie/public/vis_type/pie.ts b/src/plugins/vis_types/pie/public/vis_type/pie.ts index 827b25d541c9..c9cc573e2778 100644 --- a/src/plugins/vis_types/pie/public/vis_type/pie.ts +++ b/src/plugins/vis_types/pie/public/vis_type/pie.ts @@ -13,11 +13,12 @@ import { VIS_EVENT_TO_TRIGGER, VisTypeDefinition } from '../../../../visualizati import { DEFAULT_PERCENT_DECIMALS } from '../../common'; import { PieTypeProps } from '../types'; import { - PieVisParams, + PartitionVisParams, LabelPositions, ValueFormats, EmptySizeRatios, -} from '../../../../chart_expressions/expression_pie/common'; + LegendDisplay, +} from '../../../../chart_expressions/expression_partition_vis/common'; import { toExpressionAst } from '../to_ast'; import { getPieOptions } from '../editor/components'; @@ -25,7 +26,7 @@ export const getPieVisTypeDefinition = ({ showElasticChartsOptions = false, palettes, trackUiMetric, -}: PieTypeProps): VisTypeDefinition => ({ +}: PieTypeProps): VisTypeDefinition => ({ name: 'pie', title: i18n.translate('visTypePie.pie.pieTitle', { defaultMessage: 'Pie' }), icon: 'visPie', @@ -38,7 +39,7 @@ export const getPieVisTypeDefinition = ({ defaults: { type: 'pie', addTooltip: true, - addLegend: !showElasticChartsOptions, + legendDisplay: !showElasticChartsOptions ? LegendDisplay.SHOW : LegendDisplay.HIDE, legendPosition: Position.Right, nestedLegend: false, truncateLegend: true, diff --git a/src/plugins/vis_types/pie/tsconfig.json b/src/plugins/vis_types/pie/tsconfig.json index 9ad4e8efe907..ed052af072f2 100644 --- a/src/plugins/vis_types/pie/tsconfig.json +++ b/src/plugins/vis_types/pie/tsconfig.json @@ -21,6 +21,6 @@ { "path": "../../usage_collection/tsconfig.json" }, { "path": "../../vis_default_editor/tsconfig.json" }, { "path": "../../field_formats/tsconfig.json" }, - { "path": "../../chart_expressions/expression_pie/tsconfig.json" } + { "path": "../../chart_expressions/expression_partition_vis/tsconfig.json" } ] } \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_types/vislib/public/__snapshots__/pie_fn.test.ts.snap index b64366c1ce0f..5c6bc7e8dc5d 100644 --- a/src/plugins/vis_types/vislib/public/__snapshots__/pie_fn.test.ts.snap +++ b/src/plugins/vis_types/vislib/public/__snapshots__/pie_fn.test.ts.snap @@ -6,7 +6,6 @@ Object { "type": "render", "value": Object { "visConfig": Object { - "addLegend": true, "addTooltip": true, "dimensions": Object { "metric": Object { @@ -25,6 +24,7 @@ Object { "truncate": 100, "values": true, }, + "legendDisplay": "show", "legendPosition": "right", "type": "pie", }, diff --git a/src/plugins/vis_types/vislib/public/__snapshots__/to_ast_pie.test.ts.snap b/src/plugins/vis_types/vislib/public/__snapshots__/to_ast_pie.test.ts.snap index b8dc4b31747c..1eedae99ffed 100644 --- a/src/plugins/vis_types/vislib/public/__snapshots__/to_ast_pie.test.ts.snap +++ b/src/plugins/vis_types/vislib/public/__snapshots__/to_ast_pie.test.ts.snap @@ -5,7 +5,7 @@ Object { "addArgument": [Function], "arguments": Object { "visConfig": Array [ - "{\\"type\\":\\"pie\\",\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"right\\",\\"isDonut\\":true,\\"labels\\":{\\"show\\":true,\\"values\\":true,\\"last_level\\":true,\\"truncate\\":100},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{}},\\"buckets\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "{\\"type\\":\\"pie\\",\\"addTooltip\\":true,\\"legendDisplay\\":\\"show\\",\\"legendPosition\\":\\"right\\",\\"isDonut\\":true,\\"labels\\":{\\"show\\":true,\\"values\\":true,\\"last_level\\":true,\\"truncate\\":100},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{}},\\"buckets\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", ], }, "getArgument": [Function], diff --git a/src/plugins/vis_types/vislib/public/pie.ts b/src/plugins/vis_types/vislib/public/pie.ts index 45794776bc99..66d4c326fa47 100644 --- a/src/plugins/vis_types/vislib/public/pie.ts +++ b/src/plugins/vis_types/vislib/public/pie.ts @@ -11,7 +11,13 @@ import { VisTypeDefinition } from '../../../visualizations/public'; import { CommonVislibParams } from './types'; import { toExpressionAst } from './to_ast_pie'; -export interface PieVisParams extends CommonVislibParams { +export enum LegendDisplay { + SHOW = 'show', + HIDE = 'hide', + DEFAULT = 'default', +} + +export type PieVisParams = Omit & { type: 'pie'; isDonut: boolean; labels: { @@ -20,7 +26,8 @@ export interface PieVisParams extends CommonVislibParams { last_level: boolean; truncate: number | null; }; -} + legendDisplay: LegendDisplay; +}; export const pieVisTypeDefinition = { ...pieVisType({}), diff --git a/src/plugins/vis_types/vislib/public/pie_fn.test.ts b/src/plugins/vis_types/vislib/public/pie_fn.test.ts index 9c317f9e72dc..42061397d0ac 100644 --- a/src/plugins/vis_types/vislib/public/pie_fn.test.ts +++ b/src/plugins/vis_types/vislib/public/pie_fn.test.ts @@ -39,7 +39,7 @@ describe('interpreter/functions#pie', () => { const visConfig = { type: 'pie', addTooltip: true, - addLegend: true, + legendDisplay: 'show', legendPosition: 'right', isDonut: true, labels: { diff --git a/src/plugins/vis_types/vislib/public/vis_controller.tsx b/src/plugins/vis_types/vislib/public/vis_controller.tsx index 1e940d23e83d..94a7e819f16f 100644 --- a/src/plugins/vis_types/vislib/public/vis_controller.tsx +++ b/src/plugins/vis_types/vislib/public/vis_controller.tsx @@ -17,7 +17,7 @@ import { IInterpreterRenderHandlers } from '../../../expressions/public'; import { VisTypeVislibCoreSetup } from './plugin'; import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; import { BasicVislibParams } from './types'; -import { PieVisParams } from './pie'; +import { LegendDisplay, PieVisParams } from './pie'; const legendClassName = { top: 'vislib--legend-top', @@ -94,7 +94,7 @@ export const createVislibVisController = ( this.vislibVis.initVisConfig(esResponse, uiState); - if (visParams.addLegend) { + if (this.showLegend(visParams)) { $(this.container) .attr('class', (i, cls) => { return cls.replace(/vislib--legend-\S+/g, ''); @@ -110,7 +110,7 @@ export const createVislibVisController = ( // this is necessary because some visualizations // provide data necessary for the legend only after a render cycle. if ( - visParams.addLegend && + this.showLegend(visParams) && CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) ) { this.unmountLegend?.(); @@ -121,10 +121,11 @@ export const createVislibVisController = ( mountLegend( visData: unknown, - { legendPosition, addLegend }: BasicVislibParams | PieVisParams, + visParams: BasicVislibParams | PieVisParams, fireEvent: IInterpreterRenderHandlers['event'], uiState?: PersistedState ) { + const { legendPosition } = visParams; this.unmountLegend = mountReactNode( )(this.legendEl); @@ -151,5 +152,16 @@ export const createVislibVisController = ( delete this.vislibVis; } } + + showLegend(visParams: BasicVislibParams | PieVisParams) { + if (this.arePieVisParams(visParams)) { + return visParams.legendDisplay === LegendDisplay.SHOW; + } + return visParams.addLegend ?? false; + } + + arePieVisParams(visParams: BasicVislibParams | PieVisParams): visParams is PieVisParams { + return Object.values(LegendDisplay).includes((visParams as PieVisParams).legendDisplay); + } }; }; diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/pie_chart.test.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/pie_chart.test.js index ba11df923b8a..6705875ce140 100644 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/pie_chart.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/pie_chart.test.js @@ -32,7 +32,7 @@ describe('No global chart settings', function () { const vislibParams1 = { el: '
', type: 'pie', - addLegend: true, + legendDisplay: 'show', addTooltip: true, }; let chart1; @@ -144,7 +144,7 @@ describe('Vislib PieChart Class Test Suite', function () { const vislibParams = { type: 'pie', - addLegend: true, + legendDisplay: 'show', addTooltip: true, }; let vis; diff --git a/src/plugins/visualizations/server/embeddable/make_visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/make_visualize_embeddable_factory.ts index fa41cf6354d6..4b72cc0320c1 100644 --- a/src/plugins/visualizations/server/embeddable/make_visualize_embeddable_factory.ts +++ b/src/plugins/visualizations/server/embeddable/make_visualize_embeddable_factory.ts @@ -25,6 +25,7 @@ import { commonAddDropLastBucketIntoTSVBModel, commonAddDropLastBucketIntoTSVBModel714Above, commonRemoveMarkdownLessFromTSVB, + commonUpdatePieVisApi, } from '../migrations/visualization_common_migrations'; import { SerializedVis } from '../../common'; @@ -91,6 +92,11 @@ const byValueRemoveMarkdownLessFromTSVB = (state: SerializableRecord) => { }; }; +const byValueUpdatePieVisApi = (state: SerializableRecord) => ({ + ...state, + savedVis: commonUpdatePieVisApi(state.savedVis), +}); + const getEmbeddedVisualizationSearchSourceMigrations = ( searchSourceMigrations: MigrateFunctionsObject ) => @@ -137,6 +143,7 @@ export const makeVisualizeEmbeddableFactory = )(state), '7.17.0': (state) => flow(byValueAddDropLastBucketIntoTSVBModel714Above)(state), '8.0.0': (state) => flow(byValueRemoveMarkdownLessFromTSVB)(state), + '8.1.0': (state) => flow(byValueUpdatePieVisApi)(state), } ), }; diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts index 65e61c4cd81a..aec452e356ab 100644 --- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts @@ -199,3 +199,19 @@ export const commonRemoveMarkdownLessFromTSVB = (visState: any) => { return visState; }; + +export const commonUpdatePieVisApi = (visState: any) => { + if (visState && visState.type === 'pie') { + const { addLegend, ...restParams } = visState.params; + + return { + ...visState, + params: { + ...restParams, + legendDisplay: addLegend ? 'show' : 'hide', + }, + }; + } + + return visState; +}; diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 093990eab858..083e1ae51575 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2469,4 +2469,74 @@ describe('migration visualization', () => { }, }); }); + + describe('8.1.0 pie - labels and addLegend migration', () => { + const getDoc = (addLegend: boolean, lastLevel: boolean = false) => ({ + attributes: { + title: 'Pie Vis', + description: 'Pie vis', + visState: JSON.stringify({ + type: 'pie', + title: 'Pie vis', + params: { + addLegend, + addTooltip: true, + isDonut: true, + labels: { + position: 'default', + show: true, + truncate: 100, + values: true, + valuesFormat: 'percent', + percentDecimals: 2, + last_level: lastLevel, + }, + legendPosition: 'right', + nestedLegend: false, + maxLegendLines: 1, + truncateLegend: true, + distinctColors: false, + palette: { + name: 'default', + type: 'palette', + }, + dimensions: { + metric: { + type: 'vis_dimension', + accessor: 1, + format: { + id: 'number', + params: { + id: 'number', + }, + }, + }, + buckets: [], + }, + }, + }), + }, + }); + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['8.1.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + it('should migrate addLegend to legendDisplay', () => { + const pie = getDoc(true); + const migrated = migrate(pie); + const params = JSON.parse(migrated.attributes.visState).params; + + expect(params.legendDisplay).toBe('show'); + expect(params.addLegend).toBeUndefined(); + + const otherPie = getDoc(false); + const otherMigrated = migrate(otherPie); + const otherParams = JSON.parse(otherMigrated.attributes.visState).params; + + expect(otherParams.legendDisplay).toBe('hide'); + expect(otherParams.addLegend).toBeUndefined(); + }); + }); }); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index d168e82f6973..4855b2589bed 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -26,6 +26,7 @@ import { commonAddDropLastBucketIntoTSVBModel, commonAddDropLastBucketIntoTSVBModel714Above, commonRemoveMarkdownLessFromTSVB, + commonUpdatePieVisApi, } from './visualization_common_migrations'; import { VisualizationSavedObjectAttributes } from '../../common'; @@ -1132,6 +1133,30 @@ export const removeMarkdownLessFromTSVB: SavedObjectMigrationFn = (doc return doc; }; +export const updatePieVisApi: SavedObjectMigrationFn = (doc) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + + const newVisState = commonUpdatePieVisApi(visState); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; + } + + return doc; +}; + const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -1187,6 +1212,7 @@ const visualizationSavedObjectTypeMigrations = { ), '7.17.0': flow(addDropLastBucketIntoTSVBModel714Above), '8.0.0': flow(removeMarkdownLessFromTSVB), + '8.1.0': flow(updatePieVisApi), }; /** diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2dd515f42ff3..43db19704dcf 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2956,6 +2956,35 @@ "expressionTagcloud.functions.tagcloudHelpText": "Tagcloudのビジュアライゼーションです。", "expressionTagcloud.renderer.tagcloud.displayName": "Tag Cloudのビジュアライゼーションです", "expressionTagcloud.renderer.tagcloud.helpDescription": "Tag Cloudを表示", + "expressionPartitionVis.reusable.function.dimension.buckets": "スライス", + "expressionPartitionVis.reusable.function.args.legendDisplayHelpText": "グラフ凡例を表示", + "expressionPartitionVis.reusable.function.args.addTooltipHelpText": "スライスにカーソルを置いたときにツールチップを表示", + "expressionPartitionVis.reusable.function.args.bucketsHelpText": "バケットディメンション構成", + "expressionPartitionVis.pieVis.function.args.distinctColorsHelpText": "スライスごとに異なる色をマッピングします。同じ値のスライスは同じ色になります", + "expressionPartitionVis.reusable.function.args.isDonutHelpText": "円グラフをドーナツグラフとして表示します", + "expressionPartitionVis.reusable.function.args.labelsHelpText": "円グラフラベル構成", + "expressionPartitionVis.reusable.function.args.legendPositionHelpText": "グラフの上、下、左、右に凡例を配置", + "expressionPartitionVis.reusable.function.args.maxLegendLinesHelpText": "凡例項目ごとの行数を定義します", + "expressionPartitionVis.reusable.function.args.metricHelpText": "メトリックディメンション構成", + "expressionPartitionVis.reusable.function.args.nestedLegendHelpText": "詳細凡例を表示", + "expressionPartitionVis.reusable.function.args.paletteHelpText": "グラフパレット名を定義します", + "expressionPartitionVis.reusable.function.args.splitColumnHelpText": "列ディメンション構成で分割", + "expressionPartitionVis.reusable.function.args.splitRowHelpText": "行ディメンション構成で分割", + "expressionPartitionVis.reusable.function.args.truncateLegendHelpText": "凡例項目が切り捨てられるかどうかを定義します", + "expressionPartitionVis.reusable.function.dimension.metric": "スライスサイズ", + "expressionPartitionVis.reusable.function.dimension.splitcolumn": "列分割", + "expressionPartitionVis.reusable.function.dimension.splitrow": "行分割", + "expressionPartitionVis.partitionLabels.function.help": "円グラフラベルオブジェクトを生成します", + "expressionPartitionVis.partitionLabels.function.args.percentDecimals.help": "割合として値に表示される10進数を定義します", + "expressionPartitionVis.partitionLabels.function.args.position.help": "ラベル位置を定義します", + "expressionPartitionVis.partitionLabels.function.args.values.help": "スライス内の値を定義します", + "expressionPartitionVis.partitionLabels.function.args.valuesFormat.help": "値の形式を定義します", + "expressionPartitionVis.pieVis.function.help": "パイビジュアライゼーション", + "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "値でフィルター", + "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", + "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "値を除外", + "expressionPartitionVis.negativeValuesFound": "円/ドーナツグラフは負の値では表示できません。", + "expressionPartitionVis.noResultsFoundTitle": "結果が見つかりませんでした", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", "fieldFormats.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト{numeralFormatLink}です", "fieldFormats.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", @@ -5175,7 +5204,6 @@ "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name": "円グラフのレガシーグラフライブラリ", "visTypePie.controls.truncateLabel": "切り捨て", "visTypePie.controls.truncateTooltip": "グラフ外に配置されたラベルの文字数。", - "visTypePie.editors.pie.addLegendLabel": "凡例を表示", "visTypePie.editors.pie.decimalSliderLabel": "割合の最大小数点桁数", "visTypePie.editors.pie.distinctColorsLabel": "スライスごとに異なる色を使用", "visTypePie.editors.pie.donutLabel": "ドーナッツ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7fa49ec24716..d299a58c3bdb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -923,255 +923,8 @@ "xpack.lens.xyVisualization.stackedPercentageBarLabel": "垂直百分比条形图", "xpack.lens.xyVisualization.xyLabel": "XY", "advancedSettings.advancedSettingsLabel": "高级设置", - "advancedSettings.badge.readOnly.text": "只读", - "advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", - "advancedSettings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", - "advancedSettings.callOutCautionTitle": "注意:在这里您可能会使问题出现", - "advancedSettings.categoryNames.dashboardLabel": "仪表板", - "advancedSettings.categoryNames.discoverLabel": "Discover", - "advancedSettings.categoryNames.generalLabel": "常规", - "advancedSettings.categoryNames.machineLearningLabel": "Machine Learning", - "advancedSettings.categoryNames.notificationsLabel": "通知", - "advancedSettings.categoryNames.observabilityLabel": "Observability", - "advancedSettings.categoryNames.reportingLabel": "报告", - "advancedSettings.categoryNames.searchLabel": "搜索", - "advancedSettings.categoryNames.securitySolutionLabel": "安全解决方案", - "advancedSettings.categoryNames.timelionLabel": "Timelion", - "advancedSettings.categoryNames.visualizationsLabel": "可视化", - "advancedSettings.categorySearchLabel": "类别", - "advancedSettings.featureCatalogueTitle": "定制您的 Kibana 体验 — 更改日期格式、打开深色模式,等等。", - "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", - "advancedSettings.field.changeImageLinkText": "更改图片", - "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", - "advancedSettings.field.customSettingAriaLabel": "定制设置", - "advancedSettings.field.customSettingTooltip": "定制设置", - "advancedSettings.field.defaultValueText": "默认值:{value}", - "advancedSettings.field.defaultValueTypeJsonText": "默认值:{value}", - "advancedSettings.field.deprecationClickAreaLabel": "单击以查看 {settingName} 的过时文档。", - "advancedSettings.field.helpText": "此设置已由 Kibana 服务器覆盖,无法更改。", - "advancedSettings.field.imageChangeErrorMessage": "图片无法保存", - "advancedSettings.field.invalidIconLabel": "无效", - "advancedSettings.field.offLabel": "关闭", - "advancedSettings.field.onLabel": "开启", - "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", - "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", - "advancedSettings.field.settingIsUnsaved": "设备当前未保存。", - "advancedSettings.field.unsavedIconLabel": "未保存", - "advancedSettings.form.cancelButtonLabel": "取消更改", - "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", - "advancedSettings.form.clearSearchResultText": "(清除搜索)", - "advancedSettings.form.countOfSettingsChanged": "{unsavedCount} 个未保存{unsavedCount, plural, other {设置} }{hiddenCount, plural, =0 {} other {,# 个已隐藏} }", - "advancedSettings.form.noSearchResultText": "找不到 {queryText} {clearSearch} 的设置", - "advancedSettings.form.requiresPageReloadToastButtonLabel": "重新加载页面", - "advancedSettings.form.requiresPageReloadToastDescription": "一个或多个设置需要您重新加载页面才能生效。", - "advancedSettings.form.saveButtonLabel": "保存更改", - "advancedSettings.form.saveButtonTooltipWithInvalidChanges": "保存前请修复无效的设置。", - "advancedSettings.form.saveErrorMessage": "无法保存", - "advancedSettings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", - "advancedSettings.pageTitle": "设置", - "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", - "advancedSettings.searchBarAriaLabel": "搜索高级设置", - "advancedSettings.voiceAnnouncement.ariaLabel": "“高级设置”的结果信息", - "advancedSettings.voiceAnnouncement.noSearchResultScreenReaderMessage": "{sectionLenght, plural, other {# 个部分}}中有 {optionLenght, plural, other {# 个选项}}", - "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您搜索了“{query}”。{sectionLenght, plural, other {# 个部分}}中有 {optionLenght, plural, other {# 个选项}}", - "alerts.documentationTitle": "查看文档", - "alerts.noPermissionsMessage": "要查看告警,必须对 Kibana 工作区中的告警功能有权限。有关详细信息,请联系您的 Kibana 管理员。", - "alerts.noPermissionsTitle": "需要 Kibana 功能权限", - "autocomplete.fieldRequiredError": "值不能为空", - "autocomplete.invalidDateError": "不是有效日期", - "autocomplete.invalidNumberError": "不是有效数字", - "autocomplete.loadingDescription": "正在加载……", - "autocomplete.selectField": "请首先选择字段......", - "bfetch.disableBfetchCompression": "禁用批量压缩", - "bfetch.disableBfetchCompressionDesc": "禁用批量压缩。这允许您对单个请求进行故障排查,但会增加响应大小。", - "charts.advancedSettings.visualization.colorMappingText": "使用兼容性调色板将值映射到图表中的特定颜色。", - "charts.advancedSettings.visualization.colorMappingTextDeprecation": "此设置已过时,在未来版本中将不受支持。", - "charts.advancedSettings.visualization.colorMappingTitle": "颜色映射", "charts.advancedSettings.visualization.useLegacyTimeAxis.description": "在 Lens、Discover、Visualize 和 TSVB 中为图表启用旧版时间轴", "charts.advancedSettings.visualization.useLegacyTimeAxis.name": "旧版图表时间轴", - "charts.colormaps.bluesText": "蓝色", - "charts.colormaps.greensText": "绿色", - "charts.colormaps.greenToRedText": "绿到红", - "charts.colormaps.greysText": "灰色", - "charts.colormaps.redsText": "红色", - "charts.colormaps.yellowToRedText": "黄到红", - "charts.colorPicker.clearColor": "重置颜色", - "charts.colorPicker.setColor.screenReaderDescription": "为值 {legendDataLabel} 设置颜色", - "charts.countText": "计数", - "charts.functions.palette.args.colorHelpText": "调色板颜色。接受 {html} 颜色名称 {hex}、{hsl}、{hsla}、{rgb} 或 {rgba}。", - "charts.functions.palette.args.gradientHelpText": "受支持时提供渐变的调色板?", - "charts.functions.palette.args.reverseHelpText": "反转调色板?", - "charts.functions.palette.args.stopHelpText": "调色板颜色停止。使用时,必须与每个颜色关联。", - "charts.functions.paletteHelpText": "创建颜色调色板。", - "charts.functions.systemPalette.args.nameHelpText": "调色板列表中的调色板名称", - "charts.functions.systemPaletteHelpText": "创建动态颜色调色板。", - "charts.legend.toggleLegendButtonAriaLabel": "切换图例", - "charts.legend.toggleLegendButtonTitle": "切换图例", - "charts.palettes.complimentaryLabel": "免费", - "charts.palettes.coolLabel": "冷", - "charts.palettes.customLabel": "定制", - "charts.palettes.defaultPaletteLabel": "默认", - "charts.palettes.grayLabel": "灰", - "charts.palettes.kibanaPaletteLabel": "兼容性", - "charts.palettes.negativeLabel": "负", - "charts.palettes.positiveLabel": "正", - "charts.palettes.statusLabel": "状态", - "charts.palettes.temperatureLabel": "温度", - "charts.palettes.warmLabel": "暖", - "charts.partialData.bucketTooltipText": "选定的时间范围不包括此整个存储桶。其可能包含部分数据。", - "console.autocomplete.addMethodMetaText": "方法", - "console.consoleDisplayName": "控制台", - "console.consoleMenu.copyAsCurlFailedMessage": "无法将请求复制为 cURL", - "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", - "console.deprecations.enabled.manualStepOneMessage": "打开 kibana.yml 配置文件。", - "console.deprecations.enabled.manualStepTwoMessage": "将“console.enabled”设置更改为“console.ui.enabled”。", - "console.deprecations.enabledMessage": "要禁止用户访问 Console UI,请使用“console.ui.enabled”设置,而不是“console.enabled”。", - "console.deprecations.enabledTitle": "“console.enabled”设置已过时", - "console.deprecations.proxyConfig.manualStepOneMessage": "打开 kibana.yml 配置文件。", - "console.deprecations.proxyConfig.manualStepThreeMessage": "使用“server.ssl.*”设置配置 Kibana 与 Elasticsearch 之间的安全连接。", - "console.deprecations.proxyConfig.manualStepTwoMessage": "移除“console.proxyConfig”设置。", - "console.deprecations.proxyConfigMessage": "配置“console.proxyConfig”已过时,将在 8.0.0 中移除。为保护 Kibana 与 Elasticsearch 之间的连接,请改为使用标准“server.ssl.*”设置。", - "console.deprecations.proxyConfigTitle": "“console.proxyConfig”设置已过时", - "console.deprecations.proxyFilter.manualStepOneMessage": "打开 kibana.yml 配置文件。", - "console.deprecations.proxyFilter.manualStepThreeMessage": "使用“server.ssl.*”设置配置 Kibana 与 Elasticsearch 之间的安全连接。", - "console.deprecations.proxyFilter.manualStepTwoMessage": "移除“console.proxyFilter”设置。", - "console.deprecations.proxyFilterMessage": "配置“console.proxyFilter”已过时,将在 8.0.0 中移除。为保护 Kibana 与 Elasticsearch 之间的连接,请改为使用标准“server.ssl.*”设置。", - "console.deprecations.proxyFilterTitle": "“console.proxyFilter”设置已过时", - "console.devToolsDescription": "跳过 cURL 并使用 JSON 接口在控制台中处理您的数据。", - "console.devToolsTitle": "与 Elasticsearch API 进行交互", - "console.exampleOutputTextarea": "开发工具控制台编辑器示例", - "console.helpPage.keyboardCommands.autoIndentDescription": "自动缩进当前请求", - "console.helpPage.keyboardCommands.closeAutoCompleteMenuDescription": "关闭自动完成菜单", - "console.helpPage.keyboardCommands.collapseAllScopesDescription": "折叠当前范围除外的所有范围。通过加按 Shift 键来展开。", - "console.helpPage.keyboardCommands.collapseExpandCurrentScopeDescription": "折叠/展开当前范围。", - "console.helpPage.keyboardCommands.jumpToPreviousNextRequestDescription": "跳转至前一/后一请求开头或结尾。", - "console.helpPage.keyboardCommands.openAutoCompleteDescription": "打开自动完成(即使未键入)", - "console.helpPage.keyboardCommands.openDocumentationDescription": "打开当前请求的文档", - "console.helpPage.keyboardCommands.selectCurrentlySelectedInAutoCompleteMenuDescription": "选择自动完成菜单中当前选定的词或最顶部的词", - "console.helpPage.keyboardCommands.submitRequestDescription": "提交请求", - "console.helpPage.keyboardCommands.switchFocusToAutoCompleteMenuDescription": "将焦点切换到自动完成菜单。使用箭头进一步选择词", - "console.helpPage.keyboardCommandsTitle": "键盘命令", - "console.helpPage.pageTitle": "帮助", - "console.helpPage.requestFormatDescription": "您可以在空白编辑器中键入一个或多个请求。Console 理解紧凑格式的请求:", - "console.helpPage.requestFormatTitle": "请求格式", - "console.historyPage.applyHistoryButtonLabel": "应用", - "console.historyPage.clearHistoryButtonLabel": "清除", - "console.historyPage.closehistoryButtonLabel": "关闭", - "console.historyPage.itemOfRequestListAriaLabel": "请求:{historyItem}", - "console.historyPage.noHistoryTextMessage": "没有可用的历史记录", - "console.historyPage.pageTitle": "历史记录", - "console.historyPage.requestListAriaLabel": "已发送请求的历史记录", - "console.inputTextarea": "开发工具控制台", - "console.loadFromDataUriErrorMessage": "无法从 URL 中的 load_from 查询参数加载数据", - "console.loadingError.buttonLabel": "重新加载控制台", - "console.loadingError.message": "尝试重新加载以获取最新的数据。", - "console.loadingError.title": "无法加载控制台", - "console.notification.error.couldNotSaveRequestTitle": "无法将请求保存到控制台历史记录。", - "console.notification.error.historyQuotaReachedMessage": "请求历史记录已满。请清除控制台历史记录以保存新的请求。", - "console.notification.error.noRequestSelectedTitle": "未选择任何请求。将鼠标置于请求内即可选择。", - "console.notification.error.unknownErrorTitle": "未知请求错误", - "console.outputTextarea": "开发工具控制台输出", - "console.pageHeading": "控制台", - "console.requestInProgressBadgeText": "进行中的请求", - "console.requestOptions.autoIndentButtonLabel": "自动缩进", - "console.requestOptions.copyAsUrlButtonLabel": "复制为 cURL", - "console.requestOptions.openDocumentationButtonLabel": "打开文档", - "console.requestOptionsButtonAriaLabel": "请求选项", - "console.requestTimeElapasedBadgeTooltipContent": "已用时间", - "console.sendRequestButtonTooltip": "单击以发送请求", - "console.settingsPage.autocompleteLabel": "自动完成", - "console.settingsPage.cancelButtonLabel": "取消", - "console.settingsPage.fieldsLabelText": "字段", - "console.settingsPage.fontSizeLabel": "字体大小", - "console.settingsPage.indicesAndAliasesLabelText": "索引和别名", - "console.settingsPage.jsonSyntaxLabel": "JSON 语法", - "console.settingsPage.pageTitle": "控制台设置", - "console.settingsPage.refreshButtonLabel": "刷新自动完成建议", - "console.settingsPage.refreshingDataDescription": "控制台通过查询 Elasticsearch 来刷新自动完成建议。如果您的集群较大或您的网络有限制,则自动刷新可能会造成问题。", - "console.settingsPage.refreshingDataLabel": "正在刷新自动完成建议", - "console.settingsPage.saveButtonLabel": "保存", - "console.settingsPage.templatesLabelText": "模板", - "console.settingsPage.tripleQuotesMessage": "在输出窗格中使用三重引号", - "console.settingsPage.wrapLongLinesLabelText": "长行换行", - "console.topNav.helpTabDescription": "帮助", - "console.topNav.helpTabLabel": "帮助", - "console.topNav.historyTabDescription": "历史记录", - "console.topNav.historyTabLabel": "历史记录", - "console.topNav.settingsTabDescription": "设置", - "console.topNav.settingsTabLabel": "设置", - "console.welcomePage.closeButtonLabel": "关闭", - "console.welcomePage.pageTitle": "欢迎使用 Console", - "console.welcomePage.quickIntroDescription": "Console UI 分为两个窗格:编辑器窗格(左)和响应窗格(右)。使用编辑器键入请求并将它们提交到 Elasticsearch。结果将显示在右侧的响应窗格中。", - "console.welcomePage.quickIntroTitle": "UI 简介", - "console.welcomePage.quickTips.cUrlFormatForRequestsDescription": "您可以粘贴 cURL 格式的请求,这些请求将转换成 Console 语法格式。", - "console.welcomePage.quickTips.keyboardShortcutsDescription": "学习“帮助”按钮下的键盘快捷方式。那里有非常实用的信息!", - "console.welcomePage.quickTips.resizeEditorDescription": "您可以通过拖动编辑器和输出窗格之间的分隔条来调整它们的大小。", - "console.welcomePage.quickTips.submitRequestDescription": "使用绿色三角按钮将请求提交到 ES。", - "console.welcomePage.quickTips.useWrenchMenuDescription": "使用扳手菜单执行其他有用的操作。", - "console.welcomePage.quickTipsTitle": "有几个需要您注意的有用提示", - "console.welcomePage.supportedRequestFormatDescription": "键入请求时,控制台将提供建议,您可以通过按 Enter/Tab 键来接受建议。这些建议基于请求结构以及索引和类型进行提供。", - "console.welcomePage.supportedRequestFormatTitle": "Console 理解紧凑格式的请求,类似于 cURL:", - "core.application.appContainer.loadingAriaLabel": "正在加载应用程序", - "core.application.appNotFound.pageDescription": "在此 URL 未找到任何应用程序。尝试返回或从菜单中选择应用。", - "core.application.appNotFound.title": "未找到应用程序", - "core.application.appRenderError.defaultTitle": "应用程序错误", - "core.chrome.browserDeprecationLink": "我们网站上的支持矩阵", - "core.chrome.browserDeprecationWarning": "本软件的未来版本将放弃对 Internet Explorer 的支持,请查看{link}。", - "core.chrome.legacyBrowserWarning": "您的浏览器不满足 Kibana 的安全要求。", - "core.deprecations.deprecations.fetchFailed.manualStepOneMessage": "请在 Kibana 服务器日志中查看错误消息。", - "core.deprecations.deprecations.fetchFailedMessage": "无法提取插件 {domainId} 的弃用信息。", - "core.deprecations.deprecations.fetchFailedTitle": "无法提取 {domainId} 的弃用信息", - "core.deprecations.elasticsearchSSL.manualSteps1": "将“{missingSetting}”设置添加到 kibana.yml。", - "core.deprecations.elasticsearchSSL.manualSteps2": "或者,如果不想使用相互 TLS 身份验证,请从 kibana.yml 中移除“{existingSetting}”。", - "core.deprecations.elasticsearchSSL.message": "同时使用“{existingSetting}”和“{missingSetting}”,以便 Kibana 将相互 TLS 身份验证用于 Elasticsearch。", - "core.deprecations.elasticsearchSSL.title": "使用不含“{missingSetting}”的“{existingSetting}”无效", - "core.deprecations.elasticsearchUsername.manualSteps1": "使用 elasticsearch-service-tokens CLI 工具为“elastic/kibana”服务帐户创建新的服务帐户令牌。", - "core.deprecations.elasticsearchUsername.manualSteps2": "将“elasticsearch.serviceAccountToken”设置添加到 kibana.yml。", - "core.deprecations.elasticsearchUsername.manualSteps3": "从 kibana.yml 中移除“elasticsearch.username”和“elasticsearch.password”。", - "core.deprecations.elasticsearchUsername.message": "Kibana 已配置为通过“{username}”用户验证到 Elasticsearch。改为使用服务帐户令牌。", - "core.deprecations.elasticsearchUsername.title": "使用“elasticsearch.username: {username}”已过时", - "core.deprecations.noCorrectiveAction": "无法自动解决此弃用。", - "core.euiAccordion.isLoading": "正在加载", - "core.euiBasicTable.noItemsMessage": "找不到项目", - "core.euiBasicTable.selectAllRows": "选择所有行", - "core.euiBasicTable.selectThisRow": "选择此行", - "core.euiBasicTable.tableAutoCaptionWithoutPagination": "此表包含 {itemCount} 行。", - "core.euiBasicTable.tableAutoCaptionWithPagination": "此表包含 {itemCount} 行,共有 {totalItemCount} 行;第 {page} 页,共 {pageCount} 页。", - "core.euiBasicTable.tableCaptionWithPagination": "{tableCaption};第 {page} 页,共 {pageCount} 页。", - "core.euiBasicTable.tablePagination": "表分页:{tableCaption}", - "core.euiBasicTable.tableSimpleAutoCaptionWithPagination": "此表包含 {itemCount} 行;第 {page} 页,共 {pageCount} 页。", - "core.euiBottomBar.customScreenReaderAnnouncement": "有称作 {landmarkHeading} 且页面级别控件位于文档结尾的新地区地标。", - "core.euiBottomBar.screenReaderAnnouncement": "有页面级别控件位于文档结尾的新地区地标。", - "core.euiBottomBar.screenReaderHeading": "页面级别控件", - "core.euiBreadcrumbs.collapsedBadge.ariaLabel": "查看折叠的痕迹导航", - "core.euiBreadcrumbs.nav.ariaLabel": "痕迹导航", - "core.euiCardSelect.select": "选择", - "core.euiCardSelect.selected": "已选定", - "core.euiCardSelect.unavailable": "不可用", - "core.euiCodeBlock.copyButton": "复制", - "core.euiCodeBlock.fullscreenCollapse": "折叠", - "core.euiCodeBlock.fullscreenExpand": "展开", - "core.euiCollapsedItemActions.allActions": "所有操作", - "core.euiColorPicker.alphaLabel": "Alpha 通道(不透明度)值", - "core.euiColorPicker.closeLabel": "按向下箭头键可打开包含颜色选项的弹出框", - "core.euiColorPicker.colorErrorMessage": "颜色值无效", - "core.euiColorPicker.colorLabel": "颜色值", - "core.euiColorPicker.openLabel": "按 Esc 键关闭弹出框", - "core.euiColorPicker.popoverLabel": "颜色选择对话框", - "core.euiColorPicker.transparent": "透明", - "core.euiColorPickerSwatch.ariaLabel": "选择 {color} 作为颜色", - "core.euiColorStops.screenReaderAnnouncement": "{label}:{readOnly} {disabled} 颜色停止点选取器。每个停止点由数字和相应颜色值构成。使用向下和向上箭头键选择单个停止点。按 Enter 键创建新的停止点。", - "core.euiColorStopThumb.buttonAriaLabel": "按 Enter 键修改此停止点。按 Esc 键聚焦该组", - "core.euiColorStopThumb.buttonTitle": "单击编辑,拖动重新定位", - "core.euiColorStopThumb.removeLabel": "删除此停止点", - "core.euiColorStopThumb.screenReaderAnnouncement": "打开颜色停止点编辑表单的弹出式窗口。按 Tab 键正向依次选择表单控件或按 Esc 键关闭此弹出式窗口。", - "core.euiColorStopThumb.stopErrorMessage": "值超出范围", - "core.euiColorStopThumb.stopLabel": "停止点值", - "core.euiColumnActions.hideColumn": "隐藏列", - "core.euiColumnActions.moveLeft": "左移", - "core.euiColumnActions.moveRight": "右移", - "core.euiColumnActions.sort": "排序 {schemaLabel}", - "core.euiColumnSelector.button": "列", "core.euiColumnSelector.buttonActivePlural": "{numberOfHiddenFields} 列已隐藏", "core.euiColumnSelector.buttonActiveSingular": "{numberOfHiddenFields} 列已隐藏", "core.euiColumnSelector.hideAll": "全部隐藏", @@ -2987,6 +2740,35 @@ "expressionTagcloud.functions.tagcloudHelpText": "标签云图可视化。", "expressionTagcloud.renderer.tagcloud.displayName": "标签云图可视化", "expressionTagcloud.renderer.tagcloud.helpDescription": "呈现标签云图", + "expressionPartitionVis.reusable.function.dimension.buckets": "切片", + "expressionPartitionVis.reusable.function.args.legendDisplayHelpText": "显示图表图例", + "expressionPartitionVis.reusable.function.args.addTooltipHelpText": "在切片上悬浮时显示工具提示", + "expressionPartitionVis.reusable.function.args.bucketsHelpText": "存储桶维度配置", + "expressionPartitionVis.pieVis.function.args.distinctColorsHelpText": "每个切片映射不同颜色。具有相同值的切片具有相同的颜色", + "expressionPartitionVis.reusable.function.args.isDonutHelpText": "将饼图显示为圆环图", + "expressionPartitionVis.reusable.function.args.labelsHelpText": "饼图标签配置", + "expressionPartitionVis.reusable.function.args.legendPositionHelpText": "将图例定位于图表的顶部、底部、左侧、右侧", + "expressionPartitionVis.reusable.function.args.maxLegendLinesHelpText": "定义每个图例项的行数", + "expressionPartitionVis.reusable.function.args.metricHelpText": "指标维度配置", + "expressionPartitionVis.reusable.function.args.nestedLegendHelpText": "显示更详细的图例", + "expressionPartitionVis.reusable.function.args.paletteHelpText": "定义图表调色板名称", + "expressionPartitionVis.reusable.function.args.splitColumnHelpText": "按列维度配置拆分", + "expressionPartitionVis.reusable.function.args.splitRowHelpText": "按行维度配置拆分", + "expressionPartitionVis.reusable.function.args.truncateLegendHelpText": "定义是否将截断图例项", + "expressionPartitionVis.reusable.function.dimension.metric": "切片大小", + "expressionPartitionVis.reusable.function.dimension.splitcolumn": "列拆分", + "expressionPartitionVis.reusable.function.dimension.splitrow": "行拆分", + "expressionPartitionVis.partitionLabels.function.help": "生成饼图标签对象", + "expressionPartitionVis.partitionLabels.function.args.percentDecimals.help": "定义在值中将显示为百分比的小数位数", + "expressionPartitionVis.partitionLabels.function.args.position.help": "定义标签位置", + "expressionPartitionVis.partitionLabels.function.args.values.help": "定义切片内的值", + "expressionPartitionVis.partitionLabels.function.args.valuesFormat.help": "定义值的格式", + "expressionPartitionVis.pieVis.function.help": "饼图可视化", + "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "筛留值", + "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", + "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "筛除值", + "expressionPartitionVis.negativeValuesFound": "饼图/圆环图无法使用负值进行呈现。", + "expressionPartitionVis.noResultsFoundTitle": "找不到结果", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", "fieldFormats.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", "fieldFormats.advancedSettings.format.bytesFormatTitle": "字节格式", @@ -5222,7 +5004,6 @@ "visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name": "饼图旧版图表库", "visTypePie.controls.truncateLabel": "截断", "visTypePie.controls.truncateTooltip": "标签位于图表之外的字符数。", - "visTypePie.editors.pie.addLegendLabel": "显示图例", "visTypePie.editors.pie.decimalSliderLabel": "百分比的最大小数位数", "visTypePie.editors.pie.distinctColorsLabel": "每个切片使用不同的颜色", "visTypePie.editors.pie.donutLabel": "圆环图",