@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
@@ -8,7 +8,11 @@ import clsx from 'clsx';
8
8
  import { useCallback, useContext, useMemo } from 'react';
9
9
  import { Legend, useChartLegendItems } from '../../components/legend';
10
10
  import { BaseTooltip } from '../../components/tooltip';
11
- import { useElementSize, useInteractiveLegendData, usePrefersReducedMotion } from '../../hooks';
11
+ import {
12
+ useDataWithPercentages,
13
+ useInteractiveLegendData,
14
+ usePrefersReducedMotion,
15
+ } from '../../hooks';
12
16
  import {
13
17
  GlobalChartsProvider,
14
18
  useChartId,
@@ -18,12 +22,19 @@ import {
18
22
  } from '../../providers';
19
23
  import { attachSubComponents } from '../../utils';
20
24
  import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
25
+ import { ChartLayout } from '../private/chart-layout';
21
26
  import { RadialWipeAnimation } from '../private/radial-wipe-animation';
22
27
  import { SingleChartContext } from '../private/single-chart-context';
28
+ import { SvgEmptyState } from '../private/svg-empty-state';
23
29
  import { withResponsive } from '../private/with-responsive';
24
30
  import styles from './pie-semi-circle-chart.module.scss';
25
31
  import type { LegendValueDisplay } from '../../components/legend';
26
- import type { BaseChartProps, DataPointPercentage, Optional } from '../../types';
32
+ import type {
33
+ BaseChartProps,
34
+ DataPointPercentage,
35
+ DataPointPercentageCalculated,
36
+ Optional,
37
+ } from '../../types';
27
38
  import type { ChartComponentWithComposition } from '../private/chart-composition';
28
39
  import type { ResponsiveConfig } from '../private/with-responsive';
29
40
  import type { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
@@ -34,9 +45,9 @@ import type { FC, MouseEvent, ReactNode } from 'react';
34
45
  */
35
46
  export type PieSemiCircleChartRenderTooltipParams = {
36
47
  /**
37
- * The data point being hovered, including label, value, and percentage.
48
+ * The data point being hovered, including label, value, and calculated percentage.
38
49
  */
39
- tooltipData: DataPointPercentage;
50
+ tooltipData: DataPointPercentageCalculated;
40
51
  };
41
52
 
42
53
  /**
@@ -98,13 +109,6 @@ export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercen
98
109
  */
99
110
  legendValueDisplay?: LegendValueDisplay;
100
111
 
101
- /**
102
- * Enable interactive legend items that can toggle segment visibility.
103
- * Requires chartId and GlobalChartsProvider.
104
- * When segments are hidden, percentages are recalculated so visible segments total 100%.
105
- */
106
- legendInteractive?: boolean;
107
-
108
112
  /**
109
113
  * Horizontal offset for tooltip positioning in pixels (default: 0)
110
114
  */
@@ -131,7 +135,7 @@ type PieSemiCircleChartResponsiveComponent = ChartComponentWithComposition<
131
135
  PieSemiCircleChartBaseProps & ResponsiveConfig
132
136
  >;
133
137
 
134
- export type ArcData = PieArcDatum< DataPointPercentage >;
138
+ export type ArcData = PieArcDatum< DataPointPercentageCalculated >;
135
139
 
136
140
  /**
137
141
  * Validates the semi-circle pie chart data
@@ -144,15 +148,15 @@ const validateData = ( data: DataPointPercentage[] ) => {
144
148
  }
145
149
 
146
150
  // Check for negative values
147
- const hasNegativeValues = data.some( item => item.percentage < 0 || item.value < 0 );
151
+ const hasNegativeValues = data.some( item => item.value < 0 );
148
152
  if ( hasNegativeValues ) {
149
153
  return { isValid: false, message: 'Invalid data: Negative values are not allowed' };
150
154
  }
151
155
 
152
- // Validate total percentage is greater than 0
153
- const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 );
154
- if ( totalPercentage <= 0 ) {
155
- return { isValid: false, message: 'Invalid percentage total: Must be greater than 0' };
156
+ // Validate total value is greater than 0
157
+ const totalValue = data.reduce( ( sum, item ) => sum + item.value, 0 );
158
+ if ( totalValue <= 0 ) {
159
+ return { isValid: false, message: 'Invalid data: Total value must be greater than 0' };
156
160
  }
157
161
 
158
162
  return { isValid: true, message: '' };
@@ -167,15 +171,8 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
167
171
  clockwise = true,
168
172
  withTooltips = false,
169
173
  showLegend = false,
170
- legendOrientation = 'horizontal',
171
- legendPosition = 'bottom',
172
- legendAlignment = 'center',
173
- legendMaxWidth,
174
- legendTextOverflow = 'wrap',
175
- legendItemClassName,
176
- legendShape = 'circle',
174
+ legend = {},
177
175
  legendValueDisplay = 'percentage',
178
- legendInteractive = false,
179
176
  label,
180
177
  animation,
181
178
  note,
@@ -186,11 +183,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
186
183
  renderTooltip = renderDefaultPieSemiCircleTooltip,
187
184
  gap = 'md',
188
185
  } ) => {
186
+ const legendInteractive = legend.interactive ?? false;
187
+ const legendPosition = legend.position ?? 'bottom';
188
+
189
189
  const chartId = useChartId( providedChartId );
190
- // Measure the SVG wrapper to calculate constrained dimensions
191
- const [ svgWrapperRef, svgWrapperWidth, svgWrapperHeight ] = useElementSize< HTMLDivElement >();
192
190
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
193
- useTooltip< DataPointPercentage >();
191
+ useTooltip< DataPointPercentageCalculated >();
194
192
 
195
193
  // Set up portal tooltip for better z-index handling
196
194
  // We get containerBounds to cancel out stale offsets in the position calculation
@@ -245,9 +243,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
245
243
 
246
244
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
247
245
 
246
+ // Calculate percentages from values (single source of truth)
247
+ const dataWithPercentages = useDataWithPercentages( data );
248
+
248
249
  // Filter and recalculate data for interactive legends
249
250
  const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
250
- data,
251
+ data: dataWithPercentages,
251
252
  chartId,
252
253
  legendInteractive,
253
254
  isSeriesVisible,
@@ -256,12 +257,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
256
257
  // Define accessors with useMemo to avoid changing dependencies
257
258
  const accessors = useMemo(
258
259
  () => ( {
259
- value: ( d: DataPointPercentage ) => d.value,
260
+ value: ( d: DataPointPercentageCalculated ) => d.value,
260
261
  sort: (
261
- a: DataPointPercentage & { index: number },
262
- b: DataPointPercentage & { index: number }
262
+ a: DataPointPercentageCalculated & { index: number },
263
+ b: DataPointPercentageCalculated & { index: number }
263
264
  ) => b.value - a.value,
264
- fill: ( d: DataPointPercentage & { index: number } ) =>
265
+ fill: ( d: DataPointPercentageCalculated & { index: number } ) =>
265
266
  getElementStyles( { data: d, index: d.index } ).color,
266
267
  } ),
267
268
  [ getElementStyles ]
@@ -277,7 +278,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
277
278
  const legendItems = useChartLegendItems( legendData, legendOptions );
278
279
 
279
280
  // Process children to extract compound components
280
- const { svgChildren, htmlChildren, otherChildren } = useChartChildren(
281
+ const { svgChildren, htmlChildren, legendChildren, otherChildren } = useChartChildren(
281
282
  children,
282
283
  'PieSemiCircleChart'
283
284
  );
@@ -321,18 +322,6 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
321
322
  );
322
323
  }
323
324
 
324
- // Calculate chart dimensions maintaining the 2:1 width-to-height ratio.
325
- // Use measured SVG wrapper dimensions to respect height constraints, falling back
326
- // to explicit props during initial render before measurement is available.
327
- const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : effectiveWidth;
328
- const availableHeight =
329
- svgWrapperHeight > 0 ? svgWrapperHeight : propHeight || effectiveWidth / 2;
330
- // Constrain width so that height (= width / 2) never exceeds the available height
331
- const width = Math.min( availableWidth, availableHeight * 2 );
332
- const height = width / 2;
333
- const radius = height; // For a semi-circle, radius equals the SVG height
334
- const innerRadius = radius * ( 1 - thickness );
335
-
336
325
  // Map data with index for color assignment
337
326
  // When interactive, we need to find the original index to maintain consistent colors
338
327
  const dataWithIndex = visibleData.map( d => {
@@ -349,28 +338,25 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
349
338
 
350
339
  const legendElement = showLegend && (
351
340
  <Legend
352
- orientation={ legendOrientation }
341
+ orientation={ legend.orientation ?? 'horizontal' }
353
342
  position={ legendPosition }
354
- alignment={ legendAlignment }
355
- labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
356
- itemClassName={ legendItemClassName }
357
- shape={ legendShape }
343
+ alignment={ legend.alignment ?? 'center' }
344
+ labelStyles={ legend.labelStyles }
345
+ itemClassName={ legend.itemClassName }
346
+ itemStyles={ legend.itemStyles }
347
+ shapeStyles={ legend.shapeStyles }
348
+ shape={ legend.shape ?? 'circle' }
358
349
  chartId={ chartId }
359
350
  interactive={ legendInteractive }
360
351
  />
361
352
  );
362
353
 
363
354
  return (
364
- <SingleChartContext.Provider
365
- value={ {
366
- chartId,
367
- chartWidth: width,
368
- chartHeight: height,
369
- } }
370
- >
371
- <Stack
372
- ref={ containerRef }
373
- direction="column"
355
+ <SingleChartContext.Provider value={ { chartId } }>
356
+ <ChartLayout
357
+ legendPosition={ legendPosition }
358
+ legendElement={ legendElement }
359
+ legendChildren={ legendChildren }
374
360
  gap={ gap }
375
361
  className={ clsx(
376
362
  'pie-semi-circle-chart',
@@ -385,118 +371,130 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
385
371
  height: propHeight || undefined,
386
372
  } }
387
373
  data-testid="pie-chart-container"
374
+ trailingContent={
375
+ <>
376
+ { withTooltips && tooltipOpen && tooltipData && (
377
+ <TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
378
+ <div role="tooltip">{ renderTooltip( { tooltipData } ) }</div>
379
+ </TooltipInPortal>
380
+ ) }
381
+ { htmlChildren }
382
+ { otherChildren }
383
+ </>
384
+ }
388
385
  >
389
- { legendPosition === 'top' && legendElement }
390
-
391
- <div ref={ svgWrapperRef } className={ styles[ 'pie-semi-circle-chart__svg-wrapper' ] }>
392
- <svg
393
- width={ width }
394
- height={ height }
395
- viewBox={ `0 0 ${ width } ${ height }` }
396
- data-testid="pie-chart-svg"
397
- >
398
- <defs>
399
- <RadialWipeAnimation
400
- id={ `radial-wipe-${ chartId }` }
401
- radius={ radius }
402
- innerRadius={ innerRadius }
403
- startAngle="-180deg"
404
- wipePercentage={ 50 }
405
- />
406
- </defs>
407
-
408
- { /* Main chart group centered horizontally and positioned at bottom */ }
409
- <Group
410
- top={ height }
411
- left={ width / 2 }
412
- mask={ animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null }
386
+ { ( { contentWidth, contentHeight } ) => {
387
+ // Calculate chart dimensions maintaining the 2:1 width-to-height ratio.
388
+ // Use measured dimensions to respect height constraints, falling back
389
+ // to explicit props during initial render before measurement is available.
390
+ const availableWidth = contentWidth > 0 ? contentWidth : effectiveWidth;
391
+ const availableHeight =
392
+ contentHeight > 0 ? contentHeight : propHeight || effectiveWidth / 2;
393
+ // Constrain width so that height (= width / 2) never exceeds the available height
394
+ const width = Math.min( availableWidth, availableHeight * 2 );
395
+ const height = width / 2;
396
+ const radius = height; // For a semi-circle, radius equals the SVG height
397
+ const innerRadius = radius * ( 1 - thickness );
398
+
399
+ return (
400
+ <Stack
401
+ ref={ containerRef }
402
+ align="center"
403
+ justify="center"
404
+ className={ styles[ 'pie-semi-circle-chart__centering' ] }
413
405
  >
414
- { allSegmentsHidden ? (
415
- <text
416
- textAnchor="middle"
417
- y={ -radius / 2 }
418
- fill="#ccc"
419
- fontSize="14"
420
- fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
421
- >
422
- { __(
423
- 'All segments are hidden. Click legend items to show data.',
424
- 'jetpack-charts'
425
- ) }
426
- </text>
427
- ) : (
428
- <>
429
- { /* Pie chart */ }
430
- <Pie< DataPointPercentage & { index: number } >
431
- data={ dataWithIndex }
432
- pieValue={ accessors.value }
433
- outerRadius={ radius }
406
+ <svg
407
+ width={ width }
408
+ height={ height }
409
+ viewBox={ `0 0 ${ width } ${ height }` }
410
+ data-testid="pie-chart-svg"
411
+ >
412
+ <defs>
413
+ <RadialWipeAnimation
414
+ id={ `radial-wipe-${ chartId }` }
415
+ radius={ radius }
434
416
  innerRadius={ innerRadius }
435
- cornerRadius={ 3 }
436
- padAngle={ PAD_ANGLE }
437
- startAngle={ startAngle }
438
- endAngle={ endAngle }
439
- pieSort={ accessors.sort }
440
- >
441
- { pie => {
442
- return pie.arcs.map( arc => (
443
- <g
444
- key={ arc.data.label }
445
- onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
446
- onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
417
+ startAngle="-180deg"
418
+ wipePercentage={ 50 }
419
+ />
420
+ </defs>
421
+
422
+ { /* Main chart group centered horizontally and positioned at bottom */ }
423
+ <Group
424
+ top={ height }
425
+ left={ width / 2 }
426
+ mask={
427
+ animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null
428
+ }
429
+ >
430
+ { allSegmentsHidden ? (
431
+ <SvgEmptyState x={ 0 } y={ -radius / 2 } width={ width } height={ height }>
432
+ { __(
433
+ 'All segments are hidden. Click legend items to show data.',
434
+ 'jetpack-charts'
435
+ ) }
436
+ </SvgEmptyState>
437
+ ) : (
438
+ <>
439
+ { /* Pie chart */ }
440
+ <Pie< DataPointPercentageCalculated & { index: number } >
441
+ data={ dataWithIndex }
442
+ pieValue={ accessors.value }
443
+ outerRadius={ radius }
444
+ innerRadius={ innerRadius }
445
+ cornerRadius={ 3 }
446
+ padAngle={ PAD_ANGLE }
447
+ startAngle={ startAngle }
448
+ endAngle={ endAngle }
449
+ pieSort={ accessors.sort }
450
+ >
451
+ { pie => {
452
+ return pie.arcs.map( arc => (
453
+ <g
454
+ key={ arc.data.label }
455
+ onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
456
+ onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
457
+ >
458
+ <path
459
+ d={ pie.path( arc ) || '' }
460
+ fill={ accessors.fill( arc.data ) }
461
+ data-testid="pie-segment"
462
+ />
463
+ </g>
464
+ ) );
465
+ } }
466
+ </Pie>
467
+
468
+ { /* Label and note text */ }
469
+ <Group>
470
+ <Text
471
+ textAnchor="middle"
472
+ verticalAnchor="start"
473
+ y={ -40 } // Position above the chart with space for note
474
+ className={ styles.label }
447
475
  >
448
- <path
449
- d={ pie.path( arc ) || '' }
450
- fill={ accessors.fill( arc.data ) }
451
- data-testid="pie-segment"
452
- />
453
- </g>
454
- ) );
455
- } }
456
- </Pie>
457
-
458
- { /* Label and note text */ }
459
- <Group>
460
- <Text
461
- textAnchor="middle"
462
- verticalAnchor="start"
463
- y={ -40 } // Position above the chart with space for note
464
- className={ styles.label }
465
- >
466
- { label }
467
- </Text>
468
- <Text
469
- textAnchor="middle"
470
- verticalAnchor="start"
471
- y={ -20 } // Position between label and chart
472
- className={ styles.note }
473
- >
474
- { note }
475
- </Text>
476
- </Group>
477
-
478
- { /* Render SVG children from composition API */ }
479
- { ! allSegmentsHidden && svgChildren }
480
- </>
481
- ) }
482
- </Group>
483
- </svg>
484
- </div>
485
-
486
- { legendPosition !== 'top' && legendElement }
487
-
488
- { withTooltips && tooltipOpen && tooltipData && (
489
- <TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
490
- <div role="tooltip">{ renderTooltip( { tooltipData } ) }</div>
491
- </TooltipInPortal>
492
- ) }
493
-
494
- { /* Render HTML children from composition API */ }
495
- { htmlChildren }
476
+ { label }
477
+ </Text>
478
+ <Text
479
+ textAnchor="middle"
480
+ verticalAnchor="start"
481
+ y={ -20 } // Position between label and chart
482
+ className={ styles.note }
483
+ >
484
+ { note }
485
+ </Text>
486
+ </Group>
496
487
 
497
- { /* Render any other children that aren't compound components */ }
498
- { otherChildren }
499
- </Stack>
488
+ { /* Render SVG children from composition API */ }
489
+ { ! allSegmentsHidden && svgChildren }
490
+ </>
491
+ ) }
492
+ </Group>
493
+ </svg>
494
+ </Stack>
495
+ );
496
+ } }
497
+ </ChartLayout>
500
498
  </SingleChartContext.Provider>
501
499
  );
