@automattic/charts 0.57.0 → 0.59.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 (267) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/README.md +7 -54
  3. package/dist/index.cjs +9607 -21
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +32 -49
  6. package/dist/index.css.map +1 -1
  7. package/dist/index.d.cts +1612 -33
  8. package/dist/index.d.ts +1612 -33
  9. package/dist/index.js +9640 -54
  10. package/dist/index.js.map +1 -1
  11. package/package.json +9 -126
  12. package/src/charts/bar-chart/bar-chart.module.scss +0 -5
  13. package/src/charts/bar-chart/bar-chart.tsx +142 -149
  14. package/src/charts/bar-chart/test/bar-chart.test.tsx +48 -31
  15. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +54 -74
  16. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +4 -5
  17. package/src/charts/leaderboard-chart/types.ts +1 -11
  18. package/src/charts/line-chart/line-chart.module.scss +0 -5
  19. package/src/charts/line-chart/line-chart.tsx +202 -193
  20. package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +1 -2
  21. package/src/charts/line-chart/test/line-chart.test.tsx +49 -27
  22. package/src/charts/line-chart/types.ts +0 -1
  23. package/src/charts/pie-chart/pie-chart.module.scss +2 -10
  24. package/src/charts/pie-chart/pie-chart.tsx +212 -212
  25. package/src/charts/pie-chart/test/composition-api.test.tsx +44 -3
  26. package/src/charts/pie-chart/test/pie-chart.test.tsx +51 -44
  27. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -8
  28. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +166 -168
  29. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +58 -30
  30. package/src/charts/private/chart-composition/index.ts +2 -0
  31. package/src/charts/private/chart-composition/render-legend-slot.ts +22 -0
  32. package/src/charts/private/chart-composition/test/render-legend-slot.test.tsx +60 -0
  33. package/src/charts/private/chart-composition/test/use-chart-children.test.tsx +91 -0
  34. package/src/charts/private/chart-composition/use-chart-children.ts +34 -2
  35. package/src/charts/private/chart-layout/chart-layout.module.scss +7 -0
  36. package/src/charts/private/chart-layout/chart-layout.tsx +106 -0
  37. package/src/charts/private/chart-layout/index.ts +2 -0
  38. package/src/charts/private/chart-layout/test/chart-layout.test.tsx +167 -0
  39. package/src/charts/private/single-chart-context/single-chart-context.tsx +2 -2
  40. package/src/charts/private/svg-empty-state/index.ts +1 -0
  41. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +7 -0
  42. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +40 -0
  43. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +12 -8
  44. package/src/components/legend/hooks/use-chart-legend-items.ts +12 -13
  45. package/src/components/legend/index.ts +1 -8
  46. package/src/components/legend/legend.tsx +33 -8
  47. package/src/components/legend/private/base-legend.module.scss +19 -37
  48. package/src/components/legend/private/base-legend.tsx +0 -2
  49. package/src/components/legend/test/legend.test.tsx +93 -1
  50. package/src/components/legend/types.ts +7 -34
  51. package/src/hooks/index.ts +1 -1
  52. package/src/hooks/use-data-with-percentages.ts +24 -0
  53. package/src/hooks/use-interactive-legend-data.ts +18 -15
  54. package/src/index.ts +66 -9
  55. package/src/providers/chart-context/global-charts-provider.tsx +7 -1
  56. package/src/providers/chart-context/hooks/use-chart-registration.ts +2 -1
  57. package/src/providers/chart-context/types.ts +2 -2
  58. package/src/types.ts +110 -45
  59. package/src/utils/date-parsing.ts +10 -1
  60. package/src/utils/test/date-parsing.test.ts +12 -0
  61. package/src/utils/test/resolve-css-var.test.ts +4 -2
  62. package/tsup.config.ts +1 -1
  63. package/dist/base-tooltip-DOq93wjU.d.cts +0 -38
  64. package/dist/base-tooltip-DOq93wjU.d.ts +0 -38
  65. package/dist/charts/bar-chart/index.cjs +0 -15
  66. package/dist/charts/bar-chart/index.cjs.map +0 -1
  67. package/dist/charts/bar-chart/index.css +0 -153
  68. package/dist/charts/bar-chart/index.css.map +0 -1
  69. package/dist/charts/bar-chart/index.d.cts +0 -37
  70. package/dist/charts/bar-chart/index.d.ts +0 -37
  71. package/dist/charts/bar-chart/index.js +0 -15
  72. package/dist/charts/bar-chart/index.js.map +0 -1
  73. package/dist/charts/bar-list-chart/index.cjs +0 -16
  74. package/dist/charts/bar-list-chart/index.cjs.map +0 -1
  75. package/dist/charts/bar-list-chart/index.css +0 -153
  76. package/dist/charts/bar-list-chart/index.css.map +0 -1
  77. package/dist/charts/bar-list-chart/index.d.cts +0 -92
  78. package/dist/charts/bar-list-chart/index.d.ts +0 -92
  79. package/dist/charts/bar-list-chart/index.js +0 -16
  80. package/dist/charts/bar-list-chart/index.js.map +0 -1
  81. package/dist/charts/conversion-funnel-chart/index.cjs +0 -11
  82. package/dist/charts/conversion-funnel-chart/index.cjs.map +0 -1
  83. package/dist/charts/conversion-funnel-chart/index.css +0 -251
  84. package/dist/charts/conversion-funnel-chart/index.css.map +0 -1
  85. package/dist/charts/conversion-funnel-chart/index.d.cts +0 -97
  86. package/dist/charts/conversion-funnel-chart/index.d.ts +0 -97
  87. package/dist/charts/conversion-funnel-chart/index.js +0 -11
  88. package/dist/charts/conversion-funnel-chart/index.js.map +0 -1
  89. package/dist/charts/geo-chart/index.cjs +0 -13
  90. package/dist/charts/geo-chart/index.cjs.map +0 -1
  91. package/dist/charts/geo-chart/index.css +0 -117
  92. package/dist/charts/geo-chart/index.css.map +0 -1
  93. package/dist/charts/geo-chart/index.d.cts +0 -67
  94. package/dist/charts/geo-chart/index.d.ts +0 -67
  95. package/dist/charts/geo-chart/index.js +0 -13
  96. package/dist/charts/geo-chart/index.js.map +0 -1
  97. package/dist/charts/leaderboard-chart/index.cjs +0 -20
  98. package/dist/charts/leaderboard-chart/index.cjs.map +0 -1
  99. package/dist/charts/leaderboard-chart/index.css +0 -172
  100. package/dist/charts/leaderboard-chart/index.css.map +0 -1
  101. package/dist/charts/leaderboard-chart/index.d.cts +0 -46
  102. package/dist/charts/leaderboard-chart/index.d.ts +0 -46
  103. package/dist/charts/leaderboard-chart/index.js +0 -20
  104. package/dist/charts/leaderboard-chart/index.js.map +0 -1
  105. package/dist/charts/line-chart/index.cjs +0 -15
  106. package/dist/charts/line-chart/index.cjs.map +0 -1
  107. package/dist/charts/line-chart/index.css +0 -225
  108. package/dist/charts/line-chart/index.css.map +0 -1
  109. package/dist/charts/line-chart/index.d.cts +0 -99
  110. package/dist/charts/line-chart/index.d.ts +0 -99
  111. package/dist/charts/line-chart/index.js +0 -15
  112. package/dist/charts/line-chart/index.js.map +0 -1
  113. package/dist/charts/pie-chart/index.cjs +0 -18
  114. package/dist/charts/pie-chart/index.cjs.map +0 -1
  115. package/dist/charts/pie-chart/index.css +0 -143
  116. package/dist/charts/pie-chart/index.css.map +0 -1
  117. package/dist/charts/pie-chart/index.d.cts +0 -97
  118. package/dist/charts/pie-chart/index.d.ts +0 -97
  119. package/dist/charts/pie-chart/index.js +0 -18
  120. package/dist/charts/pie-chart/index.js.map +0 -1
  121. package/dist/charts/pie-semi-circle-chart/index.cjs +0 -17
  122. package/dist/charts/pie-semi-circle-chart/index.cjs.map +0 -1
  123. package/dist/charts/pie-semi-circle-chart/index.css +0 -144
  124. package/dist/charts/pie-semi-circle-chart/index.css.map +0 -1
  125. package/dist/charts/pie-semi-circle-chart/index.d.cts +0 -94
  126. package/dist/charts/pie-semi-circle-chart/index.d.ts +0 -94
  127. package/dist/charts/pie-semi-circle-chart/index.js +0 -17
  128. package/dist/charts/pie-semi-circle-chart/index.js.map +0 -1
  129. package/dist/charts/sparkline/index.cjs +0 -16
  130. package/dist/charts/sparkline/index.cjs.map +0 -1
  131. package/dist/charts/sparkline/index.css +0 -242
  132. package/dist/charts/sparkline/index.css.map +0 -1
  133. package/dist/charts/sparkline/index.d.cts +0 -113
  134. package/dist/charts/sparkline/index.d.ts +0 -113
  135. package/dist/charts/sparkline/index.js +0 -16
  136. package/dist/charts/sparkline/index.js.map +0 -1
  137. package/dist/chunk-2A34OA5O.cjs +0 -51
  138. package/dist/chunk-2A34OA5O.cjs.map +0 -1
  139. package/dist/chunk-2NCY7R4G.js +0 -3897
  140. package/dist/chunk-2NCY7R4G.js.map +0 -1
  141. package/dist/chunk-32DH6JDF.js +0 -1263
  142. package/dist/chunk-32DH6JDF.js.map +0 -1
  143. package/dist/chunk-4OPFE4RM.js +0 -614
  144. package/dist/chunk-4OPFE4RM.js.map +0 -1
  145. package/dist/chunk-6CCZL2JJ.js +0 -63
  146. package/dist/chunk-6CCZL2JJ.js.map +0 -1
  147. package/dist/chunk-77OKCVQN.cjs +0 -421
  148. package/dist/chunk-77OKCVQN.cjs.map +0 -1
  149. package/dist/chunk-7FQX4ALL.cjs +0 -219
  150. package/dist/chunk-7FQX4ALL.cjs.map +0 -1
  151. package/dist/chunk-ASLARV7L.cjs +0 -81
  152. package/dist/chunk-ASLARV7L.cjs.map +0 -1
  153. package/dist/chunk-BCX5THDQ.js +0 -403
  154. package/dist/chunk-BCX5THDQ.js.map +0 -1
  155. package/dist/chunk-BPYKWMI7.js +0 -194
  156. package/dist/chunk-BPYKWMI7.js.map +0 -1
  157. package/dist/chunk-CZGYJKG6.js +0 -421
  158. package/dist/chunk-CZGYJKG6.js.map +0 -1
  159. package/dist/chunk-D2UH4CFE.cjs +0 -120
  160. package/dist/chunk-D2UH4CFE.cjs.map +0 -1
  161. package/dist/chunk-DAU3HNEG.js +0 -344
  162. package/dist/chunk-DAU3HNEG.js.map +0 -1
  163. package/dist/chunk-H2V4JMSA.js +0 -219
  164. package/dist/chunk-H2V4JMSA.js.map +0 -1
  165. package/dist/chunk-I2276W3I.cjs +0 -66
  166. package/dist/chunk-I2276W3I.cjs.map +0 -1
  167. package/dist/chunk-I35UYJJR.cjs +0 -468
  168. package/dist/chunk-I35UYJJR.cjs.map +0 -1
  169. package/dist/chunk-IU4DYUAV.js +0 -120
  170. package/dist/chunk-IU4DYUAV.js.map +0 -1
  171. package/dist/chunk-KXRWNFQJ.js +0 -51
  172. package/dist/chunk-KXRWNFQJ.js.map +0 -1
  173. package/dist/chunk-OP6PHB2U.js +0 -81
  174. package/dist/chunk-OP6PHB2U.js.map +0 -1
  175. package/dist/chunk-PXLEMUGJ.js +0 -165
  176. package/dist/chunk-PXLEMUGJ.js.map +0 -1
  177. package/dist/chunk-RCY6XLGU.cjs +0 -63
  178. package/dist/chunk-RCY6XLGU.cjs.map +0 -1
  179. package/dist/chunk-RHHVEJHJ.cjs +0 -1263
  180. package/dist/chunk-RHHVEJHJ.cjs.map +0 -1
  181. package/dist/chunk-TO3OQBXG.cjs +0 -165
  182. package/dist/chunk-TO3OQBXG.cjs.map +0 -1
  183. package/dist/chunk-V36ERY7Y.js +0 -375
  184. package/dist/chunk-V36ERY7Y.js.map +0 -1
  185. package/dist/chunk-VJM5XCB4.cjs +0 -614
  186. package/dist/chunk-VJM5XCB4.cjs.map +0 -1
  187. package/dist/chunk-VTS3PNMS.cjs +0 -344
  188. package/dist/chunk-VTS3PNMS.cjs.map +0 -1
  189. package/dist/chunk-WLODYNLB.js +0 -1067
  190. package/dist/chunk-WLODYNLB.js.map +0 -1
  191. package/dist/chunk-XKRJL2QT.cjs +0 -375
  192. package/dist/chunk-XKRJL2QT.cjs.map +0 -1
  193. package/dist/chunk-XWYZIFZW.js +0 -66
  194. package/dist/chunk-XWYZIFZW.js.map +0 -1
  195. package/dist/chunk-Y3NNQMAX.cjs +0 -194
  196. package/dist/chunk-Y3NNQMAX.cjs.map +0 -1
  197. package/dist/chunk-YE2T52VZ.cjs +0 -1067
  198. package/dist/chunk-YE2T52VZ.cjs.map +0 -1
  199. package/dist/chunk-Z26M4V2M.js +0 -468
  200. package/dist/chunk-Z26M4V2M.js.map +0 -1
  201. package/dist/chunk-Z45KX47P.cjs +0 -3897
  202. package/dist/chunk-Z45KX47P.cjs.map +0 -1
  203. package/dist/chunk-ZH4F5RMG.cjs +0 -403
  204. package/dist/chunk-ZH4F5RMG.cjs.map +0 -1
  205. package/dist/components/legend/index.cjs +0 -11
  206. package/dist/components/legend/index.cjs.map +0 -1
  207. package/dist/components/legend/index.css +0 -103
  208. package/dist/components/legend/index.css.map +0 -1
  209. package/dist/components/legend/index.d.cts +0 -37
  210. package/dist/components/legend/index.d.ts +0 -37
  211. package/dist/components/legend/index.js +0 -11
  212. package/dist/components/legend/index.js.map +0 -1
  213. package/dist/components/tooltip/index.cjs +0 -12
  214. package/dist/components/tooltip/index.cjs.map +0 -1
  215. package/dist/components/tooltip/index.css +0 -13
  216. package/dist/components/tooltip/index.css.map +0 -1
  217. package/dist/components/tooltip/index.d.cts +0 -71
  218. package/dist/components/tooltip/index.d.ts +0 -71
  219. package/dist/components/tooltip/index.js +0 -12
  220. package/dist/components/tooltip/index.js.map +0 -1
  221. package/dist/components/trend-indicator/index.cjs +0 -8
  222. package/dist/components/trend-indicator/index.cjs.map +0 -1
  223. package/dist/components/trend-indicator/index.css +0 -27
  224. package/dist/components/trend-indicator/index.css.map +0 -1
  225. package/dist/components/trend-indicator/index.d.cts +0 -44
  226. package/dist/components/trend-indicator/index.d.ts +0 -44
  227. package/dist/components/trend-indicator/index.js +0 -8
  228. package/dist/components/trend-indicator/index.js.map +0 -1
  229. package/dist/format-metric-value-MXm5DtQ_.d.cts +0 -24
  230. package/dist/format-metric-value-MXm5DtQ_.d.ts +0 -24
  231. package/dist/hooks/index.cjs +0 -31
  232. package/dist/hooks/index.cjs.map +0 -1
  233. package/dist/hooks/index.css +0 -103
  234. package/dist/hooks/index.css.map +0 -1
  235. package/dist/hooks/index.d.cts +0 -272
  236. package/dist/hooks/index.d.ts +0 -272
  237. package/dist/hooks/index.js +0 -31
  238. package/dist/hooks/index.js.map +0 -1
  239. package/dist/leaderboard-chart-BKYYXcg2.d.ts +0 -83
  240. package/dist/leaderboard-chart-DR7CGb0L.d.cts +0 -83
  241. package/dist/legend-C2grwnWk.d.cts +0 -9
  242. package/dist/legend-Cj0xM5dU.d.ts +0 -9
  243. package/dist/providers/index.cjs +0 -21
  244. package/dist/providers/index.cjs.map +0 -1
  245. package/dist/providers/index.css +0 -103
  246. package/dist/providers/index.css.map +0 -1
  247. package/dist/providers/index.d.cts +0 -28
  248. package/dist/providers/index.d.ts +0 -28
  249. package/dist/providers/index.js +0 -21
  250. package/dist/providers/index.js.map +0 -1
  251. package/dist/themes-BmVGrYnF.d.ts +0 -67
  252. package/dist/themes-CyjKm-P_.d.cts +0 -67
  253. package/dist/types-CuUEszrM.d.ts +0 -19
  254. package/dist/types-DZordNiO.d.cts +0 -505
  255. package/dist/types-DZordNiO.d.ts +0 -505
  256. package/dist/types-I67mddpr.d.cts +0 -78
  257. package/dist/types-I67mddpr.d.ts +0 -78
  258. package/dist/types-KtOPPzPX.d.cts +0 -19
  259. package/dist/utils/index.cjs +0 -44
  260. package/dist/utils/index.cjs.map +0 -1
  261. package/dist/utils/index.d.cts +0 -226
  262. package/dist/utils/index.d.ts +0 -226
  263. package/dist/utils/index.js +0 -44
  264. package/dist/utils/index.js.map +0 -1
  265. package/dist/with-responsive-CNfhzAUu.d.cts +0 -18
  266. package/dist/with-responsive-CNfhzAUu.d.ts +0 -18
  267. package/src/hooks/use-has-legend-child.ts +0 -22
