diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 6c695cd3a74a9..de23c3a9962e0 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -172,6 +172,20 @@ Compare your real-time data to the results that are offset by a time increment. For a time shift example, refer to <>. +[float] +[[multi-metric-partition-chart]] +==== Build a partition chart from multiple metrics + +By default, partition charts (e.g. pie) are built from one or more "slice-by" dimensions to define the partitions and a single metric dimension to define their size. However, you can also build a partition chart from multiple metric dimensions. + +. Open the layer context menu at the top right of the layer panel. + +. Click *Layer settings*. + +. Click the switch labeled *Multiple metrics*. + +Note: this option is not available for mosaic charts. + [float] [[add-annotations]] ==== Add annotations 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 index 859f644454169..c00de511b8afb 100644 --- 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 @@ -89,14 +89,16 @@ Object { "type": "vis_dimension", }, ], - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], "splitColumn": undefined, "splitRow": undefined, }, @@ -122,6 +124,7 @@ Object { }, "type": "vis_dimension", }, + "metricsToLabels": Object {}, "nestedLegend": true, "palette": Object { "name": "kibana_palette", 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 index 3fd9966e7524e..65cd755d51a07 100644 --- 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 @@ -87,14 +87,16 @@ Object { "type": "vis_dimension", }, ], - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], "splitColumn": undefined, "splitRow": undefined, }, @@ -115,14 +117,17 @@ Object { "legendPosition": "right", "legendSize": "small", "maxLegendLines": 2, - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], + "metricsToLabels": Object {}, "nestedLegend": true, "palette": Object { "name": "kibana_palette", @@ -222,14 +227,16 @@ Object { "type": "vis_dimension", }, ], - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], "splitColumn": undefined, "splitRow": undefined, }, @@ -250,14 +257,17 @@ Object { "legendPosition": "right", "legendSize": "small", "maxLegendLines": 2, - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], + "metricsToLabels": Object {}, "nestedLegend": true, "palette": Object { "name": "kibana_palette", 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 index ef1c7be526670..5388a47242fb4 100644 --- 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 @@ -89,14 +89,16 @@ Object { "type": "vis_dimension", }, ], - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], "splitColumn": undefined, "splitRow": undefined, }, @@ -114,14 +116,17 @@ Object { "legendPosition": "right", "legendSize": "medium", "maxLegendLines": 2, - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], + "metricsToLabels": Object {}, "nestedLegend": true, "palette": Object { "name": "kibana_palette", 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 index 9cdc69904460a..180c3221240ce 100644 --- 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 @@ -63,14 +63,16 @@ Object { "type": "vis_dimension", }, ], - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], "splitColumn": undefined, "splitRow": undefined, }, @@ -88,14 +90,17 @@ Object { "legendPosition": "right", "legendSize": "medium", "maxLegendLines": 2, - "metric": Object { - "accessor": 0, - "format": Object { - "id": "number", - "params": Object {}, + "metrics": Array [ + Object { + "accessor": 0, + "format": Object { + "id": "number", + "params": Object {}, + }, + "type": "vis_dimension", }, - "type": "vis_dimension", - }, + ], + "metricsToLabels": Object {}, "palette": Object { "name": "kibana_palette", "type": "system_palette", 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 index ec4357c269f37..b312de7bf1583 100644 --- 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 @@ -17,6 +17,10 @@ export const strings = { i18n.translate('expressionPartitionVis.reusable.function.args.metricHelpText', { defaultMessage: 'Metric dimensions config', }), + getMetricToLabelHelp: () => + i18n.translate('expressionPartitionVis.metricToLabel.help', { + defaultMessage: 'JSON key-value pairs of column ID to label', + }), getBucketsArgHelp: () => i18n.translate('expressionPartitionVis.reusable.function.args.bucketsHelpText', { defaultMessage: 'Buckets dimensions config', 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 index ae3f17ff8df3a..75f2aa3c17dc1 100644 --- 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 @@ -130,13 +130,14 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ const visConfig: PartitionVisParams = { ...args, + metricsToLabels: {}, ariaLabel: args.ariaLabel ?? (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { - metric: args.metric, + metrics: [args.metric], buckets: args.buckets, splitColumn: args.splitColumn, splitRow: args.splitRow, diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts index 9a18a348be16f..0c222758d912a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts @@ -30,6 +30,7 @@ describe('interpreter/functions#pieVis', () => { const visConfig: PieVisConfig = { addTooltip: true, + metricsToLabels: JSON.stringify({}), legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', legendSize: LegendSize.SMALL, @@ -53,14 +54,16 @@ describe('interpreter/functions#pieVis', () => { truncate: 100, last_level: false, }, - metric: { - type: 'vis_dimension', - accessor: 0, - format: { - id: 'number', - params: {}, + metrics: [ + { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: {}, + }, }, - }, + ], buckets: [ { type: 'vis_dimension', 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 index 119d45f579ebf..4bf2ead1b9c52 100644 --- 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 @@ -25,10 +25,15 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ inputTypes: ['datatable'], help: strings.getPieVisFunctionName(), args: { - metric: { + metrics: { types: ['vis_dimension', 'string'], help: strings.getMetricArgHelp(), required: true, + multi: true, + }, + metricsToLabels: { + types: ['string'], + help: strings.getMetricToLabelHelp(), }, buckets: { types: ['vis_dimension', 'string'], @@ -137,9 +142,10 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } - validateAccessor(args.metric, context.columns); + args.metrics.forEach((accessor) => validateAccessor(accessor, context.columns)); + if (args.buckets) { - args.buckets.forEach((bucket) => validateAccessor(bucket, context.columns)); + args.buckets.forEach((accessor) => validateAccessor(accessor, context.columns)); } if (args.splitColumn) { args.splitColumn.forEach((splitColumn) => validateAccessor(splitColumn, context.columns)); @@ -150,13 +156,14 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ const visConfig: PartitionVisParams = { ...args, + metricsToLabels: args.metricsToLabels ? JSON.parse(args.metricsToLabels) : {}, ariaLabel: args.ariaLabel ?? (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { - metric: args.metric, + metrics: args.metrics, buckets: args.buckets, splitColumn: args.splitColumn, splitRow: args.splitRow, @@ -170,7 +177,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ const logTable = prepareLogTable( context, [ - [[args.metric], strings.getSliceSizeHelp()], + [args.metrics, strings.getSliceSizeHelp()], [args.buckets, strings.getSliceHelp()], [args.splitColumn, strings.getColumnSplitHelp()], [args.splitRow, strings.getRowSplitHelp()], 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 index 14aaea3a0cf5e..e5bc4115c1461 100644 --- 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 @@ -34,6 +34,7 @@ describe('interpreter/functions#treemapVis', () => { const visConfig: TreemapVisConfig = { addTooltip: true, + metricsToLabels: JSON.stringify({}), legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', nestedLegend: true, @@ -53,14 +54,16 @@ describe('interpreter/functions#treemapVis', () => { truncate: 100, last_level: false, }, - metric: { - type: 'vis_dimension', - accessor: 0, - format: { - id: 'number', - params: {}, + metrics: [ + { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: {}, + }, }, - }, + ], buckets: [ { type: 'vis_dimension', 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 index 427179ca5a25a..d5f91b1f0e1d3 100644 --- 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 @@ -25,10 +25,15 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => inputTypes: ['datatable'], help: strings.getPieVisFunctionName(), args: { - metric: { + metrics: { types: ['vis_dimension'], help: strings.getMetricArgHelp(), required: true, + multi: true, + }, + metricsToLabels: { + types: ['string'], + help: strings.getMetricToLabelHelp(), }, buckets: { types: ['vis_dimension'], @@ -117,7 +122,8 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } - validateAccessor(args.metric, context.columns); + args.metrics.forEach((accessor) => validateAccessor(accessor, context.columns)); + if (args.buckets) { args.buckets.forEach((bucket) => validateAccessor(bucket, context.columns)); } @@ -130,13 +136,14 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => const visConfig: PartitionVisParams = { ...args, + metricsToLabels: args.metricsToLabels ? JSON.parse(args.metricsToLabels) : {}, ariaLabel: args.ariaLabel ?? (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { - metric: args.metric, + metrics: args.metrics, buckets: args.buckets, splitColumn: args.splitColumn, splitRow: args.splitRow, @@ -150,7 +157,7 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => const logTable = prepareLogTable( context, [ - [[args.metric], strings.getSliceSizeHelp()], + [args.metrics, strings.getSliceSizeHelp()], [args.buckets, strings.getSliceHelp()], [args.splitColumn, strings.getColumnSplitHelp()], [args.splitRow, strings.getRowSplitHelp()], 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 index 608c40b501066..4c81f64428a74 100644 --- 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 @@ -35,6 +35,7 @@ describe('interpreter/functions#waffleVis', () => { const visConfig: WaffleVisConfig = { addTooltip: true, showValuesInLegend: true, + metricsToLabels: JSON.stringify({}), legendDisplay: LegendDisplay.SHOW, legendPosition: 'right', truncateLegend: true, @@ -53,14 +54,16 @@ describe('interpreter/functions#waffleVis', () => { truncate: 100, last_level: false, }, - metric: { - type: 'vis_dimension', - accessor: 0, - format: { - id: 'number', - params: {}, + metrics: [ + { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'number', + params: {}, + }, }, - }, + ], bucket: { type: 'vis_dimension', accessor: 1, 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 index 0867e6cb9bd76..1568454b86eb2 100644 --- 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 @@ -25,10 +25,15 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ inputTypes: ['datatable'], help: strings.getPieVisFunctionName(), args: { - metric: { + metrics: { types: ['vis_dimension'], help: strings.getMetricArgHelp(), required: true, + multi: true, + }, + metricsToLabels: { + types: ['string'], + help: strings.getMetricToLabelHelp(), }, bucket: { types: ['vis_dimension'], @@ -111,7 +116,8 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError()); } - validateAccessor(args.metric, context.columns); + args.metrics.forEach((accessor) => validateAccessor(accessor, context.columns)); + if (args.bucket) { validateAccessor(args.bucket, context.columns); } @@ -125,13 +131,14 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ const buckets = args.bucket ? [args.bucket] : []; const visConfig: PartitionVisParams = { ...args, + metricsToLabels: args.metricsToLabels ? JSON.parse(args.metricsToLabels) : {}, ariaLabel: args.ariaLabel ?? (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, palette: args.palette, dimensions: { - metric: args.metric, + metrics: args.metrics, buckets, splitColumn: args.splitColumn, splitRow: args.splitRow, @@ -145,7 +152,7 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ const logTable = prepareLogTable( context, [ - [[args.metric], strings.getSliceSizeHelp()], + [args.metrics, strings.getSliceSizeHelp()], [buckets, strings.getSliceHelp()], [args.splitColumn, strings.getColumnSplitHelp()], [args.splitRow, strings.getRowSplitHelp()], diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 6a8fd2935ba54..9584a810d7ca4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -29,7 +29,7 @@ export interface Dimension { } export interface Dimensions { - metric?: ExpressionValueVisDimension | string; + metrics: Array; buckets?: Array; splitRow?: Array; splitColumn?: Array; @@ -58,7 +58,9 @@ interface VisCommonParams { } interface VisCommonConfig extends VisCommonParams { - metric: ExpressionValueVisDimension | string; + metrics: Array; + metricsToLabels?: string; + buckets?: Array; splitColumn?: Array; splitRow?: Array; labels: ExpressionValuePartitionLabels; @@ -67,6 +69,7 @@ interface VisCommonConfig extends VisCommonParams { export interface PartitionVisParams extends VisCommonParams { dimensions: Dimensions; + metricsToLabels: Record; labels: LabelsParams; palette: PaletteOutput; isDonut?: boolean; @@ -79,7 +82,7 @@ export interface PartitionVisParams extends VisCommonParams { } export interface PieVisConfig extends VisCommonConfig { - buckets?: Array; + partitionByColumn?: boolean; isDonut: boolean; emptySizeRatio?: EmptySizeRatios; respectSourceOrder?: boolean; @@ -89,16 +92,15 @@ export interface PieVisConfig extends VisCommonConfig { } export interface TreemapVisConfig extends VisCommonConfig { - buckets?: Array; nestedLegend: boolean; } -export interface MosaicVisConfig extends VisCommonConfig { - buckets?: Array; +export interface MosaicVisConfig extends Omit { + metric: ExpressionValueVisDimension | string; nestedLegend: boolean; } -export interface WaffleVisConfig extends VisCommonConfig { +export interface WaffleVisConfig extends Omit { bucket?: ExpressionValueVisDimension | string; showValuesInLegend: boolean; } diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/utils/consolidate_metric_columns.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/utils/consolidate_metric_columns.test.ts new file mode 100644 index 0000000000000..6d94809e403b7 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/utils/consolidate_metric_columns.test.ts @@ -0,0 +1,287 @@ +/* + * 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 { Datatable } from '@kbn/expressions-plugin/common'; +import { consolidateMetricColumns } from './consolidate_metric_columns'; + +describe('consolidateMetricColumns', () => { + it('collapses multiple metrics into a single metric column', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { + id: '1', + name: 'bucket1', + meta: { + type: 'string', + }, + }, + { + id: '2', + name: 'bucket2', + meta: { + type: 'string', + }, + }, + { + id: '3', + name: 'metric1', + meta: { + type: 'number', + }, + }, + { + id: '4', + name: 'metric2', + meta: { + type: 'number', + }, + }, + ], + rows: [ + { '1': 'square', '2': 'red', '3': 1, '4': 2 }, + { '1': 'square', '2': 'blue', '3': 3, '4': 4 }, + { '1': 'circle', '2': 'red', '3': 5, '4': 6 }, + { '1': 'circle', '2': 'blue', '3': 7, '4': 8 }, + ], + }; + + const result = consolidateMetricColumns(table, ['1', '2'], ['3', '4'], { + 3: 'metric1 label', + 4: 'metric2 label', + }); + expect(result.bucketAccessors).toEqual(['1', '2', 'metric-name']); + expect(result.metricAccessor).toEqual('value'); + expect(result.table).toMatchInlineSnapshot(` + Object { + "columns": Array [ + Object { + "id": "1", + "meta": Object { + "type": "string", + }, + "name": "bucket1", + }, + Object { + "id": "2", + "meta": Object { + "type": "string", + }, + "name": "bucket2", + }, + Object { + "id": "metric-name", + "meta": Object { + "sourceParams": Object { + "consolidatedMetricsColumn": true, + }, + "type": "string", + }, + "name": "metric-name", + }, + Object { + "id": "value", + "meta": Object { + "type": "number", + }, + "name": "value", + }, + ], + "rows": Array [ + Object { + "1": "square", + "2": "red", + "metric-name": "metric1 label", + "value": 1, + }, + Object { + "1": "square", + "2": "red", + "metric-name": "metric2 label", + "value": 2, + }, + Object { + "1": "square", + "2": "blue", + "metric-name": "metric1 label", + "value": 3, + }, + Object { + "1": "square", + "2": "blue", + "metric-name": "metric2 label", + "value": 4, + }, + Object { + "1": "circle", + "2": "red", + "metric-name": "metric1 label", + "value": 5, + }, + Object { + "1": "circle", + "2": "red", + "metric-name": "metric2 label", + "value": 6, + }, + Object { + "1": "circle", + "2": "blue", + "metric-name": "metric1 label", + "value": 7, + }, + Object { + "1": "circle", + "2": "blue", + "metric-name": "metric2 label", + "value": 8, + }, + ], + "type": "datatable", + } + `); + }); + + it('leaves single metric tables alone', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { + id: '1', + name: 'bucket1', + meta: { + type: 'string', + }, + }, + { + id: '2', + name: 'bucket2', + meta: { + type: 'string', + }, + }, + { + id: '3', + name: 'metric1', + meta: { + type: 'number', + }, + }, + ], + rows: [ + { '1': 'square', '2': 'red', '3': 1 }, + { '1': 'square', '2': 'blue', '3': 3 }, + { '1': 'circle', '2': 'red', '3': 5 }, + { '1': 'circle', '2': 'blue', '3': 7 }, + ], + }; + + const bucketAccessors = ['1', '2']; + const metricAccessors = ['3']; + const result = consolidateMetricColumns(table, bucketAccessors, metricAccessors, { + 3: 'metric1', + }); + + expect(result.table).toEqual(table); + expect(result.bucketAccessors).toEqual(bucketAccessors); + expect(result.metricAccessor).toEqual(metricAccessors[0]); + }); + + it('does not blow up when there are no bucket accessors', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { + id: '3', + name: 'metric1', + meta: { + type: 'number', + }, + }, + { + id: '4', + name: 'metric2', + meta: { + type: 'number', + }, + }, + ], + rows: [ + { '3': 1, '4': 2 }, + { '3': 3, '4': 4 }, + { '3': 5, '4': 6 }, + { '3': 7, '4': 8 }, + ], + }; + + const result = consolidateMetricColumns(table, undefined, ['3', '4'], { + 3: 'metric1', + 4: 'metric2', + }); + expect(result.bucketAccessors).toEqual(['metric-name']); + expect(result.metricAccessor).toEqual('value'); + expect(result.table).toMatchInlineSnapshot(` + Object { + "columns": Array [ + Object { + "id": "metric-name", + "meta": Object { + "sourceParams": Object { + "consolidatedMetricsColumn": true, + }, + "type": "string", + }, + "name": "metric-name", + }, + Object { + "id": "value", + "meta": Object { + "type": "number", + }, + "name": "value", + }, + ], + "rows": Array [ + Object { + "metric-name": "metric1", + "value": 1, + }, + Object { + "metric-name": "metric2", + "value": 2, + }, + Object { + "metric-name": "metric1", + "value": 3, + }, + Object { + "metric-name": "metric2", + "value": 4, + }, + Object { + "metric-name": "metric1", + "value": 5, + }, + Object { + "metric-name": "metric2", + "value": 6, + }, + Object { + "metric-name": "metric1", + "value": 7, + }, + Object { + "metric-name": "metric2", + "value": 8, + }, + ], + "type": "datatable", + } + `); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/utils/consolidate_metric_columns.ts b/src/plugins/chart_expressions/expression_partition_vis/common/utils/consolidate_metric_columns.ts new file mode 100644 index 0000000000000..009744cc06f3e --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/utils/consolidate_metric_columns.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 { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common'; +import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; +import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; + +function nonNullable(value: T): value is NonNullable { + return value !== null && value !== undefined; +} + +export const consolidateMetricColumns = ( + table: Datatable, + bucketAccessors: Array = [], + metricAccessors: Array, + metricsToLabels: Record +): { + table: Datatable; + metricAccessor: string | ExpressionValueVisDimension | undefined; + bucketAccessors: Array; +} => { + if (metricAccessors.length < 2) { + return { + table, + metricAccessor: metricAccessors[0], + bucketAccessors, + }; + } + + const bucketColumns = bucketAccessors + ?.map((accessor) => getColumnByAccessor(accessor, table.columns)) + .filter(nonNullable); + + const metricColumns = metricAccessors + ?.map((accessor) => getColumnByAccessor(accessor, table.columns)) + .filter(nonNullable); + + const transposedRows: DatatableRow[] = []; + + const nameColumnId = 'metric-name'; + const valueColumnId = 'value'; + + table.rows.forEach((row) => { + metricColumns.forEach((metricCol) => { + const newRow: DatatableRow = {}; + + bucketColumns.forEach(({ id }) => { + newRow[id] = row[id]; + }); + + newRow[nameColumnId] = metricsToLabels[metricCol.id]; + newRow[valueColumnId] = row[metricCol.id]; + + transposedRows.push(newRow); + }); + }); + + const transposedColumns: DatatableColumn[] = [ + ...bucketColumns, + { + id: nameColumnId, + name: nameColumnId, + meta: { + type: 'string', + sourceParams: { + consolidatedMetricsColumn: true, + }, + }, + }, + { + id: valueColumnId, + name: valueColumnId, + meta: { + type: 'number', + }, + }, + ]; + + return { + metricAccessor: valueColumnId, + bucketAccessors: [...bucketColumns.map(({ id }) => id), nameColumnId], + table: { + type: 'datatable', + columns: transposedColumns, + rows: transposedRows, + }, + }; +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/utils/index.ts b/src/plugins/chart_expressions/expression_partition_vis/common/utils/index.ts new file mode 100644 index 0000000000000..12ea01c9177cb --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/common/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './consolidate_metric_columns'; 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 index d16802518cce4..aa4023006d486 100644 --- 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 @@ -17,6 +17,7 @@ import { export const config: RenderValue['visConfig'] = { addTooltip: true, legendDisplay: LegendDisplay.HIDE, + metricsToLabels: { percent_uptime: 'percent_uptime' }, truncateLegend: true, respectSourceOrder: true, legendPosition: Position.Bottom, @@ -35,20 +36,22 @@ export const config: RenderValue['visConfig'] = { last_level: false, }, dimensions: { - metric: { - type: 'vis_dimension', - accessor: { - id: 'percent_uptime', - name: 'percent_uptime', - meta: { - type: 'number', + metrics: [ + { + type: 'vis_dimension', + accessor: { + id: 'percent_uptime', + name: 'percent_uptime', + meta: { + type: 'number', + }, + }, + format: { + id: 'string', + params: {}, }, }, - format: { - id: 'string', - params: {}, - }, - }, + ], }, }; 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 index 121c40d50afe2..c91e491887a99 100644 --- 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 @@ -194,14 +194,6 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` "vis.colors", Object {}, ], - Array [ - "vis.legendOpen", - true, - ], - Array [ - "vis.colors", - Object {}, - ], ], "results": Array [ Object { @@ -212,14 +204,6 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` "type": "return", "value": Object {}, }, - Object { - "type": "return", - "value": true, - }, - Object { - "type": "return", - "value": Object {}, - }, ], }, "set": [MockFunction], @@ -597,30 +581,6 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] = "vis.colors", Object {}, ], - Array [ - "vis.legendOpen", - true, - ], - Array [ - "vis.colors", - Object {}, - ], - Array [ - "vis.legendOpen", - true, - ], - Array [ - "vis.colors", - Object {}, - ], - Array [ - "vis.legendOpen", - true, - ], - Array [ - "vis.colors", - Object {}, - ], ], "results": Array [ Object { @@ -631,30 +591,6 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] = "type": "return", "value": Object {}, }, - Object { - "type": "return", - "value": true, - }, - Object { - "type": "return", - "value": Object {}, - }, - Object { - "type": "return", - "value": true, - }, - Object { - "type": "return", - "value": Object {}, - }, - Object { - "type": "return", - "value": true, - }, - Object { - "type": "return", - "value": Object {}, - }, ], }, "set": [MockFunction], @@ -840,7 +776,7 @@ 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 multi-metric pie 1`] = `
`; -exports[`PartitionVisComponent should render correct structure for treemap 1`] = ` +exports[`PartitionVisComponent should render correct structure for pie 1`] = `
} @@ -1520,12 +1546,12 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] = "linkLabel": Object { "fontSize": 11, "maxCount": 5, - "maxTextLength": undefined, + "maxTextLength": 100, "textColor": undefined, }, "maxFontSize": 16, "minFontSize": 10, - "outerSizeRatio": 1, + "outerSizeRatio": undefined, "sectorLineStroke": undefined, "sectorLineWidth": 1.5, }, @@ -1599,7 +1625,7 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] = }, ] } - id="treemap" + id="pie" layers={ Array [ Object { @@ -1632,7 +1658,7 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] = }, ] } - layout="treemap" + layout="sunburst" percentFormatter={[Function]} smallMultiples="__pie_chart_sm__" valueAccessor={[Function]} @@ -1645,7 +1671,7 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] =
`; -exports[`PartitionVisComponent should render correct structure for waffle 1`] = ` +exports[`PartitionVisComponent should render correct structure for treemap 1`] = `
+ + + + + } + onElementClick={[Function]} + onRenderChange={[Function]} + showLegend={true} + theme={ + Array [ + Object { + "background": Object { + "color": "transparent", }, - Object { - "type": "return", - "value": Object {}, + }, + Object { + "chartMargins": Object { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, }, - Object { - "type": "return", - "value": true, + "partition": Object { + "circlePadding": 4, + "emptySizeRatio": 0, + "fontFamily": undefined, + "linkLabel": Object { + "fontSize": 11, + "maxCount": 5, + "maxTextLength": undefined, + "textColor": undefined, + }, + "maxFontSize": 16, + "minFontSize": 10, + "outerSizeRatio": 1, + "sectorLineStroke": undefined, + "sectorLineWidth": 1.5, }, - Object { - "type": "return", - "value": Object {}, + }, + Object {}, + Object { + "legend": Object { + "labelOptions": Object { + "maxLines": 1, + }, }, - Object { - "type": "return", - "value": true, + }, + ] + } + tooltip={ + Object { + "type": "follow", + } + } + /> + + + +
+
+`; + +exports[`PartitionVisComponent should render correct structure for waffle 1`] = ` +
+
+ { const original = jest.requireActual('@elastic/charts'); @@ -83,11 +84,27 @@ describe('PartitionVisComponent', function () { }; }); + afterEach(() => { + mockState.clear(); + jest.clearAllMocks(); + }); + it('should render correct structure for pie', function () { const component = shallow(); expect(component).toMatchSnapshot(); }); + it('should render correct structure for multi-metric pie', function () { + const localParams = cloneDeep(wrapperProps.visParams); + + localParams.dimensions.metrics = [...localParams.dimensions.metrics, 'col-3-1']; + + localParams.metricsToLabels = { 'col-3-1': 'metric1 label', 'col-1-1': 'metric2 label' }; + + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + it('should render correct structure for donut', function () { const donutVisParams = createMockDonutParams(); const component = shallow( diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index 6ef1bb79f0f3f..ed1789f2ae4a9 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -34,6 +34,7 @@ import { IInterpreterRenderHandlers, } from '@kbn/expressions-plugin/public'; import type { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { consolidateMetricColumns } from '../../common/utils'; import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants'; import { PartitionVisParams, @@ -91,14 +92,40 @@ export interface PartitionVisComponentProps { } const PartitionVisComponent = (props: PartitionVisComponentProps) => { - const { visData, visParams: preVisParams, visType, services, syncColors } = props; + const { + visData: originalVisData, + visParams: preVisParams, + visType, + services, + syncColors, + } = props; const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]); const chartTheme = props.chartsThemeService.useChartsTheme(); const chartBaseTheme = props.chartsThemeService.useChartsBaseTheme(); + const { + table: visData, + metricAccessor, + bucketAccessors, + } = useMemo( + () => + consolidateMetricColumns( + originalVisData, + visParams.dimensions.buckets, + visParams.dimensions.metrics, + visParams.metricsToLabels + ), + [ + originalVisData, + visParams.dimensions.buckets, + visParams.dimensions.metrics, + visParams.metricsToLabels, + ] + ); + const { bucketColumns, metricColumn } = useMemo( - () => getColumns(props.visParams, props.visData), - [props.visData, props.visParams] + () => getColumns({ metric: metricAccessor, buckets: bucketAccessors }, visData), + [bucketAccessors, metricAccessor, visData] ); const formatters = useMemo( @@ -115,7 +142,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { const showToggleLegendElement = props.uiState !== undefined; - const [dimensions, setDimensions] = useState(); + const [containerDimensions, setContainerDimensions] = useState< + undefined | PieContainerDimensions + >(); const parentRef = useRef(null); @@ -123,7 +152,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { if (parentRef && parentRef.current) { const parentHeight = parentRef.current!.getBoundingClientRect().height; const parentWidth = parentRef.current!.getBoundingClientRect().width; - setDimensions({ width: parentWidth, height: parentHeight }); + setContainerDimensions({ width: parentWidth, height: parentHeight }); } }, [parentRef]); @@ -153,13 +182,16 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { const data = getFilterClickData( clickedLayers, buckets, + metricColumn.id, vData, + originalVisData, + visParams.dimensions.metrics.length, splitChartDimension, splitChartFormatter ); props.fireEvent({ name: 'filter', data: { data } }); }, - [props] + [metricColumn.id, originalVisData, props, visParams.dimensions.metrics.length] ); // handles legend action event data @@ -292,8 +324,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { }, [visData.rows, metricColumn]); const themeOverrides = useMemo( - () => getPartitionTheme(visType, visParams, chartTheme, dimensions, rescaleFactor), - [visType, visParams, chartTheme, dimensions, rescaleFactor] + () => getPartitionTheme(visType, visParams, chartTheme, containerDimensions, rescaleFactor), + [visType, visParams, chartTheme, containerDimensions, rescaleFactor] ); const fixedViewPort = document.getElementById('app-fixed-viewport'); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts b/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts index 455dd111179d2..c125243f3a09a 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/mocks.ts @@ -292,17 +292,20 @@ export const createMockPartitionVisParams = (): PartitionVisParams => { name: 'default', type: 'palette', }, + metricsToLabels: {}, dimensions: { - metric: { - type: 'vis_dimension', - accessor: 1, - format: { - id: 'number', - params: { + metrics: [ + { + type: 'vis_dimension', + accessor: 1, + format: { id: 'number', + params: { + id: 'number', + }, }, }, - }, + ], buckets: [ { type: 'vis_dimension', diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.test.ts index 62da48b6ec5a0..07646450a43a0 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.test.ts @@ -5,16 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { DatatableColumn } from '@kbn/expressions-plugin/public'; +import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public'; import { getFilterClickData, getFilterEventData } from './filter_helpers'; import { createMockBucketColumns, createMockVisData } from '../mocks'; +import { consolidateMetricColumns } from '../../common/utils'; +import { LayerValue } from '@elastic/charts'; +import faker from 'faker'; const bucketColumns = createMockBucketColumns(); const visData = createMockVisData(); describe('getFilterClickData', () => { it('returns the correct filter data for the specific layer', () => { - const clickedLayers = [ + const clickedLayers: LayerValue[] = [ { groupByRollup: 'Logstash Airways', value: 729, @@ -24,7 +27,14 @@ describe('getFilterClickData', () => { smAccessorValue: '', }, ]; - const data = getFilterClickData(clickedLayers, bucketColumns, visData); + const data = getFilterClickData( + clickedLayers, + bucketColumns, + visData.columns[1].id, + visData, + visData, + 1 + ); expect(data.length).toEqual(clickedLayers.length); expect(data[0].value).toEqual('Logstash Airways'); expect(data[0].row).toEqual(0); @@ -32,7 +42,7 @@ describe('getFilterClickData', () => { }); it('changes the filter if the user clicks on another layer', () => { - const clickedLayers = [ + const clickedLayers: LayerValue[] = [ { groupByRollup: 'ES-Air', value: 572, @@ -42,7 +52,14 @@ describe('getFilterClickData', () => { smAccessorValue: '', }, ]; - const data = getFilterClickData(clickedLayers, bucketColumns, visData); + const data = getFilterClickData( + clickedLayers, + bucketColumns, + visData.columns[1].id, + visData, + visData, + 1 + ); expect(data.length).toEqual(clickedLayers.length); expect(data[0].value).toEqual('ES-Air'); expect(data[0].row).toEqual(4); @@ -50,7 +67,7 @@ describe('getFilterClickData', () => { }); it('returns the correct filters for small multiples', () => { - const clickedLayers = [ + const clickedLayers: LayerValue[] = [ { groupByRollup: 'ES-Air', value: 572, @@ -64,7 +81,15 @@ describe('getFilterClickData', () => { id: 'col-2-3', name: 'Cancelled: Descending', } as DatatableColumn; - const data = getFilterClickData(clickedLayers, bucketColumns, visData, splitDimension); + const data = getFilterClickData( + clickedLayers, + bucketColumns, + visData.columns[1].id, + visData, + visData, + 1, + splitDimension + ); expect(data.length).toEqual(2); expect(data[0].value).toEqual('ES-Air'); expect(data[0].row).toEqual(5); @@ -87,12 +112,132 @@ describe('getFilterClickData', () => { id: 'col-0-2', name: 'Carrier: Descending', } as DatatableColumn; - const data = getFilterClickData(clickedLayers, [{ name: 'Count' }], visData, splitDimension); + const data = getFilterClickData( + clickedLayers, + [{ name: 'Count' }], + visData.columns[1].id, + visData, + visData, + 1, + splitDimension + ); expect(data.length).toEqual(2); expect(data[0].value).toEqual('Count'); expect(data[0].row).toEqual(4); - expect(data[1].column).toEqual(0); + expect(data[0].column).toEqual(1); + expect(data[1].value).toEqual('ES-Air'); + expect(data[1].row).toEqual(4); + expect(data[1].column).toEqual(0); + }); + + describe('multi-metric scenarios', () => { + describe('with original bucket columns', () => { + const originalTable: Datatable = { + type: 'datatable', + columns: [ + { name: 'shape', id: '0', meta: { type: 'string' } }, + { name: 'color', id: '1', meta: { type: 'string' } }, + { + name: 'metric1', + id: '2', + meta: { + type: 'number', + }, + }, + { + name: 'metric2', + id: '3', + meta: { + type: 'number', + }, + }, + ], + rows: [ + { '0': 'square', '1': 'red', '2': 1, '3': 2 }, + { '0': 'square', '1': 'blue', '2': 3, '3': 4 }, + { '0': 'circle', '1': 'green', '2': 5, '3': 6 }, + { '0': 'circle', '1': 'gray', '2': 7, '3': 8 }, + ], + }; + + const { table: consolidatedTable } = consolidateMetricColumns( + originalTable, + ['0', '1'], + ['2', '3'], + { + 2: 'metric1', + 3: 'metric2', + } + ); + + it('generates the correct filters', () => { + const localBucketColumns = consolidatedTable.columns.slice(0, 3); + + const clickedLayers: LayerValue[] = [ + { + groupByRollup: 'circle', + value: faker.random.number(), + depth: faker.random.number(), + path: [], + sortIndex: faker.random.number(), + smAccessorValue: '', + }, + { + groupByRollup: 'green', + value: faker.random.number(), + depth: faker.random.number(), + path: [], + sortIndex: faker.random.number(), + smAccessorValue: '', + }, + { + groupByRollup: 'metric2', + value: faker.random.number(), + depth: faker.random.number(), + path: [], + sortIndex: faker.random.number(), + smAccessorValue: '', + }, + ]; + + const data = getFilterClickData( + clickedLayers, + localBucketColumns, + 'value', + consolidatedTable, + originalTable, + 2 + ); + + expect(data).toHaveLength(3); + + expect(data.map((datum) => ({ ...datum, table: undefined }))).toMatchInlineSnapshot(` + Array [ + Object { + "column": 0, + "row": 2, + "table": undefined, + "value": "circle", + }, + Object { + "column": 1, + "row": 2, + "table": undefined, + "value": "green", + }, + Object { + "column": 3, + "row": 2, + "table": undefined, + "value": "metric2", + }, + ] + `); + + expect(data.map((datum) => datum.table === originalTable).every(Boolean)).toBe(true); + }); + }); }); }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts index 3d129094ebb19..6a42bc6f7b601 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts @@ -28,12 +28,15 @@ export const canFilter = async ( export const getFilterClickData = ( clickedLayers: LayerValue[], bucketColumns: Array>, + metricColId: string, visData: Datatable, + originalVisData: Datatable, // before multiple metrics are consolidated with collapseMetricColumns + numOriginalMetrics: number, splitChartDimension?: DatatableColumn, splitChartFormatter?: FieldFormat ): ValueClickContext['data']['data'] => { const data: ValueClickContext['data']['data'] = []; - const matchingIndex = visData.rows.findIndex((row) => + const rowIndex = visData.rows.findIndex((row) => clickedLayers.every((layer, index) => { const columnId = bucketColumns[index].id; if (!columnId && !splitChartDimension) return; @@ -48,20 +51,53 @@ export const getFilterClickData = ( }) ); + const originalRowIndex = Math.floor(rowIndex / numOriginalMetrics); + data.push( - ...clickedLayers.map((clickedLayer, index) => ({ - column: visData.columns.findIndex((col) => col.id === bucketColumns[index].id), - row: matchingIndex, - value: clickedLayer.groupByRollup, - table: visData, - })) + ...(clickedLayers + .map((clickedLayer, index) => { + const currentColumnIndex = visData.columns.findIndex( + (col) => col.id === bucketColumns[index].id + ); + + if (currentColumnIndex === -1) { + return undefined; + } + + const currentColumn = visData.columns[currentColumnIndex]; + + // this logic maps the indices of the elements in the + // visualization's table to the indices in the table before + // any multiple metrics were collapsed into one metric column + const originalColumnIndex = currentColumn.meta?.sourceParams?.consolidatedMetricsColumn + ? currentColumnIndex + (rowIndex % numOriginalMetrics) + : currentColumnIndex; + + return { + column: originalColumnIndex, + row: originalRowIndex, + value: clickedLayer.groupByRollup, + table: originalVisData, + }; + }) + .filter(Boolean) as ValueClickContext['data']['data']) ); // Allows filtering with the small multiples value if (splitChartDimension) { + if (!bucketColumns[0].id) { + // this is a split chart without any real bucket columns, so filter by the metric column + data.push({ + column: visData.columns.findIndex((col) => col.id === metricColId), + row: rowIndex, + table: visData, + value: visData.columns.find((col) => col.id === metricColId)?.name, + }); + } + data.push({ column: visData.columns.findIndex((col) => col.id === splitChartDimension.id), - row: matchingIndex, + row: rowIndex, table: visData, value: clickedLayers[0].smAccessorValue, }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts index 157336599a26e..a544dfe1f8537 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.test.ts @@ -7,20 +7,18 @@ */ import { getColumns } from './get_columns'; -import { - LabelPositions, - LegendDisplay, - PartitionVisParams, - ValueFormats, -} from '../../common/types'; import { createMockPieParams, createMockVisData } from '../mocks'; const visParams = createMockPieParams(); +const dimensions = { + metric: visParams.dimensions.metrics[0], + buckets: visParams.dimensions.buckets!, +}; const visData = createMockVisData(); describe('getColumns', () => { it('should return the correct bucket columns if visParams returns dimensions', () => { - const { bucketColumns } = getColumns(visParams, visData); + const { bucketColumns } = getColumns(dimensions, visData); expect(bucketColumns.length).toEqual(visParams.dimensions.buckets?.length); expect(bucketColumns).toEqual([ { @@ -115,11 +113,8 @@ describe('getColumns', () => { it('should return the correct metric column if visParams returns dimensions', () => { const { metricColumn } = getColumns( { - ...visParams, - dimensions: { - ...visParams.dimensions, - metric: undefined, - }, + ...dimensions, + metric: undefined, }, visData ); @@ -144,28 +139,8 @@ describe('getColumns', () => { }); it('should return the first data column if no buckets specified', () => { - const visParamsOnlyMetric: PartitionVisParams = { - legendDisplay: LegendDisplay.SHOW, - addTooltip: true, - labels: { - position: LabelPositions.DEFAULT, - show: true, - truncate: 100, - values: true, - valuesFormat: ValueFormats.PERCENT, - percentDecimals: 2, - last_level: false, - }, - legendPosition: 'right', - nestedLegend: false, - maxLegendLines: 1, - truncateLegend: false, - distinctColors: false, - palette: { - name: 'default', - type: 'palette', - }, - dimensions: { + const { metricColumn } = getColumns( + { metric: { type: 'vis_dimension', accessor: 1, @@ -174,9 +149,10 @@ describe('getColumns', () => { params: {}, }, }, + buckets: [], }, - }; - const { metricColumn } = getColumns(visParamsOnlyMetric, visData); + visData + ); expect(metricColumn).toEqual({ id: 'col-1-1', meta: { @@ -200,29 +176,8 @@ describe('getColumns', () => { }); it('should return an object with the name of the metric if no buckets specified', () => { - const visParamsOnlyMetric: PartitionVisParams = { - legendDisplay: LegendDisplay.SHOW, - addTooltip: true, - isDonut: true, - labels: { - position: LabelPositions.DEFAULT, - show: true, - truncate: 100, - values: true, - valuesFormat: ValueFormats.PERCENT, - percentDecimals: 2, - last_level: false, - }, - truncateLegend: false, - maxLegendLines: 100, - distinctColors: false, - legendPosition: 'right', - nestedLegend: false, - palette: { - name: 'default', - type: 'palette', - }, - dimensions: { + const { bucketColumns, metricColumn } = getColumns( + { metric: { type: 'vis_dimension', accessor: 1, @@ -231,9 +186,10 @@ describe('getColumns', () => { params: {}, }, }, + buckets: [], }, - }; - const { bucketColumns, metricColumn } = getColumns(visParamsOnlyMetric, visData); + visData + ); expect(bucketColumns).toEqual([{ name: metricColumn.name }]); }); }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts index 0b5d0d101cf38..53d039a9e5199 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_columns.ts @@ -9,7 +9,7 @@ import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { DatatableColumn, Datatable } from '@kbn/expressions-plugin/public'; -import { BucketColumns, PartitionVisParams } from '../../common/types'; +import { BucketColumns } from '../../common/types'; const getMetricColumn = ( metricAccessor: ExpressionValueVisDimension | string, @@ -19,14 +19,17 @@ const getMetricColumn = ( }; export const getColumns = ( - visParams: PartitionVisParams, + dimensions: { + metric: string | ExpressionValueVisDimension | undefined; + buckets: Array; + }, visData: Datatable ): { metricColumn: DatatableColumn; bucketColumns: Array>; } => { - const { metric, buckets } = visParams.dimensions; - if (buckets && buckets.length > 0) { + const { metric, buckets } = dimensions; + if (buckets.length > 0) { const bucketColumns: Array> = buckets.map((bucket) => { const column = getColumnByAccessor(bucket, visData.columns); return { 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 b9dcb3f6bff6a..253057cd8c2f5 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 @@ -67,7 +67,7 @@ Object { "legendSize": Array [ "large", ], - "metric": Array [ + "metrics": Array [ Object { "chain": Array [ Object { diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts index 87ec0d3b57b3f..1f558da376e0f 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.test.ts @@ -32,7 +32,7 @@ describe('getConfiguration', () => { legendMaxLines: 1, legendPosition: 'right', legendSize: 'large', - metric: 'metric-1', + metrics: ['metric-1'], nestedLegend: true, numberDisplay: 'percent', percentDecimals: 2, diff --git a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts index d1d1daf9fe009..1ff3148315fac 100644 --- a/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts +++ b/src/plugins/vis_types/pie/public/convert_to_lens/configurations/index.ts @@ -35,7 +35,7 @@ const getLayers = ( layerType: 'data' as const, primaryGroups: buckets, secondaryGroups: [], - metric: metrics[0], + metrics: metrics.length ? [metrics[0]] : [], numberDisplay: showValuesInLegend === false ? NumberDisplayTypes.HIDDEN diff --git a/src/plugins/vis_types/pie/public/to_ast.ts b/src/plugins/vis_types/pie/public/to_ast.ts index 91ff6b0b6c17d..853e354cf4d06 100644 --- a/src/plugins/vis_types/pie/public/to_ast.ts +++ b/src/plugins/vis_types/pie/public/to_ast.ts @@ -70,7 +70,7 @@ export const toExpressionAst: VisToExpressionAst = async (vi emptySizeRatio: vis.params.emptySizeRatio, palette: preparePalette(vis.params.palette), labels: prepareLabels(vis.params.labels), - metric: schemas.metric.map(prepareDimension), + metrics: prepareDimension(schemas.metric[schemas.metric.length - 1]), buckets: schemas.segment?.map(prepareDimension), splitColumn: schemas.split_column?.map(prepareDimension), splitRow: schemas.split_row?.map(prepareDimension), diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts index 8a6e70669dcf4..d19f3cd8318bd 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -223,9 +223,9 @@ export interface MetricVisConfiguration { export interface PartitionLayerState { layerId: string; layerType: LayerType; + metrics: string[]; primaryGroups: string[]; secondaryGroups?: string[]; - metric?: string; collapseFns?: Record; numberDisplay: NumberDisplayType; categoryDisplay: CategoryDisplayType; diff --git a/x-pack/examples/testing_embedded_lens/public/app.tsx b/x-pack/examples/testing_embedded_lens/public/app.tsx index 2b8799c1951a3..ab717da3b6268 100644 --- a/x-pack/examples/testing_embedded_lens/public/app.tsx +++ b/x-pack/examples/testing_embedded_lens/public/app.tsx @@ -312,7 +312,7 @@ function getLensAttributesPartition( layers: [ { primaryGroups: ['col1'], - metric: 'col2', + metrics: ['col2'], layerId: 'layer1', layerType: 'data', numberDisplay: 'percent', diff --git a/x-pack/performance/kbn_archives/flights_no_map_dashboard.json b/x-pack/performance/kbn_archives/flights_no_map_dashboard.json index f0afa9052ddae..cac545ba1d836 100644 --- a/x-pack/performance/kbn_archives/flights_no_map_dashboard.json +++ b/x-pack/performance/kbn_archives/flights_no_map_dashboard.json @@ -173,7 +173,7 @@ "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" }, "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.6.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":47,\"w\":48,\"h\":15,\"i\":\"4\"},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":16,\"w\":24,\"h\":9,\"i\":\"7\"},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":36,\"w\":24,\"h\":11,\"i\":\"10\"},\"panelIndex\":\"10\",\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false},\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":36,\"y\":36,\"w\":12,\"h\":11,\"i\":\"21\"},\"panelIndex\":\"21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_21\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":8,\"i\":\"6afc61f7-e2d5-45a3-9e7a-281160ad3eb9\"},\"panelIndex\":\"6afc61f7-e2d5-45a3-9e7a-281160ad3eb9\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"[Flights] Markdown Instructions\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":10,\"openLinksInNewTab\":true,\"markdown\":\"## Sample Flight data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html).\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{}}},\"hidePanelTitles\":true,\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":8,\"h\":8,\"i\":\"392b4936-f753-47bc-a98d-a4e41a0a4cd4\"},\"panelIndex\":\"392b4936-f753-47bc-a98d-a4e41a0a4cd4\",\"embeddableConfig\":{\"enhancements\":{},\"attributes\":{\"title\":\"[Flights] Total Flights\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"8fa993db-c147-4954-adf7-4ff264d42576\":{\"columns\":{\"81124c45-6ab6-42f4-8859-495d55eb8065\":{\"label\":\"Total flights\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true}},\"columnOrder\":[\"81124c45-6ab6-42f4-8859-495d55eb8065\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"8fa993db-c147-4954-adf7-4ff264d42576\",\"accessor\":\"81124c45-6ab6-42f4-8859-495d55eb8065\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-8fa993db-c147-4954-adf7-4ff264d42576\",\"type\":\"index-pattern\"}]},\"hidePanelTitles\":true}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":32,\"y\":0,\"w\":8,\"h\":4,\"i\":\"9271deff-5a61-4665-83fc-f9fdc6bf0c0b\"},\"panelIndex\":\"9271deff-5a61-4665-83fc-f9fdc6bf0c0b\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\":{\"label\":\"Part of count(kql='FlightDelay : true') / count()\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"FlightDelay : true\",\"language\":\"kuery\"},\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\":{\"label\":\"Part of count(kql='FlightDelay : true') / count()\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\":{\"label\":\"Part of count(kql='FlightDelay : true') / count()\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"location\":{\"min\":0,\"max\":41},\"text\":\"count(kql='FlightDelay : true') / count()\"}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\":{\"label\":\"Delayed\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='FlightDelay : true') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"customLabel\":true}},\"columnOrder\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":40,\"y\":0,\"w\":8,\"h\":4,\"i\":\"aa591c29-1a31-4ee1-a71d-b829c06fd162\"},\"panelIndex\":\"aa591c29-1a31-4ee1-a71d-b829c06fd162\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"c7851241-5526-499a-960b-357af8c2ce5bX0\":{\"label\":\"Part of Delayed\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX1\":{\"label\":\"Part of Delayed\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"timeShift\":\"1w\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX2\":{\"label\":\"Part of Delayed\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"subtract\",\"args\":[{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"location\":{\"min\":0,\"max\":28},\"text\":\"count() / count(shift='1w') \"},1],\"location\":{\"min\":0,\"max\":31},\"text\":\"count() / count(shift='1w') - 1\"}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5b\":{\"label\":\"Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count() / count(shift='1w') - 1\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX2\"],\"customLabel\":true}},\"columnOrder\":[\"c7851241-5526-499a-960b-357af8c2ce5b\",\"c7851241-5526-499a-960b-357af8c2ce5bX2\",\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"c7851241-5526-499a-960b-357af8c2ce5b\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"FlightDelay\",\"params\":{\"query\":true},\"index\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"FlightDelay\":true}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":32,\"y\":4,\"w\":8,\"h\":4,\"i\":\"b766e3b8-4544-46ed-99e6-9ecc4847e2a2\"},\"panelIndex\":\"b766e3b8-4544-46ed-99e6-9ecc4847e2a2\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\":{\"label\":\"Part of Cancelled\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"Cancelled : true\",\"language\":\"kuery\"},\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\":{\"label\":\"Part of Cancelled\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\":{\"label\":\"Part of Cancelled\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"location\":{\"min\":0,\"max\":39},\"text\":\"count(kql='Cancelled : true') / count()\"}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\":{\"label\":\"Cancelled\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='Cancelled : true') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"customLabel\":true}},\"columnOrder\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":40,\"y\":4,\"w\":8,\"h\":4,\"i\":\"2e33ade5-96e5-40b4-b460-493e5d4fa834\"},\"panelIndex\":\"2e33ade5-96e5-40b4-b460-493e5d4fa834\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"c7851241-5526-499a-960b-357af8c2ce5bX0\":{\"label\":\"Part of Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX1\":{\"label\":\"Part of Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"timeShift\":\"1w\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX2\":{\"label\":\"Part of Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"subtract\",\"args\":[{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"location\":{\"min\":0,\"max\":28},\"text\":\"count() / count(shift='1w') \"},1],\"location\":{\"min\":0,\"max\":31},\"text\":\"count() / count(shift='1w') - 1\"}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5b\":{\"label\":\"Cancelled vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count() / count(shift='1w') - 1\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX2\"],\"customLabel\":true}},\"columnOrder\":[\"c7851241-5526-499a-960b-357af8c2ce5b\",\"c7851241-5526-499a-960b-357af8c2ce5bX2\",\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"c7851241-5526-499a-960b-357af8c2ce5b\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"Cancelled\",\"params\":{\"query\":true},\"index\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"Cancelled\":true}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":8,\"w\":24,\"h\":8,\"i\":\"086ac2e9-dd16-4b45-92b8-1e43ff7e3f65\"},\"panelIndex\":\"086ac2e9-dd16-4b45-92b8-1e43ff7e3f65\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"03c34665-471c-49c7-acf1-5a11f517421c\":{\"columns\":{\"a5b94e30-4e77-4b0a-9187-1d8b13de1456\":{\"label\":\"timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3e267327-7317-4310-aee3-320e0f7c1e70\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\"}},\"columnOrder\":[\"a5b94e30-4e77-4b0a-9187-1d8b13de1456\",\"3e267327-7317-4310-aee3-320e0f7c1e70\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"custom\",\"lowerBound\":0,\"upperBound\":1},\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":false,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"03c34665-471c-49c7-acf1-5a11f517421c\",\"accessors\":[\"3e267327-7317-4310-aee3-320e0f7c1e70\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"a5b94e30-4e77-4b0a-9187-1d8b13de1456\",\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-03c34665-471c-49c7-acf1-5a11f517421c\",\"type\":\"index-pattern\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[Flights] Flight count\"},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":8,\"w\":24,\"h\":28,\"i\":\"fb86b32f-fb7a-45cf-9511-f366fef51bbd\"},\"panelIndex\":\"fb86b32f-fb7a-45cf-9511-f366fef51bbd\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Cities by delay, cancellation\",\"type\":\"lens\",\"visualizationType\":\"lnsDatatable\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"f26e8f7a-4118-4227-bea0-5c02d8b270f7\":{\"columns\":{\"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0\":{\"label\":\"Top values of OriginCityName\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"OriginCityName\",\"isBucketed\":true,\"params\":{\"size\":1000,\"orderBy\":{\"type\":\"alphabetical\",\"fallback\":true},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\":{\"label\":\"Part of Delay %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"FlightDelay : true \",\"language\":\"kuery\"},\"customLabel\":true},\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\":{\"label\":\"Part of Delay %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"52f6f2e9-6242-4c44-be63-b799150e7e60X2\":{\"label\":\"Part of Delay %\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\"],\"location\":{\"min\":0,\"max\":42},\"text\":\"count(kql='FlightDelay : true ') / count()\"}},\"references\":[\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\"],\"customLabel\":true},\"52f6f2e9-6242-4c44-be63-b799150e7e60\":{\"label\":\"Delay %\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='FlightDelay : true ') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":0}}},\"references\":[\"52f6f2e9-6242-4c44-be63-b799150e7e60X2\"],\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\":{\"label\":\"Part of Cancel %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"Cancelled: true\",\"language\":\"kuery\"},\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\":{\"label\":\"Part of Cancel %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2\":{\"label\":\"Part of Cancel %\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\"],\"location\":{\"min\":0,\"max\":38},\"text\":\"count(kql='Cancelled: true') / count()\"}},\"references\":[\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\"],\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6\":{\"label\":\"Cancel %\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='Cancelled: true') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":0}}},\"references\":[\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2\"],\"customLabel\":true}},\"columnOrder\":[\"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X2\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"columns\":[{\"isTransposed\":false,\"columnId\":\"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0\",\"width\":262.75},{\"columnId\":\"52f6f2e9-6242-4c44-be63-b799150e7e60\",\"isTransposed\":false,\"width\":302.5,\"colorMode\":\"cell\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#f7e0b8\",\"stop\":0.6},{\"color\":\"#e7664c\",\"stop\":1}],\"name\":\"custom\",\"colorStops\":[{\"color\":\"#f7e0b8\",\"stop\":0.2},{\"color\":\"#e7664c\",\"stop\":0.6}],\"rangeType\":\"number\",\"rangeMin\":0.2,\"rangeMax\":0.6}},\"alignment\":\"center\"},{\"columnId\":\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6\",\"isTransposed\":false,\"alignment\":\"center\",\"colorMode\":\"cell\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#f7e0b8\",\"stop\":0.6},{\"color\":\"#e7664c\",\"stop\":0.6666666666666666}],\"rangeType\":\"number\",\"name\":\"custom\",\"colorStops\":[{\"color\":\"#f7e0b8\",\"stop\":0.2},{\"color\":\"#e7664c\",\"stop\":0.6}],\"rangeMin\":0.2,\"rangeMax\":0.6}}}],\"layerId\":\"f26e8f7a-4118-4227-bea0-5c02d8b270f7\",\"sorting\":{\"columnId\":\"52f6f2e9-6242-4c44-be63-b799150e7e60\",\"direction\":\"desc\"},\"layerType\":\"data\",\"rowHeight\":\"single\",\"rowHeightLines\":1},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-f26e8f7a-4118-4227-bea0-5c02d8b270f7\",\"type\":\"index-pattern\"}]},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"[Flights] Most delayed cities\"},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":25,\"w\":24,\"h\":11,\"i\":\"0cc42484-16f7-42ec-b38c-9bf8be69cde7\"},\"panelIndex\":\"0cc42484-16f7-42ec-b38c-9bf8be69cde7\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"e80cc05e-c52a-4e5f-ac71-4b37274867f5\":{\"columns\":{\"caf7421e-93a3-439e-ab0a-fbdead93c21c\":{\"label\":\"Top values of FlightDelayType\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"FlightDelayType\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"0233d302-ec81-4fbe-96cb-7fac84cf035c\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"13ec79e3-9d73-4536-9056-3d92802bb30a\":{\"label\":\"timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"0233d302-ec81-4fbe-96cb-7fac84cf035c\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\"}},\"columnOrder\":[\"caf7421e-93a3-439e-ab0a-fbdead93c21c\",\"13ec79e3-9d73-4536-9056-3d92802bb30a\",\"0233d302-ec81-4fbe-96cb-7fac84cf035c\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":false,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_percentage_stacked\",\"layers\":[{\"layerId\":\"e80cc05e-c52a-4e5f-ac71-4b37274867f5\",\"accessors\":[\"0233d302-ec81-4fbe-96cb-7fac84cf035c\"],\"position\":\"top\",\"seriesType\":\"bar_percentage_stacked\",\"showGridlines\":false,\"palette\":{\"type\":\"palette\",\"name\":\"cool\"},\"xAccessor\":\"13ec79e3-9d73-4536-9056-3d92802bb30a\",\"splitAccessor\":\"caf7421e-93a3-439e-ab0a-fbdead93c21c\",\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-e80cc05e-c52a-4e5f-ac71-4b37274867f5\",\"type\":\"index-pattern\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[Flights] Delay Type\"},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":36,\"w\":12,\"h\":11,\"i\":\"5d53db36-2d5a-4adc-af7b-cec4c1a294e0\"},\"panelIndex\":\"5d53db36-2d5a-4adc-af7b-cec4c1a294e0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"0c8e136b-a822-4fb3-836d-e06cbea4eea4\":{\"columns\":{\"d1cee8bf-34cf-4141-99d7-ff043ee77b56\":{\"label\":\"Top values of FlightDelayType\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"FlightDelayType\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"aa152ace-ee2d-447b-b86d-459bef4d7880\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"aa152ace-ee2d-447b-b86d-459bef4d7880\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\"}},\"columnOrder\":[\"d1cee8bf-34cf-4141-99d7-ff043ee77b56\",\"aa152ace-ee2d-447b-b86d-459bef4d7880\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"pie\",\"palette\":{\"type\":\"palette\",\"name\":\"cool\"},\"layers\":[{\"layerId\":\"0c8e136b-a822-4fb3-836d-e06cbea4eea4\",\"metric\":\"aa152ace-ee2d-447b-b86d-459bef4d7880\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false,\"layerType\":\"data\",\"legendSize\":\"auto\",\"primaryGroups\":[\"d1cee8bf-34cf-4141-99d7-ff043ee77b56\"]}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"phrase\",\"key\":\"FlightDelayType\",\"params\":{\"query\":\"No Delay\"},\"disabled\":false,\"negate\":true,\"alias\":null,\"index\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"FlightDelayType\":\"No Delay\"}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-0c8e136b-a822-4fb3-836d-e06cbea4eea4\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\"}]},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"[Flights] Delay Type\"}]", + "panelsJSON": "[{\"version\":\"8.6.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":47,\"w\":48,\"h\":15,\"i\":\"4\"},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":16,\"w\":24,\"h\":9,\"i\":\"7\"},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":36,\"w\":24,\"h\":11,\"i\":\"10\"},\"panelIndex\":\"10\",\"embeddableConfig\":{\"vis\":{\"colors\":{\"Count\":\"#1F78C1\"},\"legendOpen\":false},\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":36,\"y\":36,\"w\":12,\"h\":11,\"i\":\"21\"},\"panelIndex\":\"21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_21\"},{\"version\":\"8.6.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":8,\"i\":\"6afc61f7-e2d5-45a3-9e7a-281160ad3eb9\"},\"panelIndex\":\"6afc61f7-e2d5-45a3-9e7a-281160ad3eb9\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"[Flights] Markdown Instructions\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":10,\"openLinksInNewTab\":true,\"markdown\":\"## Sample Flight data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html).\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{}}},\"hidePanelTitles\":true,\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":8,\"h\":8,\"i\":\"392b4936-f753-47bc-a98d-a4e41a0a4cd4\"},\"panelIndex\":\"392b4936-f753-47bc-a98d-a4e41a0a4cd4\",\"embeddableConfig\":{\"enhancements\":{},\"attributes\":{\"title\":\"[Flights] Total Flights\",\"description\":\"\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"8fa993db-c147-4954-adf7-4ff264d42576\":{\"columns\":{\"81124c45-6ab6-42f4-8859-495d55eb8065\":{\"label\":\"Total flights\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true}},\"columnOrder\":[\"81124c45-6ab6-42f4-8859-495d55eb8065\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"8fa993db-c147-4954-adf7-4ff264d42576\",\"accessor\":\"81124c45-6ab6-42f4-8859-495d55eb8065\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-8fa993db-c147-4954-adf7-4ff264d42576\",\"type\":\"index-pattern\"}]},\"hidePanelTitles\":true}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":32,\"y\":0,\"w\":8,\"h\":4,\"i\":\"9271deff-5a61-4665-83fc-f9fdc6bf0c0b\"},\"panelIndex\":\"9271deff-5a61-4665-83fc-f9fdc6bf0c0b\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\":{\"label\":\"Part of count(kql='FlightDelay : true') / count()\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"FlightDelay : true\",\"language\":\"kuery\"},\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\":{\"label\":\"Part of count(kql='FlightDelay : true') / count()\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\":{\"label\":\"Part of count(kql='FlightDelay : true') / count()\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"location\":{\"min\":0,\"max\":41},\"text\":\"count(kql='FlightDelay : true') / count()\"}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\":{\"label\":\"Delayed\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='FlightDelay : true') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"customLabel\":true}},\"columnOrder\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":40,\"y\":0,\"w\":8,\"h\":4,\"i\":\"aa591c29-1a31-4ee1-a71d-b829c06fd162\"},\"panelIndex\":\"aa591c29-1a31-4ee1-a71d-b829c06fd162\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"c7851241-5526-499a-960b-357af8c2ce5bX0\":{\"label\":\"Part of Delayed\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX1\":{\"label\":\"Part of Delayed\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"timeShift\":\"1w\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX2\":{\"label\":\"Part of Delayed\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"subtract\",\"args\":[{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"location\":{\"min\":0,\"max\":28},\"text\":\"count() / count(shift='1w') \"},1],\"location\":{\"min\":0,\"max\":31},\"text\":\"count() / count(shift='1w') - 1\"}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5b\":{\"label\":\"Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count() / count(shift='1w') - 1\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX2\"],\"customLabel\":true}},\"columnOrder\":[\"c7851241-5526-499a-960b-357af8c2ce5b\",\"c7851241-5526-499a-960b-357af8c2ce5bX2\",\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"c7851241-5526-499a-960b-357af8c2ce5b\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"FlightDelay\",\"params\":{\"query\":true},\"index\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"FlightDelay\":true}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":32,\"y\":4,\"w\":8,\"h\":4,\"i\":\"b766e3b8-4544-46ed-99e6-9ecc4847e2a2\"},\"panelIndex\":\"b766e3b8-4544-46ed-99e6-9ecc4847e2a2\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\":{\"label\":\"Part of Cancelled\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"Cancelled : true\",\"language\":\"kuery\"},\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\":{\"label\":\"Part of Cancelled\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\":{\"label\":\"Part of Cancelled\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"location\":{\"min\":0,\"max\":39},\"text\":\"count(kql='Cancelled : true') / count()\"}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\"],\"customLabel\":true},\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\":{\"label\":\"Cancelled\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='Cancelled : true') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"customLabel\":true}},\"columnOrder\":[\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX0\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX1\",\"7e8fe9b1-f45c-4f3d-9561-30febcd357ecX2\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"7e8fe9b1-f45c-4f3d-9561-30febcd357ec\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":40,\"y\":4,\"w\":8,\"h\":4,\"i\":\"2e33ade5-96e5-40b4-b460-493e5d4fa834\"},\"panelIndex\":\"2e33ade5-96e5-40b4-b460-493e5d4fa834\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsLegacyMetric\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"b4712d43-1e84-4f5b-878d-8e38ba748317\":{\"columns\":{\"c7851241-5526-499a-960b-357af8c2ce5bX0\":{\"label\":\"Part of Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX1\":{\"label\":\"Part of Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"timeShift\":\"1w\",\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5bX2\":{\"label\":\"Part of Delayed vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"subtract\",\"args\":[{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"location\":{\"min\":0,\"max\":28},\"text\":\"count() / count(shift='1w') \"},1],\"location\":{\"min\":0,\"max\":31},\"text\":\"count() / count(shift='1w') - 1\"}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"customLabel\":true},\"c7851241-5526-499a-960b-357af8c2ce5b\":{\"label\":\"Cancelled vs 1 week earlier\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count() / count(shift='1w') - 1\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":1}}},\"references\":[\"c7851241-5526-499a-960b-357af8c2ce5bX2\"],\"customLabel\":true}},\"columnOrder\":[\"c7851241-5526-499a-960b-357af8c2ce5b\",\"c7851241-5526-499a-960b-357af8c2ce5bX2\",\"c7851241-5526-499a-960b-357af8c2ce5bX0\",\"c7851241-5526-499a-960b-357af8c2ce5bX1\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"b4712d43-1e84-4f5b-878d-8e38ba748317\",\"accessor\":\"c7851241-5526-499a-960b-357af8c2ce5b\",\"layerType\":\"data\",\"textAlign\":\"center\",\"titlePosition\":\"bottom\",\"size\":\"xl\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"Cancelled\",\"params\":{\"query\":true},\"index\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"Cancelled\":true}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-b4712d43-1e84-4f5b-878d-8e38ba748317\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\"}]},\"enhancements\":{}}},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":8,\"w\":24,\"h\":8,\"i\":\"086ac2e9-dd16-4b45-92b8-1e43ff7e3f65\"},\"panelIndex\":\"086ac2e9-dd16-4b45-92b8-1e43ff7e3f65\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"03c34665-471c-49c7-acf1-5a11f517421c\":{\"columns\":{\"a5b94e30-4e77-4b0a-9187-1d8b13de1456\":{\"label\":\"timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"3e267327-7317-4310-aee3-320e0f7c1e70\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\"}},\"columnOrder\":[\"a5b94e30-4e77-4b0a-9187-1d8b13de1456\",\"3e267327-7317-4310-aee3-320e0f7c1e70\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"custom\",\"lowerBound\":0,\"upperBound\":1},\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":false,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"03c34665-471c-49c7-acf1-5a11f517421c\",\"accessors\":[\"3e267327-7317-4310-aee3-320e0f7c1e70\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"a5b94e30-4e77-4b0a-9187-1d8b13de1456\",\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-03c34665-471c-49c7-acf1-5a11f517421c\",\"type\":\"index-pattern\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[Flights] Flight count\"},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":8,\"w\":24,\"h\":28,\"i\":\"fb86b32f-fb7a-45cf-9511-f366fef51bbd\"},\"panelIndex\":\"fb86b32f-fb7a-45cf-9511-f366fef51bbd\",\"embeddableConfig\":{\"attributes\":{\"title\":\"Cities by delay, cancellation\",\"type\":\"lens\",\"visualizationType\":\"lnsDatatable\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"f26e8f7a-4118-4227-bea0-5c02d8b270f7\":{\"columns\":{\"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0\":{\"label\":\"Top values of OriginCityName\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"OriginCityName\",\"isBucketed\":true,\"params\":{\"size\":1000,\"orderBy\":{\"type\":\"alphabetical\",\"fallback\":true},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\":{\"label\":\"Part of Delay %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"FlightDelay : true \",\"language\":\"kuery\"},\"customLabel\":true},\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\":{\"label\":\"Part of Delay %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"52f6f2e9-6242-4c44-be63-b799150e7e60X2\":{\"label\":\"Part of Delay %\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\"],\"location\":{\"min\":0,\"max\":42},\"text\":\"count(kql='FlightDelay : true ') / count()\"}},\"references\":[\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\"],\"customLabel\":true},\"52f6f2e9-6242-4c44-be63-b799150e7e60\":{\"label\":\"Delay %\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='FlightDelay : true ') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":0}}},\"references\":[\"52f6f2e9-6242-4c44-be63-b799150e7e60X2\"],\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\":{\"label\":\"Part of Cancel %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"filter\":{\"query\":\"Cancelled: true\",\"language\":\"kuery\"},\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\":{\"label\":\"Part of Cancel %\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\",\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2\":{\"label\":\"Part of Cancel %\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"divide\",\"args\":[\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\"],\"location\":{\"min\":0,\"max\":38},\"text\":\"count(kql='Cancelled: true') / count()\"}},\"references\":[\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\"],\"customLabel\":true},\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6\":{\"label\":\"Cancel %\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"count(kql='Cancelled: true') / count()\",\"isFormulaBroken\":false,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":0}}},\"references\":[\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2\"],\"customLabel\":true}},\"columnOrder\":[\"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X0\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X1\",\"52f6f2e9-6242-4c44-be63-b799150e7e60X2\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X0\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X1\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6X2\",\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"columns\":[{\"isTransposed\":false,\"columnId\":\"3dd24cb4-45ef-4dd8-b22a-d7b802cb6da0\",\"width\":262.75},{\"columnId\":\"52f6f2e9-6242-4c44-be63-b799150e7e60\",\"isTransposed\":false,\"width\":302.5,\"colorMode\":\"cell\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#f7e0b8\",\"stop\":0.6},{\"color\":\"#e7664c\",\"stop\":1}],\"name\":\"custom\",\"colorStops\":[{\"color\":\"#f7e0b8\",\"stop\":0.2},{\"color\":\"#e7664c\",\"stop\":0.6}],\"rangeType\":\"number\",\"rangeMin\":0.2,\"rangeMax\":0.6}},\"alignment\":\"center\"},{\"columnId\":\"7b9f3ece-9da3-4c27-b582-d3f8e8cc31d6\",\"isTransposed\":false,\"alignment\":\"center\",\"colorMode\":\"cell\",\"palette\":{\"name\":\"custom\",\"type\":\"palette\",\"params\":{\"steps\":5,\"stops\":[{\"color\":\"#f7e0b8\",\"stop\":0.6},{\"color\":\"#e7664c\",\"stop\":0.6666666666666666}],\"rangeType\":\"number\",\"name\":\"custom\",\"colorStops\":[{\"color\":\"#f7e0b8\",\"stop\":0.2},{\"color\":\"#e7664c\",\"stop\":0.6}],\"rangeMin\":0.2,\"rangeMax\":0.6}}}],\"layerId\":\"f26e8f7a-4118-4227-bea0-5c02d8b270f7\",\"sorting\":{\"columnId\":\"52f6f2e9-6242-4c44-be63-b799150e7e60\",\"direction\":\"desc\"},\"layerType\":\"data\",\"rowHeight\":\"single\",\"rowHeightLines\":1},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-f26e8f7a-4118-4227-bea0-5c02d8b270f7\",\"type\":\"index-pattern\"}]},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"[Flights] Most delayed cities\"},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":25,\"w\":24,\"h\":11,\"i\":\"0cc42484-16f7-42ec-b38c-9bf8be69cde7\"},\"panelIndex\":\"0cc42484-16f7-42ec-b38c-9bf8be69cde7\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"e80cc05e-c52a-4e5f-ac71-4b37274867f5\":{\"columns\":{\"caf7421e-93a3-439e-ab0a-fbdead93c21c\":{\"label\":\"Top values of FlightDelayType\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"FlightDelayType\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"0233d302-ec81-4fbe-96cb-7fac84cf035c\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"13ec79e3-9d73-4536-9056-3d92802bb30a\":{\"label\":\"timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true}},\"0233d302-ec81-4fbe-96cb-7fac84cf035c\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\"}},\"columnOrder\":[\"caf7421e-93a3-439e-ab0a-fbdead93c21c\",\"13ec79e3-9d73-4536-9056-3d92802bb30a\",\"0233d302-ec81-4fbe-96cb-7fac84cf035c\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"legendSize\":\"auto\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":false,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_percentage_stacked\",\"layers\":[{\"layerId\":\"e80cc05e-c52a-4e5f-ac71-4b37274867f5\",\"accessors\":[\"0233d302-ec81-4fbe-96cb-7fac84cf035c\"],\"position\":\"top\",\"seriesType\":\"bar_percentage_stacked\",\"showGridlines\":false,\"palette\":{\"type\":\"palette\",\"name\":\"cool\"},\"xAccessor\":\"13ec79e3-9d73-4536-9056-3d92802bb30a\",\"splitAccessor\":\"caf7421e-93a3-439e-ab0a-fbdead93c21c\",\"layerType\":\"data\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-e80cc05e-c52a-4e5f-ac71-4b37274867f5\",\"type\":\"index-pattern\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[Flights] Delay Type\"},{\"version\":\"8.6.0\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":36,\"w\":12,\"h\":11,\"i\":\"5d53db36-2d5a-4adc-af7b-cec4c1a294e0\"},\"panelIndex\":\"5d53db36-2d5a-4adc-af7b-cec4c1a294e0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"formBased\":{\"layers\":{\"0c8e136b-a822-4fb3-836d-e06cbea4eea4\":{\"columns\":{\"d1cee8bf-34cf-4141-99d7-ff043ee77b56\":{\"label\":\"Top values of FlightDelayType\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"FlightDelayType\",\"isBucketed\":true,\"params\":{\"size\":10,\"orderBy\":{\"type\":\"column\",\"columnId\":\"aa152ace-ee2d-447b-b86d-459bef4d7880\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"aa152ace-ee2d-447b-b86d-459bef4d7880\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"___records___\"}},\"columnOrder\":[\"d1cee8bf-34cf-4141-99d7-ff043ee77b56\",\"aa152ace-ee2d-447b-b86d-459bef4d7880\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"pie\",\"palette\":{\"type\":\"palette\",\"name\":\"cool\"},\"layers\":[{\"layerId\":\"0c8e136b-a822-4fb3-836d-e06cbea4eea4\",\"metrics\":[\"aa152ace-ee2d-447b-b86d-459bef4d7880\"],\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false,\"layerType\":\"data\",\"legendSize\":\"auto\",\"primaryGroups\":[\"d1cee8bf-34cf-4141-99d7-ff043ee77b56\"]}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"type\":\"phrase\",\"key\":\"FlightDelayType\",\"params\":{\"query\":\"No Delay\"},\"disabled\":false,\"negate\":true,\"alias\":null,\"index\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"FlightDelayType\":\"No Delay\"}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"indexpattern-datasource-layer-0c8e136b-a822-4fb3-836d-e06cbea4eea4\",\"type\":\"index-pattern\"},{\"id\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\"}]},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"[Flights] Delay Type\"}]", "refreshInterval": { "pause": true, "value": 0 diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/__snapshots__/most_used_chart.test.tsx.snap b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/__snapshots__/most_used_chart.test.tsx.snap index 5dcaf604acfb9..bc2ecef25d310 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/__snapshots__/most_used_chart.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/__snapshots__/most_used_chart.test.tsx.snap @@ -86,7 +86,9 @@ Object { "layerType": "data", "legendDisplay": "hide", "legendPosition": "bottom", - "metric": "countColumn", + "metrics": Array [ + "countColumn", + ], "nestedLegend": false, "numberDisplay": "percent", "primaryGroups": Array [ diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/get_lens_attributes.ts b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/get_lens_attributes.ts index bd43169f94399..c311fa561f81c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/get_lens_attributes.ts +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_charts/most_used_chart/get_lens_attributes.ts @@ -93,7 +93,7 @@ export function getLensAttributes({ { layerId: metricId, primaryGroups: [columnA], - metric: columnB, + metrics: [columnB], categoryDisplay: 'default', legendDisplay: 'hide', nestedLegend: false, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie_vis/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie_vis/index.ts index e7ece1155560a..52368f2718843 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie_vis/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie_vis/index.ts @@ -16,6 +16,6 @@ export const pieVis: ElementFactory = () => ({ | selectFilter | demodata | head 10 -| pieVis metric={visdimension "age"} buckets={visdimension "project"} buckets={visdimension "cost"} legendDisplay="default" +| pieVis metrics={visdimension "age"} buckets={visdimension "project"} buckets={visdimension "cost"} legendDisplay="default" | render`, }); diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 3bf19d4d78b5c..c852acd9f1953 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -52,9 +52,10 @@ export enum EmptySizeRatios { } export interface SharedPieLayerState { + metrics: string[]; primaryGroups: string[]; secondaryGroups?: string[]; - metric?: string; + allowMultipleMetrics?: boolean; collapseFns?: Record; numberDisplay: NumberDisplayType; categoryDisplay: CategoryDisplayType; diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 5409b135b4b98..a464e12c80d1c 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -618,22 +618,6 @@ export function getFormBasedDatasource({ getDropProps, onDrop, - getSupportedActionsForLayer(layerId, state, _, openLayerSettings) { - if (!openLayerSettings) { - return []; - } - return [ - { - displayName: i18n.translate('xpack.lens.indexPattern.layerSettingsAction', { - defaultMessage: 'Layer settings', - }), - execute: openLayerSettings, - icon: 'gear', - isCompatible: Boolean(state.layers[layerId]), - 'data-test-subj': 'lnsLayerSettings', - }, - ]; - }, getCustomWorkspaceRenderer: ( state: FormBasedPrivateState, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/clone_layer_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/clone_layer_action.tsx index 97d0cf73b80dd..64a59df81c911 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/clone_layer_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/clone_layer_action.tsx @@ -22,6 +22,7 @@ export const getCloneLayerAction = (props: CloneLayerAction): LayerAction => { }); return { + id: 'cloneLayerAction', execute: props.execute, displayName, isCompatible: Boolean(props.activeVisualization.cloneLayer && !props.isTextBasedLanguage), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/layer_actions.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/layer_actions.tsx index 9c32a24eac4dd..397325be3f4d7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/layer_actions.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/layer_actions.tsx @@ -22,6 +22,7 @@ import type { LayerType } from '../../../..'; import type { LayerAction, Visualization } from '../../../../types'; import { getCloneLayerAction } from './clone_layer_action'; import { getRemoveLayerAction } from './remove_layer_action'; +import { getOpenLayerSettingsAction } from './open_layer_settings'; export interface LayerActionsProps { layerIndex: number; @@ -36,18 +37,28 @@ export const getSharedActions = ({ activeVisualization, isOnlyLayer, isTextBasedLanguage, + hasLayerSettings, + openLayerSettings, onCloneLayer, onRemoveLayer, }: { onRemoveLayer: () => void; onCloneLayer: () => void; layerIndex: number; + layerId: string; isOnlyLayer: boolean; activeVisualization: Visualization; + visualizationState: unknown; layerType?: LayerType; isTextBasedLanguage?: boolean; + hasLayerSettings: boolean; + openLayerSettings: () => void; core: Pick; }) => [ + getOpenLayerSettingsAction({ + hasLayerSettings, + openLayerSettings, + }), getCloneLayerAction({ execute: onCloneLayer, layerIndex, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/open_layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/open_layer_settings.tsx new file mode 100644 index 0000000000000..13570df68c581 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/open_layer_settings.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { LayerAction } from '../../../../types'; + +export const getOpenLayerSettingsAction = (props: { + openLayerSettings: () => void; + hasLayerSettings: boolean; +}): LayerAction => { + const displayName = i18n.translate('xpack.lens.layerActions.layerSettingsAction', { + defaultMessage: 'Layer settings', + }); + + return { + id: 'openLayerSettings', + displayName, + execute: props.openLayerSettings, + icon: 'gear', + isCompatible: props.hasLayerSettings, + 'data-test-subj': 'lnsLayerSettings', + }; +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/remove_layer_action.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/remove_layer_action.tsx index 5156fd7efd755..c9a353f07d14b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/remove_layer_action.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions/remove_layer_action.tsx @@ -207,6 +207,7 @@ export const getRemoveLayerAction = (props: RemoveLayerAction): LayerAction => { ); return { + id: 'removeLayerAction', execute: async () => { const storage = new Storage(localStorage); const lensLocalStorage = storage.get(LOCAL_STORAGE_LENS_KEY) ?? {}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 173378c6fb9ef..7389f423b7a85 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -698,6 +698,54 @@ describe('LayerPanel', () => { expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled(); expect(updateDatasource).toHaveBeenCalledWith('testDatasource', { newState: true }); }); + + it('should display the fake final accessor if present in the group config', async () => { + const fakeAccessorLabel = "I'm a fake!"; + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }], + filterOperations: () => true, + fakeFinalAccessor: { + label: fakeAccessorLabel, + }, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const { instance } = await mountWithProvider(); + + expect(instance.exists('[data-test-subj="lns-fakeDimension"]')).toBeTruthy(); + expect( + instance + .find('[data-test-subj="lns-fakeDimension"] .lnsLayerPanel__triggerTextLabel') + .text() + ).toBe(fakeAccessorLabel); + }); + + it('should not display the fake final accessor if not present in the group config', async () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }], + filterOperations: () => true, + fakeFinalAccessor: undefined, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const { instance } = await mountWithProvider(); + + expect(instance.exists('[data-test-subj="lns-fakeDimension"]')).toBeFalsy(); + }); }); // This test is more like an integration test, since the layer panel owns all diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 656daef46fccd..0e759db7fc9fc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -18,6 +18,8 @@ import { EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; import { LayerType } from '../../../../common'; import { LayerActions } from './layer_actions'; import { IndexPatternServiceAPI } from '../../../data_views_service/service'; @@ -28,6 +30,7 @@ import { DragDropOperation, DropType, isOperation, + LayerAction, VisualizationDimensionGroupConfig, } from '../../../types'; import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; @@ -48,13 +51,13 @@ import { onDropForVisualization, shouldRemoveSource } from './buttons/drop_targe import { getSharedActions } from './layer_actions/layer_actions'; import { FlyoutContainer } from './flyout_container'; +// hide the random sampling settings from the UI +const DISPLAY_RANDOM_SAMPLING_SETTINGS = false; + const initialActiveDimensionState = { isNew: false, }; -// hide the random sampling settings from the UI -const DISPLAY_RANDOM_SAMPLING_SETTINGS = false; - export function LayerPanel( props: Exclude & { activeVisualization: Visualization; @@ -317,30 +320,33 @@ export function LayerPanel( const [datasource] = Object.values(framePublicAPI.datasourceLayers); const isTextBasedLanguage = Boolean(datasource?.isTextBasedLanguage()); - const compatibleActions = useMemo( + const compatibleActions = useMemo( () => [ - ...(activeVisualization.getSupportedActionsForLayer?.( - layerId, - visualizationState, - updateVisualization, - () => setPanelSettingsOpen(true) - ) || []), - ...((DISPLAY_RANDOM_SAMPLING_SETTINGS && - layerDatasource?.getSupportedActionsForLayer?.( - layerId, - layerDatasourceState, - (newState) => updateDatasource(datasourceId, newState), - () => setPanelSettingsOpen(true) - )) || - []), + ...(activeVisualization + .getSupportedActionsForLayer?.(layerId, visualizationState) + .map((action) => ({ + ...action, + execute: () => { + updateVisualization( + activeVisualization.onLayerAction?.(layerId, action.id, visualizationState) + ); + }, + })) || []), ...getSharedActions({ + layerId, activeVisualization, + visualizationState, core, layerIndex, layerType: activeVisualization.getLayerType(layerId, visualizationState), isOnlyLayer, isTextBasedLanguage, + hasLayerSettings: Boolean( + activeVisualization.renderLayerSettings || + (layerDatasource?.renderLayerSettings && DISPLAY_RANDOM_SAMPLING_SETTINGS) + ), + openLayerSettings: () => setPanelSettingsOpen(true), onCloneLayer, onRemoveLayer: () => onRemoveLayer(layerId), }), @@ -348,16 +354,13 @@ export function LayerPanel( [ activeVisualization, core, - datasourceId, isOnlyLayer, isTextBasedLanguage, layerDatasource, - layerDatasourceState, layerId, layerIndex, onCloneLayer, onRemoveLayer, - updateDatasource, updateVisualization, visualizationState, ] @@ -599,6 +602,30 @@ export function LayerPanel( ) : null} + {group.fakeFinalAccessor && ( +
+ + + {group.fakeFinalAccessor.label} + + +
+ )} + {group.supportsMoreColumns ? ( - {((DISPLAY_RANDOM_SAMPLING_SETTINGS && layerDatasource?.renderLayerSettings) || - activeVisualization?.renderLayerSettings) && ( + {(layerDatasource?.renderLayerSettings || activeVisualization?.renderLayerSettings) && ( (settingsPanelRef.current = el)} isOpen={isPanelSettingsOpen} @@ -661,11 +687,14 @@ export function LayerPanel( >
- {DISPLAY_RANDOM_SAMPLING_SETTINGS && layerDatasource?.renderLayerSettings && ( - + {layerDatasource?.renderLayerSettings && DISPLAY_RANDOM_SAMPLING_SETTINGS && ( + <> + + + )} {activeVisualization?.renderLayerSettings && ( { */ getUsedDataViews: (state: T) => string[]; - getSupportedActionsForLayer?: ( - layerId: string, - state: T, - setState: StateSetter, - openLayerSettings?: () => void - ) => LayerAction[]; - getDatasourceInfo: ( state: T, references?: SavedObjectReference[], @@ -581,6 +574,7 @@ export interface DatasourceDataPanelProps { /** @internal **/ export interface LayerAction { + id: string; displayName: string; description?: string; execute: () => void | Promise; @@ -590,6 +584,8 @@ export interface LayerAction { 'data-test-subj'?: string; } +export type LayerActionFromVisualization = Omit; + interface SharedDimensionProps { /** Visualizations can restrict operations based on their own rules. * For example, limiting to only bucketed or only numeric operations. @@ -796,6 +792,10 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { /** ID is passed back to visualization. For example, `x` */ groupId: string; accessors: AccessorConfig[]; + // currently used only on partition charts to display non-editable UI dimension trigger in the buckets group when multiple metrics exist + fakeFinalAccessor?: { + label: string; + }; supportsMoreColumns: boolean; dimensionsTooMany?: number; /** If required, a warning will appear if accessors are empty */ @@ -1065,12 +1065,13 @@ export interface Visualization { * returns a list of custom actions supported by the visualization layer. * Default actions like delete/clear are not included in this list and are managed by the editor frame * */ - getSupportedActionsForLayer?: ( - layerId: string, - state: T, - setState: StateSetter, - openLayerSettings?: () => void - ) => LayerAction[]; + getSupportedActionsForLayer?: (layerId: string, state: T) => LayerActionFromVisualization[]; + + /** + * Perform state mutations in response to a layer action + */ + onLayerAction?: (layerId: string, actionId: string, state: T) => T; + /** returns the type string of the given layer */ getLayerType: (layerId: string, state?: T) => LayerType | undefined; diff --git a/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx new file mode 100644 index 0000000000000..9acedbc9b8dd2 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { PieLayerState, PieVisualizationState } from '../..'; +import { LayerSettings } from './layer_settings'; +import { FramePublicAPI, VisualizationLayerSettingsProps } from '../../types'; +import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; + +describe('layer settings', () => { + describe('multiple metrics switch', () => { + const getState = (allowMultipleMetrics: boolean): PieVisualizationState => ({ + shape: 'pie', + layers: [ + { + layerId, + allowMultipleMetrics, + } as PieLayerState, + ], + }); + + const layerId = 'layer-id'; + const props: VisualizationLayerSettingsProps = { + setState: jest.fn(), + layerId, + state: getState(false), + frame: {} as FramePublicAPI, + panelRef: {} as React.MutableRefObject, + }; + + it('toggles multiple metrics', () => { + const toggleOn = () => + shallow() + .find(EuiSwitch) + .props() + .onChange({} as EuiSwitchEvent); + + const toggleOff = () => + shallow() + .find(EuiSwitch) + .props() + .onChange({} as EuiSwitchEvent); + + expect(props.setState).not.toHaveBeenCalled(); + + toggleOn(); + + expect(props.setState).toHaveBeenLastCalledWith({ + ...props.state, + layers: [ + { + ...props.state.layers[0], + allowMultipleMetrics: true, + }, + ], + }); + + toggleOff(); + + expect(props.setState).toHaveBeenLastCalledWith({ + ...props.state, + layers: [ + { + ...props.state.layers[0], + allowMultipleMetrics: false, + }, + ], + }); + }); + + test('switch reflects state', () => { + const isChecked = (state: PieVisualizationState) => + shallow() + .find(EuiSwitch) + .props().checked; + + expect(isChecked(getState(false))).toBeFalsy(); + expect(isChecked(getState(true))).toBeTruthy(); + }); + + test('hides option for mosaic', () => { + expect( + shallow( + + ).isEmptyRender() + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx new file mode 100644 index 0000000000000..6876b218c235e --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/partition/layer_settings.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { PieChartTypes } from '../../../common'; +import { PieVisualizationState } from '../..'; +import { VisualizationLayerSettingsProps } from '../../types'; + +export function LayerSettings(props: VisualizationLayerSettingsProps) { + if (props.state.shape === PieChartTypes.MOSAIC) { + return null; + } + + const currentLayer = props.state.layers.find((layer) => layer.layerId === props.layerId); + + if (!currentLayer) { + return null; + } + + return ( + <> + + { + props.setState({ + ...props.state, + layers: props.state.layers.map((layer) => + layer.layerId !== props.layerId + ? layer + : { + ...layer, + allowMultipleMetrics: !layer.allowMultipleMetrics, + } + ), + }); + }} + /> + + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts index c7285fb8771a6..0be4fbde6c1d2 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts @@ -16,7 +16,7 @@ import { PieLayerState, PieVisualizationState, } from '../../../common'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import { layerTypes } from '../../../common/layer_types'; describe('suggestions', () => { describe('pie', () => { @@ -64,9 +64,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: [], - metric: 'a', + metrics: ['a'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, @@ -267,6 +267,88 @@ describe('suggestions', () => { ).toHaveLength(0); }); + it('should accept multiple metrics when active and multi-metric', () => { + const chk = suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'b', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'c', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'd', + operation: { label: 'Avg', dataType: 'number' as DataType, isBucketed: false }, + }, + { + columnId: 'e', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: { + shape: PieChartTypes.PIE, + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + primaryGroups: ['a'], + metrics: ['b'], + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, + allowMultipleMetrics: true, + }, + ], + }, + keptLayerIds: ['first'], + }); + + expect(chk).toHaveLength(2); + chk.forEach(({ state }) => { + expect(state.layers[0].allowMultipleMetrics).toBeTruthy(); + expect(state.layers[0].metrics).toEqual(['d', 'e']); + }); + }); + + it('should reject multiple metrics when NOT currently active', () => { + const chk = suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'b', + operation: { label: 'Avg', dataType: 'number' as DataType, isBucketed: false }, + }, + { + columnId: 'c', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + }); + + expect(chk).toHaveLength(0); + }); + it('should reject if there are no buckets and it is not a specific chart type switch', () => { expect( suggestions({ @@ -566,10 +648,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a'], - metric: 'b', - + metrics: ['b'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.INSIDE, legendDisplay: LegendDisplay.SHOW, @@ -590,9 +671,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a'], - metric: 'b', + metrics: ['b'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.INSIDE, legendDisplay: 'show', @@ -623,9 +704,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: [], - metric: 'a', + metrics: ['a'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.DEFAULT, @@ -673,9 +754,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a', 'b'], - metric: 'e', + metrics: ['e'], numberDisplay: NumberDisplay.VALUE, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, @@ -687,7 +768,7 @@ describe('suggestions', () => { ).toHaveLength(0); }); - it('should reject when there are too many metrics', () => { + it('should accept multiple metrics if active visualization', () => { expect( suggestions({ table: { @@ -722,9 +803,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a', 'b'], - metric: 'e', + metrics: ['e'], numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, @@ -733,6 +814,42 @@ describe('suggestions', () => { }, keptLayerIds: ['first'], }) + ).toHaveLength(2); + }); + + it('should reject multiple metrics if not active visualization', () => { + expect( + suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'b', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'c', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'd', + operation: { label: 'Avg', dataType: 'number' as DataType, isBucketed: false }, + }, + { + columnId: 'e', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + }) ).toHaveLength(0); }); @@ -759,9 +876,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a'], - metric: 'b', + metrics: ['b'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.INSIDE, @@ -782,9 +899,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a'], - metric: 'b', + metrics: ['b'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.DEFAULT, // This is changed @@ -816,9 +933,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: [], - metric: 'a', + metrics: ['a'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.DEFAULT, @@ -831,6 +948,50 @@ describe('suggestions', () => { ).toHaveLength(0); }); + it('should turn off multiple metrics for mosaic when switching from other partition type', () => { + const suggs = suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'b', + operation: { label: 'Avg', dataType: 'number' as DataType, isBucketed: false }, + }, + { + columnId: 'c', + operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, + }, + ], + changeType: 'initial', + }, + state: { + shape: PieChartTypes.PIE, + layers: [ + { + layerId: 'first', + layerType: layerTypes.DATA, + primaryGroups: ['a'], + metrics: ['b', 'c'], + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, + allowMultipleMetrics: true, + }, + ], + }, + keptLayerIds: ['first'], + subVisualizationId: 'mosaic', + }); + + expect(suggs).toHaveLength(1); + expect(suggs[0].state.layers[0].allowMultipleMetrics).toBeFalsy(); + }); + it('mosaic type should be hidden from the suggestion list', () => { expect( suggestions({ @@ -858,9 +1019,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a', 'b'], - metric: 'c', + metrics: ['c'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.INSIDE, @@ -893,9 +1054,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: [], - metric: 'a', + metrics: ['a'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.DEFAULT, @@ -931,9 +1092,9 @@ describe('suggestions', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, primaryGroups: ['a', 'b'], - metric: 'c', + metrics: ['c'], numberDisplay: NumberDisplay.HIDDEN, categoryDisplay: CategoryDisplay.INSIDE, legendDisplay: LegendDisplay.SHOW, diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts index 961b6a68e745c..93887559a094e 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts @@ -7,7 +7,6 @@ import { partition } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import type { SuggestionRequest, TableSuggestionColumn, @@ -23,6 +22,7 @@ import { import { isPartitionShape } from '../../../common/visualizations'; import type { PieChartType } from '../../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; +import { layerTypes } from '../..'; function hasIntervalScale(columns: TableSuggestionColumn[]) { return columns.some((col) => col.operation.scale === 'interval'); @@ -80,6 +80,8 @@ export function suggestions({ return []; } + const isActive = Boolean(state); + const [groups, metrics] = partition( // filter out all metrics which are not number based table.columns.filter((col) => col.operation.isBucketed || col.operation.dataType === 'number'), @@ -90,12 +92,11 @@ export function suggestions({ return []; } - if (metrics.length > 1 || groups.length > maximumGroupLength) { + if ((metrics.length > 1 && !isActive) || groups.length > maximumGroupLength) { return []; } const incompleteConfiguration = metrics.length === 0 || groups.length === 0; - const metricColumnId = metrics.length > 0 ? metrics[0].columnId : undefined; if (incompleteConfiguration && state && !subVisualizationId) { // reject incomplete configurations if the sub visualization isn't specifically requested @@ -104,6 +105,8 @@ export function suggestions({ return []; } + const metricColumnIds = metrics.map(({ columnId }) => columnId); + const results: Array> = []; // Histograms are not good for pi. But we should not hide suggestion on switching between partition charts. @@ -131,18 +134,18 @@ export function suggestions({ ...state.layers[0], layerId: table.layerId, primaryGroups: groups.map((col) => col.columnId), - metric: metricColumnId, - layerType: LayerTypes.DATA, + metrics: metricColumnIds, + layerType: layerTypes.DATA, } : { layerId: table.layerId, primaryGroups: groups.map((col) => col.columnId), - metric: metricColumnId, + metrics: metricColumnIds, numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, }, ], }, @@ -178,7 +181,7 @@ export function suggestions({ if ( groups.length <= PartitionChartsMeta.treemap.maxBuckets && - (!subVisualizationId || subVisualizationId === 'treemap') + (!subVisualizationId || subVisualizationId === PieChartTypes.TREEMAP) ) { results.push({ title: i18n.translate('xpack.lens.pie.treemapSuggestionLabel', { @@ -196,22 +199,22 @@ export function suggestions({ ...state.layers[0], layerId: table.layerId, primaryGroups: groups.map((col) => col.columnId), - metric: metricColumnId, + metrics: metricColumnIds, categoryDisplay: state.layers[0].categoryDisplay === CategoryDisplay.INSIDE ? CategoryDisplay.DEFAULT : state.layers[0].categoryDisplay, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, } : { layerId: table.layerId, primaryGroups: groups.map((col) => col.columnId), - metric: metricColumnId, + metrics: metricColumnIds, numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, }, ], }, @@ -244,20 +247,22 @@ export function suggestions({ layerId: table.layerId, primaryGroups: groups[0] ? [groups[0].columnId] : [], secondaryGroups: groups[1] ? [groups[1].columnId] : [], - metric: metricColumnId, + metrics: metricColumnIds, categoryDisplay: CategoryDisplay.DEFAULT, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, + allowMultipleMetrics: false, } : { layerId: table.layerId, primaryGroups: groups[0] ? [groups[0].columnId] : [], secondaryGroups: groups[1] ? [groups[1].columnId] : [], - metric: metricColumnId, + metrics: metricColumnIds, numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, + allowMultipleMetrics: false, }, ], }, @@ -284,20 +289,20 @@ export function suggestions({ ...state.layers[0], layerId: table.layerId, primaryGroups: groups.map((col) => col.columnId), + metrics: metricColumnIds, secondaryGroups: [], - metric: metricColumnId, categoryDisplay: CategoryDisplay.DEFAULT, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, } : { layerId: table.layerId, primaryGroups: groups.map((col) => col.columnId), - metric: metricColumnId, + metrics: metricColumnIds, numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, }, ], }, @@ -309,7 +314,9 @@ export function suggestions({ return [...results] .map((suggestion) => ({ ...suggestion, - score: shouldHideSuggestion ? 0 : suggestion.score + 0.05 * groups.length, + score: shouldHideSuggestion + ? 0 + : suggestion.score + 0.05 * groups.length + 0.01 * metricColumnIds.length, })) .sort((a, b) => b.score - a.score) .map((suggestion) => ({ diff --git a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts index 42371f85e5984..2aa6651865d0c 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts @@ -64,6 +64,20 @@ type GenerateLabelsAstArguments = ( layer: PieLayerState ) => [Ast]; +export const getColumnToLabelMap = ( + columnIds: string[], + datasource: DatasourcePublicAPI | undefined +) => { + const columnToLabel: Record = {}; + columnIds.forEach((accessor) => { + const operation = datasource?.getOperationForColumnId(accessor); + if (operation?.label) { + columnToLabel[accessor] = operation.label; + } + }); + return columnToLabel; +}; + export const getSortedGroups = ( datasource: DatasourcePublicAPI | undefined, layer: PieLayerState, @@ -139,7 +153,12 @@ const generateCommonArguments = ( .filter(({ columnId }) => !isCollapsed(columnId, layer)) .map(({ columnId }) => columnId) .map(prepareDimension), - metric: layer.metric ? prepareDimension(layer.metric) : '', + metrics: (layer.allowMultipleMetrics ? layer.metrics : [layer.metrics[0]]).map( + prepareDimension + ), + metricsToLabels: JSON.stringify( + getColumnToLabelMap(layer.metrics, datasourceLayers[layer.layerId]) + ), legendDisplay: (attributes.isPreview ? LegendDisplay.HIDE : layer.legendDisplay) as PartitionVisLegendDisplay, @@ -189,10 +208,13 @@ const generateTreemapVisAst: GenerateExpressionAstFunction = (...rest) => { ]).toAst(); }; -const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => - buildExpression([ +const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => { + const { metrics, ...args } = generateCommonArguments(...rest); + + return buildExpression([ buildExpressionFunction('mosaicVis', { - ...generateCommonArguments(...rest), + ...{ ...args, metricsToLabels: undefined }, + metric: metrics, // flip order of bucket dimensions so the rows are fetched before the columns to keep them stable buckets: rest[2] .filter(({ columnId }) => !isCollapsed(columnId, rest[3])) @@ -201,6 +223,7 @@ const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => .map(prepareDimension), }), ]).toAst(); +}; const generateWaffleVisAst: GenerateExpressionAstFunction = (...rest) => { const { buckets, nestedLegend, ...args } = generateCommonArguments(...rest); @@ -251,7 +274,7 @@ function expressionHelper( })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); - if (!layer.metric || !operations.length) { + if (!layer.metrics.length) { return null; } const visualizationAst = generateExprAst( @@ -273,7 +296,7 @@ function expressionHelper( .map((columnId) => { return buildExpressionFunction('lens_collapse', { by: groups.filter((chk) => chk !== columnId), - metric: layer.metric ? [layer.metric] : [], + metric: layer.metrics, fn: [layer.collapseFns![columnId]!], }).toAst(); }), diff --git a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx index e92ce29c4d368..cab4781a6a319 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/toolbar.tsx @@ -315,7 +315,7 @@ export function DimensionEditor( } const firstNonCollapsedColumnId = currentLayer.primaryGroups.find( - (columnId) => !isCollapsed(columnId, currentLayer) + (id) => !isCollapsed(id, currentLayer) ); return ( @@ -346,25 +346,29 @@ export function DimensionDataExtraEditor( return ( <> - { - props.setState({ - ...props.state, - layers: props.state.layers.map((layer) => - layer.layerId !== props.layerId - ? layer - : { - ...layer, - collapseFns: { - ...layer.collapseFns, - [props.accessor]: collapseFn, - }, - } - ), - }); - }} - /> + {[...currentLayer.primaryGroups, ...(currentLayer.secondaryGroups ?? [])].includes( + props.accessor + ) && ( + { + props.setState({ + ...props.state, + layers: props.state.layers.map((layer) => + layer.layerId !== props.layerId + ? layer + : { + ...layer, + collapseFns: { + ...layer.collapseFns, + [props.accessor]: collapseFn, + }, + } + ), + }); + }} + /> + )} ); } diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts index a08250cb3d5b9..1266169e6673e 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts @@ -16,16 +16,23 @@ import { import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; -import { FramePublicAPI } from '../../types'; +import { FramePublicAPI, Visualization } from '../../types'; import { themeServiceMock } from '@kbn/core/public/mocks'; import { cloneDeep } from 'lodash'; import { PartitionChartsMeta } from './partition_charts_meta'; import { CollapseFunction } from '../../../common/expressions'; +import { PaletteOutput } from '@kbn/coloring'; jest.mock('../../id_generator'); const LAYER_ID = 'l1'; +const findPrimaryGroup = (config: ReturnType) => + config.groups.find((group) => group.groupId === 'primaryGroups'); + +const findMetricGroup = (config: ReturnType) => + config.groups.find((group) => group.groupId === 'metric'); + const pieVisualization = getPieVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), kibanaTheme: themeServiceMock.createStartContract(), @@ -39,7 +46,7 @@ function getExampleState(): PieVisualizationState { layerId: LAYER_ID, layerType: LayerTypes.DATA, primaryGroups: [], - metric: undefined, + metrics: [], numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, @@ -76,11 +83,22 @@ describe('pie_visualization', () => { }); it("doesn't count collapsed dimensions", () => { - state.layers[0].collapseFns = { + const localState = cloneDeep(state); + localState.layers[0].collapseFns = { [colIds[0]]: 'some-fn' as CollapseFunction, }; - expect(pieVisualization.getErrorMessages(state)).toHaveLength(0); + expect(pieVisualization.getErrorMessages(localState)).toHaveLength(0); + }); + + it('counts multiple metrics as an extra bucket dimension', () => { + const localState = cloneDeep(state); + localState.layers[0].primaryGroups.pop(); + expect(pieVisualization.getErrorMessages(localState)).toHaveLength(0); + + localState.layers[0].metrics.push('one-metric', 'another-metric'); + + expect(pieVisualization.getErrorMessages(localState)).toHaveLength(1); }); }); }); @@ -110,7 +128,7 @@ describe('pie_visualization', () => { categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, - metric: undefined, + metrics: [], }, ], shape: PieChartTypes.DONUT, @@ -153,6 +171,34 @@ describe('pie_visualization', () => { expect(newState.layers[0].collapseFns).not.toHaveProperty('3'); }); + it('removes custom palette if removing final slice-by dimension in multi-metric chart', () => { + const state = getExampleState(); + + state.layers[0].primaryGroups = ['1', '2']; + state.layers[0].allowMultipleMetrics = true; + state.layers[0].metrics = ['3', '4']; + state.palette = {} as PaletteOutput; + + let newState = pieVisualization.removeDimension({ + layerId: LAYER_ID, + columnId: '1', + prevState: state, + frame: mockFrame(), + }); + + expect(newState.layers[0].primaryGroups).toEqual(['2']); + expect(newState.palette).toBeDefined(); + + newState = pieVisualization.removeDimension({ + layerId: LAYER_ID, + columnId: '2', + prevState: newState, + frame: mockFrame(), + }); + + expect(newState.layers[0].primaryGroups).toEqual([]); + expect(newState.palette).toBeUndefined(); + }); }); describe('#getConfiguration', () => { @@ -265,12 +311,128 @@ describe('pie_visualization', () => { layerId: state.layers[0].layerId, }); - expect(getConfig(state).groups[0].supportsMoreColumns).toBeFalsy(); + expect(findPrimaryGroup(getConfig(state))?.supportsMoreColumns).toBeFalsy(); const stateWithCollapsed = cloneDeep(state); stateWithCollapsed.layers[0].collapseFns = { '1': 'sum' }; - expect(getConfig(stateWithCollapsed).groups[0].supportsMoreColumns).toBeTruthy(); + expect(findPrimaryGroup(getConfig(stateWithCollapsed))?.supportsMoreColumns).toBeTruthy(); + }); + + it('counts multiple metrics toward the dimension limits when not mosaic', () => { + const colIds = new Array(PartitionChartsMeta.pie.maxBuckets - 1) + .fill(undefined) + .map((_, i) => String(i + 1)); + + const frame = mockFrame(); + frame.datasourceLayers[LAYER_ID]!.getTableSpec = () => + colIds.map((id) => ({ columnId: id, fields: [] })); + + const state = getExampleState(); + state.layers[0].primaryGroups = colIds; + state.layers[0].allowMultipleMetrics = true; + + const getConfig = (_state: PieVisualizationState) => + pieVisualization.getConfiguration({ + state: _state, + frame, + layerId: state.layers[0].layerId, + }); + + expect(findPrimaryGroup(getConfig(state))?.supportsMoreColumns).toBeTruthy(); + + const stateWithMultipleMetrics = cloneDeep(state); + stateWithMultipleMetrics.layers[0].metrics.push('1', '2'); + + expect( + findPrimaryGroup(getConfig(stateWithMultipleMetrics))?.supportsMoreColumns + ).toBeFalsy(); }); + + it('does NOT count multiple metrics toward the dimension limits when mosaic', () => { + const frame = mockFrame(); + frame.datasourceLayers[LAYER_ID]!.getTableSpec = () => []; + + const state = getExampleState(); + state.shape = 'mosaic'; + state.layers[0].primaryGroups = []; + state.layers[0].allowMultipleMetrics = false; // always true for mosaic + + const getConfig = (_state: PieVisualizationState) => + pieVisualization.getConfiguration({ + state: _state, + frame, + layerId: state.layers[0].layerId, + }); + + expect(findPrimaryGroup(getConfig(state))?.supportsMoreColumns).toBeTruthy(); + + const stateWithMultipleMetrics = cloneDeep(state); + stateWithMultipleMetrics.layers[0].metrics.push('1', '2'); + + expect( + findPrimaryGroup(getConfig(stateWithMultipleMetrics))?.supportsMoreColumns + ).toBeTruthy(); + }); + + it('reports too many metric dimensions if multiple not enabled', () => { + const colIds = ['1', '2', '3', '4']; + + const frame = mockFrame(); + frame.datasourceLayers[LAYER_ID]!.getTableSpec = () => + colIds.map((id) => ({ columnId: id, fields: [] })); + + const state = getExampleState(); + state.layers[0].metrics = colIds; + state.layers[0].allowMultipleMetrics = false; + expect( + findMetricGroup( + pieVisualization.getConfiguration({ + state, + frame, + layerId: state.layers[0].layerId, + }) + )?.dimensionsTooMany + ).toBe(3); + + state.layers[0].allowMultipleMetrics = true; + expect( + findMetricGroup( + pieVisualization.getConfiguration({ + state, + frame, + layerId: state.layers[0].layerId, + }) + )?.dimensionsTooMany + ).toBe(0); + }); + + it.each(Object.values(PieChartTypes).filter((type) => type !== 'mosaic'))( + '%s adds fake dimension', + (type) => { + const state = { ...getExampleState(), type }; + state.layers[0].metrics.push('1', '2'); + state.layers[0].allowMultipleMetrics = true; + expect( + findPrimaryGroup( + pieVisualization.getConfiguration({ + state, + frame: mockFrame(), + layerId: state.layers[0].layerId, + }) + )?.fakeFinalAccessor + ).toEqual({ label: '2 metrics' }); + + // but not when multiple metrics aren't allowed + state.layers[0].allowMultipleMetrics = false; + expect( + pieVisualization.getConfiguration({ + state, + frame: mockFrame(), + layerId: state.layers[0].layerId, + }).groups[1].fakeFinalAccessor + ).toBeUndefined(); + } + ); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 2c64acf64285b..0ba29502cf280 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -24,6 +24,7 @@ import type { VisualizationDimensionGroupConfig, Suggestion, VisualizeEditorContext, + VisualizationInfo, } from '../../types'; import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression'; import { @@ -37,14 +38,19 @@ import { import { suggestions } from './suggestions'; import { PartitionChartsMeta } from './partition_charts_meta'; import { DimensionDataExtraEditor, DimensionEditor, PieToolbar } from './toolbar'; +import { LayerSettings } from './layer_settings'; import { checkTableForContainsSmallValues } from './render_helpers'; +const metricLabel = i18n.translate('xpack.lens.pie.groupMetricLabelSingular', { + defaultMessage: 'Metric', +}); + function newLayerState(layerId: string): PieLayerState { return { layerId, primaryGroups: [], secondaryGroups: undefined, - metric: undefined, + metrics: [], numberDisplay: NumberDisplay.PERCENT, categoryDisplay: CategoryDisplay.DEFAULT, legendDisplay: LegendDisplay.DEFAULT, @@ -66,25 +72,22 @@ const numberMetricOperations = (op: OperationMetadata) => export const isCollapsed = (columnId: string, layer: PieLayerState) => Boolean(layer.collapseFns?.[columnId]); -const applyPaletteToColumnConfig = ( +const applyPaletteToAccessorConfigs = ( columns: AccessorConfig[], layer: PieLayerState, palette: PieVisualizationState['palette'], paletteService: PaletteRegistry ) => { - const firstNonCollapsedColumnIdx = columns.findIndex( - (column) => !isCollapsed(column.columnId, layer) - ); - - if (firstNonCollapsedColumnIdx > -1) { - columns[firstNonCollapsedColumnIdx] = { - columnId: columns[firstNonCollapsedColumnIdx].columnId, - triggerIcon: 'colorBy', - palette: paletteService + const firstNonCollapsedColumnId = layer.primaryGroups.find((id) => !isCollapsed(id, layer)); + + columns.forEach((accessorConfig) => { + if (firstNonCollapsedColumnId === accessorConfig.columnId) { + accessorConfig.triggerIcon = 'colorBy'; + accessorConfig.palette = paletteService .get(palette?.name || 'default') - .getCategoricalColors(10, palette?.params), - }; - } + .getCategoricalColors(10, palette?.params); + } + }); }; export const getPieVisualization = ({ @@ -161,21 +164,45 @@ export const getPieVisualization = ({ })); if (accessors.length) { - applyPaletteToColumnConfig(accessors, layer, state.palette, paletteService); + applyPaletteToAccessorConfigs(accessors, layer, state.palette, paletteService); } const primaryGroupConfigBaseProps = { - requiredMinDimensionCount: 1, groupId: 'primaryGroups', accessors, enableDimensionEditor: true, filterOperations: bucketedOperations, }; - const totalNonCollapsedAccessors = accessors.reduce( - (total, { columnId }) => total + (isCollapsed(columnId, layer) ? 0 : 1), - 0 - ); + // We count multiple metrics as a bucket dimension. + // + // However, if this is a mosaic chart, we don't support multiple metrics + // so if there is more than one metric we got here via a chart switch from + // a subtype that supports multi-metrics e.g. pie. + // + // The user will be prompted to remove the extra metric dimensions and we don't + // count multiple metrics as a bucket dimension so that the rest of the dimension + // groups UI behaves correctly. + const multiMetricsBucketDimensionCount = + layer.metrics.length > 1 && state.shape !== 'mosaic' ? 1 : 0; + + const totalNonCollapsedAccessors = + accessors.reduce( + (total, { columnId }) => total + (isCollapsed(columnId, layer) ? 0 : 1), + 0 + ) + multiMetricsBucketDimensionCount; + + const fakeFinalAccessor = + layer.metrics.length > 1 && layer.allowMultipleMetrics + ? { + label: i18n.translate('xpack.lens.pie.multiMetricAccessorLabel', { + defaultMessage: '{number} metrics', + values: { + number: layer.metrics.length, + }, + }), + } + : undefined; switch (state.shape) { case 'donut': @@ -188,6 +215,7 @@ export const getPieVisualization = ({ dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.sliceDimensionGroupLabel', { defaultMessage: 'Slice', }), + fakeFinalAccessor, supportsMoreColumns: totalNonCollapsedAccessors < PartitionChartsMeta.pie.maxBuckets, dimensionsTooMany: totalNonCollapsedAccessors - PartitionChartsMeta.pie.maxBuckets, dataTestSubj: 'lnsPie_sliceByDimensionPanel', @@ -215,6 +243,7 @@ export const getPieVisualization = ({ dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.treemapDimensionGroupLabel', { defaultMessage: 'Group', }), + fakeFinalAccessor, supportsMoreColumns: totalNonCollapsedAccessors < PartitionChartsMeta[state.shape].maxBuckets, dimensionsTooMany: @@ -267,26 +296,34 @@ export const getPieVisualization = ({ } }; - const getMetricGroupConfig = (): VisualizationDimensionGroupConfig => ({ - groupId: 'metric', - groupLabel: i18n.translate('xpack.lens.pie.groupsizeLabel', { - defaultMessage: 'Size by', - }), - isMetricDimension: true, - dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.groupSizeLabel', { - defaultMessage: 'Size', - }), - paramEditorCustomProps: { - headingLabel: i18n.translate('xpack.lens.pie.headingLabel', { - defaultMessage: 'Value', - }), - }, - accessors: layer.metric ? [{ columnId: layer.metric }] : [], - supportsMoreColumns: !layer.metric, - filterOperations: numberMetricOperations, - requiredMinDimensionCount: 1, - dataTestSubj: 'lnsPie_sizeByDimensionPanel', - }); + const getMetricGroupConfig = (): VisualizationDimensionGroupConfig => { + const accessors = layer.metrics.map((columnId) => ({ columnId })); + applyPaletteToAccessorConfigs(accessors, layer, state.palette, paletteService); + + const groupLabel = layer.allowMultipleMetrics + ? i18n.translate('xpack.lens.pie.groupMetricLabel', { + defaultMessage: 'Metrics', + }) + : metricLabel; + + return { + groupId: 'metric', + groupLabel, + dimensionEditorGroupLabel: groupLabel, + paramEditorCustomProps: { + headingLabel: i18n.translate('xpack.lens.pie.headingLabel', { + defaultMessage: 'Value', + }), + }, + accessors, + supportsMoreColumns: layer.metrics.length === 0 || Boolean(layer.allowMultipleMetrics), + filterOperations: numberMetricOperations, + requiredMinDimensionCount: 1, + dimensionsTooMany: layer.allowMultipleMetrics ? 0 : layer.metrics.length - 1, + dataTestSubj: 'lnsPie_sizeByDimensionPanel', + enableDimensionEditor: true, + }; + }; return { groups: [getPrimaryGroupConfig(), getSecondaryGroupConfig(), getMetricGroupConfig()].filter( @@ -317,35 +354,48 @@ export const getPieVisualization = ({ ], }; } - return { ...l, metric: columnId }; + return { ...l, metrics: [...l.metrics.filter((metric) => metric !== columnId), columnId] }; }), }; }, removeDimension({ prevState, layerId, columnId }) { - return { - ...prevState, - layers: prevState.layers.map((l) => { - if (l.layerId !== layerId) { - return l; - } + const newState = { ...prevState }; - const newLayer = { ...l }; + const layerToChange = prevState.layers.find((l) => l.layerId === layerId); - if (l.collapseFns?.[columnId]) { - const newCollapseFns = { ...l.collapseFns }; - delete newCollapseFns[columnId]; - newLayer.collapseFns = newCollapseFns; - } + if (!layerToChange) { + return prevState; + } - if (newLayer.metric === columnId) { - return { ...newLayer, metric: undefined }; - } - return { - ...newLayer, - primaryGroups: newLayer.primaryGroups.filter((c) => c !== columnId), - secondaryGroups: newLayer.secondaryGroups?.filter((c) => c !== columnId) ?? undefined, - }; - }), + if ( + layerToChange.primaryGroups.includes(columnId) && + layerToChange.primaryGroups.length === 1 && + layerToChange.allowMultipleMetrics && + layerToChange.metrics.length + ) { + // we don't support palette selection for multiple metrics without a slice-by dimension + // so revert to default if the last slice-by is removed + delete newState.palette; + } + + let newLayer = { ...layerToChange }; + + if (layerToChange.collapseFns?.[columnId]) { + const newCollapseFns = { ...layerToChange.collapseFns }; + delete newCollapseFns[columnId]; + newLayer.collapseFns = newCollapseFns; + } + + newLayer = { + ...newLayer, + primaryGroups: newLayer.primaryGroups.filter((c) => c !== columnId), + secondaryGroups: newLayer.secondaryGroups?.filter((c) => c !== columnId) ?? undefined, + metrics: newLayer.metrics.filter((c) => c !== columnId), + }; + + return { + ...newState, + layers: newState.layers.map((l) => (l.layerId === layerId ? newLayer : l)), }; }, renderDimensionEditor(domElement, props) { @@ -401,6 +451,17 @@ export const getPieVisualization = ({ ); }, + renderLayerSettings(domElement, props) { + render( + + + + + , + domElement + ); + }, + getWarningMessages(state, frame) { if (state?.layers.length === 0 || !frame.activeData) { return; @@ -408,13 +469,13 @@ export const getPieVisualization = ({ const warningMessages = []; for (const layer of state.layers) { - const { layerId, metric } = layer; + const { layerId, metrics } = layer; const rows = frame.activeData[layerId]?.rows; const numericColumn = frame.activeData[layerId]?.columns.find( ({ meta }) => meta?.type === 'number' ); - if (!rows || !metric) { + if (!rows || !metrics.length) { break; } @@ -432,17 +493,26 @@ export const getPieVisualization = ({ ); } - const columnToLabel = frame.datasourceLayers[layerId]?.getOperationForColumnId(metric)?.label; - const hasArrayValues = rows.some((row) => Array.isArray(row[metric])); - if (hasArrayValues) { + const metricsWithArrayValues = metrics + .map((metricColId) => { + if (rows.some((row) => Array.isArray(row[metricColId]))) { + return metricColId; + } + }) + .filter(Boolean) as string[]; + + if (metricsWithArrayValues.length) { + const labels = metricsWithArrayValues.map( + (colId) => frame.datasourceLayers[layerId]?.getOperationForColumnId(colId)?.label || colId + ); warningMessages.push( {columnToLabel || metric}, + label: {labels.join(', ')}, }} /> ); @@ -478,12 +548,15 @@ export const getPieVisualization = ({ getErrorMessages(state) { const hasTooManyBucketDimensions = state.layers - .map( - (layer) => + .map((layer) => { + const totalBucketDimensions = Array.from(new Set([...layer.primaryGroups, ...(layer.secondaryGroups ?? [])])).filter( (columnId) => !isCollapsed(columnId, layer) - ).length > PartitionChartsMeta[state.shape].maxBuckets - ) + ).length + + // multiple metrics counts as a dimension + (layer.metrics.length > 1 ? 1 : 0); + return totalBucketDimensions > PartitionChartsMeta[state.shape].maxBuckets; + }) .some(Boolean); return hasTooManyBucketDimensions @@ -511,15 +584,14 @@ export const getPieVisualization = ({ getVisualizationInfo(state: PieVisualizationState) { const layer = state.layers[0]; - const dimensions = []; - if (layer.metric) { + const dimensions: VisualizationInfo['layers'][number]['dimensions'] = []; + + layer.metrics.forEach((metric) => { dimensions.push({ - id: layer.metric, - name: i18n.translate('xpack.lens.pie.groupsizeLabel', { - defaultMessage: 'Size by', - }), + id: metric, + name: metricLabel, }); - } + }); if (state.shape === 'mosaic' && layer.secondaryGroups && layer.secondaryGroups.length) { layer.secondaryGroups.forEach((accessor) => { diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts index d7d3c9ca8a56a..4bd8a2f67d64d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions.ts @@ -6,20 +6,21 @@ */ import { i18n } from '@kbn/i18n'; -import type { LayerAction, StateSetter } from '../../../types'; +import type { LayerActionFromVisualization } from '../../../types'; import type { XYState, XYAnnotationLayerConfig } from '../types'; +export const IGNORE_GLOBAL_FILTERS_ACTION_ID = 'ignoreGlobalFilters'; +export const KEEP_GLOBAL_FILTERS_ACTION_ID = 'keepGlobalFilters'; + export const createAnnotationActions = ({ state, layer, layerIndex, - setState, }: { state: XYState; layer: XYAnnotationLayerConfig; layerIndex: number; - setState: StateSetter; -}): LayerAction[] => { +}): LayerActionFromVisualization[] => { const label = !layer.ignoreGlobalFilters ? i18n.translate('xpack.lens.xyChart.annotations.ignoreGlobalFiltersLabel', { defaultMessage: 'Ignore global filters', @@ -29,6 +30,9 @@ export const createAnnotationActions = ({ }); return [ { + id: !layer.ignoreGlobalFilters + ? IGNORE_GLOBAL_FILTERS_ACTION_ID + : KEEP_GLOBAL_FILTERS_ACTION_ID, displayName: label, description: !layer.ignoreGlobalFilters ? i18n.translate('xpack.lens.xyChart.annotations.ignoreGlobalFiltersDescription', { @@ -39,11 +43,6 @@ export const createAnnotationActions = ({ defaultMessage: 'All the dimensions configured in this layer respect filters defined at kibana level.', }), - execute: () => { - const newLayers = [...state.layers]; - newLayers[layerIndex] = { ...layer, ignoreGlobalFilters: !layer.ignoreGlobalFilters }; - return setState({ ...state, layers: newLayers }); - }, icon: !layer.ignoreGlobalFilters ? 'eyeClosed' : 'eye', isCompatible: true, 'data-test-subj': !layer.ignoreGlobalFilters diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 12baaab25af84..e1fe1d9c7f4cb 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -16,7 +16,6 @@ import type { XYReferenceLineLayerConfig, SeriesType, } from './types'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; import { IconChartBar } from '@kbn/chart-icons'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; @@ -31,6 +30,8 @@ import { DataViewsState } from '../../state_management'; import { createMockedIndexPattern } from '../../datasources/form_based/mocks'; import { createMockDataViewsState } from '../../data_views_service/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { KEEP_GLOBAL_FILTERS_ACTION_ID } from './annotations/actions'; +import { layerTypes } from '../..'; const exampleAnnotation: EventAnnotationConfig = { id: 'an1', @@ -61,7 +62,7 @@ function exampleState(): XYState { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'd', xAccessor: 'a', @@ -221,7 +222,7 @@ describe('xy_visualization', () => { ...exampleState().layers, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'e', xAccessor: 'f', @@ -239,7 +240,7 @@ describe('xy_visualization', () => { const layers = xyVisualization.appendLayer!( exampleState(), 'foo', - LayerTypes.DATA, + layerTypes.DATA, 'indexPattern1' ).layers; expect(layers.length).toEqual(exampleState().layers.length + 1); @@ -329,7 +330,7 @@ describe('xy_visualization', () => { describe('#getLayerType', () => { it('should return the type only if the layer is in the state', () => { - expect(xyVisualization.getLayerType('first', exampleState())).toEqual(LayerTypes.DATA); + expect(xyVisualization.getLayerType('first', exampleState())).toEqual(layerTypes.DATA); expect(xyVisualization.getLayerType('foo', exampleState())).toBeUndefined(); }); }); @@ -378,7 +379,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -391,7 +392,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'newCol', accessors: [], @@ -407,7 +408,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], @@ -420,7 +421,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'newCol', accessors: [], @@ -436,7 +437,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'referenceLine', - layerType: LayerTypes.REFERENCELINE, + layerType: layerTypes.REFERENCELINE, accessors: [], }, ], @@ -447,7 +448,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'referenceLine', - layerType: LayerTypes.REFERENCELINE, + layerType: layerTypes.REFERENCELINE, accessors: ['newCol'], yConfig: [ { @@ -468,7 +469,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -481,7 +482,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', ignoreGlobalFilters: true, annotations: [ @@ -717,7 +718,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -746,7 +747,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [ exampleAnnotation2, @@ -777,7 +778,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -806,7 +807,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [ { @@ -837,7 +838,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -863,7 +864,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2, { ...exampleAnnotation2, id: 'newColId' }], ignoreGlobalFilters: true, @@ -878,7 +879,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation, exampleAnnotation2], ignoreGlobalFilters: true, @@ -905,7 +906,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2, exampleAnnotation], ignoreGlobalFilters: true, @@ -921,14 +922,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -956,14 +957,14 @@ describe('xy_visualization', () => { ).toEqual([ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [{ ...exampleAnnotation, id: 'an2' }], ignoreGlobalFilters: true, @@ -979,14 +980,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -1014,14 +1015,14 @@ describe('xy_visualization', () => { ).toEqual([ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -1037,14 +1038,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -1071,14 +1072,14 @@ describe('xy_visualization', () => { ).toEqual([ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -1094,14 +1095,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [], ignoreGlobalFilters: true, @@ -1129,14 +1130,14 @@ describe('xy_visualization', () => { ).toEqual([ { layerId: 'first', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [], ignoreGlobalFilters: true, }, { layerId: 'second', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -1184,7 +1185,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], @@ -1196,7 +1197,7 @@ describe('xy_visualization', () => { }).layers[0] ).toEqual({ layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -1211,14 +1212,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], }, { layerId: 'ann', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation, { ...exampleAnnotation, id: 'an2' }], ignoreGlobalFilters: true, @@ -1231,14 +1232,14 @@ describe('xy_visualization', () => { ).toEqual([ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], }, { layerId: 'ann', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -1534,7 +1535,7 @@ describe('xy_visualization', () => { ...baseState.layers[0], accessors: ['e'], seriesType: 'bar_percentage_stacked', - layerType: LayerTypes.REFERENCELINE, + layerType: layerTypes.REFERENCELINE, }, ], ], @@ -1601,7 +1602,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: undefined, xAccessor: undefined, @@ -1609,7 +1610,7 @@ describe('xy_visualization', () => { }, { layerId: 'referenceLine', - layerType: LayerTypes.REFERENCELINE, + layerType: layerTypes.REFERENCELINE, accessors: [], yConfig: [{ axisMode: 'left', forAccessor: 'a' }], }, @@ -1957,7 +1958,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: undefined, xAccessor: 'a', @@ -1965,7 +1966,7 @@ describe('xy_visualization', () => { }, { layerId: 'annotations', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation], ignoreGlobalFilters: true, @@ -2189,7 +2190,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], @@ -2205,14 +2206,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], @@ -2228,14 +2229,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: ['a'], }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: ['a'], @@ -2252,7 +2253,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -2268,7 +2269,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -2276,7 +2277,7 @@ describe('xy_visualization', () => { }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -2293,14 +2294,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: ['a'], @@ -2321,14 +2322,14 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: ['a'], }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -2336,7 +2337,7 @@ describe('xy_visualization', () => { }, { layerId: 'third', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: undefined, accessors: [], @@ -2358,21 +2359,21 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: [], }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: ['a'], }, { layerId: 'third', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: ['a'], @@ -2395,7 +2396,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'd', xAccessor: 'a', @@ -2443,7 +2444,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'd', xAccessor: 'a', @@ -2451,7 +2452,7 @@ describe('xy_visualization', () => { }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'd', xAccessor: 'e', @@ -2499,7 +2500,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'd', xAccessor: 'a', @@ -2507,7 +2508,7 @@ describe('xy_visualization', () => { }, { layerId: 'second', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', splitAccessor: 'd', xAccessor: 'e', @@ -2677,7 +2678,7 @@ describe('xy_visualization', () => { layers: [ { layerId: 'first', - layerType: LayerTypes.DATA, + layerType: layerTypes.DATA, seriesType: 'area', xAccessor: 'a', accessors: ['b'], @@ -2797,7 +2798,7 @@ describe('xy_visualization', () => { ...baseState.layers, { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, annotations: [exampleAnnotation2], ignoreGlobalFilters: true, }, @@ -2817,7 +2818,7 @@ describe('xy_visualization', () => { ...baseState.layers, { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -2836,7 +2837,7 @@ describe('xy_visualization', () => { ...baseState.layers, { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, annotations: [exampleAnnotation2], ignoreGlobalFilters: true, }, @@ -2856,7 +2857,7 @@ describe('xy_visualization', () => { ...baseState.layers, { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, indexPatternId: 'indexPattern1', annotations: [exampleAnnotation2], ignoreGlobalFilters: true, @@ -2866,33 +2867,29 @@ describe('xy_visualization', () => { }); }); - describe('getSupportedActionsForLayer', () => { + describe('layer actions', () => { it('should return no actions for a data layer', () => { - expect( - xyVisualization.getSupportedActionsForLayer?.('first', exampleState(), jest.fn()) - ).toHaveLength(0); + expect(xyVisualization.getSupportedActionsForLayer?.('first', exampleState())).toHaveLength( + 0 + ); }); it('should return one action for an annotation layer', () => { const baseState = exampleState(); expect( - xyVisualization.getSupportedActionsForLayer?.( - 'annotation', - { - ...baseState, - layers: [ - ...baseState.layers, - { - layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, - indexPatternId: 'myIndexPattern', - }, - ], - }, - jest.fn() - ) + xyVisualization.getSupportedActionsForLayer?.('annotation', { + ...baseState, + layers: [ + ...baseState.layers, + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + indexPatternId: 'myIndexPattern', + }, + ], + }) ).toEqual([ expect.objectContaining({ displayName: 'Keep global filters', @@ -2905,34 +2902,34 @@ describe('xy_visualization', () => { ]); }); - it('should return an action that performs a state update on click', () => { + it('should handle an annotation action', () => { const baseState = exampleState(); - const setState = jest.fn(); - const [action] = xyVisualization.getSupportedActionsForLayer?.( + const state = { + ...baseState, + layers: [ + ...baseState.layers, + { + layerId: 'annotation', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation2], + ignoreGlobalFilters: true, + indexPatternId: 'myIndexPattern', + }, + ], + }; + + const newState = xyVisualization.onLayerAction!( 'annotation', - { - ...baseState, - layers: [ - ...baseState.layers, - { - layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, - annotations: [exampleAnnotation2], - ignoreGlobalFilters: true, - indexPatternId: 'myIndexPattern', - }, - ], - }, - setState - )!; - action.execute(); + KEEP_GLOBAL_FILTERS_ACTION_ID, + state + ); - expect(setState).toHaveBeenCalledWith( + expect(newState).toEqual( expect.objectContaining({ layers: expect.arrayContaining([ { layerId: 'annotation', - layerType: LayerTypes.ANNOTATIONS, + layerType: layerTypes.ANNOTATIONS, annotations: [exampleAnnotation2], ignoreGlobalFilters: false, indexPatternId: 'myIndexPattern', diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 45ec49bea847b..590c8a36a2f5e 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -43,6 +43,7 @@ import { type XYDataLayerConfig, type SeriesType, type PersistedState, + type XYAnnotationLayerConfig, visualizationTypes, } from './types'; import { @@ -94,7 +95,11 @@ import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; import { DimensionTrigger } from '../../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; import { onDropForVisualization } from '../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; -import { createAnnotationActions } from './annotations/actions'; +import { + createAnnotationActions, + IGNORE_GLOBAL_FILTERS_ACTION_ID, + KEEP_GLOBAL_FILTERS_ACTION_ID, +} from './annotations/actions'; const XY_ID = 'lnsXY'; export const getXyVisualization = ({ @@ -249,16 +254,34 @@ export const getXyVisualization = ({ ]; }, - getSupportedActionsForLayer(layerId, state, setState) { + getSupportedActionsForLayer(layerId, state) { const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[layerIndex]; const actions = []; if (isAnnotationsLayer(layer)) { - actions.push(...createAnnotationActions({ state, layerIndex, layer, setState })); + actions.push(...createAnnotationActions({ state, layerIndex, layer })); } return actions; }, + onLayerAction(layerId, actionId, state) { + if ([IGNORE_GLOBAL_FILTERS_ACTION_ID, KEEP_GLOBAL_FILTERS_ACTION_ID].includes(actionId)) { + return { + ...state, + layers: state.layers.map((layer) => + layer.layerId === layerId + ? { + ...layer, + ignoreGlobalFilters: !(layer as XYAnnotationLayerConfig).ignoreGlobalFilters, + } + : layer + ), + }; + } + + return state; + }, + onIndexPatternChange(state, indexPatternId, layerId) { const layerIndex = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[layerIndex]; diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts index 42846e84377dd..aa4f634b700af 100644 --- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts @@ -32,6 +32,7 @@ import { getLensDataViewMigrations, commonMigrateMetricIds, commonMigratePartitionChartGroups, + commonMigratePartitionMetrics, commonMigrateIndexPatternDatasource, } from '../migrations/common_migrations'; import { @@ -160,12 +161,15 @@ export const makeLensEmbeddableFactory = '8.6.0': (state) => { const lensState = state as unknown as SavedObject>; - const migratedLensState = commonMigrateIndexPatternDatasource(lensState.attributes); + let migratedLensState = commonMigrateIndexPatternDatasource(lensState.attributes); + migratedLensState = commonMigratePartitionMetrics(migratedLensState); return { ...lensState, attributes: migratedLensState, } as unknown as SerializableRecord; }, + // FOLLOW THESE GUIDELINES IF YOU ARE ADDING A NEW MIGRATION! + // 1. Make sure you are applying migrations for a given version in the same order here as they are applied in x-pack/plugins/lens/server/migrations/saved_object_migrations.ts }), getLensCustomVisualizationMigrations(customVisualizationMigrations) ), diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index b56f4b691911b..3559e79def68a 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -541,3 +541,32 @@ export const commonMigratePartitionChartGroups = ( layers: Array<{ primaryGroups?: string[]; secondaryGroups?: string[] }>; }>; }; + +export const commonMigratePartitionMetrics = (attributes: LensDocShape860) => { + if (attributes.visualizationType !== 'lnsPie') { + return attributes as LensDocShape860; + } + + const partitionAttributes = attributes as LensDocShape860<{ + shape: string; + layers: Array<{ metric: string }>; + }>; + + return { + ...attributes, + state: { + ...attributes.state, + visualization: { + ...partitionAttributes.state.visualization, + layers: partitionAttributes.state.visualization.layers.map((layer) => ({ + ...layer, + metrics: [layer.metric], + metric: undefined, + })), + }, + }, + } as LensDocShape860<{ + shape: string; + layers: Array<{ metrics: string[] }>; + }>; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 54504b9201f67..eec31924fdc85 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -2364,6 +2364,49 @@ describe('Lens migrations', () => { }); }); + describe('8.6.0 migrates partition metrics', () => { + const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'some title', + description: '', + visualizationType: 'lnsPie', + state: { + visualization: { + layers: [ + { + metric: 'some-metric', + }, + ], + }, + datasourceStates: { + indexpattern: {}, + }, + }, + }, + } as unknown as SavedObjectUnsanitizedDoc; + + it('make metric an array', () => { + const result = migrations['8.6.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + expect( + (result.attributes.state.visualization as { layers: Array<{ metrics: string[] }> }) + .layers[0] + ).toMatchInlineSnapshot(` + Object { + "metric": undefined, + "metrics": Array [ + "some-metric", + ], + } + `); + }); + }); + describe('8.6.0 migrates indexpattern datasource', () => { const context = { log: { warn: () => {} } } as unknown as SavedObjectMigrationContext; const example = { diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 8e4914b94fe83..aa480c4c0b151 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -60,6 +60,7 @@ import { getLensDataViewMigrations, commonMigrateMetricIds, commonMigratePartitionChartGroups, + commonMigratePartitionMetrics, commonMigrateIndexPatternDatasource, } from './common_migrations'; @@ -554,6 +555,13 @@ const migratePartitionChartGroups: SavedObjectMigrationFn = ( + doc +) => ({ + ...doc, + attributes: commonMigratePartitionMetrics(doc.attributes), +}); + const lensMigrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -575,7 +583,9 @@ const lensMigrations: SavedObjectMigrationMap = { ), '8.3.0': flow(lockOldMetricVisSettings, preserveOldLegendSizeDefault, fixValueLabelsInXY), '8.5.0': flow(migrateMetricIds, enrichAnnotationLayers, migratePartitionChartGroups), - '8.6.0': flow(migrateIndexPatternDatasource), + '8.6.0': flow(migrateIndexPatternDatasource, migratePartitionMetrics), + // FOLLOW THESE GUIDELINES IF YOU ARE ADDING A NEW MIGRATION! + // 1. Make sure you are applying migrations for a given version in the same order here as they are applied in x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts }; export const getAllMigrations = ( diff --git a/x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx b/x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx index 247255d9ffba3..48fdd928f7aea 100644 --- a/x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx +++ b/x-pack/plugins/osquery/public/lens/view_results_in_lens.tsx @@ -140,7 +140,7 @@ function getLensAttributes( legendDisplay: 'default', nestedLegend: false, layerId: 'layer1', - metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed', + metrics: ['ed999e9d-204c-465b-897f-fe1a125b39ed'], numberDisplay: 'percent', primaryGroups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'], categoryDisplay: 'default', diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index c9a323d1c4a5a..26ea480f26edc 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -114,7 +114,7 @@ function getLensAttributes( legendDisplay: 'default', nestedLegend: false, layerId: 'layer1', - metric: 'ed999e9d-204c-465b-897f-fe1a125b39ed', + metrics: ['ed999e9d-204c-465b-897f-fe1a125b39ed'], numberDisplay: 'percent', primaryGroups: ['8690befd-fd69-4246-af4a-dd485d2a3b38'], categoryDisplay: 'default', diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index bf3e06f2764ab..67ed617822058 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -17845,7 +17845,6 @@ "xpack.lens.pie.addLayer": "Visualisation", "xpack.lens.pie.donutLabel": "Graphique en anneau", "xpack.lens.pie.groupLabel": "Proportion", - "xpack.lens.pie.groupsizeLabel": "Taille par", "xpack.lens.pie.mosaiclabel": "Mosaïque", "xpack.lens.pie.mosaicSuggestionLabel": "En mosaïque", "xpack.lens.pie.pielabel": "Camembert", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fe6bbcf474741..7a2cbd5929707 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17828,7 +17828,6 @@ "xpack.lens.pie.addLayer": "ビジュアライゼーション", "xpack.lens.pie.donutLabel": "ドーナッツ", "xpack.lens.pie.groupLabel": "比率", - "xpack.lens.pie.groupsizeLabel": "サイズ単位", "xpack.lens.pie.mosaiclabel": "モザイク", "xpack.lens.pie.mosaicSuggestionLabel": "モザイクとして", "xpack.lens.pie.pielabel": "円", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e28df61f1966d..af00cf9497094 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17853,7 +17853,6 @@ "xpack.lens.pie.addLayer": "可视化", "xpack.lens.pie.donutLabel": "圆环图", "xpack.lens.pie.groupLabel": "比例", - "xpack.lens.pie.groupsizeLabel": "大小调整依据", "xpack.lens.pie.mosaiclabel": "马赛克", "xpack.lens.pie.mosaicSuggestionLabel": "为马赛克", "xpack.lens.pie.pielabel": "饼图", diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/__snapshots__/visitor_breakdown_chart.test.tsx.snap b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/__snapshots__/visitor_breakdown_chart.test.tsx.snap index 1e7d3ca13e43c..ad309f99bf04c 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/__snapshots__/visitor_breakdown_chart.test.tsx.snap +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/__snapshots__/visitor_breakdown_chart.test.tsx.snap @@ -112,7 +112,9 @@ Object { "layerId": "layer1", "layerType": "data", "legendDisplay": "hide", - "metric": "col2", + "metrics": Array [ + "col2", + ], "nestedLegend": false, "numberDisplay": "percent", "primaryGroups": Array [ diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx index e20e9b1f89f7e..87c3779365677 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/charts/visitor_breakdown_chart.tsx @@ -105,7 +105,7 @@ const visConfig: PieVisualizationState = { { layerId: 'layer1', primaryGroups: ['col1'], - metric: 'col2', + metrics: ['col2'], categoryDisplay: 'default', legendDisplay: 'hide', numberDisplay: 'percent',