502
500
  };
@@ -18,21 +18,19 @@ const mockData = [
18
18
  label: 'Category A',
19
19
  value: 30,
20
20
  valueDisplay: '30%',
21
- percentage: 30,
22
21
  },
23
22
  {
24
23
  label: 'Category B',
25
24
  value: 70,
26
25
  valueDisplay: '70%',
27
- percentage: 70,
28
26
  },
29
27
  ];
30
28
 
31
29
  // Helper function to render component with providers
32
- const renderPieChart = props =>
30
+ const renderPieChart = ( props, children = undefined ) =>
33
31
  render(
34
32
  <GlobalChartsProvider>
35
- <PieSemiCircleChart { ...props } />
33
+ <PieSemiCircleChart { ...props }>{ children }</PieSemiCircleChart>
36
34
  </GlobalChartsProvider>
37
35
  );
38
36
 
@@ -64,9 +62,9 @@ describe( 'PieSemiCircleChart', () => {
64
62
  it( 'shows tooltip on segment hover when withTooltips is true', async () => {
65
63
  const user = userEvent.setup();
66
64
  const testData = [
67
- { label: 'MacOS', value: 30000, valueDisplay: '30K', percentage: 5 },
68
- { label: 'Linux', value: 22000, valueDisplay: '22K', percentage: 1 },
69
- { label: 'Windows', value: 80000, valueDisplay: '80K', percentage: 2 },
65
+ { label: 'MacOS', value: 30000, valueDisplay: '30K' },
66
+ { label: 'Linux', value: 22000, valueDisplay: '22K' },
67
+ { label: 'Windows', value: 80000, valueDisplay: '80K' },
70
68
  ];
71
69
 
72
70
  renderPieChart( { data: testData, withTooltips: true, width: 400 } );
@@ -86,9 +84,9 @@ describe( 'PieSemiCircleChart', () => {
86
84
  it( 'hides tooltip on mouse leave', async () => {
87
85
  const user = userEvent.setup();
88
86
  const testData = [
89
- { label: 'MacOS', value: 30000, valueDisplay: '30K', percentage: 5 },
90
- { label: 'Linux', value: 22000, valueDisplay: '22K', percentage: 1 },
91
- { label: 'Windows', value: 80000, valueDisplay: '80K', percentage: 2 },
87
+ { label: 'MacOS', value: 30000, valueDisplay: '30K' },
88
+ { label: 'Linux', value: 22000, valueDisplay: '22K' },
89
+ { label: 'Windows', value: 80000, valueDisplay: '80K' },
92
90
  ];
93
91
 
94
92
  renderPieChart( { data: testData, withTooltips: true, width: 400 } );
@@ -113,9 +111,9 @@ describe( 'PieSemiCircleChart', () => {
113
111
  it( 'renders custom tooltip when renderTooltip prop is provided', async () => {
114
112
  const user = userEvent.setup();
115
113
  const testData = [
116
- { label: 'MacOS', value: 30000, valueDisplay: '30K', percentage: 5 },
117
- { label: 'Linux', value: 22000, valueDisplay: '22K', percentage: 1 },
118
- { label: 'Windows', value: 80000, valueDisplay: '80K', percentage: 2 },
114
+ { label: 'MacOS', value: 30000, valueDisplay: '30K' },
115
+ { label: 'Linux', value: 22000, valueDisplay: '22K' },
116
+ { label: 'Windows', value: 80000, valueDisplay: '80K' },
119
117
  ];
120
118
 
121
119
  const customTooltipRenderer = jest.fn( ( { tooltipData } ) => (
@@ -148,10 +146,12 @@ describe( 'PieSemiCircleChart', () => {
148
146
  tooltipData: expect.objectContaining( {
149
147
  label: 'MacOS',
150
148
  value: 30000,
151
- percentage: 5,
152
149
  } ),
153
150
  } )
154
151
  );
152
+ // Verify percentage is calculated (approximately 22.73%)
153
+ const callArgs = customTooltipRenderer.mock.calls[ 0 ][ 0 ];
154
+ expect( callArgs.tooltipData.percentage ).toBeCloseTo( 22.73, 1 );
155
155
  } );
156
156
 
157
157
  it( 'applies custom className', () => {
@@ -193,21 +193,21 @@ describe( 'PieSemiCircleChart', () => {
193
193
  expect( screen.getByText( 'No data available' ) ).toBeInTheDocument();
194
194
  } );
195
195
 
196
- test( 'handles zero total percentage', () => {
196
+ test( 'handles zero total value', () => {
197
197
  renderPieChart( {
198
198
  data: [
199
- { label: 'A', value: 0, percentage: 0 },
200
- { label: 'B', value: 0, percentage: 0 },
199
+ { label: 'A', value: 0 },
200
+ { label: 'B', value: 0 },
201
201
  ],
202
202
  } );
203
203
  expect(
204
- screen.getByText( 'Invalid percentage total: Must be greater than 0' )
204
+ screen.getByText( 'Invalid data: Total value must be greater than 0' )
205
205
  ).toBeInTheDocument();
206
206
  } );
207
207
 
208
208
  test( 'handles single data point', () => {
209
209
  renderPieChart( {
210
- data: [ { label: 'Single', value: 100, percentage: 50 } ],
210
+ data: [ { label: 'Single', value: 100 } ],
211
211
  } );
212
212
  expect( screen.getByTestId( 'pie-segment' ) ).toBeInTheDocument();
213
213
  } );
@@ -215,8 +215,8 @@ describe( 'PieSemiCircleChart', () => {
215
215
  test( 'handles negative values', () => {
216
216
  renderPieChart( {
217
217
  data: [
218
- { label: 'A', value: -30, percentage: -30 },
219
- { label: 'B', value: 130, percentage: 130 },
218
+ { label: 'A', value: -30 },
219
+ { label: 'B', value: 130 },
220
220
  ],
221
221
  } );
222
222
  expect(
@@ -256,18 +256,46 @@ describe( 'PieSemiCircleChart', () => {
256
256
  } );
257
257
  } );
258
258
 
259
+ describe( 'Composition Legend', () => {
260
+ test( 'renders composition legend as child component', () => {
261
+ renderPieChart( { data: mockData }, <PieSemiCircleChart.Legend /> );
262
+
263
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
264
+ expect( screen.getByText( 'Category A' ) ).toBeInTheDocument();
265
+ expect( screen.getByText( 'Category B' ) ).toBeInTheDocument();
266
+ } );
267
+
268
+ test( 'renders composition legend regardless of showLegend value', () => {
269
+ renderPieChart( { data: mockData, showLegend: false }, <PieSemiCircleChart.Legend /> );
270
+
271
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
272
+ } );
273
+
274
+ test( 'renders composition legend in top position', () => {
275
+ renderPieChart( { data: mockData }, <PieSemiCircleChart.Legend position="top" /> );
276
+
277
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
278
+
279
+ // Legend should appear before the chart SVG in DOM order
280
+ const html = document.body.innerHTML;
281
+ expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
282
+ html.indexOf( 'data-testid="pie-chart-svg"' )
283
+ );
284
+ } );
285
+ } );
286
+
259
287
  describe( 'Interactive Legend', () => {
260
288
  test( 'filters segments when interactive legend is enabled and segment is toggled', async () => {
261
289
  const user = userEvent.setup();
262
290
  const testData = [
263
- { label: 'Segment A', value: 50, percentage: 50 },
264
- { label: 'Segment B', value: 50, percentage: 50 },
291
+ { label: 'Segment A', value: 50 },
292
+ { label: 'Segment B', value: 50 },
265
293
  ];
266
294
 
267
295
  renderPieChart( {
268
296
  data: testData,
269
297
  showLegend: true,
270
- legendInteractive: true,
298
+ legend: { interactive: true },
271
299
  chartId: 'test-interactive-semi-circle-chart',
272
300
  } );
273
301
 
@@ -292,14 +320,14 @@ describe( 'PieSemiCircleChart', () => {
292
320
  test( 'shows empty state when all segments are hidden', async () => {
293
321
  const user = userEvent.setup();
294
322
  const testData = [
295
- { label: 'Segment A', value: 50, percentage: 50 },
296
- { label: 'Segment B', value: 50, percentage: 50 },
323
+ { label: 'Segment A', value: 50 },
324
+ { label: 'Segment B', value: 50 },
297
325
  ];
298
326
 
299
327
  renderPieChart( {
300
328
  data: testData,
301
329
  showLegend: true,
302
- legendInteractive: true,
330
+ legend: { interactive: true },
303
331
  chartId: 'test-all-hidden-semi-circle-chart',
304
332
  } );
305
333
 
@@ -319,14 +347,14 @@ describe( 'PieSemiCircleChart', () => {
319
347
 
320
348
  test( 'does not filter segments when legendInteractive is false', () => {
321
349
  const testData = [
322
- { label: 'Segment A', value: 50, percentage: 50 },
323
- { label: 'Segment B', value: 50, percentage: 50 },
350
+ { label: 'Segment A', value: 50 },
351
+ { label: 'Segment B', value: 50 },
324
352
  ];
325
353
 
326
354
  renderPieChart( {
327
355
  data: testData,
328
356
  showLegend: true,
329
- legendInteractive: false,
357
+ legend: { interactive: false },
330
358
  chartId: 'test-non-interactive-semi-circle-chart',
331
359
  } );
332
360
 
@@ -1,4 +1,6 @@
1
1
  export { ChartSVG } from './chart-svg';
2
2
  export { ChartHTML } from './chart-html';
3
+ export { renderLegendSlot } from './render-legend-slot';
3
4
  export { useChartChildren } from './use-chart-children';
5
+ export type { LegendChild } from './use-chart-children';
4
6
  export type { BaseChartSubComponents, ChartComponentWithComposition } from './types';