@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
@@ -7,7 +7,11 @@ import clsx from 'clsx';
7
7
  import { useCallback, useContext, useMemo } from 'react';
8
8
  import { Legend, useChartLegendItems } from '../../components/legend';
9
9
  import { BaseTooltip } from '../../components/tooltip';
10
- import { useElementSize, useInteractiveLegendData, usePrefersReducedMotion } from '../../hooks';
10
+ import {
11
+ useDataWithPercentages,
12
+ useInteractiveLegendData,
13
+ usePrefersReducedMotion,
14
+ } from '../../hooks';
11
15
  import {
12
16
  GlobalChartsProvider,
13
17
  useChartId,
@@ -19,12 +23,19 @@ import {
19
23
  import { attachSubComponents } from '../../utils';
20
24
  import { getStringWidth } from '../../visx/text';
21
25
  import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
26
+ import { ChartLayout } from '../private/chart-layout';
22
27
  import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
23
28
  import { SingleChartContext } from '../private/single-chart-context';
29
+ import { SvgEmptyState } from '../private/svg-empty-state';
24
30
  import { withResponsive, ResponsiveConfig } from '../private/with-responsive';
25
31
  import styles from './pie-chart.module.scss';
26
32
  import type { LegendValueDisplay } from '../../components/legend';
27
- import type { BaseChartProps, DataPointPercentage, Optional } from '../../types';
33
+ import type {
34
+ BaseChartProps,
35
+ DataPointPercentage,
36
+ DataPointPercentageCalculated,
37
+ Optional,
38
+ } from '../../types';
28
39
  import type { ChartComponentWithComposition } from '../private/chart-composition';
29
40
  import type { SVGProps, MouseEvent, ReactNode, FC } from 'react';
30
41
 
@@ -33,9 +44,9 @@ import type { SVGProps, MouseEvent, ReactNode, FC } from 'react';
33
44
  */
34
45
  export type PieChartRenderTooltipParams = {
35
46
  /**
36
- * The data point being hovered, including label, value, and percentage.
47
+ * The data point being hovered, including label, value, and calculated percentage.
37
48
  */
38
- tooltipData: DataPointPercentage;
49
+ tooltipData: DataPointPercentageCalculated;
39
50
  };
40
51
 
41
52
  /**
@@ -93,13 +104,6 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
93
104
  */
94
105
  legendValueDisplay?: LegendValueDisplay;
95
106
 
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
107
  /**
104
108
  * Use the children prop to render additional elements on the chart.
105
109
  */
@@ -142,16 +146,15 @@ const validateData = ( data: DataPointPercentage[] ) => {
142
146
  }
143
147
 
144
148
  // Check for negative values
145
- const hasNegativeValues = data.some( item => item.percentage < 0 || item.value < 0 );
149
+ const hasNegativeValues = data.some( item => item.value < 0 );
146
150
  if ( hasNegativeValues ) {
147
151
  return { isValid: false, message: 'Invalid data: Negative values are not allowed' };
148
152
  }
149
153
 
150
- // Validate total percentage
151
- const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 );
152
- if ( Math.abs( totalPercentage - 100 ) > 0.01 ) {
153
- // Using small epsilon for floating point comparison
154
- return { isValid: false, message: 'Invalid percentage total: Must equal 100' };
154
+ // Validate total value is greater than 0
155
+ const totalValue = data.reduce( ( sum, item ) => sum + item.value, 0 );
156
+ if ( totalValue <= 0 ) {
157
+ return { isValid: false, message: 'Invalid data: Total value must be greater than 0' };
155
158
  }
156
159
 
157
160
  return { isValid: true, message: '' };
@@ -169,13 +172,7 @@ const PieChartInternal = ( {
169
172
  withTooltips = false,
170
173
  className,
171
174
  showLegend = false,
172
- legendOrientation = 'horizontal',
173
- legendPosition = 'bottom',
174
- legendAlignment = 'center',
175
- legendMaxWidth,
176
- legendTextOverflow = 'wrap',
177
- legendItemClassName,
178
- legendShape = 'circle',
175
+ legend = {},
179
176
  width: propWidth,
180
177
  height: propHeight,
181
178
  size,
@@ -186,18 +183,19 @@ const PieChartInternal = ( {
186
183
  cornerScale = 0,
187
184
  showLabels = true,
188
185
  legendValueDisplay = 'percentage',
189
- legendInteractive = false,
190
186
  children = null,
191
187
  tooltipOffsetX = 0,
192
188
  tooltipOffsetY = -15,
193
189
  renderTooltip = renderDefaultPieTooltip,
194
190
  gap = 'md',
195
191
  }: PieChartProps ) => {
192
+ const legendInteractive = legend.interactive ?? false;
193
+ const legendPosition = legend.position ?? 'bottom';
194
+
196
195
  const providerTheme = useGlobalChartsTheme();
197
196
  const chartId = useChartId( providedChartId );
198
- const [ svgWrapperRef, svgWrapperWidth, svgWrapperHeight ] = useElementSize< HTMLDivElement >();
199
197
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
200
- useTooltip< DataPointPercentage >();
198
+ useTooltip< DataPointPercentageCalculated >();
201
199
 
202
200
  // Set up portal tooltip for better z-index handling
203
201
  // We get containerBounds to cancel out stale offsets in the position calculation
@@ -216,9 +214,12 @@ const PieChartInternal = ( {
216
214
 
217
215
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
218
216
 
217
+ // Calculate percentages from values (single source of truth)
218
+ const dataWithPercentages = useDataWithPercentages( data );
219
+
219
220
  // Filter and recalculate data for interactive legends
220
221
  const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
221
- data,
222
+ data: dataWithPercentages,
222
223
  chartId,
223
224
  legendInteractive,
224
225
  isSeriesVisible,
@@ -236,7 +237,10 @@ const PieChartInternal = ( {
236
237
  const { isValid, message } = validateData( data );
237
238
 
238
239
  // Process children to extract compound components
239
- const { svgChildren, htmlChildren, otherChildren } = useChartChildren( children, 'PieChart' );
240
+ const { svgChildren, htmlChildren, legendChildren, otherChildren } = useChartChildren(
241
+ children,
242
+ 'PieChart'
243
+ );
240
244
 
241
245
  // Memoize metadata to prevent unnecessary re-registration
242
246
  const chartMetadata = useMemo(
@@ -267,34 +271,9 @@ const PieChartInternal = ( {
267
271
  );
268
272
  }
269
273
 
270
- // Calculate the actual pie size:
271
- // - Measure available space from the svg-wrapper
272
- // - If size prop provided: use it as max, but shrink if container is smaller
273
- // - If no size prop: fill available space
274
- const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : 300;
275
- const availableHeight = svgWrapperHeight > 0 ? svgWrapperHeight : 300;
276
- const availableSize = Math.min( availableWidth, availableHeight );
277
- const actualSize = size ? Math.min( size, availableSize ) : availableSize;
278
-
279
- const width = actualSize;
280
- const height = actualSize;
281
-
282
- // Calculate radius based on width/height
283
- const radius = Math.min( width, height ) / 2;
284
-
285
- // Center the chart in the available space
286
- const centerX = width / 2;
287
- const centerY = height / 2;
288
-
289
274
  // Calculate the angle between each (use original data length for consistent spacing)
290
275
  const padAngle = gapScale * ( ( 2 * Math.PI ) / data.length );
291
276
 
292
- const outerRadius = radius - padding;
293
- const innerRadius = thickness === 0 ? 0 : outerRadius * ( 1 - thickness );
294
-
295
- const maxCornerRadius = ( outerRadius - innerRadius ) / 2;
296
- const cornerRadius = cornerScale ? Math.min( cornerScale * outerRadius, maxCornerRadius ) : 0;
297
-
298
277
  // Map the data to include index for color assignment
299
278
  // When interactive, we need to find the original index to maintain consistent colors
300
279
  const dataWithIndex = visibleData.map( d => {
@@ -306,36 +285,33 @@ const PieChartInternal = ( {
306
285
  } );
307
286
 
308
287
  const accessors = {
309
- value: ( d: DataPointPercentage ) => d.value,
310
- fill: ( d: DataPointPercentage & { index: number } ) => {
288
+ value: ( d: DataPointPercentageCalculated ) => d.value,
289
+ fill: ( d: DataPointPercentageCalculated & { index: number } ) => {
311
290
  return getElementStyles( { data: d, index: d.index } ).color;
312
291
  },
313
292
  };
314
293
 
315
294
  const legendElement = showLegend && (
316
295
  <Legend
317
- orientation={ legendOrientation }
296
+ orientation={ legend.orientation ?? 'horizontal' }
318
297
  position={ legendPosition }
319
- alignment={ legendAlignment }
320
- labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
321
- itemClassName={ legendItemClassName }
322
- shape={ legendShape }
298
+ alignment={ legend.alignment ?? 'center' }
299
+ labelStyles={ legend.labelStyles }
300
+ itemClassName={ legend.itemClassName }
301
+ itemStyles={ legend.itemStyles }
302
+ shapeStyles={ legend.shapeStyles }
303
+ shape={ legend.shape ?? 'circle' }
323
304
  chartId={ chartId }
324
305
  interactive={ legendInteractive }
325
306
  />
326
307
  );
327
308
 
328
309
  return (
329
- <SingleChartContext.Provider
330
- value={ {
331
- chartId,
332
- chartWidth: width,
333
- chartHeight: height,
334
- } }
335
- >
336
- <Stack
337
- ref={ containerRef }
338
- direction="column"
310
+ <SingleChartContext.Provider value={ { chartId } }>
311
+ <ChartLayout
312
+ legendPosition={ legendPosition }
313
+ legendElement={ legendElement }
314
+ legendChildren={ legendChildren }
339
315
  gap={ gap }
340
316
  className={ clsx(
341
317
  'pie-chart',
@@ -348,153 +324,177 @@ const PieChartInternal = ( {
348
324
  width: propWidth || undefined,
349
325
  height: propHeight || undefined,
350
326
  } }
327
+ trailingContent={
328
+ <>
329
+ { withTooltips && tooltipOpen && tooltipData && (
330
+ <TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
331
+ <div role="tooltip">{ renderTooltip( { tooltipData } ) }</div>
332
+ </TooltipInPortal>
333
+ ) }
334
+ { htmlChildren }
335
+ { otherChildren }
336
+ </>
337
+ }
351
338
  >
352
- { legendPosition === 'top' && legendElement }
353
-
354
- <div className={ styles[ 'pie-chart__svg-wrapper' ] } ref={ svgWrapperRef }>
355
- <svg
356
- viewBox={ `0 0 ${ width } ${ height }` }
357
- preserveAspectRatio="xMidYMid meet"
358
- width={ width }
359
- height={ height }
360
- >
361
- <defs>
362
- <RadialWipeAnimation
363
- id={ `radial-wipe-${ chartId }` }
364
- radius={ outerRadius }
365
- innerRadius={ innerRadius }
366
- />
367
- </defs>
368
-
369
- <Group
370
- top={ centerY }
371
- left={ centerX }
372
- mask={ animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null }
339
+ { ( { contentWidth, contentHeight } ) => {
340
+ const availableWidth = contentWidth > 0 ? contentWidth : 300;
341
+ const availableHeight = contentHeight > 0 ? contentHeight : 300;
342
+ const availableSize = Math.min( availableWidth, availableHeight );
343
+ const actualSize = size ? Math.min( size, availableSize ) : availableSize;
344
+
345
+ const width = actualSize;
346
+ const height = actualSize;
347
+
348
+ const radius = Math.min( width, height ) / 2;
349
+ const centerX = width / 2;
350
+ const centerY = height / 2;
351
+
352
+ const outerRadius = radius - padding;
353
+ const innerRadius = thickness === 0 ? 0 : outerRadius * ( 1 - thickness );
354
+
355
+ const maxCornerRadius = ( outerRadius - innerRadius ) / 2;
356
+ const cornerRadius = cornerScale
357
+ ? Math.min( cornerScale * outerRadius, maxCornerRadius )
358
+ : 0;
359
+
360
+ return (
361
+ <Stack
362
+ ref={ containerRef }
363
+ align="center"
364
+ justify="center"
365
+ className={ styles[ 'pie-chart__centering' ] }
373
366
  >
374
- { allSegmentsHidden ? (
375
- <text
376
- textAnchor="middle"
377
- dy=".33em"
378
- fill={ providerTheme.gridColor || '#ccc' }
379
- fontSize="14"
380
- fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
367
+ <svg
368
+ viewBox={ `0 0 ${ width } ${ height }` }
369
+ preserveAspectRatio="xMidYMid meet"
370
+ width={ width }
371
+ height={ height }
372
+ >
373
+ <defs>
374
+ <RadialWipeAnimation
375
+ id={ `radial-wipe-${ chartId }` }
376
+ radius={ outerRadius }
377
+ innerRadius={ innerRadius }
378
+ />
379
+ </defs>
380
+
381
+ <Group
382
+ top={ centerY }
383
+ left={ centerX }
384
+ mask={
385
+ animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null
386
+ }
381
387
  >
382
- { __(
383
- 'All segments are hidden. Click legend items to show data.',
384
- 'jetpack-charts'
385
- ) }
386
- </text>
387
- ) : (
388
- <Pie< DataPointPercentage & { index: number } >
389
- data={ dataWithIndex }
390
- pieValue={ accessors.value }
391
- outerRadius={ outerRadius }
392
- innerRadius={ innerRadius }
393
- padAngle={ padAngle }
394
- cornerRadius={ cornerRadius }
395
- >
396
- { pie => {
397
- return pie.arcs.map( ( arc, index ) => {
398
- const [ centroidX, centroidY ] = pie.path.centroid( arc );
399
- const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
400
- const handleMouseMove = ( event: MouseEvent< SVGElement > ) => {
401
- if ( ! withTooltips ) {
402
- return;
403
- }
404
-
405
- // Don't show tooltip until container bounds are measured
406
- if ( containerBounds.width === 0 || containerBounds.height === 0 ) {
407
- return;
408
- }
409
-
410
- // Use clientX/Y and subtract containerBounds to cancel out any stale offset.
411
- // TooltipInPortal calculates: tooltipLeft + containerBounds.left + scrollX
412
- // By passing (clientX - containerBounds.left), we get:
413
- // (clientX - containerBounds.left) + containerBounds.left + scrollX = clientX + scrollX
414
- // This gives correct page coordinates regardless of stale bounds.
415
- showTooltip( {
416
- tooltipData: arc.data,
417
- tooltipLeft: event.clientX - containerBounds.left + tooltipOffsetX,
418
- tooltipTop: event.clientY - containerBounds.top + tooltipOffsetY,
419
- } );
420
- };
421
-
422
- const pathProps: SVGProps< SVGPathElement > & { 'data-testid'?: string } = {
423
- d: pie.path( arc ) || '',
424
- fill: accessors.fill( arc.data ),
425
- 'data-testid': 'pie-segment',
426
- };
427
-
428
- const groupProps: SVGProps< SVGGElement > = {};
429
- if ( withTooltips ) {
430
- groupProps.onMouseMove = handleMouseMove;
431
- groupProps.onMouseLeave = onMouseLeave;
432
- }
433
-
434
- // Estimate text width more accurately for background sizing
435
- const fontSize = 12;
436
- const estimatedTextWidth = getStringWidth( arc.data.label, { fontSize } );
437
- const labelPadding = 6;
438
- const backgroundWidth = estimatedTextWidth + labelPadding * 2;
439
- const backgroundHeight = fontSize + labelPadding * 2;
440
-
441
- return (
442
- <g key={ `arc-${ index }` } { ...groupProps }>
443
- <path { ...pathProps } />
444
- { showLabels && hasSpaceForLabel && (
445
- <g>
446
- { providerTheme.labelBackgroundColor && (
447
- <rect
448
- x={ centroidX - backgroundWidth / 2 }
449
- y={ centroidY - backgroundHeight / 2 }
450
- width={ backgroundWidth }
451
- height={ backgroundHeight }
452
- fill={ providerTheme.labelBackgroundColor }
453
- rx={ 4 }
454
- ry={ 4 }
455
- pointerEvents="none"
456
- />
388
+ { allSegmentsHidden ? (
389
+ <SvgEmptyState x={ 0 } y={ 0 } width={ width } height={ height }>
390
+ { __(
391
+ 'All segments are hidden. Click legend items to show data.',
392
+ 'jetpack-charts'
393
+ ) }
394
+ </SvgEmptyState>
395
+ ) : (
396
+ <Pie< DataPointPercentageCalculated & { index: number } >
397
+ data={ dataWithIndex }
398
+ pieValue={ accessors.value }
399
+ outerRadius={ outerRadius }
400
+ innerRadius={ innerRadius }
401
+ padAngle={ padAngle }
402
+ cornerRadius={ cornerRadius }
403
+ >
404
+ { pie => {
405
+ return pie.arcs.map( ( arc, index ) => {
406
+ const [ centroidX, centroidY ] = pie.path.centroid( arc );
407
+ const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
408
+ const handleMouseMove = ( event: MouseEvent< SVGElement > ) => {
409
+ if ( ! withTooltips ) {
410
+ return;
411
+ }
412
+
413
+ // Don't show tooltip until container bounds are measured
414
+ if ( containerBounds.width === 0 || containerBounds.height === 0 ) {
415
+ return;
416
+ }
417
+
418
+ // Use clientX/Y and subtract containerBounds to cancel out any stale offset.
419
+ // TooltipInPortal calculates: tooltipLeft + containerBounds.left + scrollX
420
+ // By passing (clientX - containerBounds.left), we get:
421
+ // (clientX - containerBounds.left) + containerBounds.left + scrollX = clientX + scrollX
422
+ // This gives correct page coordinates regardless of stale bounds.
423
+ showTooltip( {
424
+ tooltipData: arc.data,
425
+ tooltipLeft: event.clientX - containerBounds.left + tooltipOffsetX,
426
+ tooltipTop: event.clientY - containerBounds.top + tooltipOffsetY,
427
+ } );
428
+ };
429
+
430
+ const pathProps: SVGProps< SVGPathElement > & {
431
+ 'data-testid'?: string;
432
+ } = {
433
+ d: pie.path( arc ) || '',
434
+ fill: accessors.fill( arc.data ),
435
+ 'data-testid': 'pie-segment',
436
+ };
437
+
438
+ const groupProps: SVGProps< SVGGElement > = {};
439
+ if ( withTooltips ) {
440
+ groupProps.onMouseMove = handleMouseMove;
441
+ groupProps.onMouseLeave = onMouseLeave;
442
+ }
443
+
444
+ // Estimate text width more accurately for background sizing
445
+ const fontSize = 12;
446
+ const estimatedTextWidth = getStringWidth( arc.data.label, {
447
+ fontSize,
448
+ } );
449
+ const labelPadding = 6;
450
+ const backgroundWidth = estimatedTextWidth + labelPadding * 2;
451
+ const backgroundHeight = fontSize + labelPadding * 2;
452
+
453
+ return (
454
+ <g key={ `arc-${ index }` } { ...groupProps }>
455
+ <path { ...pathProps } />
456
+ { showLabels && hasSpaceForLabel && (
457
+ <g>
458
+ { providerTheme.labelBackgroundColor && (
459
+ <rect
460
+ x={ centroidX - backgroundWidth / 2 }
461
+ y={ centroidY - backgroundHeight / 2 }
462
+ width={ backgroundWidth }
463
+ height={ backgroundHeight }
464
+ fill={ providerTheme.labelBackgroundColor }
465
+ rx={ 4 }
466
+ ry={ 4 }
467
+ pointerEvents="none"
468
+ />
469
+ ) }
470
+ <text
471
+ x={ centroidX }
472
+ y={ centroidY }
473
+ dy=".33em"
474
+ fill={ providerTheme.labelTextColor || '#333' }
475
+ fontSize={ fontSize }
476
+ textAnchor="middle"
477
+ pointerEvents="none"
478
+ >
479
+ { arc.data.label }
480
+ </text>
481
+ </g>
457
482
  ) }
458
- <text
459
- x={ centroidX }
460
- y={ centroidY }
461
- dy=".33em"
462
- fill={ providerTheme.labelTextColor || '#333' }
463
- fontSize={ fontSize }
464
- textAnchor="middle"
465
- pointerEvents="none"
466
- >
467
- { arc.data.label }
468
- </text>
469
483
  </g>
470
- ) }
471
- </g>
472
- );
473
- } );
474
- } }
475
- </Pie>
476
- ) }
477
-
478
- { /* Render SVG children (like Group, Text) inside the SVG */ }
479
- { ! allSegmentsHidden && svgChildren }
480
- </Group>
481
- </svg>
482
- </div>
483
-
484
- { legendPosition === 'bottom' && legendElement }
485
-
486
- { withTooltips && tooltipOpen && tooltipData && (
487
- <TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
488
- <div role="tooltip">{ renderTooltip( { tooltipData } ) }</div>
489
- </TooltipInPortal>
490
- ) }
491
-
492
- { /* Render HTML component children from PieChart.HTML */ }
493
- { htmlChildren }
484
+ );
485
+ } );
486
+ } }
487
+ </Pie>
488
+ ) }
494
489
 
495
- { /* Render other React children for backward compatibility */ }
496
- { otherChildren }
497
- </Stack>
490
+ { /* Render SVG children (like Group, Text) inside the SVG */ }
491
+ { ! allSegmentsHidden && svgChildren }
492
+ </Group>
493
+ </svg>
494
+ </Stack>
495
+ );
496
+ } }
497
+ </ChartLayout>
498
498
  </SingleChartContext.Provider>
499
499
  );
500
500
  };
@@ -1,15 +1,26 @@
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', () => {
7
8
  const mockData = [
8
- { label: 'A', value: 30, percentage: 30 },
9
- { label: 'B', value: 40, percentage: 40 },
10
- { label: 'C', value: 30, percentage: 30 },
9
+ { label: 'A', value: 30 },
10
+ { label: 'B', value: 40 },
11
+ { label: 'C', value: 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
  } );