@@ -4,18 +4,23 @@ import { LinearGradient } from '@visx/gradient';
4
4
  import { scaleTime } from '@visx/scale';
5
5
  import { XYChart, AreaSeries, Grid, Axis, DataContext } from '@visx/xychart';
6
6
  import { __ } from '@wordpress/i18n';
7
- import { Stack } from '@wordpress/ui';
8
7
  import clsx from 'clsx';
9
8
  import { differenceInHours, differenceInYears } from 'date-fns';
10
- import { useMemo, useContext, forwardRef, useImperativeHandle, useState, useRef } from 'react';
9
+ import {
10
+ useMemo,
11
+ useContext,
12
+ forwardRef,
13
+ useImperativeHandle,
14
+ useState,
15
+ useRef,
16
+ useCallback,
17
+ } from 'react';
11
18
  import { Legend, useChartLegendItems } from '../../components/legend';
12
19
  import { AccessibleTooltip, useKeyboardNavigation } from '../../components/tooltip';
13
20
  import {
14
21
  useXYChartTheme,
15
22
  useChartDataTransform,
16
23
  useChartMargin,
17
- useElementSize,
18
- useHasLegendChild,
19
24
  usePrefersReducedMotion,
20
25
  } from '../../hooks';
21
26
  import {
@@ -27,8 +32,11 @@ import {
27
32
  useGlobalChartsTheme,
28
33
  } from '../../providers';
29
34
  import { attachSubComponents } from '../../utils';
35
+ import { useChartChildren } from '../private/chart-composition';
36
+ import { ChartLayout } from '../private/chart-layout';
30
37
  import { DefaultGlyph } from '../private/default-glyph';
31
38
  import { SingleChartContext, type SingleChartRef } from '../private/single-chart-context';
39
+ import { SvgEmptyState } from '../private/svg-empty-state';
32
40
  import { withResponsive } from '../private/with-responsive';
33
41
  import styles from './line-chart.module.scss';
34
42
  import { LineChartAnnotation, LineChartAnnotationsOverlay, LineChartGlyph } from './private';
@@ -256,15 +264,9 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
256
264
  withTooltips = true,
257
265
  withTooltipCrosshairs,
258
266
  showLegend = false,
259
- legendOrientation = 'horizontal',
260
- legendAlignment = 'center',
261
- legendPosition = 'bottom',
262
- legendMaxWidth,
263
- legendTextOverflow = 'wrap',
264
- legendItemClassName,
267
+ legend = {},
265
268
  renderGlyph = defaultRenderGlyph,
266
269
  glyphStyle = {},
267
- legendShape = 'line',
268
270
  withLegendGlyph = false,
269
271
  withGradientFill = false,
270
272
  smoothing = true,
@@ -272,7 +274,6 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
272
274
  renderTooltip = renderDefaultTooltip,
273
275
  withStartGlyphs = false,
274
276
  withEndGlyphs = false,
275
- legendInteractive = false,
276
277
  animation,
277
278
  options = {},
278
279
  onPointerDown = undefined,
@@ -285,24 +286,31 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
285
286
  },
