11import { Group } from '@visx/group' ;
22import { Pie } from '@visx/shape' ;
33import clsx from 'clsx' ;
4- import { useContext , useMemo } from 'react' ;
4+ import { useContext , useMemo , Children , isValidElement } from 'react' ;
55import { useChartMouseHandler , useGlobalChartTheme } from '../../hooks' ;
66import {
77 GlobalChartsProvider ,
88 useChartId ,
99 useChartRegistration ,
1010} from '../../providers/chart-context' ;
1111import { GlobalChartsContext } from '../../providers/chart-context/global-charts-provider' ;
12+ import { attachSubComponents } from '../../utils/create-composition' ;
1213import { Legend } from '../legend' ;
1314import { useChartLegendData } from '../legend/use-chart-legend-data' ;
15+ import { SingleChartContext } from '../shared/single-chart-context' ;
1416import { useElementHeight } from '../shared/use-element-height' ;
1517import { withResponsive } from '../shared/with-responsive' ;
1618import { BaseTooltip } from '../tooltip' ;
1719import 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
2124interface 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