Skip to content

Commit cd9e13b

Browse files
authored
Charts: adds legend composition for pie chart family (#44796)
* fix(pie-chart): render SVG children inside SVG and React children outside * fix(pie-semi-circle-chart): render SVG children inside SVG and React children outside * refactor(pie-chart): move composition story after WithLegend and rename * refactor(donut-chart): move composition story after WithLegend and rename * refactor(pie-semi-circle-chart): move composition story after WithLegend and rename * changelog * perf(pie-chart): optimize Children.map to use single pass * perf(pie-semi-circle-chart): optimize Children.map to use single pass * export PieChartUnresponsive for composition API * add PropsWithChildren import and SVG/HTML types * add PieChartSVG and PieChartHTML compound components * update children processing for compound components * attach SVG and HTML compound components to exports * add composition API story example * add composition API documentation * add composition API tests * add SVG and HTML to PieSemiCircleChartSubComponents interface * create PieSemiCircleChartSVG compound component * create PieSemiCircleChartHTML compound component * update child rendering to use new compound component structure * add SVG and HTML to attachSubComponents calls * add CompositionAPI story demonstrating new features * fix import issues and missing defaultTheme after rebase
1 parent 7feb63e commit cd9e13b

File tree

9 files changed

+1044
-189
lines changed

9 files changed

+1044
-189
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Charts: adds composition legend to pie family charts
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default as PieChart } from './pie-chart';
1+
export { default as PieChart, PieChartUnresponsive } from './pie-chart';

projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx

Lines changed: 205 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { Group } from '@visx/group';
22
import { Pie } from '@visx/shape';
33
import clsx from 'clsx';
4-
import { useContext, useMemo } from 'react';
4+
import { useContext, useMemo, Children, isValidElement } from 'react';
55
import { useChartMouseHandler, useGlobalChartTheme } from '../../hooks';
66
import {
77
GlobalChartsProvider,
88
useChartId,
99
useChartRegistration,
1010
} from '../../providers/chart-context';
1111
import { GlobalChartsContext } from '../../providers/chart-context/global-charts-provider';
12+
import { attachSubComponents } from '../../utils/create-composition';
1213
import { Legend } from '../legend';
1314
import { useChartLegendData } from '../legend/use-chart-legend-data';
15+
import { SingleChartContext } from '../shared/single-chart-context';
1416
import { useElementHeight } from '../shared/use-element-height';
1517
import { withResponsive } from '../shared/with-responsive';
1618
import { BaseTooltip } from '../tooltip';
1719
import styles from './pie-chart.module.scss';
18-
import type { BaseChartProps, DataPointPercentage } from '../../types';
19-
import type { SVGProps, MouseEvent, ReactNode } from 'react';
20+
import type { BaseChartProps, DataPointPercentage, Optional } from '../../types';
21+
import type { ResponsiveConfig } from '../shared/with-responsive';
22+
import type { SVGProps, MouseEvent, ReactNode, FC, ComponentType, PropsWithChildren } from 'react';
2023