286
287
  ref
287
288
  ) => {
289
+ const legendInteractive = legend.interactive ?? false;
290
+ const legendShape = legend.shape ?? 'line';
291
+ const legendPosition = legend.position ?? 'bottom';
292
+
288
293
  const providerTheme = useGlobalChartsTheme();
289
294
  const theme = useXYChartTheme( data );
290
295
  const chartId = useChartId( providedChartId );
291
- const [ svgWrapperRef, , svgWrapperHeight ] = useElementSize< HTMLDivElement >();
292
296
  const chartRef = useRef< HTMLDivElement >( null );
293
297
  const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined );
294
298
  const [ isNavigating, setIsNavigating ] = useState( false );
295
299
  const internalChartRef = useRef< SingleChartRef >( null );
296
300
 
297
- // Check if children contain a Legend component (composition pattern)
298
- const hasLegendChild = useHasLegendChild( children );
301
+ // Process children for composition API (Legend, etc.)
302
+ const { legendChildren, nonLegendChildren } = useChartChildren( children, 'LineChart' );
303
+ const [ measuredChartHeight, setMeasuredChartHeight ] = useState< number | undefined >();
299
304
 
300
- // Use the measured SVG wrapper height, falling back to the passed height if provided.
301
- // When there's a legend (via prop or composition), we must wait for measurement because
302
- // the legend takes space and the svg-wrapper height will be less than the total height.
303
- const chartHeight = svgWrapperHeight > 0 ? svgWrapperHeight : height;
304
- const hasLegend = showLegend || hasLegendChild;
305
- const isWaitingForMeasurement = hasLegend ? svgWrapperHeight === 0 : ! chartHeight;
305
+ // Callback for ChartLayout to notify us when the measured content height changes.
306
+ // We compute chartHeight the same way the render prop does so the context stays in sync.
307
+ const handleContentHeightChange = useCallback(
308
+ ( contentHeight: number ) => {
309
+ const chartHeight = contentHeight > 0 ? contentHeight : height;
310
+ setMeasuredChartHeight( chartHeight );
311
+ },
312
+ [ height ]
313
+ );
306
314
 
