@automattic/charts 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/dist/charts/bar-chart/index.cjs +7 -5
  3. package/dist/charts/bar-chart/index.cjs.map +1 -1
  4. package/dist/charts/bar-chart/index.css +12 -24
  5. package/dist/charts/bar-chart/index.css.map +1 -1
  6. package/dist/charts/bar-chart/index.d.cts +3 -4
  7. package/dist/charts/bar-chart/index.d.ts +3 -4
  8. package/dist/charts/bar-chart/index.js +6 -4
  9. package/dist/charts/bar-list-chart/index.cjs +8 -6
  10. package/dist/charts/bar-list-chart/index.cjs.map +1 -1
  11. package/dist/charts/bar-list-chart/index.css +12 -24
  12. package/dist/charts/bar-list-chart/index.css.map +1 -1
  13. package/dist/charts/bar-list-chart/index.d.cts +3 -3
  14. package/dist/charts/bar-list-chart/index.d.ts +3 -3
  15. package/dist/charts/bar-list-chart/index.js +7 -5
  16. package/dist/charts/conversion-funnel-chart/index.cjs +5 -5
  17. package/dist/charts/conversion-funnel-chart/index.css +0 -94
  18. package/dist/charts/conversion-funnel-chart/index.css.map +1 -1
  19. package/dist/charts/conversion-funnel-chart/index.d.cts +1 -1
  20. package/dist/charts/conversion-funnel-chart/index.d.ts +1 -1
  21. package/dist/charts/conversion-funnel-chart/index.js +4 -4
  22. package/dist/charts/geo-chart/index.cjs +4 -4
  23. package/dist/charts/geo-chart/index.css +0 -94
  24. package/dist/charts/geo-chart/index.css.map +1 -1
  25. package/dist/charts/geo-chart/index.d.cts +1 -1
  26. package/dist/charts/geo-chart/index.d.ts +1 -1
  27. package/dist/charts/geo-chart/index.js +3 -3
  28. package/dist/charts/leaderboard-chart/index.cjs +7 -6
  29. package/dist/charts/leaderboard-chart/index.cjs.map +1 -1
  30. package/dist/charts/leaderboard-chart/index.css +12 -24
  31. package/dist/charts/leaderboard-chart/index.css.map +1 -1
  32. package/dist/charts/leaderboard-chart/index.d.cts +3 -3
  33. package/dist/charts/leaderboard-chart/index.d.ts +3 -3
  34. package/dist/charts/leaderboard-chart/index.js +6 -5
  35. package/dist/charts/line-chart/index.cjs +7 -5
  36. package/dist/charts/line-chart/index.cjs.map +1 -1
  37. package/dist/charts/line-chart/index.css +12 -24
  38. package/dist/charts/line-chart/index.css.map +1 -1
  39. package/dist/charts/line-chart/index.d.cts +3 -4
  40. package/dist/charts/line-chart/index.d.ts +3 -4
  41. package/dist/charts/line-chart/index.js +6 -4
  42. package/dist/charts/pie-chart/index.cjs +7 -6
  43. package/dist/charts/pie-chart/index.cjs.map +1 -1
  44. package/dist/charts/pie-chart/index.css +12 -24
  45. package/dist/charts/pie-chart/index.css.map +1 -1
  46. package/dist/charts/pie-chart/index.d.cts +7 -13
  47. package/dist/charts/pie-chart/index.d.ts +7 -13
  48. package/dist/charts/pie-chart/index.js +6 -5
  49. package/dist/charts/pie-semi-circle-chart/index.cjs +7 -6
  50. package/dist/charts/pie-semi-circle-chart/index.cjs.map +1 -1
  51. package/dist/charts/pie-semi-circle-chart/index.css +12 -24
  52. package/dist/charts/pie-semi-circle-chart/index.css.map +1 -1
  53. package/dist/charts/pie-semi-circle-chart/index.d.cts +7 -13
  54. package/dist/charts/pie-semi-circle-chart/index.d.ts +7 -13
  55. package/dist/charts/pie-semi-circle-chart/index.js +6 -5
  56. package/dist/charts/sparkline/index.cjs +8 -6
  57. package/dist/charts/sparkline/index.cjs.map +1 -1
  58. package/dist/charts/sparkline/index.css +12 -24
  59. package/dist/charts/sparkline/index.css.map +1 -1
  60. package/dist/charts/sparkline/index.js +7 -5
  61. package/dist/{chunk-32DH6JDF.js → chunk-2I67QUIV.js} +52 -420
  62. package/dist/chunk-2I67QUIV.js.map +1 -0
  63. package/dist/{chunk-WLODYNLB.js → chunk-2ICEEQOC.js} +31 -27
  64. package/dist/chunk-2ICEEQOC.js.map +1 -0
  65. package/dist/{chunk-IU4DYUAV.js → chunk-4B7BL2DD.js} +3 -3
  66. package/dist/{chunk-BCX5THDQ.js → chunk-4OXMTKAL.js} +24 -26
  67. package/dist/chunk-4OXMTKAL.js.map +1 -0
  68. package/dist/{chunk-4OPFE4RM.js → chunk-B6NLZFRW.js} +30 -27
  69. package/dist/chunk-B6NLZFRW.js.map +1 -0
  70. package/dist/{chunk-D2UH4CFE.cjs → chunk-BBAUQOW6.cjs} +9 -9
  71. package/dist/{chunk-D2UH4CFE.cjs.map → chunk-BBAUQOW6.cjs.map} +1 -1
  72. package/dist/{chunk-XKRJL2QT.cjs → chunk-CMMHCTBX.cjs} +45 -47
  73. package/dist/chunk-CMMHCTBX.cjs.map +1 -0
  74. package/dist/{chunk-YE2T52VZ.cjs → chunk-CPPXJATQ.cjs} +51 -47
  75. package/dist/chunk-CPPXJATQ.cjs.map +1 -0
  76. package/dist/{chunk-H2V4JMSA.js → chunk-DKU775VC.js} +3 -3
  77. package/dist/{chunk-ZH4F5RMG.cjs → chunk-GRA7Y2ZG.cjs} +46 -48
  78. package/dist/chunk-GRA7Y2ZG.cjs.map +1 -0
  79. package/dist/{chunk-DAU3HNEG.js → chunk-JJIMABHT.js} +9 -2
  80. package/dist/chunk-JJIMABHT.js.map +1 -0
  81. package/dist/{chunk-CZGYJKG6.js → chunk-KJHWXOCZ.js} +4 -4
  82. package/dist/{chunk-6CCZL2JJ.js → chunk-KRWGSOJ2.js} +30 -2
  83. package/dist/chunk-KRWGSOJ2.js.map +1 -0
  84. package/dist/{chunk-V36ERY7Y.js → chunk-LTFH7SEG.js} +24 -26
  85. package/dist/chunk-LTFH7SEG.js.map +1 -0
  86. package/dist/{chunk-PXLEMUGJ.js → chunk-MUNOKLLE.js} +3 -3
  87. package/dist/{chunk-VTS3PNMS.cjs → chunk-MXGLYWVP.cjs} +9 -2
  88. package/dist/chunk-MXGLYWVP.cjs.map +1 -0
  89. package/dist/{chunk-Z45KX47P.cjs → chunk-OYC34VTO.cjs} +154 -94
  90. package/dist/chunk-OYC34VTO.cjs.map +1 -0
  91. package/dist/{chunk-77OKCVQN.cjs → chunk-PQL5I3F6.cjs} +17 -17
  92. package/dist/{chunk-77OKCVQN.cjs.map → chunk-PQL5I3F6.cjs.map} +1 -1
  93. package/dist/{chunk-I35UYJJR.cjs → chunk-REZTQ4PH.cjs} +41 -21
  94. package/dist/chunk-REZTQ4PH.cjs.map +1 -0
  95. package/dist/{chunk-RCY6XLGU.cjs → chunk-TZRUHQOH.cjs} +36 -8
  96. package/dist/chunk-TZRUHQOH.cjs.map +1 -0
  97. package/dist/{chunk-2NCY7R4G.js → chunk-UTYVIOWZ.js} +111 -51
  98. package/dist/chunk-UTYVIOWZ.js.map +1 -0
  99. package/dist/{chunk-TO3OQBXG.cjs → chunk-W2LDIX26.cjs} +5 -5
  100. package/dist/{chunk-TO3OQBXG.cjs.map → chunk-W2LDIX26.cjs.map} +1 -1
  101. package/dist/{chunk-7FQX4ALL.cjs → chunk-WSG64BVN.cjs} +6 -6
  102. package/dist/{chunk-7FQX4ALL.cjs.map → chunk-WSG64BVN.cjs.map} +1 -1
  103. package/dist/chunk-WTQYGUNF.js +400 -0
  104. package/dist/chunk-WTQYGUNF.js.map +1 -0
  105. package/dist/{chunk-RHHVEJHJ.cjs → chunk-WYK7EL5R.cjs} +68 -436
  106. package/dist/chunk-WYK7EL5R.cjs.map +1 -0
  107. package/dist/{chunk-VJM5XCB4.cjs → chunk-XC4KHJYX.cjs} +49 -46
  108. package/dist/chunk-XC4KHJYX.cjs.map +1 -0
  109. package/dist/chunk-XVBH5XHE.cjs +400 -0
  110. package/dist/chunk-XVBH5XHE.cjs.map +1 -0
  111. package/dist/{chunk-Z26M4V2M.js → chunk-YAFQVVDI.js} +41 -21
  112. package/dist/chunk-YAFQVVDI.js.map +1 -0
  113. package/dist/components/legend/index.cjs +4 -3
  114. package/dist/components/legend/index.cjs.map +1 -1
  115. package/dist/components/legend/index.css +12 -24
  116. package/dist/components/legend/index.css.map +1 -1
  117. package/dist/components/legend/index.d.cts +4 -4
  118. package/dist/components/legend/index.d.ts +4 -4
  119. package/dist/components/legend/index.js +3 -2
  120. package/dist/components/tooltip/index.d.cts +1 -1
  121. package/dist/components/tooltip/index.d.ts +1 -1
  122. package/dist/hooks/index.cjs +3 -5
  123. package/dist/hooks/index.cjs.map +1 -1
  124. package/dist/hooks/index.css +0 -94
  125. package/dist/hooks/index.css.map +1 -1
  126. package/dist/hooks/index.d.cts +3 -11
  127. package/dist/hooks/index.d.ts +3 -11
  128. package/dist/hooks/index.js +2 -4
  129. package/dist/index.cjs +18 -16
  130. package/dist/index.cjs.map +1 -1
  131. package/dist/index.css +12 -24
  132. package/dist/index.css.map +1 -1
  133. package/dist/index.d.cts +7 -7
  134. package/dist/index.d.ts +7 -7
  135. package/dist/index.js +17 -15
  136. package/dist/{leaderboard-chart-DR7CGb0L.d.cts → leaderboard-chart-BSbg0ufV.d.cts} +3 -7
  137. package/dist/{leaderboard-chart-BKYYXcg2.d.ts → leaderboard-chart-odEYxxEC.d.ts} +3 -7
  138. package/dist/{legend-C2grwnWk.d.cts → legend-DFkosEvC.d.cts} +1 -1
  139. package/dist/{legend-Cj0xM5dU.d.ts → legend-DLswHhOk.d.ts} +1 -1
  140. package/dist/providers/index.cjs +3 -3
  141. package/dist/providers/index.css +0 -94
  142. package/dist/providers/index.css.map +1 -1
  143. package/dist/providers/index.d.cts +3 -3
  144. package/dist/providers/index.d.ts +3 -3
  145. package/dist/providers/index.js +2 -2
  146. package/dist/{themes-CyjKm-P_.d.cts → themes-D0qc5JaW.d.cts} +2 -2
  147. package/dist/{themes-BmVGrYnF.d.ts → themes-itO4Ht5g.d.ts} +2 -2
  148. package/dist/{types-KtOPPzPX.d.cts → types-B5f6XQ7Q.d.cts} +1 -1
  149. package/dist/{types-CuUEszrM.d.ts → types-BsHooDbM.d.ts} +1 -1
  150. package/dist/{types-I67mddpr.d.cts → types-BuSrRM4p.d.ts} +3 -32
  151. package/dist/{types-DZordNiO.d.cts → types-ChOUI9-N.d.cts} +80 -40
  152. package/dist/{types-DZordNiO.d.ts → types-ChOUI9-N.d.ts} +80 -40
  153. package/dist/{types-I67mddpr.d.ts → types-Dfw9VOKI.d.cts} +3 -32
  154. package/dist/utils/index.cjs +2 -2
  155. package/dist/utils/index.d.cts +1 -1
  156. package/dist/utils/index.d.ts +1 -1
  157. package/dist/utils/index.js +1 -1
  158. package/package.json +6 -6
  159. package/src/charts/bar-chart/bar-chart.tsx +17 -18
  160. package/src/charts/bar-chart/test/bar-chart.test.tsx +48 -31
  161. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +38 -41
  162. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +4 -5
  163. package/src/charts/leaderboard-chart/types.ts +1 -11
  164. package/src/charts/line-chart/line-chart.tsx +18 -16
  165. package/src/charts/line-chart/test/line-chart.test.tsx +49 -27
  166. package/src/charts/line-chart/types.ts +0 -1
  167. package/src/charts/pie-chart/pie-chart.tsx +23 -22
  168. package/src/charts/pie-chart/test/composition-api.test.tsx +41 -0
  169. package/src/charts/pie-chart/test/pie-chart.test.tsx +9 -9
  170. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +21 -23
  171. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +33 -5
  172. package/src/charts/private/chart-composition/index.ts +2 -0
  173. package/src/charts/private/chart-composition/render-legend-slot.ts +22 -0
  174. package/src/charts/private/chart-composition/test/render-legend-slot.test.tsx +60 -0
  175. package/src/charts/private/chart-composition/test/use-chart-children.test.tsx +91 -0
  176. package/src/charts/private/chart-composition/use-chart-children.ts +34 -2
  177. package/src/components/legend/index.ts +1 -8
  178. package/src/components/legend/private/base-legend.module.scss +19 -37
  179. package/src/components/legend/private/base-legend.tsx +0 -2
  180. package/src/components/legend/types.ts +7 -34
  181. package/src/hooks/index.ts +0 -1
  182. package/src/index.ts +1 -7
  183. package/src/types.ts +83 -38
  184. package/src/utils/date-parsing.ts +10 -1
  185. package/src/utils/test/date-parsing.test.ts +12 -0
  186. package/src/utils/test/resolve-css-var.test.ts +2 -2
  187. package/tsup.config.ts +1 -1
  188. package/dist/chunk-2NCY7R4G.js.map +0 -1
  189. package/dist/chunk-32DH6JDF.js.map +0 -1
  190. package/dist/chunk-4OPFE4RM.js.map +0 -1
  191. package/dist/chunk-6CCZL2JJ.js.map +0 -1
  192. package/dist/chunk-BCX5THDQ.js.map +0 -1
  193. package/dist/chunk-DAU3HNEG.js.map +0 -1
  194. package/dist/chunk-I35UYJJR.cjs.map +0 -1
  195. package/dist/chunk-RCY6XLGU.cjs.map +0 -1
  196. package/dist/chunk-RHHVEJHJ.cjs.map +0 -1
  197. package/dist/chunk-V36ERY7Y.js.map +0 -1
  198. package/dist/chunk-VJM5XCB4.cjs.map +0 -1
  199. package/dist/chunk-VTS3PNMS.cjs.map +0 -1
  200. package/dist/chunk-WLODYNLB.js.map +0 -1
  201. package/dist/chunk-XKRJL2QT.cjs.map +0 -1
  202. package/dist/chunk-YE2T52VZ.cjs.map +0 -1
  203. package/dist/chunk-Z26M4V2M.js.map +0 -1
  204. package/dist/chunk-Z45KX47P.cjs.map +0 -1
  205. package/dist/chunk-ZH4F5RMG.cjs.map +0 -1
  206. package/src/hooks/use-has-legend-child.ts +0 -22
  207. /package/dist/{chunk-IU4DYUAV.js.map → chunk-4B7BL2DD.js.map} +0 -0
  208. /package/dist/{chunk-H2V4JMSA.js.map → chunk-DKU775VC.js.map} +0 -0
  209. /package/dist/{chunk-CZGYJKG6.js.map → chunk-KJHWXOCZ.js.map} +0 -0
  210. /package/dist/{chunk-PXLEMUGJ.js.map → chunk-MUNOKLLE.js.map} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  useGlobalChartsTheme,