2124
interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
2225
/**
@@ -54,6 +57,20 @@ interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
5457
children?: ReactNode;
5558
}
5659

60+
// Base props type with optional responsive properties
61+
type PieChartBaseProps = Optional< PieChartProps, 'size' >;
62+
63+
// Composition API types
64+
interface PieChartSubComponents {
65+
Legend: ComponentType< React.ComponentProps< typeof Legend > >;
66+
SVG: FC< PropsWithChildren >;
67+
HTML: FC< PropsWithChildren >;
68+
}
69+
70+
type PieChartComponent = FC< PieChartBaseProps > & PieChartSubComponents;
71+
type PieChartResponsiveComponent = FC< PieChartBaseProps & ResponsiveConfig > &
72+
PieChartSubComponents;
73+
5774
/**
5875
* Validates the pie chart data
5976
* @param data - The data to validate
@@ -80,6 +97,36 @@ const validateData = ( data: DataPointPercentage[] ) => {
8097
return { isValid: true, message: '' };
8198
};
8299

100+
/**
101+
* Compound component for SVG children in the PieChart
102+
* @param {PropsWithChildren} props - Component props
103+
* @param {ReactNode} props.children - Child elements to render
104+
* @return {JSX.Element} The children wrapped in a fragment
105+
*/
106+
const PieChartSVG: FC< PropsWithChildren > = ( { children } ) => {
107+
// This component doesn't render directly - its children are extracted by PieChart
108+
// We just return the children as-is
109+
return <>{ children }</>;
110+
};
111+
112+
// Set displayName for better debugging and type checking
113+
PieChartSVG.displayName = 'PieChart.SVG';
114+
115+
/**
116+
* Compound component for HTML children in the PieChart
117+
* @param {PropsWithChildren} props - Component props
118+
* @param {ReactNode} props.children - Child elements to render
119+
* @return {JSX.Element} The children wrapped in a fragment
120+
*/
121+
const PieChartHTML: FC< PropsWithChildren > = ( { children } ) => {
122+
// This component doesn't render directly - its children are extracted by PieChart
123+
// We just return the children as-is
124+
return <>{ children }</>;
125+
};
126+
127+
// Set displayName for better debugging and type checking
128+
PieChartHTML.displayName = 'PieChart.HTML';
129+
83130
/**
84131
* Renders a pie or donut chart using the provided data.
85132
*
@@ -119,6 +166,40 @@ const PieChartInternal = ( {
119166

120167
const { isValid, message } = validateData( data );
121168

169+
// Process children to extract compound components
170+
const { svgChildren, htmlChildren, otherChildren } = useMemo( () => {
171+
const svg: ReactNode[] = [];
172+
const html: ReactNode[] = [];
173+
const other: ReactNode[] = [];
174+
175+
Children.forEach( children, child => {
176+
if ( isValidElement( child ) ) {
177+
// Check displayName for compound components
178+
const childType = child.type as { displayName?: string };
179+
const displayName = childType?.displayName;
180+
181+
if ( displayName === 'PieChart.SVG' ) {
182+
// Extract children from PieChart.SVG
183+
Children.forEach( child.props.children, svgChild => {
184+
svg.push( svgChild );
185+
} );
186+
} else if ( displayName === 'PieChart.HTML' ) {
187+
// Extract children from PieChart.HTML
188+
Children.forEach( child.props.children, htmlChild => {
189+
html.push( htmlChild );
190+
} );
191+
} else if ( child.type === Group ) {
192+
// Legacy support: still check for Group type for backward compatibility
193+
svg.push( child );
194+
} else {
195+
other.push( child );
196+
}
197+
}
198+
} );
199+
200+
return { svgChildren: svg, htmlChildren: html, otherChildren: other };
201+
}, [ children ] );
202+
122203
// Memoize metadata to prevent unnecessary re-registration
123204
const chartMetadata = useMemo(
124205
() => ( {
@@ -180,99 +261,114 @@ const PieChartInternal = ( {
180261
};
181262

182263
return (
183-
<div
184-
className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) }
185-
style={ {
186-
display: 'flex',
187-
flexDirection: showLegend && legendPosition === 'top' ? 'column-reverse' : 'column',
264+
<SingleChartContext.Provider
265+
value={ {
266+
chartId,
267+
chartWidth: size,
268+
chartHeight: adjustedHeight,
188269
} }
189270
>
190-
<svg
191-
viewBox={ `0 0 ${ size } ${ adjustedHeight }` }
192-
preserveAspectRatio="xMidYMid meet"
193-
width={ size }
194-
height={ adjustedHeight }
271+
<div
272+
className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) }
273+
style={ {
274+
display: 'flex',
275+
flexDirection: showLegend && legendPosition === 'top' ? 'column-reverse' : 'column',
276+
} }
195277
>
196-
<Group top={ centerY } left={ centerX }>
197-
<Pie< DataPointPercentage & { index: number } >
198-
data={ dataWithIndex }
199-
pieValue={ accessors.value }
200-
outerRadius={ outerRadius }
201-
innerRadius={ innerRadius }
202-
padAngle={ padAngle }
203-
cornerRadius={ cornerRadius }
204-
>
205-
{ pie => {
206-
return pie.arcs.map( ( arc, index ) => {
207-
const [ centroidX, centroidY ] = pie.path.centroid( arc );
208-
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
209-
const handleMouseMove = ( event: MouseEvent< SVGElement > ) =>
210-
onMouseMove( event, arc.data );
211-
212-
const pathProps: SVGProps< SVGPathElement > = {
213-
d: pie.path( arc ) || '',
214-
fill: accessors.fill( arc.data ),
215-
};
216-
217-
if ( withTooltips ) {
218-
pathProps.onMouseMove = handleMouseMove;
219-
pathProps.onMouseLeave = onMouseLeave;
220-
}
221-
222-
return (
223-
<g key={ `arc-${ index }` }>
224-
<path { ...pathProps } />
225-
{ hasSpaceForLabel && (
226-
<text
227-
x={ centroidX }
228-
y={ centroidY }
229-
dy=".33em"
230-
fill={ providerTheme.labelBackgroundColor }
231-
fontSize={ 12 }
232-
textAnchor="middle"
233-
pointerEvents="none"
234-
>
235-
{ arc.data.label }
236-
</text>
237-
) }
238-
</g>
239-
);
240-
} );
278+
<svg
279+
viewBox={ `0 0 ${ size } ${ adjustedHeight }` }
280+
preserveAspectRatio="xMidYMid meet"
281+
width={ size }
282+
height={ adjustedHeight }
283+
>
284+
<Group top={ centerY } left={ centerX }>
285+
<Pie< DataPointPercentage & { index: number } >
286+
data={ dataWithIndex }
287+
pieValue={ accessors.value }
288+
outerRadius={ outerRadius }
289+
innerRadius={ innerRadius }
290+
padAngle={ padAngle }
291+
cornerRadius={ cornerRadius }
292+
>
293+
{ pie => {
294+
return pie.arcs.map( ( arc, index ) => {
295+
const [ centroidX, centroidY ] = pie.path.centroid( arc );
296+
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
297+
const handleMouseMove = ( event: MouseEvent< SVGElement > ) =>
298+
onMouseMove( event, arc.data );
299+
300+
const pathProps: SVGProps< SVGPathElement > = {
301+
d: pie.path( arc ) || '',
302+
fill: accessors.fill( arc.data ),
303+
};
304+
305+
if ( withTooltips ) {
306+
pathProps.onMouseMove = handleMouseMove;
307+
pathProps.onMouseLeave = onMouseLeave;
308+
}
309+
310+
return (
311+
<g key={ `arc-${ index }` }>
312+
<path { ...pathProps } />
313+
{ hasSpaceForLabel && (
314+
<text
315+
x={ centroidX }
316+
y={ centroidY }
317+
dy=".33em"
318+
fill={ providerTheme.labelBackgroundColor || '#333' }
319+
fontSize={ 12 }
320+
textAnchor="middle"
321+
pointerEvents="none"
322+
>
323+
{ arc.data.label }
324+
</text>
325+
) }
326+
</g>
327+
);
328+
} );
329+
} }
330+
</Pie>
331+
332+
{ /* Render SVG children (like Group, Text) inside the SVG */ }
333+
{ svgChildren }
334+
</Group>
335+
</svg>
336+
337+
{ showLegend && (
338+
<Legend
339+
items={ legendItems }
340+
orientation={ legendOrientation }
341+
position={ legendPosition }
342+
alignment={ legendAlignment }
343+
className={ styles[ 'pie-chart-legend' ] }
344+
shape={ legendShape }
345+
ref={ legendRef }
346+
chartId={ chartId }
347+
/>
348+
) }
349+
350+
{ withTooltips && tooltipOpen && tooltipData && (
351+
<BaseTooltip
352+
data={ tooltipData }
353+
top={ tooltipTop || 0 }
354+
left={ tooltipLeft || 0 }
355+
style={ {
356+
transform: 'translate(-50%, -100%)',
241357
} }
242-
</Pie>
243-
244-
{ children }
245-
</Group>
246-
</svg>
247-
248-
{ showLegend && (
249-
<Legend
250-
items={ legendItems }
251-
orientation={ legendOrientation }
252-
position={ legendPosition }
253-
alignment={ legendAlignment }
254-
className={ styles[ 'pie-chart-legend' ] }
255-
shape={ legendShape }
256-
ref={ legendRef }
257-
chartId={ chartId }
258-
/>
259-
) }
260-
261-
{ withTooltips && tooltipOpen && tooltipData && (
262-
<BaseTooltip
263-
data={ tooltipData }
264-
top={ tooltipTop || 0 }
265-
left={ tooltipLeft || 0 }
266-
style={ {
267-
transform: 'translate(-50%, -100%)',
268-
} }
269-
/>
270-
) }
271-
</div>
358+
/>
359+
) }
360+
361+
{ /* Render HTML component children from PieChart.HTML */ }
362+
{ htmlChildren }
363+
364+
{ /* Render other React children for backward compatibility */ }
365+
{ otherChildren }
366+
</div>
367+
</SingleChartContext.Provider>
272368
);
273369
};
274370

