@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
@@ -8,7 +8,7 @@ import { ScaleType, ScaleInput } from '@visx/scale';
8
8
  import { TextProps } from '@visx/text/lib/Text';
9
9
  import { EventHandlerParams, LineStyles, GridStyles, GlyphProps } from '@visx/xychart';
10
10
  import { GapSize } from '@wordpress/theme';
11
- import { PointerEvent, ReactNode, CSSProperties } from 'react';
11
+ import { PointerEvent, CSSProperties, ReactNode } from 'react';
12
12
  import { GoogleDataTableColumn, GoogleDataTableRow } from 'react-google-charts';
13
13
 
14
14
  type ValueOf<T> = T[keyof T];
@@ -322,6 +322,81 @@ type ScaleOptions = {
322
322
  */
323
323
  paddingOuter?: number;
324
324
  };
325
+ type LegendItemStyles = {
326
+ /** Margin around each legend item. */
327
+ margin?: CSSProperties['margin'];
328
+ /** Flex direction for items within each legend entry. */
329
+ flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
330
+ };
331
+ type LegendLabelStyles = Pick<CSSProperties, 'justifyContent' | 'flex' | 'margin'> & {
332
+ /**
333
+ * Maximum width for legend label text as a CSS value (e.g. '200px', '50%', '10rem').
334
+ * When set, text overflow behavior is controlled by textOverflow.
335
+ */
336
+ maxWidth?: string;
337
+ /**
338
+ * Controls how text behaves when it exceeds maxWidth.
339
+ * - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
340
+ * - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
341
+ */
342
+ textOverflow?: 'ellipsis' | 'wrap';
343
+ };
344
+ type LegendShapeStyles = {
345
+ /** Width of the legend shape in pixels. */
346
+ width?: number;
347
+ /** Height of the legend shape in pixels. */
348
+ height?: number;
349
+ /** Margin around the legend shape. */
350
+ margin?: CSSProperties['margin'];
351
+ };
352
+ /** Position of the legend relative to chart content. */
353
+ type LegendPosition = 'top' | 'bottom';
354
+ /**
355
+ * Configuration object for chart legend appearance and behavior.
356
+ * Consolidates all legend styling and layout props into a single structured object.
357
+ */
358
+ type ChartLegendConfig<T = DataPoint | DataPointDate | LeaderboardEntry> = {
359
+ /**
360
+ * Layout direction of legend items.
361
+ */
362
+ orientation?: 'horizontal' | 'vertical';
363
+ /**
364
+ * Position of the legend relative to the chart.
365
+ * TODO: Add 'left' | 'right' positioning support in future implementation
366
+ */
367
+ position?: LegendPosition;
368
+ /**
369
+ * Alignment of the legend within its position.
370
+ */
371
+ alignment?: 'start' | 'center' | 'end';
372
+ /**
373
+ * Shape of the legend marker icon.
374
+ */
375
+ shape?: LegendShape<T, number>;
376
+ /**
377
+ * Enable interactive legend items that can toggle series visibility.
378
+ * Supported for all chart types that render series.
379
+ * Requires chartId and GlobalChartsProvider.
380
+ * For pie charts, percentages are recalculated so visible segments total 100%.
381
+ */
382
+ interactive?: boolean;
383
+ /**
384
+ * Additional CSS class name for individual legend items.
385
+ */
386
+ itemClassName?: string;
387
+ /**
388
+ * CSS styles for each legend item (margin, flexDirection).
389
+ */
390
+ itemStyles?: LegendItemStyles;
391
+ /**
392
+ * CSS styles for legend labels (maxWidth, textOverflow, justifyContent, flex, margin).
393
+ */
394
+ labelStyles?: LegendLabelStyles;
395
+ /**
396
+ * Styles for legend shapes (width, height, margin).
397
+ */
398
+ shapeStyles?: LegendShapeStyles;
399
+ };
325
400
  /**
326
401
  * Base properties shared across all chart components
327
402
  */
