[Lens] Improve outside label placement for pie/donut charts (#115966)

* 🐛 Shrink pie/donut chart to allow more space for tiny slices

*  Add unit tests

*  Handler for small slices in Visualize pie

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2021-11-09 16:58:55 +01:00 committed by GitHub
parent c32191007d
commit 90395c5589
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 140 additions and 4 deletions

View file

@ -234,9 +234,21 @@ const PieComponent = (props: PieComponentProps) => {
syncColors,
]
);
const rescaleFactor = useMemo(() => {
const overallSum = visData.rows.reduce((sum, row) => sum + row[metricColumn.id], 0);
const slices = visData.rows.map((row) => row[metricColumn.id] / overallSum);
const smallSlices = slices.filter((value) => value < 0.02).length;
if (smallSlices) {
// shrink up to 20% to give some room for the linked values
return 1 / (1 + Math.min(smallSlices * 0.05, 0.2));
}
return 1;
}, [visData.rows, metricColumn]);
const config = useMemo(
() => getConfig(visParams, chartTheme, dimensions),
[chartTheme, visParams, dimensions]
() => getConfig(visParams, chartTheme, dimensions, rescaleFactor),
[chartTheme, visParams, dimensions, rescaleFactor]
);
const tooltip: TooltipProps = {
type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None,

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getConfig } from './get_config';
import { createMockPieParams } from '../mocks';
const visParams = createMockPieParams();
describe('getConfig', () => {
it('should cap the outerSizeRatio to 1', () => {
expect(getConfig(visParams, {}, { width: 400, height: 400 }).outerSizeRatio).toBe(1);
});
it('should not have outerSizeRatio for split chart', () => {
expect(
getConfig(
{
...visParams,
dimensions: {
...visParams.dimensions,
splitColumn: [
{
accessor: 1,
format: {
id: 'number',
},
},
],
},
},
{},
{ width: 400, height: 400 }
).outerSizeRatio
).toBeUndefined();
expect(
getConfig(
{
...visParams,
dimensions: {
...visParams.dimensions,
splitRow: [
{
accessor: 1,
format: {
id: 'number',
},
},
],
},
},
{},
{ width: 400, height: 400 }
).outerSizeRatio
).toBeUndefined();
});
it('should not set outerSizeRatio if dimensions are not defined', () => {
expect(getConfig(visParams, {}).outerSizeRatio).toBeUndefined();
});
});

View file

@ -13,7 +13,8 @@ const MAX_SIZE = 1000;
export const getConfig = (
visParams: PieVisParams,
chartTheme: RecursivePartial<Theme>,
dimensions?: PieContainerDimensions
dimensions?: PieContainerDimensions,
rescaleFactor: number = 1
): RecursivePartial<PartitionConfig> => {
// On small multiples we want the labels to only appear inside
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
@ -32,7 +33,9 @@ export const getConfig = (
const usingOuterSizeRatio =
dimensions && !isSplitChart
? {
outerSizeRatio: MAX_SIZE / Math.min(dimensions?.width, dimensions?.height),
outerSizeRatio:
// Cap the ratio to 1 and then rescale
rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1),
}
: null;
const config: RecursivePartial<PartitionConfig> = {

View file

@ -376,5 +376,50 @@ describe('PieVisualization component', () => {
expect(component.find(VisualizationContainer)).toHaveLength(1);
expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut);
});
test('it should dynamically shrink the chart area to when some small slices are detected', () => {
const defaultData = getDefaultArgs().data;
const emptyData: LensMultiTable = {
...defaultData,
tables: {
first: {
...defaultData.tables.first,
rows: [
{ a: 60, b: 'I', c: 200, d: 'Row 1' },
{ a: 1, b: 'J', c: 0.1, d: 'Row 2' },
],
},
},
};
const component = shallow(
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
);
expect(component.find(Partition).prop('config')?.outerSizeRatio).toBeCloseTo(1 / 1.05);
});
test('it should bound the shrink the chart area to ~20% when some small slices are detected', () => {
const defaultData = getDefaultArgs().data;
const emptyData: LensMultiTable = {
...defaultData,
tables: {
first: {
...defaultData.tables.first,
rows: [
{ a: 60, b: 'I', c: 200, d: 'Row 1' },
{ a: 1, b: 'J', c: 0.1, d: 'Row 2' },
{ a: 1, b: 'K', c: 0.1, d: 'Row 3' },
{ a: 1, b: 'G', c: 0.1, d: 'Row 4' },
{ a: 1, b: 'H', c: 0.1, d: 'Row 5' },
],
},
},
};
const component = shallow(
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
);
expect(component.find(Partition).prop('config')?.outerSizeRatio).toBeCloseTo(1 / 1.2);
});
});
});

View file

@ -204,6 +204,16 @@ export function PieComponent(
} else if (categoryDisplay === 'inside') {
// Prevent links from showing
config.linkLabel = { maxCount: 0 };
} else {
// if it contains any slice below 2% reduce the ratio
// first step: sum it up the overall sum
const overallSum = firstTable.rows.reduce((sum, row) => sum + row[metric!], 0);
const slices = firstTable.rows.map((row) => row[metric!] / overallSum);
const smallSlices = slices.filter((value) => value < 0.02).length;
if (smallSlices) {
// shrink up to 20% to give some room for the linked values
config.outerSizeRatio = 1 / (1 + Math.min(smallSlices * 0.05, 0.2));
}
}
}
const metricColumn = firstTable.columns.find((c) => c.id === metric)!;