275-
const PieChart = ( props: PieChartProps ) => {
371+
const PieChartWithProvider: FC< PieChartProps > = props => {
276372
const existingContext = useContext( GlobalChartsContext );
277373

278374
// If we're already in a GlobalChartsProvider context, don't create a new one
@@ -288,5 +384,23 @@ const PieChart = ( props: PieChartProps ) => {
288384
);
289385
};
290386

291-
PieChart.displayName = 'PieChart';
292-
export default withResponsive< PieChartProps >( PieChart );
387+
PieChartWithProvider.displayName = 'PieChart';
388+
389+
// Create PieChart with composition API
390+
const PieChart = attachSubComponents( PieChartWithProvider, {
391+
Legend: Legend,
392+
SVG: PieChartSVG,
393+
HTML: PieChartHTML,
394+
} ) as PieChartComponent;
395+
396+
// Create responsive PieChart with composition API
397+
const PieChartResponsive = attachSubComponents(
398+
withResponsive< PieChartProps >( PieChartWithProvider ),
399+
{
400+
Legend: Legend,
401+
SVG: PieChartSVG,
402+
HTML: PieChartHTML,
403+
}
404+
) as PieChartResponsiveComponent;
405+
406+
export { PieChartResponsive as default, PieChart as PieChartUnresponsive };

0 commit comments

Comments
 (0)