307
315
  // Forward the external ref to the internal ref
308
316
  useImperativeHandle(
@@ -454,11 +462,13 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
454
462
 
455
463
  const legendElement = showLegend && (
456
464
  <Legend
457
- orientation={ legendOrientation }
458
- alignment={ legendAlignment }
465
+ orientation={ legend.orientation ?? 'horizontal' }
466
+ alignment={ legend.alignment ?? 'center' }
459
467
  position={ legendPosition }
460
- labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
461
- itemClassName={ legendItemClassName }
468
+ labelStyles={ legend.labelStyles }
469
+ itemClassName={ legend.itemClassName }
470
+ itemStyles={ legend.itemStyles }
471
+ shapeStyles={ legend.shapeStyles }
462
472
  className={ styles[ 'line-chart__legend' ] }
463
473
  shape={ legendShape }
464
474
  chartId={ chartId }
@@ -472,11 +482,13 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
472
482
  chartId,
473
483
  chartRef: internalChartRef,
474
484
  chartWidth: width,
475
- chartHeight,
485
+ chartHeight: measuredChartHeight || 0,
476
486
  } }
477
487
  >
478
- <Stack
479
- direction="column"
488
+ <ChartLayout
489
+ legendPosition={ legendPosition }
490
+ legendElement={ legendElement }
491
+ legendChildren={ legendChildren }
480
492
  gap={ gap }
