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 (