fix(charts): Respect legend position in composition API#47478
fix(charts): Respect legend position in composition API#47478adamwoodnz wants to merge 3 commits intotrunkfrom
Conversation
Composition legends (<Chart.Legend position="top" />) always appeared at the bottom because they were rendered in the children slot, which comes after the chart in the Stack layout. Alignment and orientation worked since they affect Legend styling; position controls layout placement and was ignored. Extend useChartChildren to extract Legend children, read their position prop, and return legendChildren. Charts now render legendChildren in the top or bottom slot based on position, so composition legends honor position="top" and position="bottom" across LineChart, BarChart, PieChart, PieSemiCircleChart, and LeaderboardChart. Fixes CHARTS-91. Made-with: Cursor
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 🔴 Action required: Please add missing changelog entries for the following projects: Use the Jetpack CLI tool to generate changelog entries by running the following command: Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
There was a problem hiding this comment.
Pull request overview
This PR fixes the charts composition API so <Chart.Legend position="top" /> and position="bottom" are rendered in the correct layout slot (instead of always appearing at the bottom as a normal child), across all legend-supporting charts.
Changes:
- Extend
useChartChildrento extractLegendchildren (withposition, defaulting tobottom) into a dedicatedlegendChildrenbucket. - Update BarChart, LineChart, PieChart, PieSemiCircleChart, and LeaderboardChart to render extracted legends in top/bottom slots and to exclude legends from
otherChildren. - Add/extend tests for legend extraction and update a LineChart Storybook story related to composition legends.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| projects/js-packages/charts/src/charts/private/chart-composition/use-chart-children.ts | Adds legendChildren extraction (element + position) alongside existing SVG/HTML/other categorization. |
| projects/js-packages/charts/src/charts/private/chart-composition/test/use-chart-children.test.tsx | Adds coverage for Legend extraction and position handling. |
| projects/js-packages/charts/src/charts/private/chart-composition/index.ts | Re-exports LegendChild type. |
| projects/js-packages/charts/src/charts/pie-chart/pie-chart.tsx | Renders composition legends in top/bottom slots using legendChildren. |
| projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx | Renders composition legends in top/bottom slots using legendChildren. |
| projects/js-packages/charts/src/charts/line-chart/line-chart.tsx | Switches legend-child detection to useChartChildren; renders composition legends in top/bottom slots; renders otherChildren only. |
| projects/js-packages/charts/src/charts/bar-chart/bar-chart.tsx | Switches legend-child detection to useChartChildren; renders composition legends in top/bottom slots; renders otherChildren only. |
| projects/js-packages/charts/src/charts/leaderboard-chart/leaderboard-chart.tsx | Renders composition legends in top/bottom slots (including empty-state path) using legendChildren. |
| projects/js-packages/charts/src/charts/line-chart/stories/index.stories.tsx | Adjusts the WithCompositionLegend story render approach. |
| projects/js-packages/charts/CHANGELOG.md | Adds an “Unreleased” entry describing the fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
projects/js-packages/charts/src/charts/line-chart/stories/index.stories.tsx
Outdated
Show resolved
Hide resolved
| if ( isValidElement( child ) ) { | ||
| // Extract Legend children for position-based slot rendering | ||
| if ( child.type === Legend ) { | ||
| const position = ( child.props?.position ?? 'bottom' ) as 'top' | 'bottom'; |
There was a problem hiding this comment.
position is being coerced with a type cast. If a consumer passes an unexpected value at runtime (e.g. via JS, or a future extension like 'left'/'right'), it will be stored as 'top'|'bottom' but won’t match either filter later, so the legend silently disappears. Consider validating child.props.position against 'top'/'bottom' and defaulting to 'bottom' (or falling back to the chart’s legendPosition) when invalid.
| const position = ( child.props?.position ?? 'bottom' ) as 'top' | 'bottom'; | |
| const rawPosition = child.props?.position; | |
| const position: 'top' | 'bottom' = | |
| rawPosition === 'top' || rawPosition === 'bottom' ? rawPosition : 'bottom'; |
| .map( ( l, i ) => ( | ||
| <Fragment key={ `legend-top-${ i }` }>{ l.element }</Fragment> | ||
| ) ) } |
There was a problem hiding this comment.
These legends are rendered via .map((l, i) => ...) with an index-based key. If legend children are conditionally added/removed or reordered, this can cause unnecessary remounts and state loss. Prefer using the existing l.element.key when available (or a stable derived key), and consider returning l.element directly so React can use the element’s own key.
| .map( ( l, i ) => ( | |
| <Fragment key={ `legend-top-${ i }` }>{ l.element }</Fragment> | |
| ) ) } | |
| .map( l => l.element ) } |
| { legendChildren | ||
| .filter( l => l.position === 'top' ) | ||
| .map( ( l, i ) => ( | ||
| <Fragment key={ `legend-top-${ i }` }>{ l.element }</Fragment> | ||
| ) ) } |
There was a problem hiding this comment.
These legends are rendered via .map((l, i) => ...) with an index-based key. If legend children are conditionally added/removed or reordered, this can cause unnecessary remounts and state loss. Prefer using the existing l.element.key when available (or a stable derived key), and consider returning l.element directly so React can use the element’s own key.
| { legendChildren | ||
| .filter( l => l.position === 'top' ) | ||
| .map( ( l, i ) => ( | ||
| <Fragment key={ `legend-top-${ i }` }>{ l.element }</Fragment> | ||
| ) ) } |
There was a problem hiding this comment.
These legends are rendered via .map((l, i) => ...) with an index-based key. If legend children are conditionally added/removed or reordered, this can cause unnecessary remounts and state loss. Prefer using the existing l.element.key when available (or a stable derived key), and consider returning l.element directly so React can use the element’s own key.
| { legendChildren | ||
| .filter( l => l.position === 'top' ) | ||
| .map( ( l, i ) => ( | ||
| <Fragment key={ `legend-top-${ i }` }>{ l.element }</Fragment> | ||
| ) ) } |
There was a problem hiding this comment.
These legends are rendered via .map((l, i) => ...) with an index-based key. If legend children are conditionally added/removed or reordered, this can cause unnecessary remounts and state loss. Prefer using the existing l.element.key when available (or a stable derived key), and consider returning l.element directly so React can use the element’s own key.
| { legendChildren | ||
| .filter( l => l.position === 'top' ) | ||
| .map( ( l, i ) => ( | ||
| <Fragment key={ `legend-top-${ i }` }>{ l.element }</Fragment> | ||
| ) ) } |
There was a problem hiding this comment.
These legends are rendered via .map((l, i) => ...) with an index-based key. If legend children are conditionally added/removed or reordered, this can cause unnecessary remounts and state loss. Prefer using the existing l.element.key when available (or a stable derived key), and consider returning l.element directly so React can use the element’s own key.
Code Coverage SummaryCoverage changed in 7 files. Only the first 5 are listed here.
Full summary · PHP report · JS report If appropriate, add one of these labels to override the failing coverage check:
Covered by non-unit tests
|
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
projects/js-packages/charts/src/charts/line-chart/stories/index.stories.tsx:184
- PR description mentions updating this story to use
sampleData.slice(0, 2)(2 series), but the new implementation spreadsDefault.args, which currently setsdata: sampleData.slice(0, 4). If the intent is specifically to demonstrate 2-series composition legends, consider explicitly settingdata(orseriesCount) here to match the PR description/testing notes.
<LineChart { ...Default.args } { ...args }>
<LineChart.Legend
orientation={ args.legendOrientation || 'horizontal' }
alignment={ args.legendAlignment || 'center' }
position={ args.legendPosition || 'bottom' }
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Manual edits to the changelog are not the way. Made-with: Cursor
Fixes CHARTS-91: Composition Legend should support alignment options
Summary
Composition legends (
<Chart.Legend position="top" />) always appeared at the bottom because they were rendered in the children slot. This PR extendsuseChartChildrento extract Legend children, read theirpositionprop, and render them in the correct top/bottom layout slot across all legend-supporting charts.Proposed changes:
useChartChildrento detect and extract Legend children, readingpositionfrom props (default'bottom')legendChildrenarray with{ element, position }for each Legend; exclude Legend fromotherChildrenuseChartChildrenand renderlegendChildrenin position-based slotslegendChildrenin top/bottom slots based onpositionuse-chart-children.test.tsxsampleData.slice(0, 2)Other information:
Does this pull request change what data or activity we track or use?
No.
Testing instructions:
pnpm testinprojects/js-packages/chartsto verify all tests passpnpm run storybookfrom the charts package (orjp build plugins/jetpackand use the monorepo Storybook)legendPositiontotop— the legend should move above the chartlegendPositiontobottom— the legend should appear below the chartlegendAlignment(start/center/end) andlegendOrientation(horizontal/vertical) still work as beforeChangelog
Made with Cursor