17
17
  } from '../../providers';
18
18
  import { formatMetricValue, attachSubComponents } from '../../utils';
19
- import { useChartChildren } from '../private/chart-composition';
19
+ import { useChartChildren, renderLegendSlot } from '../private/chart-composition';
20
20
  import { SingleChartContext } from '../private/single-chart-context';
21
21
  import { withResponsive } from '../private/with-responsive';
22
22
  import { useLeaderboardLegendItems } from './hooks';
@@ -110,31 +110,26 @@ const BarWithLabel = ( {
110
110
  * LeaderboardChart component displays a ranked list of data with progress bars
111
111
  * and optional comparison values.
112
112
  *
113
- * @param props - Component props
114
- * @param props.data - Array of leaderboard entries to display
115
- * @param props.chartId - Optional unique identifier for the chart
116
- * @param props.width - Optional width of the chart container in pixels
117
- * @param props.height - Optional height of the chart container in pixels
118
- * @param props.withComparison - Whether to show comparison data
119
- * @param props.withOverlayLabel - Whether to overlay the label on top of the bar
120
- * @param props.primaryColor - Primary color for current period bars
121
- * @param props.secondaryColor - Secondary color for comparison period bars
122
- * @param props.valueFormatter - Custom formatter for values
123
- * @param props.deltaFormatter - Custom formatter for delta values
124
- * @param props.loading - Whether the chart is in loading state
125
- * @param props.animation - Whether the chart should animate on load
126
- * @param props.showLegend - Whether to show legend
127
- * @param props.legendOrientation - Legend orientation
128
- * @param props.legendPosition - Legend position
129
- * @param props.legendAlignment - Legend alignment
130
- * @param props.legendShape - Legend shape
131
- * @param props.legendShapeStyles - Styles for legend shapes (width, height, margin)
132
- * @param props.legendLabels - Custom labels for legend items
133
- * @param props.legendInteractive - Whether legend items are interactive (clickable to toggle series visibility)
134
- * @param props.gap - Spacing between legend and chart content
135
- * @param props.children - Child components for composition API
136
- * @param props.className - Additional CSS class name
137
- * @param props.style - Custom styling for the chart container
113
+ * @param props - Component props
114
+ * @param props.data - Array of leaderboard entries to display
115
+ * @param props.chartId - Optional unique identifier for the chart
116
+ * @param props.width - Optional width of the chart container in pixels
117
+ * @param props.height - Optional height of the chart container in pixels
118
+ * @param props.withComparison - Whether to show comparison data
119
+ * @param props.withOverlayLabel - Whether to overlay the label on top of the bar
120
+ * @param props.primaryColor - Primary color for current period bars
121
+ * @param props.secondaryColor - Secondary color for comparison period bars
122
+ * @param props.valueFormatter - Custom formatter for values
123
+ * @param props.deltaFormatter - Custom formatter for delta values
124
+ * @param props.loading - Whether the chart is in loading state
125
+ * @param props.animation - Whether the chart should animate on load
126
+ * @param props.showLegend - Whether to show legend
127
+ * @param props.legend - Legend configuration (orientation, position, alignment, shape, shapeStyles, interactive)
128
+ * @param props.legendLabels - Custom labels for legend items
129
+ * @param props.gap - Spacing between legend and chart content
130
+ * @param props.children - Child components for composition API
131
+ * @param props.className - Additional CSS class name
132
+ * @param props.style - Custom styling for the chart container
138
133
  * @return JSX element representing the leaderboard chart
139
134
  */
140
135
  const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
@@ -151,24 +146,22 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
151
146
  animation,
152
147
  loading = false,
153
148
  showLegend = false,
154
- legendOrientation = 'horizontal',
155
- legendPosition = 'bottom',
156
- legendAlignment = 'center',
157
- legendShape = 'circle',
158
- legendShapeStyles: legendShapeStylesProp,
149
+ legend = {},
159
150
  legendLabels,
160
- legendInteractive = false,
161
151
  gap = 'md',
162
152
  className,
163
153
  style,
164
154
  children,
165
155
  } ) => {
156
+ const legendInteractive = legend.interactive ?? false;
157
+ const legendPosition = legend.position ?? 'bottom';
158
+
166
159
  const chartId = useChartId( providedChartId );
167
160
  const { leaderboardChart: leaderboardChartSettings } = useGlobalChartsTheme();
168
- const legendShapeStyles = { width: 8, height: 8, ...legendShapeStylesProp };
161
+ const legendShapeStyles = { width: 8, height: 8, ...legend.shapeStyles };
169
162
 
170
163
  // Process children to extract compound components
171
- const { otherChildren } = useChartChildren( children, 'LeaderboardChart' );
164
+ const { legendChildren, nonLegendChildren } = useChartChildren( children, 'LeaderboardChart' );
172
165
  const {
173
166
  labelSpacing,
174
167
  rowGap,
@@ -281,8 +274,8 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
281
274
  ? __( 'Loading…', 'jetpack-charts' )
282
275
  : __( 'No data available', 'jetpack-charts' ) }
283
276
  </div>
284
- { /* Render children from composition API */ }
285
- { otherChildren }
277
+
278
+ { nonLegendChildren }
286
279
  </Stack>
287
280
  </SingleChartContext.Provider>
288
281
  );
