[Lens] Pie and donuts should have a size ratio setting (#120101)

* [WIP][Lens] Waffle visualization type

Closes:  #107059

* add showExtraLegend for waffle

* add tests

* resolved 1 and 5

* resolved 6

* add sortPredicate for waffle chart type

* [Lens] Pie and donuts should have a size ratio setting

* Add a missed condition

* Fix changing size for smallSlices

* Add donut inner area size setting to pie visualization and update it for lens

* Update test and rename some constants

* Rename the setting

* Move handler to a separate useCallback function

* Update size ratios and add condition for legacy charts

* Fix merge conflict issue

* Change constants order

* Add a couple of tests to check if the setting is displayed

* Update ratio sizes

Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Diana Derevyankina 2021-12-20 12:50:57 +03:00 committed by GitHub
parent eea9cdcd5d
commit 2d2d702a13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 221 additions and 11 deletions

View file

@ -47,6 +47,7 @@ Object {
"splitRow": undefined,
},
"distinctColors": false,
"emptySizeRatio": 0.3,
"isDonut": true,
"labels": Object {
"percentDecimals": 2,

View file

@ -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',
}),
},
];

View file

@ -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(<PieOptions {...props} />);
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(<PieOptions {...props} showElasticChartsOptions={false} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieEmptySizeRatioButtonGroup').length).toBe(0);
});
});
});

View file

@ -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<PieVisParams>, PieTypeProps {}
const emptySizeRatioLabel = i18n.translate('visTypePie.editors.pie.emptySizeRatioLabel', {
defaultMessage: 'Inner area size',
});
function DecimalSlider<ParamName extends string>({
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 (
<>
<EuiPanel paddingSize="s">
@ -116,6 +129,23 @@ const PieOptions = (props: PieOptionsProps) => {
value={stateParams.isDonut}
setValue={setValue}
/>
{props.showElasticChartsOptions && stateParams.isDonut && (
<EuiFormRow label={emptySizeRatioLabel} fullWidth>
<EuiButtonGroup
isFullWidth
name="emptySizeRatio"
buttonSize="compressed"
legend={emptySizeRatioLabel}
options={emptySizeRatioOptions}
idSelected={
emptySizeRatioOptions.find(({ value }) => value === stateParams.emptySizeRatio)
?.id ?? 'emptySizeRatioOption-small'
}
onChange={handleEmptySizeRatioChange}
data-test-subj="visTypePieEmptySizeRatioButtonGroup"
/>
</EuiFormRow>
)}
<BasicOptions {...props} legendPositions={getLegendPositions} />
{props.showElasticChartsOptions && (
<>

View file

@ -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,
}

View file

@ -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,

View file

@ -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', {

View file

@ -54,6 +54,7 @@ export const toExpressionAst: VisToExpressionAst<PieVisParams> = 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),

View file

@ -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 {

View file

@ -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) {

View file

@ -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',

View file

@ -101,6 +101,10 @@ export const pie: ExpressionFunctionDefinition<
help: '',
types: ['palette'],
},
emptySizeRatio: {
types: ['number'],
help: '',
},
},
inputTypes: ['lens_multitable'],
fn(data: LensMultiTable, args: PieExpressionArgs) {

View file

@ -20,6 +20,7 @@ export interface SharedPieLayerState {
showValuesInLegend?: boolean;
nestedLegend?: boolean;
percentDecimals?: number;
emptySizeRatio?: number;
legendMaxLines?: number;
truncateLegend?: boolean;
}

View file

@ -6,3 +6,9 @@
*/
export const DEFAULT_PERCENT_DECIMALS = 2;
export enum EMPTY_SIZE_RATIOS {
SMALL = 0.3,
MEDIUM = 0.54,
LARGE = 0.7,
}

View file

@ -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<PieChartTypes, PartitionChartMeta> = {
donut: {
icon: LensIconChartDonut,
@ -122,6 +152,7 @@ export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
toolbarPopover: {
categoryOptions,
numberOptions,
emptySizeRatioOptions,
},
legend: {
getShowLegendDefault: (bucketColumns) => bucketColumns.length > 1,

View file

@ -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

View file

@ -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'

View file

@ -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<PieVisualizationState>) {
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<string, unknown>) => {
@ -118,14 +129,17 @@ export function PieToolbar(props: VisualizationToolbarProps<PieVisualizationStat
});
}, [layer, state.shape, onStateChange]);
const onEmptySizeRatioChange = useCallback(
(sizeId) => {
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<PieVisualizationStat
/>
</EuiFormRow>
</ToolbarPopover>
{emptySizeRatioOptions?.length ? (
<ToolbarPopover
title={i18n.translate('xpack.lens.pieChart.visualOptionsLabel', {
defaultMessage: 'Visual options',
})}
type="visualOptions"
groupPosition="center"
buttonDataTestSubj="lnsVisualOptionsButton"
>
<EuiFormRow label={emptySizeRatioLabel} display="columnCompressed" fullWidth>
<EuiButtonGroup
isFullWidth
name="emptySizeRatio"
buttonSize="compressed"
legend={emptySizeRatioLabel}
options={emptySizeRatioOptions}
idSelected={
emptySizeRatioOptions.find(({ value }) => value === layer.emptySizeRatio)?.id ??
'emptySizeRatioOption-small'
}
onChange={onEmptySizeRatioChange}
data-test-subj="lnsEmptySizeRatioButtonGroup"
/>
</EuiFormRow>
</ToolbarPopover>
) : null}
<LegendSettingsPopover
legendOptions={legendOptions}
mode={layer.legendDisplay}

View file

@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const elasticChart = getService('elasticChart');
const filterBar = getService('filterBar');
const retry = getService('retry');
describe('lens smokescreen tests', () => {
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);
});
});
}

View file

@ -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');
},
});
}