-
Notifications
You must be signed in to change notification settings - Fork 826
Charts: adds legend composition for pie chart family #44796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
annacmc
merged 23 commits into
trunk
from
add/charts-90-charts-add-composition-api-for-pie-chart-family
Aug 19, 2025
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
ddd2ccf
fix(pie-chart): render SVG children inside SVG and React children out…
annacmc 722bc3a
fix(pie-semi-circle-chart): render SVG children inside SVG and React …
annacmc 26e54b4
refactor(pie-chart): move composition story after WithLegend and rename
annacmc aaadd58
refactor(donut-chart): move composition story after WithLegend and re…
annacmc ff99196
refactor(pie-semi-circle-chart): move composition story after WithLeg…
annacmc 3eb7eb4
changelog
annacmc 2a98a01
perf(pie-chart): optimize Children.map to use single pass
annacmc f825cf2
perf(pie-semi-circle-chart): optimize Children.map to use single pass
annacmc 7216e11
export PieChartUnresponsive for composition API
annacmc ac73a06
add PropsWithChildren import and SVG/HTML types
annacmc 4b10480
add PieChartSVG and PieChartHTML compound components
annacmc a4da39e
update children processing for compound components
annacmc 82b2d1f
attach SVG and HTML compound components to exports
annacmc 7f4cb3f
add composition API story example
annacmc bb1c4c4
add composition API documentation
annacmc b6b6933
add composition API tests
annacmc d8f56dc
add SVG and HTML to PieSemiCircleChartSubComponents interface
annacmc 0dd1b6e
create PieSemiCircleChartSVG compound component
annacmc ec67f69
create PieSemiCircleChartHTML compound component
annacmc 557590f
update child rendering to use new compound component structure
annacmc 3ae91a7
add SVG and HTML to attachSubComponents calls
annacmc 431a20c
add CompositionAPI story demonstrating new features
annacmc f2f0171
fix import issues and missing defaultTheme after rebase
annacmc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
...s-packages/charts/changelog/add-charts-90-charts-add-composition-api-for-pie-chart-family
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Significance: minor | ||
Type: added | ||
|
||
Charts: adds composition legend to pie family charts |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { default as PieChart } from './pie-chart'; | ||
export { default as PieChart, PieChartUnresponsive } from './pie-chart'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,25 @@ | ||
import { Group } from '@visx/group'; | ||
import { Pie } from '@visx/shape'; | ||
import clsx from 'clsx'; | ||
import { useContext, useMemo } from 'react'; | ||
import { useContext, useMemo, Children, isValidElement } from 'react'; | ||
import { useChartMouseHandler, useGlobalChartTheme } from '../../hooks'; | ||
import { | ||
GlobalChartsProvider, | ||
useChartId, | ||
useChartRegistration, | ||
} from '../../providers/chart-context'; | ||
import { GlobalChartsContext } from '../../providers/chart-context/global-charts-provider'; | ||
import { attachSubComponents } from '../../utils/create-composition'; | ||
import { Legend } from '../legend'; | ||
import { useChartLegendData } from '../legend/use-chart-legend-data'; | ||
import { SingleChartContext } from '../shared/single-chart-context'; | ||
import { useElementHeight } from '../shared/use-element-height'; | ||
import { withResponsive } from '../shared/with-responsive'; | ||
import { BaseTooltip } from '../tooltip'; | ||
import styles from './pie-chart.module.scss'; | ||
import type { BaseChartProps, DataPointPercentage } from '../../types'; | ||
import type { SVGProps, MouseEvent, ReactNode } from 'react'; | ||
import type { BaseChartProps, DataPointPercentage, Optional } from '../../types'; | ||
import type { ResponsiveConfig } from '../shared/with-responsive'; | ||
import type { SVGProps, MouseEvent, ReactNode, FC, ComponentType, PropsWithChildren } from 'react'; | ||
|
||
interface PieChartProps extends BaseChartProps< DataPointPercentage[] > { | ||
/** | ||
|
@@ -54,6 +57,20 @@ interface PieChartProps extends BaseChartProps< DataPointPercentage[] > { | |
children?: ReactNode; | ||
} | ||
|
||
// Base props type with optional responsive properties | ||
type PieChartBaseProps = Optional< PieChartProps, 'size' >; | ||
|
||
// Composition API types | ||
interface PieChartSubComponents { | ||
Legend: ComponentType< React.ComponentProps< typeof Legend > >; | ||
SVG: FC< PropsWithChildren >; | ||
HTML: FC< PropsWithChildren >; | ||
} | ||
|
||
type PieChartComponent = FC< PieChartBaseProps > & PieChartSubComponents; | ||
type PieChartResponsiveComponent = FC< PieChartBaseProps & ResponsiveConfig > & | ||
PieChartSubComponents; | ||
|
||
/** | ||
* Validates the pie chart data | ||
* @param data - The data to validate | ||
|
@@ -80,6 +97,36 @@ const validateData = ( data: DataPointPercentage[] ) => { | |
return { isValid: true, message: '' }; | ||
}; | ||
|
||
/** | ||
* Compound component for SVG children in the PieChart | ||
* @param {PropsWithChildren} props - Component props | ||
* @param {ReactNode} props.children - Child elements to render | ||
* @return {JSX.Element} The children wrapped in a fragment | ||
*/ | ||
const PieChartSVG: FC< PropsWithChildren > = ( { children } ) => { | ||
// This component doesn't render directly - its children are extracted by PieChart | ||
// We just return the children as-is | ||
return <>{ children }</>; | ||
}; | ||
|
||
// Set displayName for better debugging and type checking | ||
PieChartSVG.displayName = 'PieChart.SVG'; | ||
|
||
/** | ||
* Compound component for HTML children in the PieChart | ||
* @param {PropsWithChildren} props - Component props | ||
* @param {ReactNode} props.children - Child elements to render | ||
* @return {JSX.Element} The children wrapped in a fragment | ||
*/ | ||
const PieChartHTML: FC< PropsWithChildren > = ( { children } ) => { | ||
// This component doesn't render directly - its children are extracted by PieChart | ||
// We just return the children as-is | ||
return <>{ children }</>; | ||
}; | ||
|
||
// Set displayName for better debugging and type checking | ||
PieChartHTML.displayName = 'PieChart.HTML'; | ||
|
||
/** | ||
* Renders a pie or donut chart using the provided data. | ||
* | ||
|
@@ -119,6 +166,40 @@ const PieChartInternal = ( { | |
|
||
const { isValid, message } = validateData( data ); | ||
|
||
// Process children to extract compound components | ||
const { svgChildren, htmlChildren, otherChildren } = useMemo( () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function could be reused as well in semi circle chart |
||
const svg: ReactNode[] = []; | ||
const html: ReactNode[] = []; | ||
const other: ReactNode[] = []; | ||
|
||
Children.forEach( children, child => { | ||
if ( isValidElement( child ) ) { | ||
// Check displayName for compound components | ||
const childType = child.type as { displayName?: string }; | ||
const displayName = childType?.displayName; | ||
|
||
if ( displayName === 'PieChart.SVG' ) { | ||
// Extract children from PieChart.SVG | ||
Children.forEach( child.props.children, svgChild => { | ||
svg.push( svgChild ); | ||
} ); | ||
} else if ( displayName === 'PieChart.HTML' ) { | ||
// Extract children from PieChart.HTML | ||
Children.forEach( child.props.children, htmlChild => { | ||
html.push( htmlChild ); | ||
} ); | ||
} else if ( child.type === Group ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't think we need this, do we? |
||
// Legacy support: still check for Group type for backward compatibility | ||
svg.push( child ); | ||
} else { | ||
other.push( child ); | ||
} | ||
} | ||
} ); | ||
|
||
return { svgChildren: svg, htmlChildren: html, otherChildren: other }; | ||
}, [ children ] ); | ||
|
||
// Memoize metadata to prevent unnecessary re-registration | ||
const chartMetadata = useMemo( | ||
() => ( { | ||
|
@@ -180,99 +261,114 @@ const PieChartInternal = ( { | |
}; | ||
|
||
return ( | ||
<div | ||
className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) } | ||
style={ { | ||
display: 'flex', | ||
flexDirection: showLegend && legendPosition === 'top' ? 'column-reverse' : 'column', | ||
<SingleChartContext.Provider | ||
value={ { | ||
chartId, | ||
chartWidth: size, | ||
chartHeight: adjustedHeight, | ||
} } | ||
> | ||
<svg | ||
viewBox={ `0 0 ${ size } ${ adjustedHeight }` } | ||
preserveAspectRatio="xMidYMid meet" | ||
width={ size } | ||
height={ adjustedHeight } | ||
<div | ||
className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) } | ||
style={ { | ||
display: 'flex', | ||
flexDirection: showLegend && legendPosition === 'top' ? 'column-reverse' : 'column', | ||
} } | ||
> | ||
<Group top={ centerY } left={ centerX }> | ||
<Pie< DataPointPercentage & { index: number } > | ||
data={ dataWithIndex } | ||
pieValue={ accessors.value } | ||
outerRadius={ outerRadius } | ||
innerRadius={ innerRadius } | ||
padAngle={ padAngle } | ||
cornerRadius={ cornerRadius } | ||
> | ||
{ pie => { | ||
return pie.arcs.map( ( arc, index ) => { | ||
const [ centroidX, centroidY ] = pie.path.centroid( arc ); | ||
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25; | ||
const handleMouseMove = ( event: MouseEvent< SVGElement > ) => | ||
onMouseMove( event, arc.data ); | ||
|
||
const pathProps: SVGProps< SVGPathElement > = { | ||
d: pie.path( arc ) || '', | ||
fill: accessors.fill( arc.data ), | ||
}; | ||
|
||
if ( withTooltips ) { | ||
pathProps.onMouseMove = handleMouseMove; | ||
pathProps.onMouseLeave = onMouseLeave; | ||
} | ||
|
||
return ( | ||
<g key={ `arc-${ index }` }> | ||
<path { ...pathProps } /> | ||
{ hasSpaceForLabel && ( | ||
<text | ||
x={ centroidX } | ||
y={ centroidY } | ||
dy=".33em" | ||
fill={ providerTheme.labelBackgroundColor } | ||
fontSize={ 12 } | ||
textAnchor="middle" | ||
pointerEvents="none" | ||
> | ||
{ arc.data.label } | ||
</text> | ||
) } | ||
</g> | ||
); | ||
} ); | ||
<svg | ||
viewBox={ `0 0 ${ size } ${ adjustedHeight }` } | ||
preserveAspectRatio="xMidYMid meet" | ||
width={ size } | ||
height={ adjustedHeight } | ||
> | ||
<Group top={ centerY } left={ centerX }> | ||
<Pie< DataPointPercentage & { index: number } > | ||
data={ dataWithIndex } | ||
pieValue={ accessors.value } | ||
outerRadius={ outerRadius } | ||
innerRadius={ innerRadius } | ||
padAngle={ padAngle } | ||
cornerRadius={ cornerRadius } | ||
> | ||
{ pie => { | ||
return pie.arcs.map( ( arc, index ) => { | ||
const [ centroidX, centroidY ] = pie.path.centroid( arc ); | ||
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25; | ||
const handleMouseMove = ( event: MouseEvent< SVGElement > ) => | ||
onMouseMove( event, arc.data ); | ||
|
||
const pathProps: SVGProps< SVGPathElement > = { | ||
d: pie.path( arc ) || '', | ||
fill: accessors.fill( arc.data ), | ||
}; | ||
|
||
if ( withTooltips ) { | ||
pathProps.onMouseMove = handleMouseMove; | ||
pathProps.onMouseLeave = onMouseLeave; | ||
} | ||
|
||
return ( | ||
<g key={ `arc-${ index }` }> | ||
<path { ...pathProps } /> | ||
{ hasSpaceForLabel && ( | ||
<text | ||
x={ centroidX } | ||
y={ centroidY } | ||
dy=".33em" | ||
fill={ providerTheme.labelBackgroundColor || '#333' } | ||
fontSize={ 12 } | ||
textAnchor="middle" | ||
pointerEvents="none" | ||
> | ||
{ arc.data.label } | ||
</text> | ||
) } | ||
</g> | ||
); | ||
} ); | ||
} } | ||
</Pie> | ||
|
||
{ /* Render SVG children (like Group, Text) inside the SVG */ } | ||
{ svgChildren } | ||
</Group> | ||
</svg> | ||
|
||
{ showLegend && ( | ||
<Legend | ||
items={ legendItems } | ||
orientation={ legendOrientation } | ||
position={ legendPosition } | ||
alignment={ legendAlignment } | ||
className={ styles[ 'pie-chart-legend' ] } | ||
shape={ legendShape } | ||
ref={ legendRef } | ||
chartId={ chartId } | ||
/> | ||
) } | ||
|
||
{ withTooltips && tooltipOpen && tooltipData && ( | ||
<BaseTooltip | ||
data={ tooltipData } | ||
top={ tooltipTop || 0 } | ||
left={ tooltipLeft || 0 } | ||
style={ { | ||
transform: 'translate(-50%, -100%)', | ||
} } | ||
</Pie> | ||
|
||
{ children } | ||
</Group> | ||
</svg> | ||
|
||
{ showLegend && ( | ||
<Legend | ||
items={ legendItems } | ||
orientation={ legendOrientation } | ||
position={ legendPosition } | ||
alignment={ legendAlignment } | ||
className={ styles[ 'pie-chart-legend' ] } | ||
shape={ legendShape } | ||
ref={ legendRef } | ||
chartId={ chartId } | ||
/> | ||
) } | ||
|
||
{ withTooltips && tooltipOpen && tooltipData && ( | ||
<BaseTooltip | ||
data={ tooltipData } | ||
top={ tooltipTop || 0 } | ||
left={ tooltipLeft || 0 } | ||
style={ { | ||
transform: 'translate(-50%, -100%)', | ||
} } | ||
/> | ||
) } | ||
</div> | ||
/> | ||
) } | ||
|
||
{ /* Render HTML component children from PieChart.HTML */ } | ||
{ htmlChildren } | ||
|
||
{ /* Render other React children for backward compatibility */ } | ||
{ otherChildren } | ||
</div> | ||
</SingleChartContext.Provider> | ||
); | ||
}; | ||
|
||
const PieChart = ( props: PieChartProps ) => { | ||
const PieChartWithProvider: FC< PieChartProps > = props => { | ||
const existingContext = useContext( GlobalChartsContext ); | ||
|
||
// If we're already in a GlobalChartsProvider context, don't create a new one | ||
|
@@ -288,5 +384,23 @@ const PieChart = ( props: PieChartProps ) => { | |
); | ||
}; | ||
|
||
PieChart.displayName = 'PieChart'; | ||
export default withResponsive< PieChartProps >( PieChart ); | ||
PieChartWithProvider.displayName = 'PieChart'; | ||
|
||
// Create PieChart with composition API | ||
const PieChart = attachSubComponents( PieChartWithProvider, { | ||
Legend: Legend, | ||
SVG: PieChartSVG, | ||
HTML: PieChartHTML, | ||
} ) as PieChartComponent; | ||
|
||
// Create responsive PieChart with composition API | ||
const PieChartResponsive = attachSubComponents( | ||
withResponsive< PieChartProps >( PieChartWithProvider ), | ||
{ | ||
Legend: Legend, | ||
SVG: PieChartSVG, | ||
HTML: PieChartHTML, | ||
} | ||
) as PieChartResponsiveComponent; | ||
|
||
export { PieChartResponsive as default, PieChart as PieChartUnresponsive }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's consolidate the code with semi circle chart in follow ups, particularly L61-L129