diff --git a/src/plugins/vis_types/pie/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_types/pie/public/__snapshots__/pie_fn.test.ts.snap index fb51717d1adc0..4298654af512d 100644 --- a/src/plugins/vis_types/pie/public/__snapshots__/pie_fn.test.ts.snap +++ b/src/plugins/vis_types/pie/public/__snapshots__/pie_fn.test.ts.snap @@ -47,6 +47,7 @@ Object { "splitRow": undefined, }, "distinctColors": false, + "emptySizeRatio": 0.3, "isDonut": true, "labels": Object { "percentDecimals": 2, diff --git a/src/plugins/vis_types/pie/public/editor/collections.ts b/src/plugins/vis_types/pie/public/editor/collections.ts index d65e933a8835c..64e4694eb449f 100644 --- a/src/plugins/vis_types/pie/public/editor/collections.ts +++ b/src/plugins/vis_types/pie/public/editor/collections.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EMPTY_SIZE_RATIOS } from './constants'; import { LabelPositions, ValueFormats } from '../types'; export const getLabelPositions = [ @@ -38,3 +39,27 @@ export const getValuesFormats = [ value: ValueFormats.VALUE, }, ]; + +export const emptySizeRatioOptions = [ + { + id: 'emptySizeRatioOption-small', + value: EMPTY_SIZE_RATIOS.SMALL, + label: i18n.translate('visTypePie.emptySizeRatioOptions.small', { + defaultMessage: 'Small', + }), + }, + { + id: 'emptySizeRatioOption-medium', + value: EMPTY_SIZE_RATIOS.MEDIUM, + label: i18n.translate('visTypePie.emptySizeRatioOptions.medium', { + defaultMessage: 'Medium', + }), + }, + { + id: 'emptySizeRatioOption-large', + value: EMPTY_SIZE_RATIOS.LARGE, + label: i18n.translate('visTypePie.emptySizeRatioOptions.large', { + defaultMessage: 'Large', + }), + }, +]; diff --git a/src/plugins/vis_types/pie/public/editor/components/pie.test.tsx b/src/plugins/vis_types/pie/public/editor/components/pie.test.tsx index ac02b33b92add..d6dc4c0734e42 100644 --- a/src/plugins/vis_types/pie/public/editor/components/pie.test.tsx +++ b/src/plugins/vis_types/pie/public/editor/components/pie.test.tsx @@ -135,4 +135,18 @@ describe('PalettePicker', function () { expect(findTestSubject(component, 'visTypePieValueDecimals').length).toBe(1); }); }); + + it('renders the donut size button group for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieEmptySizeRatioButtonGroup').length).toBe(1); + }); + }); + + it('not renders the donut size button group for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'visTypePieEmptySizeRatioButtonGroup').length).toBe(0); + }); + }); }); diff --git a/src/plugins/vis_types/pie/public/editor/components/pie.tsx b/src/plugins/vis_types/pie/public/editor/components/pie.tsx index 50e599b5ef5f3..97f427d7b91b7 100644 --- a/src/plugins/vis_types/pie/public/editor/components/pie.tsx +++ b/src/plugins/vis_types/pie/public/editor/components/pie.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { METRIC_TYPE } from '@kbn/analytics'; import { EuiPanel, @@ -17,6 +17,7 @@ import { EuiIconTip, EuiFlexItem, EuiFlexGroup, + EuiButtonGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -33,11 +34,15 @@ import { TruncateLabelsOption } from './truncate_labels'; import { PaletteRegistry } from '../../../../../charts/public'; import { DEFAULT_PERCENT_DECIMALS } from '../../../common'; import { PieVisParams, LabelPositions, ValueFormats, PieTypeProps } from '../../types'; -import { getLabelPositions, getValuesFormats } from '../collections'; +import { emptySizeRatioOptions, getLabelPositions, getValuesFormats } from '../collections'; import { getLegendPositions } from '../positions'; export interface PieOptionsProps extends VisEditorOptionsProps, PieTypeProps {} +const emptySizeRatioLabel = i18n.translate('visTypePie.editors.pie.emptySizeRatioLabel', { + defaultMessage: 'Inner area size', +}); + function DecimalSlider({ paramName, value, @@ -96,6 +101,14 @@ const PieOptions = (props: PieOptionsProps) => { fetchPalettes(); }, [props.palettes]); + const handleEmptySizeRatioChange = useCallback( + (sizeId) => { + const emptySizeRatio = emptySizeRatioOptions.find(({ id }) => id === sizeId)?.value; + setValue('emptySizeRatio', emptySizeRatio); + }, + [setValue] + ); + return ( <> @@ -116,6 +129,23 @@ const PieOptions = (props: PieOptionsProps) => { value={stateParams.isDonut} setValue={setValue} /> + {props.showElasticChartsOptions && stateParams.isDonut && ( + + value === stateParams.emptySizeRatio) + ?.id ?? 'emptySizeRatioOption-small' + } + onChange={handleEmptySizeRatioChange} + data-test-subj="visTypePieEmptySizeRatioButtonGroup" + /> + + )} {props.showElasticChartsOptions && ( <> diff --git a/src/plugins/vis_types/pie/public/editor/constants.ts b/src/plugins/vis_types/pie/public/editor/constants.ts new file mode 100644 index 0000000000000..6fd0c7ab0a4a6 --- /dev/null +++ b/src/plugins/vis_types/pie/public/editor/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum EMPTY_SIZE_RATIOS { + SMALL = 0.3, + MEDIUM = 0.54, + LARGE = 0.7, +} diff --git a/src/plugins/vis_types/pie/public/pie_fn.test.ts b/src/plugins/vis_types/pie/public/pie_fn.test.ts index 9ba21cdc847e5..9a9c2410481bd 100644 --- a/src/plugins/vis_types/pie/public/pie_fn.test.ts +++ b/src/plugins/vis_types/pie/public/pie_fn.test.ts @@ -10,6 +10,7 @@ import { functionWrapper } from '../../../expressions/common/expression_function import { createPieVisFn } from './pie_fn'; import { PieVisConfig } from './types'; import { Datatable } from '../../../expressions/common/expression_types/specs'; +import { EMPTY_SIZE_RATIOS } from './editor/constants'; describe('interpreter/functions#pie', () => { const fn = functionWrapper(createPieVisFn()); @@ -23,6 +24,7 @@ describe('interpreter/functions#pie', () => { addLegend: true, legendPosition: 'right', isDonut: true, + emptySizeRatio: EMPTY_SIZE_RATIOS.SMALL, nestedLegend: true, truncateLegend: true, maxLegendLines: true, diff --git a/src/plugins/vis_types/pie/public/pie_fn.ts b/src/plugins/vis_types/pie/public/pie_fn.ts index 74e8127712399..041ecb5b7e6e5 100644 --- a/src/plugins/vis_types/pie/public/pie_fn.ts +++ b/src/plugins/vis_types/pie/public/pie_fn.ts @@ -117,6 +117,12 @@ export const createPieVisFn = (): VisTypePieExpressionFunctionDefinition => ({ }), default: false, }, + emptySizeRatio: { + types: ['number'], + help: i18n.translate('visTypePie.function.args.emptySizeRatioHelpText', { + defaultMessage: 'Defines donut inner empty area size', + }), + }, palette: { types: ['string'], help: i18n.translate('visTypePie.function.args.paletteHelpText', { diff --git a/src/plugins/vis_types/pie/public/to_ast.ts b/src/plugins/vis_types/pie/public/to_ast.ts index fbfffbb77d5fb..531b8516464f2 100644 --- a/src/plugins/vis_types/pie/public/to_ast.ts +++ b/src/plugins/vis_types/pie/public/to_ast.ts @@ -54,6 +54,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, par maxLegendLines: vis.params.maxLegendLines, distinctColors: vis.params?.distinctColors, isDonut: vis.params.isDonut, + emptySizeRatio: vis.params.emptySizeRatio, palette: vis.params?.palette?.name, labels: prepareLabels(vis.params.labels), metric: schemas.metric.map(prepareDimension), diff --git a/src/plugins/vis_types/pie/public/types/types.ts b/src/plugins/vis_types/pie/public/types/types.ts index fb5efb5971805..4745ee0f95ed5 100644 --- a/src/plugins/vis_types/pie/public/types/types.ts +++ b/src/plugins/vis_types/pie/public/types/types.ts @@ -13,6 +13,7 @@ import type { SerializedFieldFormat } from '../../../../field_formats/common'; import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { ExpressionValuePieLabels } from '../expression_functions/pie_labels'; import { PaletteOutput, ChartsPluginSetup } from '../../../../charts/public'; +import { EMPTY_SIZE_RATIOS } from '../editor/constants'; export interface Dimension { accessor: number; @@ -38,6 +39,7 @@ interface PieCommonParams { maxLegendLines: number; distinctColors: boolean; isDonut: boolean; + emptySizeRatio?: EMPTY_SIZE_RATIOS; } export interface LabelsParams { diff --git a/src/plugins/vis_types/pie/public/utils/get_config.ts b/src/plugins/vis_types/pie/public/utils/get_config.ts index 9f67155145820..67852a773d697 100644 --- a/src/plugins/vis_types/pie/public/utils/get_config.ts +++ b/src/plugins/vis_types/pie/public/utils/get_config.ts @@ -54,7 +54,7 @@ export const getConfig = ( sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, sectorLineWidth: 1.5, circlePadding: 4, - emptySizeRatio: visParams.isDonut ? 0.3 : 0, + emptySizeRatio: visParams.isDonut ? visParams.emptySizeRatio : 0, ...usingMargin, }; if (!visParams.labels.show) { diff --git a/src/plugins/vis_types/pie/public/vis_type/pie.ts b/src/plugins/vis_types/pie/public/vis_type/pie.ts index f10af053bd161..845b2bae1c89d 100644 --- a/src/plugins/vis_types/pie/public/vis_type/pie.ts +++ b/src/plugins/vis_types/pie/public/vis_type/pie.ts @@ -14,6 +14,7 @@ import { DEFAULT_PERCENT_DECIMALS } from '../../common'; import { PieVisParams, LabelPositions, ValueFormats, PieTypeProps } from '../types'; import { toExpressionAst } from '../to_ast'; import { getPieOptions } from '../editor/components'; +import { EMPTY_SIZE_RATIOS } from '../editor/constants'; export const getPieVisTypeDefinition = ({ showElasticChartsOptions = false, @@ -39,6 +40,7 @@ export const getPieVisTypeDefinition = ({ maxLegendLines: 1, distinctColors: false, isDonut: true, + emptySizeRatio: EMPTY_SIZE_RATIOS.SMALL, palette: { type: 'palette', name: 'default', diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts index 4f2736d739b11..ce909152e71b9 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts @@ -101,6 +101,10 @@ export const pie: ExpressionFunctionDefinition< help: '', types: ['palette'], }, + emptySizeRatio: { + types: ['number'], + help: '', + }, }, inputTypes: ['lens_multitable'], fn(data: LensMultiTable, args: PieExpressionArgs) { diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts index a7aa92369dce2..0a7d705ec3fbc 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts @@ -20,6 +20,7 @@ export interface SharedPieLayerState { showValuesInLegend?: boolean; nestedLegend?: boolean; percentDecimals?: number; + emptySizeRatio?: number; legendMaxLines?: number; truncateLegend?: boolean; } diff --git a/x-pack/plugins/lens/public/pie_visualization/constants.ts b/x-pack/plugins/lens/public/pie_visualization/constants.ts index bfb263b415891..e32320bb75ff0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/constants.ts +++ b/x-pack/plugins/lens/public/pie_visualization/constants.ts @@ -6,3 +6,9 @@ */ export const DEFAULT_PERCENT_DECIMALS = 2; + +export enum EMPTY_SIZE_RATIOS { + SMALL = 0.3, + MEDIUM = 0.54, + LARGE = 0.7, +} diff --git a/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts b/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts index 4f16ab01ba19c..2bafa5a1ff8e0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts +++ b/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts @@ -14,6 +14,7 @@ import { LensIconChartPie } from '../assets/chart_pie'; import { LensIconChartTreemap } from '../assets/chart_treemap'; import { LensIconChartMosaic } from '../assets/chart_mosaic'; import { LensIconChartWaffle } from '../assets/chart_waffle'; +import { EMPTY_SIZE_RATIOS } from './constants'; import type { SharedPieLayerState } from '../../common/expressions'; import type { PieChartTypes } from '../../common/expressions/pie_chart/types'; @@ -37,6 +38,11 @@ interface PartitionChartMeta { value: SharedPieLayerState['numberDisplay']; inputDisplay: string; }>; + emptySizeRatioOptions?: Array<{ + id: string; + value: EMPTY_SIZE_RATIOS; + label: string; + }>; }; legend: { flat?: boolean; @@ -110,6 +116,30 @@ const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [ }, ]; +const emptySizeRatioOptions: PartitionChartMeta['toolbarPopover']['emptySizeRatioOptions'] = [ + { + id: 'emptySizeRatioOption-small', + value: EMPTY_SIZE_RATIOS.SMALL, + label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.small', { + defaultMessage: 'Small', + }), + }, + { + id: 'emptySizeRatioOption-medium', + value: EMPTY_SIZE_RATIOS.MEDIUM, + label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.medium', { + defaultMessage: 'Medium', + }), + }, + { + id: 'emptySizeRatioOption-large', + value: EMPTY_SIZE_RATIOS.LARGE, + label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.large', { + defaultMessage: 'Large', + }), + }, +]; + export const PartitionChartsMeta: Record = { donut: { icon: LensIconChartDonut, @@ -122,6 +152,7 @@ export const PartitionChartsMeta: Record = { toolbarPopover: { categoryOptions, numberOptions, + emptySizeRatioOptions, }, legend: { getShowLegendDefault: (bucketColumns) => bucketColumns.length > 1, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 4bf7fcf9f8925..9fb016dc0a2d7 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -82,6 +82,7 @@ export function PieComponent( legendPosition, nestedLegend, percentDecimals, + emptySizeRatio, legendMaxLines, truncateLegend, hideLabels, @@ -229,7 +230,7 @@ export function PieComponent( config.fillLabel = { textColor: 'rgba(0,0,0,0)' }; } } else { - config.emptySizeRatio = shape === 'donut' ? 0.3 : 0; + config.emptySizeRatio = shape === 'donut' ? emptySizeRatio : 0; if (hideLabels || categoryDisplay === 'hide') { // Force all labels to be linked, then prevent links from showing diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index f4c951cece3c2..f7e76567743e4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -8,9 +8,8 @@ import type { Ast } from '@kbn/interpreter/common'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { Operation, DatasourcePublicAPI } from '../types'; -import { DEFAULT_PERCENT_DECIMALS } from './constants'; +import { DEFAULT_PERCENT_DECIMALS, EMPTY_SIZE_RATIOS } from './constants'; import { shouldShowValuesInLegend } from './render_helpers'; - import type { PieVisualizationState } from '../../common/expressions'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; @@ -59,6 +58,7 @@ function expressionHelper( categoryDisplay: [layer.categoryDisplay], legendDisplay: [layer.legendDisplay], legendPosition: [layer.legendPosition || 'right'], + emptySizeRatio: [layer.emptySizeRatio ?? EMPTY_SIZE_RATIOS.SMALL], showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], percentDecimals: [ state.shape === 'waffle' diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 997ebb2e3787f..6910a3ea0966c 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -14,6 +14,7 @@ import { EuiSuperSelect, EuiRange, EuiHorizontalRule, + EuiButtonGroup, } from '@elastic/eui'; import type { Position } from '@elastic/charts'; import type { PaletteRegistry } from 'src/plugins/charts/public'; @@ -54,9 +55,19 @@ const legendOptions: Array<{ }, ]; +const emptySizeRatioLabel = i18n.translate('xpack.lens.pieChart.emptySizeRatioLabel', { + defaultMessage: 'Inner area size', +}); + export function PieToolbar(props: VisualizationToolbarProps) { const { state, setState, frame } = props; const layer = state.layers[0]; + const { + categoryOptions, + numberOptions, + emptySizeRatioOptions, + isDisabled: isToolbarPopoverDisabled, + } = PartitionChartsMeta[state.shape].toolbarPopover; const onStateChange = useCallback( (part: Record) => { @@ -118,14 +129,17 @@ export function PieToolbar(props: VisualizationToolbarProps { + const emptySizeRatio = emptySizeRatioOptions?.find(({ id }) => id === sizeId)?.value; + onStateChange({ emptySizeRatio }); + }, + [emptySizeRatioOptions, onStateChange] + ); + if (!layer) { return null; } - const { - categoryOptions, - numberOptions, - isDisabled: isToolbarPopoverDisabled, - } = PartitionChartsMeta[state.shape].toolbarPopover; const defaultTruncationValue = getDefaultVisualValuesForLayer( state, @@ -193,6 +207,32 @@ export function PieToolbar(props: VisualizationToolbarProps + {emptySizeRatioOptions?.length ? ( + + + value === layer.emptySizeRatio)?.id ?? + 'emptySizeRatioOption-small' + } + onChange={onEmptySizeRatioChange} + data-test-subj="lnsEmptySizeRatioButtonGroup" + /> + + + ) : null} { it('should allow creation of lens xy chart', async () => { @@ -768,5 +769,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.removeFilter('extension.raw'); }); + + it('should show visual options button group for a donut chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.switchToVisualization('donut'); + + const hasVisualOptionsButton = await PageObjects.lens.hasVisualOptionsButton(); + expect(hasVisualOptionsButton).to.be(true); + + await PageObjects.lens.openVisualOptions(); + await retry.try(async () => { + expect(await PageObjects.lens.hasEmptySizeRatioButtonGroup()).to.be(true); + }); + }); + + it('should not show visual options button group for a pie chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.switchToVisualization('pie'); + + const hasVisualOptionsButton = await PageObjects.lens.hasVisualOptionsButton(); + expect(hasVisualOptionsButton).to.be(false); + }); }); } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index d1990d47a37e5..f55b05bc65a05 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -590,6 +590,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await colorPickerInput.type(color); await PageObjects.common.sleep(1000); // give time for debounced components to rerender }, + hasVisualOptionsButton() { + return testSubjects.exists('lnsVisualOptionsButton'); + }, async openVisualOptions() { await retry.try(async () => { await testSubjects.click('lnsVisualOptionsButton'); @@ -1236,5 +1239,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const filterIn = await testSubjects.find(`legend-${value}-filterIn`); await filterIn.click(); }, + + hasEmptySizeRatioButtonGroup() { + return testSubjects.exists('lnsEmptySizeRatioButtonGroup'); + }, }); }