Skip to content
Merged
Show file tree
Hide file tree
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 Aug 14, 2025
722bc3a
fix(pie-semi-circle-chart): render SVG children inside SVG and React …
annacmc Aug 14, 2025
26e54b4
refactor(pie-chart): move composition story after WithLegend and rename
annacmc Aug 14, 2025
aaadd58
refactor(donut-chart): move composition story after WithLegend and re…
annacmc Aug 14, 2025
ff99196
refactor(pie-semi-circle-chart): move composition story after WithLeg…
annacmc Aug 14, 2025
3eb7eb4
changelog
annacmc Aug 14, 2025
2a98a01
perf(pie-chart): optimize Children.map to use single pass
annacmc Aug 15, 2025
f825cf2
perf(pie-semi-circle-chart): optimize Children.map to use single pass
annacmc Aug 15, 2025
7216e11
export PieChartUnresponsive for composition API
annacmc Aug 18, 2025
ac73a06
add PropsWithChildren import and SVG/HTML types
annacmc Aug 18, 2025
4b10480
add PieChartSVG and PieChartHTML compound components
annacmc Aug 18, 2025
a4da39e
update children processing for compound components
annacmc Aug 18, 2025
82b2d1f
attach SVG and HTML compound components to exports
annacmc Aug 18, 2025
7f4cb3f
add composition API story example
annacmc Aug 18, 2025
bb1c4c4
add composition API documentation
annacmc Aug 18, 2025
b6b6933
add composition API tests
annacmc Aug 18, 2025
d8f56dc
add SVG and HTML to PieSemiCircleChartSubComponents interface
annacmc Aug 18, 2025
0dd1b6e
create PieSemiCircleChartSVG compound component
annacmc Aug 18, 2025
ec67f69
create PieSemiCircleChartHTML compound component
annacmc Aug 18, 2025
557590f
update child rendering to use new compound component structure
annacmc Aug 18, 2025
3ae91a7
add SVG and HTML to attachSubComponents calls
annacmc Aug 18, 2025
431a20c
add CompositionAPI story demonstrating new features
annacmc Aug 18, 2025
f2f0171
fix import issues and missing defaultTheme after rebase
annacmc Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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';
296 changes: 205 additions & 91 deletions projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx
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[] > {
/**
Expand Down Expand Up @@ -54,6 +57,20 @@ interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
children?: ReactNode;
}

// Base props type with optional responsive properties
Copy link
Contributor

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

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
Expand All @@ -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.
*
Expand Down Expand Up @@ -119,6 +166,40 @@ const PieChartInternal = ( {

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

// Process children to extract compound components
const { svgChildren, htmlChildren, otherChildren } = useMemo( () => {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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(
() => ( {
Expand Down Expand Up @@ -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
Expand All @@ -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 };
Loading
Loading