diff --git a/src/pie-chart/__tests__/__snapshots__/utils.test.tsx.snap b/src/pie-chart/__tests__/__snapshots__/utils.test.tsx.snap
index dd22c935a6..b60ebbb043 100644
--- a/src/pie-chart/__tests__/__snapshots__/utils.test.tsx.snap
+++ b/src/pie-chart/__tests__/__snapshots__/utils.test.tsx.snap
@@ -1,5 +1,111 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`balanceLabelNodes changes xOffset if vertical overlap 1`] = `
+NodeList [
+
+
+ Segment 1
+
+ ,
+
+
+ Segment 2
+
+ ,
+
+
+ Segment 3
+
+ ,
+
+
+ Segment 4
+
+ ,
+]
+`;
+
+exports[`balanceLabelNodes does not change xOffset if no vertical overlap 1`] = `
+NodeList [
+
+
+ Segment 1
+
+ ,
+
+
+ Segment 2
+
+ ,
+
+
+ Segment 3
+
+ ,
+
+
+ Segment 4
+
+ ,
+]
+`;
+
exports[`balanceLabelNodes empty 1`] = `NodeList []`;
exports[`balanceLabelNodes heavy overlap on both sides 1`] = `
diff --git a/src/pie-chart/__tests__/utils.test.tsx b/src/pie-chart/__tests__/utils.test.tsx
index df03394ab0..ae99893462 100644
--- a/src/pie-chart/__tests__/utils.test.tsx
+++ b/src/pie-chart/__tests__/utils.test.tsx
@@ -5,8 +5,10 @@ import { render } from '@testing-library/react';
import {
balanceLabelNodes,
+ computeSmartAngle,
getDimensionsBySize,
dimensionsBySize,
+ minLabelLineAngularPadding,
refreshDimensionsBySize,
} from '../../../lib/components/pie-chart/utils';
@@ -32,7 +34,7 @@ const testCases = [
>
),
- markers: [{ endY: 120 }],
+ markers: [{ endY: 120, endX: -125 }],
},
{
title: 'two equal segments',
@@ -52,7 +54,10 @@ const testCases = [
>
),
- markers: [{ endY: 0 }, { endY: 0 }],
+ markers: [
+ { endY: 0, endX: 125 },
+ { endY: 0, endX: -125 },
+ ],
},
{
title: 'heavy overlap on the right side',
@@ -98,13 +103,13 @@ const testCases = [
>
),
markers: [
- { endY: -119 },
- { endY: -117 },
- { endY: -113 },
- { endY: -108 },
- { endY: -100 },
- { endY: -92 },
- { endY: 111 },
+ { endY: -119, endX: 125 },
+ { endY: -117, endX: 125 },
+ { endY: -113, endX: 125 },
+ { endY: -108, endX: 125 },
+ { endY: -100, endX: 125 },
+ { endY: -92, endX: 125 },
+ { endY: 111, endX: -125 },
],
},
{
@@ -151,13 +156,13 @@ const testCases = [
>
),
markers: [
- { endY: 111 },
- { endY: -92 },
- { endY: -100 },
- { endY: -108 },
- { endY: -114 },
- { endY: -118 },
- { endY: -120 },
+ { endY: 111, endX: 125 },
+ { endY: -92, endX: -125 },
+ { endY: -100, endX: -125 },
+ { endY: -108, endX: -125 },
+ { endY: -114, endX: -125 },
+ { endY: -118, endX: -125 },
+ { endY: -120, endX: -125 },
],
},
{
@@ -204,13 +209,13 @@ const testCases = [
>
),
markers: [
- { endY: -120 },
- { endY: -118 },
- { endY: -109 },
- { endY: 120 },
- { endY: -109 },
- { endY: -118 },
- { endY: -120 },
+ { endY: -120, endX: 125 },
+ { endY: -118, endX: 125 },
+ { endY: -109, endX: 125 },
+ { endY: 120, endX: 125 },
+ { endY: -109, endX: -125 },
+ { endY: -118, endX: -125 },
+ { endY: -120, endX: -125 },
],
},
{
@@ -246,7 +251,83 @@ const testCases = [
>
),
- markers: [{ endY: 45 }, { endY: 45 }, { endY: -45 }, { endY: -113 }, { endY: -119 }],
+ markers: [
+ { endY: 45, endX: 125 },
+ { endY: 45, endX: -125 },
+ { endY: -45, endX: -125 },
+ { endY: -113, endX: -125 },
+ { endY: -119, endX: -125 },
+ ],
+ },
+ {
+ title: 'does not change xOffset if no vertical overlap',
+ width: 600,
+ height: 300,
+ nodes: (
+ <>
+
+
+ Segment 1
+
+
+
+
+ Segment 2
+
+
+
+
+ Segment 3
+
+
+
+
+ Segment 4
+
+
+ >
+ ),
+ markers: [
+ { endY: -50, endX: 20 },
+ { endY: -20, endX: 20 },
+ { endY: 50, endX: -20 },
+ { endY: 20, endX: -20 },
+ ],
+ },
+ {
+ title: 'changes xOffset if vertical overlap',
+ width: 600,
+ height: 300,
+ nodes: (
+ <>
+
+
+ Segment 1
+
+
+
+
+ Segment 2
+
+
+
+
+ Segment 3
+
+
+
+
+ Segment 4
+
+
+ >
+ ),
+ markers: [
+ { endY: -50, endX: 20 },
+ { endY: -49, endX: 20 },
+ { endY: 21, endX: -20 },
+ { endY: 20, endX: -20 },
+ ],
},
];
@@ -284,8 +365,8 @@ describe('balanceLabelNodes', () => {
);
const labels = container.querySelectorAll('.labels g');
- balanceLabelNodes(labels, markers, false);
- balanceLabelNodes(labels, markers, true);
+ balanceLabelNodes(labels, markers, false, 100);
+ balanceLabelNodes(labels, markers, true, 100);
expect(labels).toMatchSnapshot();
});
@@ -318,3 +399,48 @@ describe.each([false, true])('getDimensionsBySize visualRefresh=%s', visualRefre
expect(dimensions.size).toBe(matchedSize);
});
});
+
+describe('computeSmartAngle', () => {
+ const pi = Math.PI;
+ test('returns mid angle if optimization is disabled', () => {
+ expect(computeSmartAngle(0, pi / 100)).toEqual(pi / 200);
+ expect(computeSmartAngle(-1.5, 1)).toEqual(-0.25);
+ expect(computeSmartAngle(2, 4)).toEqual(3);
+ expect(computeSmartAngle(0, pi / 2)).toEqual(pi / 4);
+ });
+ test('returns mid angle if segment is too small', () => {
+ expect(computeSmartAngle(0, pi / 100, true)).toEqual(pi / 200);
+ expect(computeSmartAngle(0, 2 * minLabelLineAngularPadding, true)).toEqual(minLabelLineAngularPadding);
+ expect(computeSmartAngle(1, 1, true)).toEqual(1);
+ });
+ test('returns 0 if segment contains 0 angle', () => {
+ const startAngle = -1.5;
+ const endAngle = 1;
+ expect(computeSmartAngle(startAngle, endAngle, true)).toEqual(0);
+ });
+ test('returns PI if segment contains PI angle', () => {
+ const startAngle = 2;
+ const endAngle = 4;
+ expect(computeSmartAngle(startAngle, endAngle, true)).toEqual(pi);
+ });
+ test('returns padded start angle if closest to 0', () => {
+ const startAngle = 0;
+ const endAngle = pi / 2;
+ expect(computeSmartAngle(startAngle, endAngle, true)).toEqual(minLabelLineAngularPadding);
+ });
+ test('returns padded start angle if closest to PI', () => {
+ const startAngle = pi;
+ const endAngle = (3 * pi) / 2;
+ expect(computeSmartAngle(startAngle, endAngle, true)).toEqual(pi + minLabelLineAngularPadding);
+ });
+ test('returns padded end angle if closest to 2*PI', () => {
+ const startAngle = (3 * pi) / 2;
+ const endAngle = 2 * pi;
+ expect(computeSmartAngle(startAngle, endAngle, true)).toEqual(2 * pi - minLabelLineAngularPadding);
+ });
+ test('returns padded end angle if closest to PI', () => {
+ const startAngle = pi / 2;
+ const endAngle = pi;
+ expect(computeSmartAngle(startAngle, endAngle, true)).toEqual(pi - minLabelLineAngularPadding);
+ });
+});
diff --git a/src/pie-chart/labels.tsx b/src/pie-chart/labels.tsx
index 12889e25ba..7782452848 100644
--- a/src/pie-chart/labels.tsx
+++ b/src/pie-chart/labels.tsx
@@ -7,7 +7,7 @@ import { arc, PieArcDatum } from 'd3-shape';
import { PieChartProps } from './interfaces';
import styles from './styles.css.js';
import { InternalChartDatum } from './pie-chart';
-import { Dimension, balanceLabelNodes } from './utils';
+import { Dimension, balanceLabelNodes, computeSmartAngle } from './utils';
import ResponsiveText from './responsive-text';
import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal';
@@ -78,7 +78,9 @@ export default ({
containerRef,
}: LabelsProps) => {
const containerBoundaries = useElementBoundaries(containerRef);
-
+ const shouldOptimizeLabels =
+ containerBoundaries.right - containerBoundaries.left - (dimensions.outerRadius + dimensions.innerLabelPadding) * 2 <
+ 300;
const markers = useMemo(() => {
const { outerRadius: radius, innerLabelPadding } = dimensions;
@@ -93,16 +95,17 @@ export default ({
return pieData.map((datum, i) => {
const labelDatum = pieData[i];
- const midAngle = labelDatum.startAngle + (labelDatum.endAngle - labelDatum.startAngle) / 2;
+ const smartAngle = computeSmartAngle(labelDatum.startAngle, labelDatum.endAngle, shouldOptimizeLabels);
// Make the marker line longer if the segment is closer to the top or bottom of the chart
- arcMarkerBreak.outerRadius(radius + 20 * (0.5 * Math.cos(2 * midAngle) + 0.5));
- arcMarkerBreak.innerRadius(radius + 20 * (0.5 * Math.cos(2 * midAngle) + 0.5));
- const [startX, startY] = arcMarkerStart.centroid(datum);
- const [breakX, breakY] = arcMarkerBreak.centroid(datum);
-
- const rightSide = midAngle < Math.PI;
- const endX = (radius + 20) * (rightSide ? 1 : -1);
+ const lineExtension = 0.5 * Math.cos(2 * smartAngle) + 0.5;
+ arcMarkerBreak.outerRadius(radius + 20 * lineExtension);
+ arcMarkerBreak.innerRadius(radius + 20 * lineExtension);
+ const [startX, startY] = arcMarkerStart.centroid({ ...datum, startAngle: smartAngle, endAngle: smartAngle });
+ const [breakX, breakY] = arcMarkerBreak.centroid({ ...datum, startAngle: smartAngle, endAngle: smartAngle });
+
+ const rightSide = smartAngle < Math.PI;
+ const endX = shouldOptimizeLabels ? breakX + 20 * (rightSide ? 1 : -1) : (radius + 20) * (rightSide ? 1 : -1);
const textX = endX + 5 * (rightSide ? 1 : -1);
return {
@@ -118,7 +121,7 @@ export default ({
datum,
};
});
- }, [pieData, dimensions]);
+ }, [pieData, dimensions, shouldOptimizeLabels]);
const rootRef = useRef(null);
@@ -129,9 +132,9 @@ export default ({
// Relax labels that are overlapping
const labelNodes = rootRef.current.querySelectorAll(`.${styles['label-text']}`);
- balanceLabelNodes(labelNodes, markers, false);
- balanceLabelNodes(labelNodes, markers, true);
- }, [markers, pieData]);
+ balanceLabelNodes(labelNodes, markers, false, dimensions.outerRadius + dimensions.innerLabelPadding);
+ balanceLabelNodes(labelNodes, markers, true, dimensions.outerRadius + dimensions.innerLabelPadding);
+ }, [markers, pieData, dimensions]);
return (
diff --git a/src/pie-chart/utils.ts b/src/pie-chart/utils.ts
index bc52ddf169..2e488f643a 100644
--- a/src/pie-chart/utils.ts
+++ b/src/pie-chart/utils.ts
@@ -16,6 +16,7 @@ export interface Dimension {
const paddingLabels = 44; // = 2 * (size-lineHeight-body-100)
const defaultPadding = 12; // = space-s
const smallPadding = 8; // = space-xs
+export const minLabelLineAngularPadding = Math.PI / 20;
export const dimensionsBySize: Record, Dimension> = {
small: {
@@ -115,8 +116,9 @@ export const defaultDetails =
*/
export const balanceLabelNodes = (
nodes: NodeListOf,
- markers: Array<{ endY: number }>,
- leftSide: boolean
+ markers: Array<{ endY: number; endX: number }>,
+ leftSide: boolean,
+ radius: number
) => {
const MARGIN = 10;
@@ -163,23 +165,68 @@ export const balanceLabelNodes = (
node.setAttribute('transform', '');
// Calculate how much the current node is overlapping with the previous one.
- const offset = previousBBox.y + previousBBox.height + MARGIN - box.y;
+ const yOffset = previousBBox.y + previousBBox.height + MARGIN - box.y;
- if (offset > 0) {
+ if (yOffset > 0) {
+ const xOffset = computeXOffset(box, yOffset, radius) * (leftSide ? -1 : 1);
// Move the label down.
- node.setAttribute('transform', `translate(0 ${offset})`);
+ node.setAttribute('transform', `translate(${xOffset} ${yOffset})`);
// Adjust the attached line accordingly.
const lineNode = node.parentNode?.querySelector(`.${styles['label-line']}`);
if (lineNode) {
- const { endY } = marker;
- lineNode.setAttribute('y2', '' + (endY + offset));
+ const { endY, endX } = marker;
+ lineNode.setAttribute('y2', '' + (endY + yOffset));
+ lineNode.setAttribute('x2', '' + (endX + xOffset));
}
// Update the position accordingly to inform the next label
- box.y += offset;
+ box.y += yOffset;
+ box.x += xOffset;
}
previousBBox = box;
}
};
+
+const squareDistance = (edge: [number, number]): number => Math.pow(edge[0], 2) + Math.pow(edge[1], 2);
+
+const computeXOffset = (box: { x: number; y: number; height: number }, yOffset: number, radius: number): number => {
+ const upperEdge: [number, number] = [box.x, box.y + yOffset];
+ const lowerEdge: [number, number] = [box.x, box.y + box.height + yOffset];
+ const closestEdge = squareDistance(upperEdge) < squareDistance(lowerEdge) ? upperEdge : lowerEdge;
+
+ if (squareDistance(closestEdge) < Math.pow(radius, 2)) {
+ return Math.sqrt(Math.pow(radius, 2) - Math.pow(closestEdge[1], 2)) - Math.abs(closestEdge[0]);
+ }
+ return 0;
+};
+
+export const computeSmartAngle = (startAngle: number, endAngle: number, optimize = false): number => {
+ if (!optimize || endAngle - startAngle < 2 * minLabelLineAngularPadding) {
+ return (endAngle + startAngle) / 2;
+ }
+ const paddedStartAngle = startAngle + minLabelLineAngularPadding;
+ const paddedEndAngle = endAngle - minLabelLineAngularPadding;
+ if (paddedStartAngle < 0 && paddedEndAngle > 0) {
+ return 0;
+ }
+ if (paddedStartAngle < Math.PI && paddedEndAngle > Math.PI) {
+ return Math.PI;
+ }
+
+ const endAngleMinDistance = Math.min(
+ paddedEndAngle,
+ Math.abs(Math.PI - paddedEndAngle),
+ 2 * Math.PI - paddedEndAngle
+ );
+ const startAngleMinDistance = Math.min(
+ paddedStartAngle,
+ Math.abs(Math.PI - paddedStartAngle),
+ 2 * Math.PI - paddedStartAngle
+ );
+ if (endAngleMinDistance < startAngleMinDistance) {
+ return paddedEndAngle;
+ }
+ return paddedStartAngle;
+};