@@ -386,45 +461,10 @@ type BaseChartProps<T = DataPoint | DataPointDate | LeaderboardEntry> = {
386
461
  */
387
462
  showLegend?: boolean;
388
463
  /**
389
- * Legend orientation
390
- */
391
- legendOrientation?: 'horizontal' | 'vertical';
392
- /**
393
- * Legend shape
394
- */
395
- legendShape?: LegendShape<T, number>;
396
- /**
397
- * Legend position (where the legend appears)
398
- * TODO: Add 'left' | 'right' positioning support in future implementation
399
- */
400
- legendPosition?: 'top' | 'bottom';
401
- /**
402
- * Legend alignment within its position
403
- */
404
- legendAlignment?: 'start' | 'center' | 'end';
405
- /**
406
- * Maximum width for legend items. When set, text overflow behavior is controlled by legendTextOverflow.
407
- * Should be a CSS value string (e.g. '200px', '50%', '10rem')
408
- */
409
- legendMaxWidth?: string;
410
- /**
411
- * Controls how text behaves when it exceeds legendMaxWidth.
412
- * - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
413
- * - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
414
- */
415
- legendTextOverflow?: 'ellipsis' | 'wrap';
416
- /**
417
- * Additional CSS class name for legend items.
418
- * This allows consumers to customize individual legend item styling.
419
- */
420
- legendItemClassName?: string;
421
- /**
422
- * Enable interactive legend items that can toggle series visibility.
423
- * Supported for all chart types that render series.
424
- * Requires chartId and GlobalChartsProvider.
425
- * For pie charts, percentages are recalculated so visible segments total 100%.
464
+ * Legend configuration object for controlling legend appearance and behavior.
465
+ * Includes orientation, position, alignment, shape, styling, and interactivity options.
426
466
  */
427
- legendInteractive?: boolean;
467
+ legend?: ChartLegendConfig<T>;
428
468
  /**
429
469
  * Grid visibility. x is default when orientation is vertical. y is default when orientation is horizontal.
430
470
  */
@@ -502,4 +542,4 @@ interface ToggleEvent extends Event {
502
542
  oldState: 'open' | 'closed';
503
543
  }
504
544
 
505
- export type { AnnotationStyles as A, BaseChartProps as B, ChartTheme as C, DataPoint as D, GeoData as G, LeaderboardEntry as L, MultipleDataPointsDate as M, Optional as O, PopoverButtonAttributes as P, ScaleOptions as S, ToggleEvent as T, ButtonWithPopover as a, CompleteChartTheme as b, DataPointDate as c, DataPointPercentage as d, GradientStop as e, GridProps as f, OrientationType as g, PopoverElement as h, PopoverElementAttributes as i, SeriesData as j, SeriesDataOptions as k };
545
+ export type { AnnotationStyles as A, BaseChartProps as B, ChartLegendConfig as C, DataPoint as D, GeoData as G, LeaderboardEntry as L, MultipleDataPointsDate as M, Optional as O, PopoverButtonAttributes as P, ScaleOptions as S, ToggleEvent as T, ButtonWithPopover as a, ChartTheme as b, CompleteChartTheme as c, DataPointDate as d, DataPointPercentage as e, GradientStop as f, GridProps as g, LegendItemStyles as h, LegendLabelStyles as i, LegendPosition as j, LegendShapeStyles as k, OrientationType as l, PopoverElement as m, PopoverElementAttributes as n, SeriesData as o, SeriesDataOptions as p };
@@ -8,7 +8,7 @@ import { ScaleType, ScaleInput } from '@visx/scale';
8
8
  import { TextProps } from '@visx/text/lib/Text';
9
9
  import { EventHandlerParams, LineStyles, GridStyles, GlyphProps } from '@visx/xychart';
10
10
  import { GapSize } from '@wordpress/theme';
11
- import { PointerEvent, ReactNode, CSSProperties } from 'react';
11
+ import { PointerEvent, CSSProperties, ReactNode } from 'react';
12
12
  import { GoogleDataTableColumn, GoogleDataTableRow } from 'react-google-charts';
13
13
 
14
14
  type ValueOf<T> = T[keyof T];
@@ -322,6 +322,81 @@ type ScaleOptions = {
322
322
  */
323
323
  paddingOuter?: number;
324
324
  };
325
+ type LegendItemStyles = {
326
+ /** Margin around each legend item. */
327
+ margin?: CSSProperties['margin'];
328
+ /** Flex direction for items within each legend entry. */
329
+ flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
330
+ };
331
+ type LegendLabelStyles = Pick<CSSProperties, 'justifyContent' | 'flex' | 'margin'> & {
332
+ /**
333
+ * Maximum width for legend label text as a CSS value (e.g. '200px', '50%', '10rem').
334
+ * When set, text overflow behavior is controlled by textOverflow.
335
+ */
336
+ maxWidth?: string;
337
+ /**
338
+ * Controls how text behaves when it exceeds maxWidth.
339
+ * - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
340
+ * - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
341
+ */
342
+ textOverflow?: 'ellipsis' | 'wrap';
343
+ };
344
+ type LegendShapeStyles = {
345
+ /** Width of the legend shape in pixels. */
346
+ width?: number;
347
+ /** Height of the legend shape in pixels. */
348
+ height?: number;
349
+ /** Margin around the legend shape. */
350
+ margin?: CSSProperties['margin'];
351
+ };
352
+ /** Position of the legend relative to chart content. */
353
+ type LegendPosition = 'top' | 'bottom';
354
+ /**
355
+ * Configuration object for chart legend appearance and behavior.
356
+ * Consolidates all legend styling and layout props into a single structured object.
357
+ */
358
+ type ChartLegendConfig<T = DataPoint | DataPointDate | LeaderboardEntry> = {
359
+ /**
360
+ * Layout direction of legend items.
361
+ */
362
+ orientation?: 'horizontal' | 'vertical';
363
+ /**
364
+ * Position of the legend relative to the chart.
365
+ * TODO: Add 'left' | 'right' positioning support in future implementation
366
+ */
367
+ position?: LegendPosition;
368
+ /**
369
+ * Alignment of the legend within its position.
370
+ */
371
+ alignment?: 'start' | 'center' | 'end';
372
+ /**
373
+ * Shape of the legend marker icon.
374
+ */
375
+ shape?: LegendShape<T, number>;
376
+ /**
377
+ * Enable interactive legend items that can toggle series visibility.
378
+ * Supported for all chart types that render series.
379
+ * Requires chartId and GlobalChartsProvider.
380
+ * For pie charts, percentages are recalculated so visible segments total 100%.
381
+ */
382
+ interactive?: boolean;
383
+ /**
384
+ * Additional CSS class name for individual legend items.
385
+ */
386
+ itemClassName?: string;
387
+ /**
388
+ * CSS styles for each legend item (margin, flexDirection).
389
+ */
390
+ itemStyles?: LegendItemStyles;
391
+ /**
392
+ * CSS styles for legend labels (maxWidth, textOverflow, justifyContent, flex, margin).
393
+ */
394
+ labelStyles?: LegendLabelStyles;
395
+ /**
396
+ * Styles for legend shapes (width, height, margin).
397
+ */
398
+ shapeStyles?: LegendShapeStyles;
399
+ };
325
400
  /**
326
401
  * Base properties shared across all chart components
327
402
  */
@@ -386,45 +461,10 @@ type BaseChartProps<T = DataPoint | DataPointDate | LeaderboardEntry> = {
386
461
  */
387
462
  showLegend?: boolean;
388
463
  /**
389
- * Legend orientation
390
- */
391
- legendOrientation?: 'horizontal' | 'vertical';
392
- /**
393
- * Legend shape
394
- */
395
- legendShape?: LegendShape<T, number>;
396
- /**
397
- * Legend position (where the legend appears)
398
- * TODO: Add 'left' | 'right' positioning support in future implementation
399
- */
400
- legendPosition?: 'top' | 'bottom';
401
- /**
402
- * Legend alignment within its position
403
- */
404
- legendAlignment?: 'start' | 'center' | 'end';
405
- /**
406
- * Maximum width for legend items. When set, text overflow behavior is controlled by legendTextOverflow.
407
- * Should be a CSS value string (e.g. '200px', '50%', '10rem')
408
- */
409
- legendMaxWidth?: string;
410
- /**
411
- * Controls how text behaves when it exceeds legendMaxWidth.
412
- * - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
413
- * - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
414
- */
415
- legendTextOverflow?: 'ellipsis' | 'wrap';
416
- /**
417
- * Additional CSS class name for legend items.
418
- * This allows consumers to customize individual legend item styling.
419
- */
420
- legendItemClassName?: string;
421
- /**
422
- * Enable interactive legend items that can toggle series visibility.
423
- * Supported for all chart types that render series.
424
- * Requires chartId and GlobalChartsProvider.
425
- * For pie charts, percentages are recalculated so visible segments total 100%.
464
+ * Legend configuration object for controlling legend appearance and behavior.
465
+ * Includes orientation, position, alignment, shape, styling, and interactivity options.
426
466
  */
427
- legendInteractive?: boolean;
467
+ legend?: ChartLegendConfig<T>;
428
468
  /**
429
469
  * Grid visibility. x is default when orientation is vertical. y is default when orientation is horizontal.
430
470
  */
@@ -502,4 +542,4 @@ interface ToggleEvent extends Event {
502
542
  oldState: 'open' | 'closed';
503
543
  }
504
544
 
505
- export type { AnnotationStyles as A, BaseChartProps as B, ChartTheme as C, DataPoint as D, GeoData as G, LeaderboardEntry as L, MultipleDataPointsDate as M, Optional as O, PopoverButtonAttributes as P, ScaleOptions as S, ToggleEvent as T, ButtonWithPopover as a, CompleteChartTheme as b, DataPointDate as c, DataPointPercentage as d, GradientStop as e, GridProps as f, OrientationType as g, PopoverElement as h, PopoverElementAttributes as i, SeriesData as j, SeriesDataOptions as k };
545
+ export type { AnnotationStyles as A, BaseChartProps as B, ChartLegendConfig as C, DataPoint as D, GeoData as G, LeaderboardEntry as L, MultipleDataPointsDate as M, Optional as O, PopoverButtonAttributes as P, ScaleOptions as S, ToggleEvent as T, ButtonWithPopover as a, ChartTheme as b, CompleteChartTheme as c, DataPointDate as d, DataPointPercentage as e, GradientStop as f, GridProps as g, LegendItemStyles as h, LegendLabelStyles as i, LegendPosition as j, LegendShapeStyles as k, OrientationType as l, PopoverElement as m, PopoverElementAttributes as n, SeriesData as o, SeriesDataOptions as p };
@@ -1,42 +1,13 @@
1
1
  import { LegendOrdinal } from '@visx/legend';
2
+ import { j as LegendPosition, h as LegendItemStyles, i as LegendLabelStyles, k as LegendShapeStyles } from './types-ChOUI9-N.cjs';
2
3
  import { GlyphProps, LineStyles } from '@visx/xychart';
3
4
  import { ComponentProps, ReactNode, CSSProperties } from 'react';
4
5
 
5
6
  type VisxLegendProps = Pick<ComponentProps<typeof LegendOrdinal>, 'className' | 'shape' | 'fill' | 'size' | 'labelFormat' | 'labelTransform'>;
6
- type LegendItemStyles = {
7
- /** Margin around each legend item. */
8
- margin?: CSSProperties['margin'];
9
- /** Flex direction for items within each legend entry. */
10
- flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
11
- };
12
- type LegendLabelStyles = Pick<CSSProperties, 'justifyContent' | 'flex' | 'margin'> & {
13
- /**
14
- * Maximum width for legend label text as a CSS value (e.g. '200px', '50%', '10rem').
15
- * When set, text overflow behavior is controlled by textOverflow.
16
- */
17
- maxWidth?: string;
18
- /**
19
- * Controls how text behaves when it exceeds maxWidth.
20
- * - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
21
- * - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
22
- */
23
- textOverflow?: 'ellipsis' | 'wrap';
24
- };
25
- type LegendShapeStyles = {
26
- /** Width of the legend shape in pixels. */
27
- width?: number;
28
- /** Height of the legend shape in pixels. */
29
- height?: number;
30
- /** Margin around the legend shape. */
31
- margin?: CSSProperties['margin'];
32
- };
33
7
  type BaseLegendProps = VisxLegendProps & {
34
8
  items: BaseLegendItem[];
35
9
  orientation?: 'horizontal' | 'vertical';
36
- /**
37
- * TODO: Add 'left' | 'right' positioning support in future implementation
38
- */
39
- position?: 'top' | 'bottom';
10
+ position?: LegendPosition;
40
11
  alignment?: 'start' | 'center' | 'end';
41
12
  /** Additional CSS class name for legend items. */
42
13
  itemClassName?: string;
@@ -75,4 +46,4 @@ type BaseLegendItem = {
75
46
  shapeStyle?: CSSProperties & LineStyles;
76
47
  };
77
48
 
78
- export type { BaseLegendItem as B, LegendItemStyles as L, LegendLabelStyles as a, LegendShapeStyles as b, BaseLegendProps as c, LegendProps as d };
49
+ export type { BaseLegendItem as B, LegendProps as L, BaseLegendProps as a };
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
 
21
- var _chunkVTS3PNMScjs = require('../chunk-VTS3PNMS.cjs');
21
+ var _chunkMXGLYWVPcjs = require('../chunk-MXGLYWVP.cjs');
22
22
  require('../chunk-EMMSS5I5.cjs');
23
23
 
24
24
 
@@ -40,5 +40,5 @@ require('../chunk-EMMSS5I5.cjs');
40
40
 
41
41
 
42
42
 
43
- exports.attachSubComponents = _chunkVTS3PNMScjs.attachSubComponents; exports.formatMetricValue = _chunkVTS3PNMScjs.formatMetricValue; exports.formatPercentage = _chunkVTS3PNMScjs.formatPercentage; exports.getColorDistance = _chunkVTS3PNMScjs.getColorDistance; exports.getItemShapeStyles = _chunkVTS3PNMScjs.getItemShapeStyles; exports.getLongestTickWidth = _chunkVTS3PNMScjs.getLongestTickWidth; exports.getSeriesLineStyles = _chunkVTS3PNMScjs.getSeriesLineStyles; exports.getSeriesStroke = _chunkVTS3PNMScjs.getSeriesStroke; exports.hexToRgba = _chunkVTS3PNMScjs.hexToRgba; exports.isSafari = _chunkVTS3PNMScjs.isSafari; exports.isValidHexColor = _chunkVTS3PNMScjs.isValidHexColor; exports.lightenHexColor = _chunkVTS3PNMScjs.lightenHexColor; exports.mergeThemes = _chunkVTS3PNMScjs.mergeThemes; exports.normalizeColorToHex = _chunkVTS3PNMScjs.normalizeColorToHex; exports.parseAsLocalDate = _chunkVTS3PNMScjs.parseAsLocalDate; exports.parseHslString = _chunkVTS3PNMScjs.parseHslString; exports.parseRgbString = _chunkVTS3PNMScjs.parseRgbString; exports.resolveCssVariable = _chunkVTS3PNMScjs.resolveCssVariable; exports.validateHexColor = _chunkVTS3PNMScjs.validateHexColor;
43
+ exports.attachSubComponents = _chunkMXGLYWVPcjs.attachSubComponents; exports.formatMetricValue = _chunkMXGLYWVPcjs.formatMetricValue; exports.formatPercentage = _chunkMXGLYWVPcjs.formatPercentage; exports.getColorDistance = _chunkMXGLYWVPcjs.getColorDistance; exports.getItemShapeStyles = _chunkMXGLYWVPcjs.getItemShapeStyles; exports.getLongestTickWidth = _chunkMXGLYWVPcjs.getLongestTickWidth; exports.getSeriesLineStyles = _chunkMXGLYWVPcjs.getSeriesLineStyles; exports.getSeriesStroke = _chunkMXGLYWVPcjs.getSeriesStroke; exports.hexToRgba = _chunkMXGLYWVPcjs.hexToRgba; exports.isSafari = _chunkMXGLYWVPcjs.isSafari; exports.isValidHexColor = _chunkMXGLYWVPcjs.isValidHexColor; exports.lightenHexColor = _chunkMXGLYWVPcjs.lightenHexColor; exports.mergeThemes = _chunkMXGLYWVPcjs.mergeThemes; exports.normalizeColorToHex = _chunkMXGLYWVPcjs.normalizeColorToHex; exports.parseAsLocalDate = _chunkMXGLYWVPcjs.parseAsLocalDate; exports.parseHslString = _chunkMXGLYWVPcjs.parseHslString; exports.parseRgbString = _chunkMXGLYWVPcjs.parseRgbString; exports.resolveCssVariable = _chunkMXGLYWVPcjs.resolveCssVariable; exports.validateHexColor = _chunkMXGLYWVPcjs.validateHexColor;
44
44
  //# sourceMappingURL=index.cjs.map
@@ -1,7 +1,7 @@
1
1
  export { M as MetricValueType, f as formatMetricValue } from '../format-metric-value-MXm5DtQ_.cjs';
2
2
  import { TickFormatter } from '@visx/axis';
3
3
  import { AnyD3Scale, ScaleInput } from '@visx/scale';
4
- import { j as SeriesData, C as ChartTheme, b as CompleteChartTheme } from '../types-DZordNiO.cjs';
4
+ import { o as SeriesData, b as ChartTheme, c as CompleteChartTheme } from '../types-ChOUI9-N.cjs';
5
5
  import { LegendShape } from '@visx/legend/lib/types';
6
6
  import { LineStyles } from '@visx/xychart';
7
7
  import '@visx/annotation/lib/components/CircleSubject';
@@ -1,7 +1,7 @@
1
1
  export { M as MetricValueType, f as formatMetricValue } from '../format-metric-value-MXm5DtQ_.js';
2
2
  import { TickFormatter } from '@visx/axis';
3
3
  import { AnyD3Scale, ScaleInput } from '@visx/scale';
4
- import { j as SeriesData, C as ChartTheme, b as CompleteChartTheme } from '../types-DZordNiO.js';
4
+ import { o as SeriesData, b as ChartTheme, c as CompleteChartTheme } from '../types-ChOUI9-N.js';
5
5
  import { LegendShape } from '@visx/legend/lib/types';
6
6
  import { LineStyles } from '@visx/xychart';
7
7
  import '@visx/annotation/lib/components/CircleSubject';
@@ -18,7 +18,7 @@ import {
18
18
  parseRgbString,
19
19
  resolveCssVariable,
20
20
  validateHexColor
21
- } from "../chunk-DAU3HNEG.js";
21
+ } from "../chunk-JJIMABHT.js";
22
22
  import "../chunk-G3PMV62Z.js";
23
23
  export {
24
24
  attachSubComponents,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -180,7 +180,7 @@
180
180
  "typecheck": "tsgo --noEmit"
181
181
  },
182
182
  "dependencies": {
183
- "@automattic/number-formatters": "^1.1.1",
183
+ "@automattic/number-formatters": "^1.1.2",
184
184
  "@babel/runtime": "7.28.6",
185
185
  "@react-spring/web": "9.7.5",
186
186
  "@visx/annotation": "^3.12.0",
@@ -200,8 +200,8 @@
200
200
  "@visx/vendor": "^3.12.0",
201
201
  "@visx/xychart": "^3.12.0",
202
202
  "@wordpress/i18n": "^6.0.0",
203
- "@wordpress/theme": "0.7.0",
204
- "@wordpress/ui": "0.7.0",
203
+ "@wordpress/theme": "0.8.0",
204
+ "@wordpress/ui": "0.8.0",
205
205
  "clsx": "2.1.1",
206
206
  "date-fns": "^4.1.0",
207
207
  "deepmerge": "4.3.1",
@@ -226,8 +226,8 @@
226
226
  "@types/react-dom": "18.3.7",
227
227
  "@typescript/native-preview": "7.0.0-dev.20260225.1",
228
228
  "@visx/glyph": "3.12.0",
229
- "@wordpress/components": "32.2.0",
230
- "@wordpress/element": "6.40.0",
229
+ "@wordpress/components": "32.3.0",
230
+ "@wordpress/element": "6.41.0",
231
231
  "babel-jest": "30.2.0",
232
232
  "babel-plugin-react-remove-properties": "^0.3.1",
233
233
  "esbuild": "0.25.9",
@@ -13,7 +13,6 @@ import {
13
13
  useZeroValueDisplay,
14
14
  useChartMargin,
15
15
  useElementSize,
16
- useHasLegendChild,
17
16
  usePrefersReducedMotion,
18
17
  } from '../../hooks';
19
18
  import {
@@ -25,6 +24,7 @@ import {
25
24
  GlobalChartsContext,
26
25
  } from '../../providers';
27
26
  import { attachSubComponents } from '../../utils';
27
+ import { useChartChildren, renderLegendSlot } from '../private/chart-composition';
28
28
  import { SingleChartContext } from '../private/single-chart-context';
29
29
  import { withResponsive } from '../private/with-responsive';
30
30
  import styles from './bar-chart.module.scss';
@@ -39,7 +39,6 @@ export interface BarChartProps extends BaseChartProps< SeriesData[] > {
39
39
  orientation?: 'horizontal' | 'vertical';
40
40
  withPatterns?: boolean;
41
41
  showZeroValues?: boolean;
42
- legendInteractive?: boolean;
43
42
  children?: ReactNode;
44
43
  }
45
44
 
@@ -85,24 +84,18 @@ const BarChartInternal: FC< BarChartProps > = ( {
85
84
  margin,
86
85
  withTooltips = false,
87
86
  showLegend = false,
88
- legendOrientation = 'horizontal',
89
- legendPosition = 'bottom',
90
- legendAlignment = 'center',
91
- legendMaxWidth,
92
- legendTextOverflow = 'wrap',
93
- legendItemClassName,
94
- legendShape = 'rect',
87
+ legend = {},
95
88
  gridVisibility: gridVisibilityProp,
96
89
  renderTooltip,
97
90
  options = {},
98
91
  orientation = 'vertical',
99
92
  withPatterns = false,
100
93
  showZeroValues = false,
101
- legendInteractive = false,
102
94
  animation,
103
95
  children,
104
96
  gap = 'md',
105
97
  } ) => {
98
+ const legendInteractive = legend.interactive ?? false;
106
99
  const horizontal = orientation === 'horizontal';
107
100
  const chartId = useChartId( providedChartId );
108
101
  const theme = useXYChartTheme( data );
@@ -123,8 +116,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
123
116
  const [ svgWrapperRef, , svgWrapperHeight ] = useElementSize< HTMLDivElement >();
124
117
  const chartRef = useRef< HTMLDivElement >( null );
125
118
 
126
- // Check if children contain a Legend component (composition pattern)
127
- const hasLegendChild = useHasLegendChild( children );
119
+ // Process children for composition API (Legend, etc.)
120
+ const { legendChildren, nonLegendChildren } = useChartChildren( children, 'BarChart' );
121
+ const hasLegendChild = legendChildren.length > 0;
128
122
 
129
123
  // Use the measured SVG wrapper height, falling back to the passed height if provided.
130
124
  // When there's a legend (via prop or composition), we must wait for measurement because
@@ -327,15 +321,18 @@ const BarChartInternal: FC< BarChartProps > = ( {
327
321
  const gridVisibility = gridVisibilityProp ?? chartOptions.gridVisibility;
328
322
  const highlightedBarStyle = createKeyboardHighlightStyle();
329
323
 
324
+ const legendPosition = legend.position ?? 'bottom';
330
325
  const legendElement = showLegend && (
331
326
  <Legend
332
- orientation={ legendOrientation }
327
+ orientation={ legend.orientation ?? 'horizontal' }
333
328
  position={ legendPosition }
334
- alignment={ legendAlignment }
335
- labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
336
- itemClassName={ legendItemClassName }
329
+ alignment={ legend.alignment ?? 'center' }
330
+ labelStyles={ legend.labelStyles }
331
+ itemClassName={ legend.itemClassName }
332
+ itemStyles={ legend.itemStyles }
333
+ shapeStyles={ legend.shapeStyles }
337
334
  className={ styles[ 'bar-chart__legend' ] }
338
- shape={ legendShape }
335
+ shape={ legend.shape ?? 'rect' }
339
336
  chartId={ chartId }
340
337
  interactive={ legendInteractive }
341
338
  />
@@ -370,6 +367,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
370
367
  data-chart-id={ `bar-chart-${ chartId }` }
371
368
  >
372
369
  { legendPosition === 'top' && legendElement }
370
+ { renderLegendSlot( legendChildren, 'top' ) }
373
371
 
374
372
  <div
375
373
  className={ styles[ 'bar-chart__svg-wrapper' ] }
@@ -483,8 +481,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
483
481
  </div>
484
482
 
485
483
  { legendPosition === 'bottom' && legendElement }
484
+ { renderLegendSlot( legendChildren, 'bottom' ) }
486
485
 
487
- { children }
486
+ { nonLegendChildren }
488
487
  </Stack>
489
488
  </SingleChartContext.Provider>
490
489
  );
@@ -26,10 +26,12 @@ describe( 'BarChart', () => {
26
26
  ],
27
27
  };
28
28
 
29
- const renderWithTheme = ( props = {} ) => {
29
+ const renderWithTheme = ( props = {}, children = undefined ) => {
30
30
  return render(
31
31
  <GlobalChartsProvider>
32
- <BarChart { ...defaultProps } { ...props } />
32
+ <BarChart { ...defaultProps } { ...props }>
33
+ { children }
34
+ </BarChart>
33
35
  </GlobalChartsProvider>
34
36
  );
35
37
  };
@@ -123,39 +125,54 @@ describe( 'BarChart', () => {
123
125
  } );
124
126
 
125
127
  describe( 'Legend', () => {
128
+ const multiSeriesData = [
129
+ {
130
+ label: 'Series A',
131
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
132
+ options: {},
133
+ },
134
+ {
135
+ label: 'Series B',
136
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
137
+ options: {},
138
+ },
139
+ ];
140
+
126
141
  test( 'shows legend when showLegend is true', () => {
127
- renderWithTheme( {
128
- showLegend: true,
129
- data: [
130
- {
131
- label: 'Series A',
132
- data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
133
- options: {},
134
- },
135
- {
136
- label: 'Series B',
137
- data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
138
- options: {},
139
- },
140
- ],
141
- } );
142
+ renderWithTheme( { showLegend: true, data: multiSeriesData } );
142
143
  expect( screen.getByText( 'Series A' ) ).toBeInTheDocument();
143
144
  expect( screen.getByText( 'Series B' ) ).toBeInTheDocument();
144
145
  } );
145
146
 
146
147
  test( 'hides legend when showLegend is false', () => {
147
- renderWithTheme( {
148
- showLegend: false,
149
- data: [
150
- {
151
- label: 'Series A',
152
- data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
153
- options: {},
154
- },
155
- ],
156
- } );
148
+ renderWithTheme( { showLegend: false, data: multiSeriesData } );
157
149
  expect( screen.queryByText( 'Series A' ) ).not.toBeInTheDocument();
158
150
  } );
151
+
152
+ test( 'renders composition legend as child component', () => {
153
+ renderWithTheme( { data: multiSeriesData }, <BarChart.Legend /> );
154
+
155
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
156
+ expect( screen.getByText( 'Series A' ) ).toBeInTheDocument();
157
+ expect( screen.getByText( 'Series B' ) ).toBeInTheDocument();
158
+ } );
159
+
160
+ test( 'renders composition legend regardless of showLegend value', () => {
161
+ renderWithTheme( { data: multiSeriesData, showLegend: false }, <BarChart.Legend /> );
162
+
163
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
164
+ } );
165
+
166
+ test( 'renders composition legend in top position', () => {
167
+ renderWithTheme( { data: multiSeriesData }, <BarChart.Legend position="top" /> );
168
+
169
+ // Legend should appear before the chart content in DOM order
170
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
171
+ const html = document.body.innerHTML;
172
+ expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
173
+ html.indexOf( 'role="grid"' )
174
+ );
175
+ } );
159
176
  } );
160
177
 
161
178
  describe( 'Grid Visibility', () => {
@@ -743,7 +760,7 @@ describe( 'BarChart', () => {
743
760
 
744
761
  renderWithTheme( {
745
762
  showLegend: true,
746
- legendInteractive: true,
763
+ legend: { interactive: true },
747
764
  chartId: 'test-interactive-bar-chart',
748
765
  data: [
749
766
  {
@@ -771,7 +788,7 @@ describe( 'BarChart', () => {
771
788
  it( 'does not filter series when legendInteractive is false', () => {
772
789
  renderWithTheme( {
773
790
  showLegend: true,
774
- legendInteractive: false,
791
+ legend: { interactive: false },
775
792
  chartId: 'test-non-interactive-bar-chart',
776
793
  data: [
777
794
  {
@@ -795,7 +812,7 @@ describe( 'BarChart', () => {
795
812
  it( 'shows all series when chartId is missing even if legendInteractive is true', () => {
796
813
  renderWithTheme( {
797
814
  showLegend: true,
798
- legendInteractive: true,
815
+ legend: { interactive: true },
799
816
  // No chartId provided
800
817
  data: [
801
818
  {
@@ -823,7 +840,7 @@ describe( 'BarChart', () => {
823
840
 
824
841
  renderWithTheme( {
825
842
  showLegend: true,
826
- legendInteractive: true,
843
+ legend: { interactive: true },
827
844
  chartId: 'test-all-hidden-bar-chart',
828
845
  data: [
829
846
  {