481
493
  className={ clsx(
482
494
  'line-chart',
@@ -484,179 +496,176 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
484
496
  { [ styles[ 'line-chart--animated' ] ]: animation && ! prefersReducedMotion },
485
497
  className
486
498
  ) }
499
+ style={ { width, height } }
487
500
  data-testid="line-chart"
488
- style={ {
489
- width,
490
- height,
491
- visibility: isWaitingForMeasurement ? 'hidden' : 'visible',
492
- } }
501
+ trailingContent={ nonLegendChildren }
502
+ onContentHeightChange={ handleContentHeightChange }
493
503
  >
494
- { legendPosition === 'top' && legendElement }
495
-
496
- <div
497
- className={ styles[ 'line-chart__svg-wrapper' ] }
498
- ref={ svgWrapperRef }
499
- role="grid"
500
- aria-label={ __( 'Line chart', 'jetpack-charts' ) }
501
- tabIndex={ 0 }
502
- onKeyDown={ onChartKeyDown }
503
- onFocus={ onChartFocus }
504
- onBlur={ onChartBlur }
505
- >
506
- { ! isWaitingForMeasurement && (
507
- <div ref={ chartRef }>
508
- <XYChart
509
- theme={ theme }
510
- width={ width }
511
- height={ chartHeight }
512
- margin={ {
513
- ...defaultMargin,
514
- ...margin,
515
- } }
516
- // xScale and yScale could be set in Axis as well, but they are `scale` props there.
517
- xScale={ chartOptions.xScale }
518
- yScale={ chartOptions.yScale }
519
- onPointerDown={ onPointerDown }
520
- onPointerUp={ onPointerUp }
521
- onPointerMove={ onPointerMove }
522
- onPointerOut={ onPointerOut }
523
- pointerEventsDataKey="nearest"
524
- >
525
- { gridVisibility !== 'none' && <Grid columns={ false } numTicks={ 4 } /> }
526
- { chartOptions.axis.x.display && <Axis { ...chartOptions.axis.x } /> }
527
- { chartOptions.axis.y.display && <Axis { ...chartOptions.axis.y } /> }
528
-
529
- { allSeriesHidden ? (
530
- <text
531
- x={ width / 2 }
532
- y={ chartHeight / 2 }
533
- textAnchor="middle"
534
- fill={ providerTheme.gridStyles?.stroke || '#ccc' }
535
- fontSize="14"
536
- fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
504
+ { ( { contentHeight } ) => {
505
+ // Use the measured height, falling back to the passed height if provided.
506
+ const chartHeight = contentHeight > 0 ? contentHeight : height;
507
+
508
+ return (
509
+ <div
510
+ role="grid"
511
+ aria-label={ __( 'Line chart', 'jetpack-charts' ) }
512
+ tabIndex={ 0 }
513
+ onKeyDown={ onChartKeyDown }
514
+ onFocus={ onChartFocus }
515
+ onBlur={ onChartBlur }
516
+ >
517
+ { chartHeight > 0 && (
518
+ <div ref={ chartRef }>
519
+ <XYChart
520
+ theme={ theme }
521
+ width={ width }
522
+ height={ chartHeight }
523
+ margin={ {
524
+ ...defaultMargin,
525
+ ...margin,
526
+ } }
527
+ // xScale and yScale could be set in Axis as well, but they are `scale` props there.
528
+ xScale={ chartOptions.xScale }
529
+ yScale={ chartOptions.yScale }
530
+ onPointerDown={ onPointerDown }
531
+ onPointerUp={ onPointerUp }
532
+ onPointerMove={ onPointerMove }
533
+ onPointerOut={ onPointerOut }
534
+ pointerEventsDataKey="nearest"
537
535
  >
538
- { __(
539
- 'All series are hidden. Click legend items to show data.',
540
- 'jetpack-charts'
541
- ) }
542
- </text>
543
- ) : null }
544
-
545
- { seriesWithVisibility.map( ( { series: seriesData, index, isVisible } ) => {
546
- // Skip rendering invisible series
547
- if ( ! isVisible ) {
548
- return null;
549
- }
550
-
551
- const { color, lineStyles, glyph } = getElementStyles( {
552
- data: seriesData,
553
- index,
554
- } );
555
-
556
- const lineProps = {
557
- stroke: color,
558
- ...lineStyles,
559
- };
560
-
561
- return (
562
- <g key={ seriesData?.label || index }>
563
- { withGradientFill && (
564
- <LinearGradient
565
- id={ `area-gradient-${ chartId }-${ index + 1 }` }
566
- from={ color }
567
- fromOpacity={ 0.4 }
568
- toOpacity={ 0.1 }
569
- to={ providerTheme.backgroundColor }
570
- { ...seriesData.options?.gradient }
571
- data-testid="line-gradient"
572
- >
573
- { seriesData.options?.gradient?.stops?.map( ( stop, stopIndex ) => (
574
- <stop
575
- key={ `${ stop.offset }-${ stop.color || color }` }
576
- offset={ stop.offset }
577
- stopColor={ stop.color || color }
578
- stopOpacity={ stop.opacity ?? 1 }
579
- data-testid={ `line-gradient-stop-${ chartId }-${ index }-${ stopIndex }` }
536
+ { gridVisibility !== 'none' && <Grid columns={ false } numTicks={ 4 } /> }
537
+ { chartOptions.axis.x.display && <Axis { ...chartOptions.axis.x } /> }
538
+ { chartOptions.axis.y.display && <Axis { ...chartOptions.axis.y } /> }
539
+
540
+ { allSeriesHidden ? (
541
+ <SvgEmptyState
542
+ x={ width / 2 }
543
+ y={ chartHeight / 2 }
544
+ width={ width }
545
+ height={ chartHeight }
546
+ >
547
+ { __(
548
+ 'All series are hidden. Click legend items to show data.',
549
+ 'jetpack-charts'
550
+ ) }
551
+ </SvgEmptyState>
552
+ ) : null }
553
+
554
+ { seriesWithVisibility.map( ( { series: seriesData, index, isVisible } ) => {
555
+ // Skip rendering invisible series
556
+ if ( ! isVisible ) {
557
+ return null;
558
+ }
559
+
560
+ const { color, lineStyles, glyph } = getElementStyles( {
561
+ data: seriesData,
562
+ index,
563
+ } );
564
+
565
+ const lineProps = {
566
+ stroke: color,
567
+ ...lineStyles,
568
+ };
569
+
570
+ return (
571
+ <g key={ seriesData?.label || index }>
572
+ { withGradientFill && (
573
+ <LinearGradient
574
+ id={ `area-gradient-${ chartId }-${ index + 1 }` }
575
+ from={ color }
576
+ fromOpacity={ 0.4 }
577
+ toOpacity={ 0.1 }
578
+ to={ providerTheme.backgroundColor }
579
+ { ...seriesData.options?.gradient }
580
+ data-testid="line-gradient"
581
+ >
582
+ { seriesData.options?.gradient?.stops?.map( ( stop, stopIndex ) => (
583
+ <stop
584
+ key={ `${ stop.offset }-${ stop.color || color }` }
585
+ offset={ stop.offset }
586
+ stopColor={ stop.color || color }
587
+ stopOpacity={ stop.opacity ?? 1 }
588
+ data-testid={ `line-gradient-stop-${ chartId }-${ index }-${ stopIndex }` }
589
+ />
590
+ ) ) }
591
+ </LinearGradient>
592
+ ) }
593
+ <AreaSeries
594
+ key={ seriesData?.label }
595
+ dataKey={ seriesData?.label }
596
+ data={ seriesData.data as DataPointDate[] }
597
+ { ...accessors }
598
+ fill={
599
+ withGradientFill
600
+ ? `url(#area-gradient-${ chartId }-${ index + 1 })`
601
+ : 'transparent'
602
+ }
603
+ renderLine={ true }
604
+ curve={ getCurveType( curveType, smoothing ) }
605
+ lineProps={ lineProps }
606
+ />
607
+
608
+ { withStartGlyphs && (
609
+ <LineChartGlyph
610
+ index={ index }
611
+ data={ seriesData }
612
+ color={ color }
613
+ renderGlyph={ glyph ?? renderGlyph }
614
+ accessors={ accessors }
615
+ glyphStyle={ glyphStyle }
616
+ position="start"
580
617
  />
581
- ) ) }
582
- </LinearGradient>
583
- ) }
584
- <AreaSeries
585
- key={ seriesData?.label }
586
- dataKey={ seriesData?.label }
587
- data={ seriesData.data as DataPointDate[] }
588
- { ...accessors }
589
- fill={
590
- withGradientFill
591
- ? `url(#area-gradient-${ chartId }-${ index + 1 })`
592
- : 'transparent'
618
+ ) }
619
+
620
+ { withEndGlyphs && (
621
+ <LineChartGlyph
622
+ index={ index }
623
+ data={ seriesData }
624
+ color={ color }
625
+ renderGlyph={ glyph ?? renderGlyph }
626
+ accessors={ accessors }
627
+ glyphStyle={ glyphStyle }
628
+ position="end"
629
+ />
630
+ ) }
631
+ </g>
632
+ );
633
+ } ) }
634
+
635
+ { withTooltips && (
636
+ <AccessibleTooltip
637
+ detectBounds
638
+ snapTooltipToDatumX
639
+ snapTooltipToDatumY
640
+ showSeriesGlyphs
641
+ renderTooltip={ renderTooltip }
642
+ renderGlyph={ tooltipRenderGlyph }
643
+ glyphStyle={ glyphStyle }
644
+ showVerticalCrosshair={ withTooltipCrosshairs?.showVertical }
645
+ showHorizontalCrosshair={ withTooltipCrosshairs?.showHorizontal }
646
+ selectedIndex={ selectedIndex }
647
+ tooltipRef={ tooltipRef }
648
+ keyboardFocusedClassName={
649
+ styles[ 'line-chart__tooltip--keyboard-focused' ]
593
650
  }
594
- renderLine={ true }
595
- curve={ getCurveType( curveType, smoothing ) }
596
- lineProps={ lineProps }
651
+ series={ dataSorted }
597
652
  />
653
+ ) }
598
654
 
599
- { withStartGlyphs && (
600
- <LineChartGlyph
601
- index={ index }
602
- data={ seriesData }
603
- color={ color }
604
- renderGlyph={ glyph ?? renderGlyph }
605
- accessors={ accessors }
606
- glyphStyle={ glyphStyle }
607
- position="start"
608
- />
609
- ) }
610
-
611
- { withEndGlyphs && (
612
- <LineChartGlyph
613
- index={ index }
614
- data={ seriesData }
615
- color={ color }
616
- renderGlyph={ glyph ?? renderGlyph }
617
- accessors={ accessors }
618
- glyphStyle={ glyphStyle }
619
- position="end"
620
- />
621
- ) }
622
- </g>
623
- );
624
- } ) }
625
-
626
- { withTooltips && (
627
- <AccessibleTooltip
628
- detectBounds
629
- snapTooltipToDatumX
630
- snapTooltipToDatumY
631
- showSeriesGlyphs
632
- renderTooltip={ renderTooltip }
633
- renderGlyph={ tooltipRenderGlyph }
634
- glyphStyle={ glyphStyle }
635
- showVerticalCrosshair={ withTooltipCrosshairs?.showVertical }
636
- showHorizontalCrosshair={ withTooltipCrosshairs?.showHorizontal }
637
- selectedIndex={ selectedIndex }
638
- tooltipRef={ tooltipRef }
639
- keyboardFocusedClassName={ styles[ 'line-chart__tooltip--keyboard-focused' ] }
640
- series={ dataSorted }
641
- />
642
- ) }
643
-
644
- { /* Component to expose scale data via ref */ }
645
- <LineChartScalesRef
646
- chartRef={ internalChartRef }
647
- width={ width }
648
- height={ height }
649
- margin={ margin }
650
- />
651
- </XYChart>
655
+ { /* Component to expose scale data via ref */ }
656
+ <LineChartScalesRef
657
+ chartRef={ internalChartRef }
658
+ width={ width }
659
+ height={ height }
660
+ margin={ margin }
661
+ />
662
+ </XYChart>
663
+ </div>
664
+ ) }
652
665
  </div>
653
- ) }
654
- </div>
655
-
656
- { legendPosition === 'bottom' && legendElement }
657
-
658
- { children }
659
- </Stack>
666
+ );
667
+ } }
668
+ </ChartLayout>
660
669
  </SingleChartContext.Provider>
661
670
  );
662
671
  }
@@ -99,8 +99,7 @@ const LineChartAnnotationsOverlay: FC< LineChartAnnotationsProps > = ( { childre
99
99
  };
100
100
  }, [ getScalesData, chartWidth, chartHeight ] );
101
101
 
102
- // Early return if no chart data available
103
- if ( ! chartRef || ! children ) {
102
+ if ( ! chartRef || ! children || ! chartWidth || ! chartHeight ) {
104
103
  return null;
105
104
  }
106
105
 
@@ -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
 
@@ -1,8 +1,5 @@
1
1
  .pie-chart {
2
- display: flex;
3
- flex-direction: column;
4
2
  overflow: hidden;
5
- align-items: center;
6
3
 
7
4
  // Fill parent when no explicit width/height provided
8
5
  &--responsive {
@@ -10,13 +7,8 @@
10
7
  width: 100%;
11
8
  }
12
9
 
13
- &__svg-wrapper {
14
- flex: 1;
15
- min-height: 0; // Required for flex shrinking
16
- min-width: 0; // Required for flex shrinking
10
+ &__centering {
17
11
  width: 100%;
18
- display: flex;
19
- align-items: center;
20
- justify-content: center;
12
+ height: 100%;
21
13
  }
22
14
  }