@@ -290,10 +283,13 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
290
283
 
291
284
  const legendElement = showLegend && (
292
285
  <Legend
293
- orientation={ legendOrientation }
286
+ orientation={ legend.orientation ?? 'horizontal' }
294
287
  position={ legendPosition }
295
- alignment={ legendAlignment }
296
- shape={ legendShape }
288
+ alignment={ legend.alignment ?? 'center' }
289
+ labelStyles={ legend.labelStyles }
290
+ itemClassName={ legend.itemClassName }
291
+ itemStyles={ legend.itemStyles }
292
+ shape={ legend.shape ?? 'circle' }
297
293
  shapeStyles={ legendShapeStyles }
298
294
  chartId={ chartId }
299
295
  interactive={ legendInteractive }
@@ -327,6 +323,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
327
323
  } }
328
324
  >
329
325
  { legendPosition === 'top' && legendElement }
326
+ { renderLegendSlot( legendChildren, 'top' ) }
330
327
 
331
328
  <div className={ styles.leaderboardChart__content }>
332
329
  { allSeriesHidden ? (
@@ -377,9 +374,9 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
377
374
  </div>
378
375
 
379
376
  { legendPosition === 'bottom' && legendElement }
377
+ { renderLegendSlot( legendChildren, 'bottom' ) }
380
378
 
381
- { /* Render children from composition API */ }
382
- { otherChildren }
379
+ { nonLegendChildren }
383
380
  </Stack>
384
381
  </SingleChartContext.Provider>
385
382
  );
@@ -169,8 +169,7 @@ describe( 'LeaderboardChart', () => {
169
169
  data={ mockData }
170
170
  withComparison={ true }
171
171
  showLegend={ true }
172
- legendShape="rect"
173
- legendShapeStyles={ { width: 10, height: 6 } }
172
+ legend={ { shape: 'rect', shapeStyles: { width: 10, height: 6 } } }
174
173
  />
175
174
  );
176
175
 
@@ -306,7 +305,7 @@ describe( 'LeaderboardChart', () => {
306
305
  data={ mockData }
307
306
  withComparison={ true }
308
307
  showLegend={ true }
309
- legendInteractive={ true }
308
+ legend={ { interactive: true } }
310
309
  />
311
310
  );
312
311
 
@@ -320,7 +319,7 @@ describe( 'LeaderboardChart', () => {
320
319
  data={ mockData }
321
320
  withComparison={ true }
322
321
  showLegend={ true }
323
- legendInteractive={ false }
322
+ legend={ { interactive: false } }
324
323
  />
325
324
  );
326
325
 
@@ -335,7 +334,7 @@ describe( 'LeaderboardChart', () => {
335
334
  data={ mockData }
336
335
  withComparison={ true }
337
336
  showLegend={ true }
338
- legendInteractive={ true }
337
+ legend={ { interactive: true } }
339
338
  />
340
339
  );
341
340
 
@@ -1,6 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { BaseChartProps, LeaderboardEntry } from '../../types';
3
- import type { LegendShapeStyles } from '../../components/legend';
4
3
 
5
4
  export interface LeaderboardChartProps
6
5
  extends Pick<
@@ -8,16 +7,12 @@ export interface LeaderboardChartProps
8
7
  | 'className'
9
8
  | 'data'
10
9
  | 'showLegend'
11
- | 'legendOrientation'
12
- | 'legendPosition'
13
- | 'legendAlignment'
14
- | 'legendShape'
10
+ | 'legend'
15
11
  | 'chartId'
16
12
  | 'width'
17
13
  | 'height'
18
14
  | 'size'
19
15
  | 'gap'
20
- | 'legendInteractive'
21
16
  | 'animation'
22
17
  > {
23
18
  /**
@@ -62,11 +57,6 @@ export interface LeaderboardChartProps
62
57
  '--a8c--charts--leaderboard--bar--border-radius'?: string;
63
58
  };
64
59
 
65
- /**
66
- * Styles for legend shapes (width, height, margin).
67
- */
68
- legendShapeStyles?: LegendShapeStyles;
69
-
70
60
  /**
71
61
  * Custom labels for legend items
72
62
  */
@@ -15,7 +15,6 @@ import {
15
15
  useChartDataTransform,
16
16
  useChartMargin,
17
17
  useElementSize,
18
- useHasLegendChild,
19
18
  usePrefersReducedMotion,
20
19
  } from '../../hooks';
21
20
  import {
@@ -27,6 +26,7 @@ import {
27
26
  useGlobalChartsTheme,
28
27
  } from '../../providers';
29
28
  import { attachSubComponents } from '../../utils';
29
+ import { useChartChildren, renderLegendSlot } from '../private/chart-composition';
30
30
  import { DefaultGlyph } from '../private/default-glyph';
31
31
  import { SingleChartContext, type SingleChartRef } from '../private/single-chart-context';
32
32
  import { withResponsive } from '../private/with-responsive';
@@ -256,15 +256,9 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
256
256
  withTooltips = true,
257
257
  withTooltipCrosshairs,
258
258
  showLegend = false,
259
- legendOrientation = 'horizontal',
260
- legendAlignment = 'center',
261
- legendPosition = 'bottom',
262
- legendMaxWidth,
263
- legendTextOverflow = 'wrap',
264
- legendItemClassName,
259
+ legend = {},
265
260
  renderGlyph = defaultRenderGlyph,
266
261
  glyphStyle = {},
267
- legendShape = 'line',
268
262
  withLegendGlyph = false,
269
263
  withGradientFill = false,
270
264
  smoothing = true,
@@ -272,7 +266,6 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
272
266
  renderTooltip = renderDefaultTooltip,
273
267
  withStartGlyphs = false,
274
268
  withEndGlyphs = false,
275
- legendInteractive = false,
276
269
  animation,
277
270
  options = {},
278
271
  onPointerDown = undefined,
@@ -285,6 +278,10 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
285
278
  },
286
279
  ref
287
280
  ) => {
281
+ const legendInteractive = legend.interactive ?? false;
282
+ const legendShape = legend.shape ?? 'line';
283
+ const legendPosition = legend.position ?? 'bottom';
284
+
288
285
  const providerTheme = useGlobalChartsTheme();
289
286
  const theme = useXYChartTheme( data );
290
287
  const chartId = useChartId( providedChartId );
@@ -294,8 +291,9 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
294
291
  const [ isNavigating, setIsNavigating ] = useState( false );
295
292
  const internalChartRef = useRef< SingleChartRef >( null );
296
293
 
297
- // Check if children contain a Legend component (composition pattern)
298
- const hasLegendChild = useHasLegendChild( children );
294
+ // Process children for composition API (Legend, etc.)
295
+ const { legendChildren, nonLegendChildren } = useChartChildren( children, 'LineChart' );
296
+ const hasLegendChild = legendChildren.length > 0;
299
297
 
300
298
  // Use the measured SVG wrapper height, falling back to the passed height if provided.
301
299
  // When there's a legend (via prop or composition), we must wait for measurement because
@@ -454,11 +452,13 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
454
452
 
455
453
  const legendElement = showLegend && (
456
454
  <Legend
457
- orientation={ legendOrientation }
458
- alignment={ legendAlignment }
455
+ orientation={ legend.orientation ?? 'horizontal' }
456
+ alignment={ legend.alignment ?? 'center' }
459
457
  position={ legendPosition }
460
- labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
461
- itemClassName={ legendItemClassName }
458
+ labelStyles={ legend.labelStyles }
459
+ itemClassName={ legend.itemClassName }
460
+ itemStyles={ legend.itemStyles }
461
+ shapeStyles={ legend.shapeStyles }
462
462
  className={ styles[ 'line-chart__legend' ] }
463
463
  shape={ legendShape }
464
464
  chartId={ chartId }
@@ -492,6 +492,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
492
492
  } }
493
493
  >
494
494
  { legendPosition === 'top' && legendElement }
495
+ { renderLegendSlot( legendChildren, 'top' ) }
495
496
 
496
497
  <div
497
498
  className={ styles[ 'line-chart__svg-wrapper' ] }
@@ -654,8 +655,9 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
654
655
  </div>
655
656
 
656
657
  { legendPosition === 'bottom' && legendElement }
658
+ { renderLegendSlot( legendChildren, 'bottom' ) }
657
659
 
658
- { children }
660
+ { nonLegendChildren }
659
661
  </Stack>
660
662
  </SingleChartContext.Provider>
661
663
  );
@@ -53,13 +53,15 @@ describe( 'LineChart', () => {
53
53
  ],
54
54
  };
55
55
 
56
- const renderWithTheme = ( props = {}, themeName = 'default' ) => {
56
+ const renderWithTheme = ( props = {}, themeName = 'default', children = undefined ) => {
57
57
  const theme = THEME_MAP[ themeName ];
58
58
 
59
59
  return render(
60
60
  <GlobalChartsProvider theme={ theme }>
61
61
  { /* @ts-expect-error TODO Fix the missing props */ }
62
- <LineChart { ...defaultProps } { ...props } />
62
+ <LineChart { ...defaultProps } { ...props }>
63
+ { children }
64
+ </LineChart>
63
65
  </GlobalChartsProvider>
64
66
  );
65
67
  };
@@ -130,36 +132,56 @@ describe( 'LineChart', () => {
130
132
  } );
131
133
 
132
134
  describe( 'Legend', () => {
135
+ const multiSeriesData = [
136
+ {
137
+ label: 'Series A',
138
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
139
+ },
140
+ {
141
+ label: 'Series B',
142
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
143
+ },
144
+ ];
145
+
133
146
  test( 'shows legend when showLegend is true', () => {
134
- renderWithTheme( {
135
- showLegend: true,
136
- data: [
137
- {
138
- label: 'Series A',
139
- data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
140
- },
141
- {
142
- label: 'Series B',
143
- data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
144
- },
145
- ],
146
- } );
147
+ renderWithTheme( { showLegend: true, data: multiSeriesData } );
147
148
  expect( screen.getByText( 'Series A' ) ).toBeInTheDocument();
148
149
  expect( screen.getByText( 'Series B' ) ).toBeInTheDocument();
149
150
  } );
150
151
 
151
152
  test( 'hides legend when showLegend is false', () => {
152
- renderWithTheme( {
153
- showLegend: false,
154
- data: [
155
- {
156
- label: 'Series A',
157
- data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
158
- },
159
- ],
160
- } );
153
+ renderWithTheme( { showLegend: false, data: multiSeriesData } );
161
154
  expect( screen.queryByText( 'Series A' ) ).not.toBeInTheDocument();
162
155
  } );
156
+
157
+ test( 'renders composition legend as child component', () => {
158
+ renderWithTheme( { data: multiSeriesData }, 'default', <LineChart.Legend /> );
159
+
160
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
161
+ expect( screen.getByText( 'Series A' ) ).toBeInTheDocument();
162
+ expect( screen.getByText( 'Series B' ) ).toBeInTheDocument();
163
+ } );
164
+
165
+ test( 'renders composition legend regardless of showLegend value', () => {
166
+ renderWithTheme(
167
+ { data: multiSeriesData, showLegend: false },
168
+ 'default',
169
+ <LineChart.Legend />
170
+ );
171
+
172
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
173
+ } );
174
+
175
+ test( 'renders composition legend in top position', () => {
176
+ renderWithTheme( { data: multiSeriesData }, 'default', <LineChart.Legend position="top" /> );
177
+
178
+ // Legend should appear before the chart content in DOM order
179
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
180
+ const html = document.body.innerHTML;
181
+ expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
182
+ html.indexOf( 'role="grid"' )
183
+ );
184
+ } );
163
185
  } );
164
186
 
165
187
  describe( 'Gradient Fill', () => {
@@ -1178,7 +1200,7 @@ describe( 'LineChart', () => {
1178
1200
  { ...defaultProps }
1179
1201
  withGradientFill={ false }
1180
1202
  showLegend={ true }
1181
- legendInteractive={ true }
1203
+ legend={ { interactive: true } }
1182
1204
  chartId="test-interactive-chart"
1183
1205
  />
1184
1206
  </GlobalChartsProvider>
@@ -1200,7 +1222,7 @@ describe( 'LineChart', () => {
1200
1222
  { ...defaultProps }
1201
1223
  withGradientFill={ false }
1202
1224
  showLegend={ true }
1203
- legendInteractive={ false }
1225
+ legend={ { interactive: false } }
1204
1226
  chartId="test-non-interactive-chart"
1205
1227
  />
1206
1228
  </GlobalChartsProvider>
@@ -1218,7 +1240,7 @@ describe( 'LineChart', () => {
1218
1240
  { ...defaultProps }
1219
1241
  withGradientFill={ false }
1220
1242
  showLegend={ true }
1221
- legendInteractive={ true }
1243
+ legend={ { interactive: true } }
1222
1244
  // No chartId provided
1223
1245
  />
1224
1246
  </GlobalChartsProvider>
@@ -41,7 +41,6 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] > {
41
41
  showVertical?: boolean;
42
42
  showHorizontal?: boolean;
43
43
  };
44
- legendInteractive?: boolean;
45
44
  children?: ReactNode;
46
45
  }
47
46
 
@@ -18,7 +18,12 @@ import {
18
18
  } from '../../providers';
19
19
  import { attachSubComponents } from '../../utils';
20
20
  import { getStringWidth } from '../../visx/text';
21
- import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
21
+ import {
22
+ ChartSVG,
23
+ ChartHTML,
24
+ useChartChildren,
25
+ renderLegendSlot,
26
+ } from '../private/chart-composition';
22
27
  import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
23
28
  import { SingleChartContext } from '../private/single-chart-context';
24
29
  import { withResponsive, ResponsiveConfig } from '../private/with-responsive';
@@ -93,13 +98,6 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
93
98
  */
94
99
  legendValueDisplay?: LegendValueDisplay;
95
100
 
96
- /**
97
- * Enable interactive legend items that can toggle segment visibility.
98
- * Requires chartId and GlobalChartsProvider.
99
- * When segments are hidden, percentages are recalculated so visible segments total 100%.
100
- */
101
- legendInteractive?: boolean;
102
-
103
101
  /**
104
102
  * Use the children prop to render additional elements on the chart.
105
103
  */
@@ -169,13 +167,7 @@ const PieChartInternal = ( {
169
167
  withTooltips = false,
170
168
  className,
171
169
  showLegend = false,
172
- legendOrientation = 'horizontal',
173
- legendPosition = 'bottom',
174
- legendAlignment = 'center',
175
- legendMaxWidth,
176
- legendTextOverflow = 'wrap',
177
- legendItemClassName,
178
- legendShape = 'circle',
170
+ legend = {},
179
171
  width: propWidth,
180
172
  height: propHeight,
181
173
  size,
@@ -186,13 +178,15 @@ const PieChartInternal = ( {
186
178
  cornerScale = 0,
187
179
  showLabels = true,
188
180
  legendValueDisplay = 'percentage',
189
- legendInteractive = false,
190
181
  children = null,
191
182
  tooltipOffsetX = 0,
192
183
  tooltipOffsetY = -15,
193
184
  renderTooltip = renderDefaultPieTooltip,
194
185
  gap = 'md',
195
186
  }: PieChartProps ) => {
187
+ const legendInteractive = legend.interactive ?? false;
188
+ const legendPosition = legend.position ?? 'bottom';
189
+
196
190
  const providerTheme = useGlobalChartsTheme();
197
191
  const chartId = useChartId( providedChartId );
198
192
  const [ svgWrapperRef, svgWrapperWidth, svgWrapperHeight ] = useElementSize< HTMLDivElement >();
@@ -236,7 +230,10 @@ const PieChartInternal = ( {
236
230
  const { isValid, message } = validateData( data );
237
231
 
238
232
  // Process children to extract compound components
239
- const { svgChildren, htmlChildren, otherChildren } = useChartChildren( children, 'PieChart' );
233
+ const { svgChildren, htmlChildren, legendChildren, otherChildren } = useChartChildren(
234
+ children,
235
+ 'PieChart'
236
+ );
240
237
 
241
238
  // Memoize metadata to prevent unnecessary re-registration
242
239
  const chartMetadata = useMemo(
@@ -314,12 +311,14 @@ const PieChartInternal = ( {
314
311
 
315
312
  const legendElement = showLegend && (
316
313
  <Legend
317
- orientation={ legendOrientation }
314
+ orientation={ legend.orientation ?? 'horizontal' }
318
315
  position={ legendPosition }
319
- alignment={ legendAlignment }
320
- labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
321
- itemClassName={ legendItemClassName }
322
- shape={ legendShape }
316
+ alignment={ legend.alignment ?? 'center' }
317
+ labelStyles={ legend.labelStyles }
318
+ itemClassName={ legend.itemClassName }
319
+ itemStyles={ legend.itemStyles }
320
+ shapeStyles={ legend.shapeStyles }
321
+ shape={ legend.shape ?? 'circle' }
323
322
  chartId={ chartId }
324
323
  interactive={ legendInteractive }
325
324
  />
@@ -350,6 +349,7 @@ const PieChartInternal = ( {
350
349
  } }
351
350
  >
352
351
  { legendPosition === 'top' && legendElement }
352
+ { renderLegendSlot( legendChildren, 'top' ) }
353
353
 
354
354
  <div className={ styles[ 'pie-chart__svg-wrapper' ] } ref={ svgWrapperRef }>
355
355
  <svg
@@ -482,6 +482,7 @@ const PieChartInternal = ( {
482
482
  </div>
483
483
 
484
484
  { legendPosition === 'bottom' && legendElement }
485
+ { renderLegendSlot( legendChildren, 'bottom' ) }
485
486
 
486
487
  { withTooltips && tooltipOpen && tooltipData && (
487
488
  <TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { Group } from '@visx/group';
3
3
  import '@testing-library/jest-dom';
4
+ import { GlobalChartsProvider } from '../../../providers';
4
5
  import { PieChartUnresponsive as PieChart } from '../index';
5
6
 
6
7
  describe( 'PieChart Composition API', () => {
@@ -10,6 +11,16 @@ describe( 'PieChart Composition API', () => {
10
11
  { label: 'C', value: 30, percentage: 30 },
11
12
  ];
12
13
 
14
+ const renderWithChildren = ( props = {}, children = undefined ) => {
15
+ return render(
16
+ <GlobalChartsProvider>
17
+ <PieChart data={ mockData } size={ 400 } { ...props }>
18
+ { children }
19
+ </PieChart>
20
+ </GlobalChartsProvider>
21
+ );
22
+ };
23
+
13
24
  describe( 'Compound Components', () => {
14
25
  it( 'renders PieChart.SVG children inside the SVG element', () => {
15
26
  render(
@@ -148,4 +159,34 @@ describe( 'PieChart Composition API', () => {
148
159
  expect( svg!.contains( newHtml ) ).toBe( false );
149
160
  } );
150
161
  } );
162
+
163
+ describe( 'Composition Legend', () => {
164
+ test( 'renders composition legend as child component', () => {
165
+ renderWithChildren( {}, <PieChart.Legend /> );
166
+
167
+ const legendItems = screen.getAllByTestId( 'legend-item' );
168
+ expect( legendItems ).toHaveLength( 3 );
169
+ expect( legendItems[ 0 ] ).toHaveTextContent( 'A' );
170
+ expect( legendItems[ 1 ] ).toHaveTextContent( 'B' );
171
+ expect( legendItems[ 2 ] ).toHaveTextContent( 'C' );
172
+ } );
173
+
174
+ test( 'renders composition legend regardless of showLegend value', () => {
175
+ renderWithChildren( { showLegend: false }, <PieChart.Legend /> );
176
+
177
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 3 );
178
+ } );
179
+
180
+ test( 'renders composition legend in top position', () => {
181
+ renderWithChildren( {}, <PieChart.Legend position="top" /> );
182
+
183
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 3 );
184
+
185
+ // Legend should appear before the chart SVG in DOM order
186
+ const html = document.body.innerHTML;
187
+ expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
188
+ html.indexOf( 'data-testid="pie-segment"' )
189
+ );
190
+ } );
191
+ } );
151
192
  } );