@automattic/charts 0.58.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 (253) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +7 -54
  3. package/dist/index.cjs +9606 -22
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +20 -25
  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 -56
  10. package/dist/index.js.map +1 -1
  11. package/package.json +8 -125
  12. package/src/charts/bar-chart/bar-chart.module.scss +0 -5
  13. package/src/charts/bar-chart/bar-chart.tsx +131 -137
  14. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +23 -40
  15. package/src/charts/line-chart/line-chart.module.scss +0 -5
  16. package/src/charts/line-chart/line-chart.tsx +190 -183
  17. package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +1 -2
  18. package/src/charts/pie-chart/pie-chart.module.scss +2 -10
  19. package/src/charts/pie-chart/pie-chart.tsx +198 -199
  20. package/src/charts/pie-chart/test/composition-api.test.tsx +3 -3
  21. package/src/charts/pie-chart/test/pie-chart.test.tsx +42 -35
  22. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -8
  23. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +155 -155
  24. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +25 -25
  25. package/src/charts/private/chart-layout/chart-layout.module.scss +7 -0
  26. package/src/charts/private/chart-layout/chart-layout.tsx +106 -0
  27. package/src/charts/private/chart-layout/index.ts +2 -0
  28. package/src/charts/private/chart-layout/test/chart-layout.test.tsx +167 -0
  29. package/src/charts/private/single-chart-context/single-chart-context.tsx +2 -2
  30. package/src/charts/private/svg-empty-state/index.ts +1 -0
  31. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +7 -0
  32. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +40 -0
  33. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +12 -8
  34. package/src/components/legend/hooks/use-chart-legend-items.ts +12 -13
  35. package/src/components/legend/legend.tsx +33 -8
  36. package/src/components/legend/test/legend.test.tsx +93 -1
  37. package/src/hooks/index.ts +1 -0
  38. package/src/hooks/use-data-with-percentages.ts +24 -0
  39. package/src/hooks/use-interactive-legend-data.ts +18 -15
  40. package/src/index.ts +65 -2
  41. package/src/providers/chart-context/global-charts-provider.tsx +7 -1
  42. package/src/providers/chart-context/hooks/use-chart-registration.ts +2 -1
  43. package/src/providers/chart-context/types.ts +2 -2
  44. package/src/types.ts +27 -7
  45. package/src/utils/test/resolve-css-var.test.ts +2 -0
  46. package/dist/base-tooltip-DOq93wjU.d.cts +0 -38
  47. package/dist/base-tooltip-DOq93wjU.d.ts +0 -38
  48. package/dist/charts/bar-chart/index.cjs +0 -17
  49. package/dist/charts/bar-chart/index.cjs.map +0 -1
  50. package/dist/charts/bar-chart/index.css +0 -141
  51. package/dist/charts/bar-chart/index.css.map +0 -1
  52. package/dist/charts/bar-chart/index.d.cts +0 -36
  53. package/dist/charts/bar-chart/index.d.ts +0 -36
  54. package/dist/charts/bar-chart/index.js +0 -17
  55. package/dist/charts/bar-chart/index.js.map +0 -1
  56. package/dist/charts/bar-list-chart/index.cjs +0 -18
  57. package/dist/charts/bar-list-chart/index.cjs.map +0 -1
  58. package/dist/charts/bar-list-chart/index.css +0 -141
  59. package/dist/charts/bar-list-chart/index.css.map +0 -1
  60. package/dist/charts/bar-list-chart/index.d.cts +0 -92
  61. package/dist/charts/bar-list-chart/index.d.ts +0 -92
  62. package/dist/charts/bar-list-chart/index.js +0 -18
  63. package/dist/charts/bar-list-chart/index.js.map +0 -1
  64. package/dist/charts/conversion-funnel-chart/index.cjs +0 -11
  65. package/dist/charts/conversion-funnel-chart/index.cjs.map +0 -1
  66. package/dist/charts/conversion-funnel-chart/index.css +0 -157
  67. package/dist/charts/conversion-funnel-chart/index.css.map +0 -1
  68. package/dist/charts/conversion-funnel-chart/index.d.cts +0 -97
  69. package/dist/charts/conversion-funnel-chart/index.d.ts +0 -97
  70. package/dist/charts/conversion-funnel-chart/index.js +0 -11
  71. package/dist/charts/conversion-funnel-chart/index.js.map +0 -1
  72. package/dist/charts/geo-chart/index.cjs +0 -13
  73. package/dist/charts/geo-chart/index.cjs.map +0 -1
  74. package/dist/charts/geo-chart/index.css +0 -23
  75. package/dist/charts/geo-chart/index.css.map +0 -1
  76. package/dist/charts/geo-chart/index.d.cts +0 -67
  77. package/dist/charts/geo-chart/index.d.ts +0 -67
  78. package/dist/charts/geo-chart/index.js +0 -13
  79. package/dist/charts/geo-chart/index.js.map +0 -1
  80. package/dist/charts/leaderboard-chart/index.cjs +0 -21
  81. package/dist/charts/leaderboard-chart/index.cjs.map +0 -1
  82. package/dist/charts/leaderboard-chart/index.css +0 -160
  83. package/dist/charts/leaderboard-chart/index.css.map +0 -1
  84. package/dist/charts/leaderboard-chart/index.d.cts +0 -46
  85. package/dist/charts/leaderboard-chart/index.d.ts +0 -46
  86. package/dist/charts/leaderboard-chart/index.js +0 -21
  87. package/dist/charts/leaderboard-chart/index.js.map +0 -1
  88. package/dist/charts/line-chart/index.cjs +0 -17
  89. package/dist/charts/line-chart/index.cjs.map +0 -1
  90. package/dist/charts/line-chart/index.css +0 -213
  91. package/dist/charts/line-chart/index.css.map +0 -1
  92. package/dist/charts/line-chart/index.d.cts +0 -98
  93. package/dist/charts/line-chart/index.d.ts +0 -98
  94. package/dist/charts/line-chart/index.js +0 -17
  95. package/dist/charts/line-chart/index.js.map +0 -1
  96. package/dist/charts/pie-chart/index.cjs +0 -19
  97. package/dist/charts/pie-chart/index.cjs.map +0 -1
  98. package/dist/charts/pie-chart/index.css +0 -131
  99. package/dist/charts/pie-chart/index.css.map +0 -1
  100. package/dist/charts/pie-chart/index.d.cts +0 -91
  101. package/dist/charts/pie-chart/index.d.ts +0 -91
  102. package/dist/charts/pie-chart/index.js +0 -19
  103. package/dist/charts/pie-chart/index.js.map +0 -1
  104. package/dist/charts/pie-semi-circle-chart/index.cjs +0 -18
  105. package/dist/charts/pie-semi-circle-chart/index.cjs.map +0 -1
  106. package/dist/charts/pie-semi-circle-chart/index.css +0 -132
  107. package/dist/charts/pie-semi-circle-chart/index.css.map +0 -1
  108. package/dist/charts/pie-semi-circle-chart/index.d.cts +0 -88
  109. package/dist/charts/pie-semi-circle-chart/index.d.ts +0 -88
  110. package/dist/charts/pie-semi-circle-chart/index.js +0 -18
  111. package/dist/charts/pie-semi-circle-chart/index.js.map +0 -1
  112. package/dist/charts/sparkline/index.cjs +0 -18
  113. package/dist/charts/sparkline/index.cjs.map +0 -1
  114. package/dist/charts/sparkline/index.css +0 -230
  115. package/dist/charts/sparkline/index.css.map +0 -1
  116. package/dist/charts/sparkline/index.d.cts +0 -113
  117. package/dist/charts/sparkline/index.d.ts +0 -113
  118. package/dist/charts/sparkline/index.js +0 -18
  119. package/dist/charts/sparkline/index.js.map +0 -1
  120. package/dist/chunk-2A34OA5O.cjs +0 -51
  121. package/dist/chunk-2A34OA5O.cjs.map +0 -1
  122. package/dist/chunk-2I67QUIV.js +0 -895
  123. package/dist/chunk-2I67QUIV.js.map +0 -1
  124. package/dist/chunk-2ICEEQOC.js +0 -1071
  125. package/dist/chunk-2ICEEQOC.js.map +0 -1
  126. package/dist/chunk-4B7BL2DD.js +0 -120
  127. package/dist/chunk-4B7BL2DD.js.map +0 -1
  128. package/dist/chunk-4OXMTKAL.js +0 -401
  129. package/dist/chunk-4OXMTKAL.js.map +0 -1
  130. package/dist/chunk-ASLARV7L.cjs +0 -81
  131. package/dist/chunk-ASLARV7L.cjs.map +0 -1
  132. package/dist/chunk-B6NLZFRW.js +0 -617
  133. package/dist/chunk-B6NLZFRW.js.map +0 -1
  134. package/dist/chunk-BBAUQOW6.cjs +0 -120
  135. package/dist/chunk-BBAUQOW6.cjs.map +0 -1
  136. package/dist/chunk-BPYKWMI7.js +0 -194
  137. package/dist/chunk-BPYKWMI7.js.map +0 -1
  138. package/dist/chunk-CMMHCTBX.cjs +0 -373
  139. package/dist/chunk-CMMHCTBX.cjs.map +0 -1
  140. package/dist/chunk-CPPXJATQ.cjs +0 -1071
  141. package/dist/chunk-CPPXJATQ.cjs.map +0 -1
  142. package/dist/chunk-DKU775VC.js +0 -219
  143. package/dist/chunk-DKU775VC.js.map +0 -1
  144. package/dist/chunk-GRA7Y2ZG.cjs +0 -401
  145. package/dist/chunk-GRA7Y2ZG.cjs.map +0 -1
  146. package/dist/chunk-I2276W3I.cjs +0 -66
  147. package/dist/chunk-I2276W3I.cjs.map +0 -1
  148. package/dist/chunk-JJIMABHT.js +0 -351
  149. package/dist/chunk-JJIMABHT.js.map +0 -1
  150. package/dist/chunk-KJHWXOCZ.js +0 -421
  151. package/dist/chunk-KJHWXOCZ.js.map +0 -1
  152. package/dist/chunk-KRWGSOJ2.js +0 -91
  153. package/dist/chunk-KRWGSOJ2.js.map +0 -1
  154. package/dist/chunk-KXRWNFQJ.js +0 -51
  155. package/dist/chunk-KXRWNFQJ.js.map +0 -1
  156. package/dist/chunk-LTFH7SEG.js +0 -373
  157. package/dist/chunk-LTFH7SEG.js.map +0 -1
  158. package/dist/chunk-MUNOKLLE.js +0 -165
  159. package/dist/chunk-MUNOKLLE.js.map +0 -1
  160. package/dist/chunk-MXGLYWVP.cjs +0 -351
  161. package/dist/chunk-MXGLYWVP.cjs.map +0 -1
  162. package/dist/chunk-OP6PHB2U.js +0 -81
  163. package/dist/chunk-OP6PHB2U.js.map +0 -1
  164. package/dist/chunk-OYC34VTO.cjs +0 -3957
  165. package/dist/chunk-OYC34VTO.cjs.map +0 -1
  166. package/dist/chunk-PQL5I3F6.cjs +0 -421
  167. package/dist/chunk-PQL5I3F6.cjs.map +0 -1
  168. package/dist/chunk-REZTQ4PH.cjs +0 -488
  169. package/dist/chunk-REZTQ4PH.cjs.map +0 -1
  170. package/dist/chunk-TZRUHQOH.cjs +0 -91
  171. package/dist/chunk-TZRUHQOH.cjs.map +0 -1
  172. package/dist/chunk-UTYVIOWZ.js +0 -3957
  173. package/dist/chunk-UTYVIOWZ.js.map +0 -1
  174. package/dist/chunk-W2LDIX26.cjs +0 -165
  175. package/dist/chunk-W2LDIX26.cjs.map +0 -1
  176. package/dist/chunk-WSG64BVN.cjs +0 -219
  177. package/dist/chunk-WSG64BVN.cjs.map +0 -1
  178. package/dist/chunk-WTQYGUNF.js +0 -400
  179. package/dist/chunk-WTQYGUNF.js.map +0 -1
  180. package/dist/chunk-WYK7EL5R.cjs +0 -895
  181. package/dist/chunk-WYK7EL5R.cjs.map +0 -1
  182. package/dist/chunk-XC4KHJYX.cjs +0 -617
  183. package/dist/chunk-XC4KHJYX.cjs.map +0 -1
  184. package/dist/chunk-XVBH5XHE.cjs +0 -400
  185. package/dist/chunk-XVBH5XHE.cjs.map +0 -1
  186. package/dist/chunk-XWYZIFZW.js +0 -66
  187. package/dist/chunk-XWYZIFZW.js.map +0 -1
  188. package/dist/chunk-Y3NNQMAX.cjs +0 -194
  189. package/dist/chunk-Y3NNQMAX.cjs.map +0 -1
  190. package/dist/chunk-YAFQVVDI.js +0 -488
  191. package/dist/chunk-YAFQVVDI.js.map +0 -1
  192. package/dist/components/legend/index.cjs +0 -12
  193. package/dist/components/legend/index.cjs.map +0 -1
  194. package/dist/components/legend/index.css +0 -91
  195. package/dist/components/legend/index.css.map +0 -1
  196. package/dist/components/legend/index.d.cts +0 -37
  197. package/dist/components/legend/index.d.ts +0 -37
  198. package/dist/components/legend/index.js +0 -12
  199. package/dist/components/legend/index.js.map +0 -1
  200. package/dist/components/tooltip/index.cjs +0 -12
  201. package/dist/components/tooltip/index.cjs.map +0 -1
  202. package/dist/components/tooltip/index.css +0 -13
  203. package/dist/components/tooltip/index.css.map +0 -1
  204. package/dist/components/tooltip/index.d.cts +0 -71
  205. package/dist/components/tooltip/index.d.ts +0 -71
  206. package/dist/components/tooltip/index.js +0 -12
  207. package/dist/components/tooltip/index.js.map +0 -1
  208. package/dist/components/trend-indicator/index.cjs +0 -8
  209. package/dist/components/trend-indicator/index.cjs.map +0 -1
  210. package/dist/components/trend-indicator/index.css +0 -27
  211. package/dist/components/trend-indicator/index.css.map +0 -1
  212. package/dist/components/trend-indicator/index.d.cts +0 -44
  213. package/dist/components/trend-indicator/index.d.ts +0 -44
  214. package/dist/components/trend-indicator/index.js +0 -8
  215. package/dist/components/trend-indicator/index.js.map +0 -1
  216. package/dist/format-metric-value-MXm5DtQ_.d.cts +0 -24
  217. package/dist/format-metric-value-MXm5DtQ_.d.ts +0 -24
  218. package/dist/hooks/index.cjs +0 -29
  219. package/dist/hooks/index.cjs.map +0 -1
  220. package/dist/hooks/index.css +0 -9
  221. package/dist/hooks/index.css.map +0 -1
  222. package/dist/hooks/index.d.cts +0 -264
  223. package/dist/hooks/index.d.ts +0 -264
  224. package/dist/hooks/index.js +0 -29
  225. package/dist/hooks/index.js.map +0 -1
  226. package/dist/leaderboard-chart-BSbg0ufV.d.cts +0 -79
  227. package/dist/leaderboard-chart-odEYxxEC.d.ts +0 -79
  228. package/dist/legend-DFkosEvC.d.cts +0 -9
  229. package/dist/legend-DLswHhOk.d.ts +0 -9
  230. package/dist/providers/index.cjs +0 -21
  231. package/dist/providers/index.cjs.map +0 -1
  232. package/dist/providers/index.css +0 -9
  233. package/dist/providers/index.css.map +0 -1
  234. package/dist/providers/index.d.cts +0 -28
  235. package/dist/providers/index.d.ts +0 -28
  236. package/dist/providers/index.js +0 -21
  237. package/dist/providers/index.js.map +0 -1
  238. package/dist/themes-D0qc5JaW.d.cts +0 -67
  239. package/dist/themes-itO4Ht5g.d.ts +0 -67
  240. package/dist/types-B5f6XQ7Q.d.cts +0 -19
  241. package/dist/types-BsHooDbM.d.ts +0 -19
  242. package/dist/types-BuSrRM4p.d.ts +0 -49
  243. package/dist/types-ChOUI9-N.d.cts +0 -545
  244. package/dist/types-ChOUI9-N.d.ts +0 -545
  245. package/dist/types-Dfw9VOKI.d.cts +0 -49
  246. package/dist/utils/index.cjs +0 -44
  247. package/dist/utils/index.cjs.map +0 -1
  248. package/dist/utils/index.d.cts +0 -226
  249. package/dist/utils/index.d.ts +0 -226
  250. package/dist/utils/index.js +0 -44
  251. package/dist/utils/index.js.map +0 -1
  252. package/dist/with-responsive-CNfhzAUu.d.cts +0 -18
  253. package/dist/with-responsive-CNfhzAUu.d.ts +0 -18
@@ -7,8 +7,8 @@ describe( 'PieChart', () => {
7
7
  const defaultProps = {
8
8
  size: 500,
9
9
  data: [
10
- { label: 'A', percentage: 50, value: 50 },
11
- { label: 'B', percentage: 50, value: 50 },
10
+ { label: 'A', value: 50 },
11
+ { label: 'B', value: 50 },
12
12
  ],
13
13
  };
14
14
 
@@ -21,21 +21,21 @@ describe( 'PieChart', () => {
21
21
  };
22
22
 
23
23
  describe( 'Data Validation', () => {
24
- test( 'validates total percentage equals 100', () => {
24
+ test( 'validates total value is greater than 0', () => {
25
25
  renderWithTheme( {
26
26
  data: [
27
- { label: 'A', percentage: 60, value: 60 },
28
- { label: 'B', percentage: 50, value: 50 },
27
+ { label: 'A', value: 0 },
28
+ { label: 'B', value: 0 },
29
29
  ],
30
30
  } );
31
- expect( screen.getByText( /invalid percentage total/i ) ).toBeInTheDocument();
31
+ expect( screen.getByText( /invalid data/i ) ).toBeInTheDocument();
32
32
  } );
33
33
 
34
34
  test( 'handles negative values', () => {
35
35
  renderWithTheme( {
36
36
  data: [
37
- { label: 'A', percentage: -30, value: -30 },
38
- { label: 'B', percentage: 130, value: 130 },
37
+ { label: 'A', value: -30 },
38
+ { label: 'B', value: 130 },
39
39
  ],
40
40
  } );
41
41
  expect( screen.getByText( /invalid data/i ) ).toBeInTheDocument();
@@ -48,7 +48,7 @@ describe( 'PieChart', () => {
48
48
 
49
49
  test( 'handles single data point', () => {
50
50
  renderWithTheme( {
51
- data: [ { label: 'A', percentage: 100, value: 100 } ],
51
+ data: [ { label: 'A', value: 100 } ],
52
52
  } );
53
53
  // Use getAllByText since 'A' appears in both chart and legend
54
54
  const labels = screen.getAllByText( 'A' );
@@ -157,10 +157,11 @@ describe( 'PieChart', () => {
157
157
  } );
158
158
 
159
159
  describe( 'Legend Value Display', () => {
160
+ // Values that give clean percentages: 60/100=60%, 23/100=23%, 17/100=17%
160
161
  const testData = [
161
- { label: 'Windows', value: 80000, valueDisplay: '80K', percentage: 60 },
162
- { label: 'MacOS', value: 30000, valueDisplay: '30K', percentage: 23 },
163
- { label: 'Linux', value: 22000, valueDisplay: '22K', percentage: 17 },
162
+ { label: 'Windows', value: 60, valueDisplay: '60' },
163
+ { label: 'MacOS', value: 23, valueDisplay: '23' },
164
+ { label: 'Linux', value: 17, valueDisplay: '17' },
164
165
  ];
165
166
 
166
167
  test( 'shows percentage values by default when showLegend and showValues are enabled', () => {
@@ -170,7 +171,7 @@ describe( 'PieChart', () => {
170
171
  // legendValueDisplay defaults to 'percentage'
171
172
  } );
172
173
 
173
- // Should display percentage values (using formatPercentage which shows "60%", "23%", "17%")
174
+ // Should display calculated percentages from values
174
175
  expect( screen.getByText( '60%' ) ).toBeInTheDocument();
175
176
  expect( screen.getByText( '23%' ) ).toBeInTheDocument();
176
177
  expect( screen.getByText( '17%' ) ).toBeInTheDocument();
@@ -184,9 +185,9 @@ describe( 'PieChart', () => {
184
185
  } );
185
186
 
186
187
  // Should display localized numeric values
187
- expect( screen.getByText( '80,000' ) ).toBeInTheDocument();
188
- expect( screen.getByText( '30,000' ) ).toBeInTheDocument();
189
- expect( screen.getByText( '22,000' ) ).toBeInTheDocument();
188
+ expect( screen.getByText( '60' ) ).toBeInTheDocument();
189
+ expect( screen.getByText( '23' ) ).toBeInTheDocument();
190
+ expect( screen.getByText( '17' ) ).toBeInTheDocument();
190
191
  } );
191
192
 
192
193
  test( 'shows formatted values when legendValueDisplay is set to "valueDisplay"', () => {
@@ -197,9 +198,9 @@ describe( 'PieChart', () => {
197
198
  } );
198
199
 
199
200
  // Should display formatted values (valueDisplay field)
200
- expect( screen.getByText( '80K' ) ).toBeInTheDocument();
201
- expect( screen.getByText( '30K' ) ).toBeInTheDocument();
202
- expect( screen.getByText( '22K' ) ).toBeInTheDocument();
201
+ expect( screen.getByText( '60' ) ).toBeInTheDocument();
202
+ expect( screen.getByText( '23' ) ).toBeInTheDocument();
203
+ expect( screen.getByText( '17' ) ).toBeInTheDocument();
203
204
  } );
204
205
 
205
206
  test( 'shows no values when legendValueDisplay is set to "none"', () => {
@@ -222,9 +223,12 @@ describe( 'PieChart', () => {
222
223
  } );
223
224
 
224
225
  describe( 'Tooltip Functionality', () => {
226
+ // Values: 80000 + 30000 = 110000
227
+ // Windows: 80000/110000 = 72.727...%
228
+ // MacOS: 30000/110000 = 27.272...%
225
229
  const testData = [
226
- { label: 'Windows', value: 80000, valueDisplay: '80K', percentage: 70 },
227
- { label: 'MacOS', value: 30000, valueDisplay: '30K', percentage: 30 },
230
+ { label: 'Windows', value: 80000, valueDisplay: '80K' },
231
+ { label: 'MacOS', value: 30000, valueDisplay: '30K' },
228
232
  ];
229
233
 
230
234
  test( 'does not show tooltip when withTooltips is false', async () => {
@@ -314,7 +318,7 @@ describe( 'PieChart', () => {
314
318
 
315
319
  test( 'tooltip shows valueDisplay when available, falls back to value', async () => {
316
320
  const user = userEvent.setup();
317
- const dataWithoutValueDisplay = [ { label: 'Test', value: 42, percentage: 100 } ];
321
+ const dataWithoutValueDisplay = [ { label: 'Test', value: 42 } ];
318
322
 
319
323
  renderWithTheme( {
320
324
  data: dataWithoutValueDisplay,
@@ -356,15 +360,18 @@ describe( 'PieChart', () => {
356
360
  expect( customTooltipRenderer ).toHaveBeenCalled();
357
361
 
358
362
  // Verify the renderer received correct parameters
363
+ // Percentage is calculated from values: 80000 / (80000 + 30000) = 72.727...%
359
364
  expect( customTooltipRenderer ).toHaveBeenCalledWith(
360
365
  expect.objectContaining( {
361
366
  tooltipData: expect.objectContaining( {
362
367
  label: 'Windows',
363
368
  value: 80000,
364
- percentage: 70,
365
369
  } ),
366
370
  } )
367
371
  );
372
+ // Verify percentage is calculated (approximately 72.73%)
373
+ const callArgs = customTooltipRenderer.mock.calls[ 0 ][ 0 ];
374
+ expect( callArgs.tooltipData.percentage ).toBeCloseTo( 72.73, 1 );
368
375
  } );
369
376
  } );
370
377
 
@@ -372,8 +379,8 @@ describe( 'PieChart', () => {
372
379
  test( 'filters segments when interactive legend is enabled and segment is toggled', async () => {
373
380
  const user = userEvent.setup();
374
381
  const testData = [
375
- { label: 'Segment A', value: 50, percentage: 50 },
376
- { label: 'Segment B', value: 50, percentage: 50 },
382
+ { label: 'Segment A', value: 50 },
383
+ { label: 'Segment B', value: 50 },
377
384
  ];
378
385
 
379
386
  renderWithTheme( {
@@ -404,8 +411,8 @@ describe( 'PieChart', () => {
404
411
  test( 'shows empty state when all segments are hidden', async () => {
405
412
  const user = userEvent.setup();
406
413
  const testData = [
407
- { label: 'Segment A', value: 50, percentage: 50 },
408
- { label: 'Segment B', value: 50, percentage: 50 },
414
+ { label: 'Segment A', value: 50 },
415
+ { label: 'Segment B', value: 50 },
409
416
  ];
410
417
 
411
418
  renderWithTheme( {
@@ -443,8 +450,8 @@ describe( 'PieChart', () => {
443
450
 
444
451
  test( 'does not filter segments when legendInteractive is false', () => {
445
452
  const testData = [
446
- { label: 'Segment A', value: 50, percentage: 50 },
447
- { label: 'Segment B', value: 50, percentage: 50 },
453
+ { label: 'Segment A', value: 50 },
454
+ { label: 'Segment B', value: 50 },
448
455
  ];
449
456
 
450
457
  renderWithTheme( {
@@ -466,9 +473,9 @@ describe( 'PieChart', () => {
466
473
  test( 'maintains consistent colors when segments are hidden', async () => {
467
474
  const user = userEvent.setup();
468
475
  const testData = [
469
- { label: 'Segment A', value: 30, percentage: 30 },
470
- { label: 'Segment B', value: 40, percentage: 40 },
471
- { label: 'Segment C', value: 30, percentage: 30 },
476
+ { label: 'Segment A', value: 30 },
477
+ { label: 'Segment B', value: 40 },
478
+ { label: 'Segment C', value: 30 },
472
479
  ];
473
480
 
474
481
  renderWithTheme( {
@@ -498,9 +505,9 @@ describe( 'PieChart', () => {
498
505
  test( 'recalculates legend percentages when segments are hidden', async () => {
499
506
  const user = userEvent.setup();
500
507
  const testData = [
501
- { label: 'Segment A', value: 25, percentage: 25 },
502
- { label: 'Segment B', value: 50, percentage: 50 },
503
- { label: 'Segment C', value: 25, percentage: 25 },
508
+ { label: 'Segment A', value: 25 },
509
+ { label: 'Segment B', value: 50 },
510
+ { label: 'Segment C', value: 25 },
504
511
  ];
505
512
 
506
513
  renderWithTheme( {
@@ -5,15 +5,9 @@
5
5
  width: 100%;
6
6
  }
7
7
 
8
- // Flex wrapper that fills remaining Stack space and measures the SVG area
9
- &__svg-wrapper {
10
- flex: 1;
11
- min-height: 0; // Required for flex shrinking
12
- min-width: 0; // Required for flex shrinking
8
+ &__centering {
13
9
  width: 100%;
14
- display: flex;
15
- align-items: center;
16
- justify-content: center;
10
+ height: 100%;
17
11
  }
18
12
 
19
13
  .label {
@@ -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,
@@ -17,18 +21,20 @@ import {
17
21
  GlobalChartsContext,
18
22
  } from '../../providers';
19
23
  import { attachSubComponents } from '../../utils';
20
- import {
21
- ChartSVG,
22
- ChartHTML,
23
- useChartChildren,
24
- renderLegendSlot,
25
- } from '../private/chart-composition';
24
+ import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
25
+ import { ChartLayout } from '../private/chart-layout';
26
26
  import { RadialWipeAnimation } from '../private/radial-wipe-animation';
27
27
  import { SingleChartContext } from '../private/single-chart-context';
28
+ import { SvgEmptyState } from '../private/svg-empty-state';
28
29
  import { withResponsive } from '../private/with-responsive';
29
30
  import styles from './pie-semi-circle-chart.module.scss';
30
31
  import type { LegendValueDisplay } from '../../components/legend';
31
- import type { BaseChartProps, DataPointPercentage, Optional } from '../../types';
32
+ import type {
33
+ BaseChartProps,
34
+ DataPointPercentage,
35
+ DataPointPercentageCalculated,
36
+ Optional,
37
+ } from '../../types';
32
38
  import type { ChartComponentWithComposition } from '../private/chart-composition';
33
39
  import type { ResponsiveConfig } from '../private/with-responsive';
34
40
  import type { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
@@ -39,9 +45,9 @@ import type { FC, MouseEvent, ReactNode } from 'react';
39
45
  */
40
46
  export type PieSemiCircleChartRenderTooltipParams = {
41
47
  /**
42
- * The data point being hovered, including label, value, and percentage.
48
+ * The data point being hovered, including label, value, and calculated percentage.
43
49
  */
44
- tooltipData: DataPointPercentage;
50
+ tooltipData: DataPointPercentageCalculated;
45
51
  };
46
52
 
47
53
  /**
@@ -129,7 +135,7 @@ type PieSemiCircleChartResponsiveComponent = ChartComponentWithComposition<
129
135
  PieSemiCircleChartBaseProps & ResponsiveConfig
130
136
  >;
131
137
 
132
- export type ArcData = PieArcDatum< DataPointPercentage >;
138
+ export type ArcData = PieArcDatum< DataPointPercentageCalculated >;
133
139
 
134
140
  /**
135
141
  * Validates the semi-circle pie chart data
@@ -142,15 +148,15 @@ const validateData = ( data: DataPointPercentage[] ) => {
142
148
  }
143
149
 
144
150
  // Check for negative values
145
- const hasNegativeValues = data.some( item => item.percentage < 0 || item.value < 0 );
151
+ const hasNegativeValues = data.some( item => item.value < 0 );
146
152
  if ( hasNegativeValues ) {
147
153
  return { isValid: false, message: 'Invalid data: Negative values are not allowed' };
148
154
  }
149
155
 
150
- // Validate total percentage is greater than 0
151
- const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 );
152
- if ( totalPercentage <= 0 ) {
153
- 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' };
154
160
  }
155
161
 
156
162
  return { isValid: true, message: '' };
@@ -181,10 +187,8 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
181
187
  const legendPosition = legend.position ?? 'bottom';
182
188
 
183
189
  const chartId = useChartId( providedChartId );
184
- // Measure the SVG wrapper to calculate constrained dimensions
185
- const [ svgWrapperRef, svgWrapperWidth, svgWrapperHeight ] = useElementSize< HTMLDivElement >();
186
190
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
187
- useTooltip< DataPointPercentage >();
191
+ useTooltip< DataPointPercentageCalculated >();
188
192
 
189
193
  // Set up portal tooltip for better z-index handling
190
194
  // We get containerBounds to cancel out stale offsets in the position calculation
@@ -239,9 +243,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
239
243
 
240
244
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
241
245
 
246
+ // Calculate percentages from values (single source of truth)
247
+ const dataWithPercentages = useDataWithPercentages( data );
248
+
242
249
  // Filter and recalculate data for interactive legends
243
250
  const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
244
- data,
251
+ data: dataWithPercentages,
245
252
  chartId,
246
253
  legendInteractive,
247
254
  isSeriesVisible,
@@ -250,12 +257,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
250
257
  // Define accessors with useMemo to avoid changing dependencies
251
258
  const accessors = useMemo(
252
259
  () => ( {
253
- value: ( d: DataPointPercentage ) => d.value,
260
+ value: ( d: DataPointPercentageCalculated ) => d.value,
254
261
  sort: (
255
- a: DataPointPercentage & { index: number },
256
- b: DataPointPercentage & { index: number }
262
+ a: DataPointPercentageCalculated & { index: number },
263
+ b: DataPointPercentageCalculated & { index: number }
257
264
  ) => b.value - a.value,
258
- fill: ( d: DataPointPercentage & { index: number } ) =>
265
+ fill: ( d: DataPointPercentageCalculated & { index: number } ) =>
259
266
  getElementStyles( { data: d, index: d.index } ).color,
260
267
  } ),
261
268
  [ getElementStyles ]
@@ -315,18 +322,6 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
315
322
  );
316
323
  }
317
324
 
318
- // Calculate chart dimensions maintaining the 2:1 width-to-height ratio.
319
- // Use measured SVG wrapper dimensions to respect height constraints, falling back
320
- // to explicit props during initial render before measurement is available.
321
- const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : effectiveWidth;
322
- const availableHeight =
323
- svgWrapperHeight > 0 ? svgWrapperHeight : propHeight || effectiveWidth / 2;
324
- // Constrain width so that height (= width / 2) never exceeds the available height
325
- const width = Math.min( availableWidth, availableHeight * 2 );
326
- const height = width / 2;
327
- const radius = height; // For a semi-circle, radius equals the SVG height
328
- const innerRadius = radius * ( 1 - thickness );
329
-
330
325
  // Map data with index for color assignment
331
326
  // When interactive, we need to find the original index to maintain consistent colors
332
327
  const dataWithIndex = visibleData.map( d => {
@@ -357,16 +352,11 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
357
352
  );
358
353
 
359
354
  return (
360
- <SingleChartContext.Provider
361
- value={ {
362
- chartId,
363
- chartWidth: width,
364
- chartHeight: height,
365
- } }
366
- >
367
- <Stack
368
- ref={ containerRef }
369
- direction="column"
355
+ <SingleChartContext.Provider value={ { chartId } }>
356
+ <ChartLayout
357
+ legendPosition={ legendPosition }
358
+ legendElement={ legendElement }
359
+ legendChildren={ legendChildren }
370
360
  gap={ gap }
371
361
  className={ clsx(
372
362
  'pie-semi-circle-chart',
@@ -381,120 +371,130 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
381
371
  height: propHeight || undefined,
382
372
  } }
383
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
+ }
384
385
  >
385
- { legendPosition === 'top' && legendElement }
386
- { renderLegendSlot( legendChildren, 'top' ) }
387
-
388
- <div ref={ svgWrapperRef } className={ styles[ 'pie-semi-circle-chart__svg-wrapper' ] }>
389
- <svg
390
- width={ width }
391
- height={ height }
392
- viewBox={ `0 0 ${ width } ${ height }` }
393
- data-testid="pie-chart-svg"
394
- >
395
- <defs>
396
- <RadialWipeAnimation
397
- id={ `radial-wipe-${ chartId }` }
398
- radius={ radius }
399
- innerRadius={ innerRadius }
400
- startAngle="-180deg"
401
- wipePercentage={ 50 }
402
- />
403
- </defs>
404
-
405
- { /* Main chart group centered horizontally and positioned at bottom */ }
406
- <Group
407
- top={ height }
408
- left={ width / 2 }
409
- 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' ] }
410
405
  >
411
- { allSegmentsHidden ? (
412
- <text
413
- textAnchor="middle"
414
- y={ -radius / 2 }
415
- fill="#ccc"
416
- fontSize="14"
417
- fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
418
- >
419
- { __(
420
- 'All segments are hidden. Click legend items to show data.',
421
- 'jetpack-charts'
422
- ) }
423
- </text>
424
- ) : (
425
- <>
426
- { /* Pie chart */ }
427
- <Pie< DataPointPercentage & { index: number } >
428
- data={ dataWithIndex }
429
- pieValue={ accessors.value }
430
- 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 }
431
416
  innerRadius={ innerRadius }
432
- cornerRadius={ 3 }
433
- padAngle={ PAD_ANGLE }
434
- startAngle={ startAngle }
435
- endAngle={ endAngle }
436
- pieSort={ accessors.sort }
437
- >
438
- { pie => {
439
- return pie.arcs.map( arc => (
440
- <g
441
- key={ arc.data.label }
442
- onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
443
- 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 }
444
475
  >
445
- <path
446
- d={ pie.path( arc ) || '' }
447
- fill={ accessors.fill( arc.data ) }
448
- data-testid="pie-segment"
449
- />
450
- </g>
451
- ) );
452
- } }
453
- </Pie>
454
-
455
- { /* Label and note text */ }
456
- <Group>
457
- <Text
458
- textAnchor="middle"
459
- verticalAnchor="start"
460
- y={ -40 } // Position above the chart with space for note
461
- className={ styles.label }
462
- >
463
- { label }
464
- </Text>
465
- <Text
466
- textAnchor="middle"
467
- verticalAnchor="start"
468
- y={ -20 } // Position between label and chart
469
- className={ styles.note }
470
- >
471
- { note }
472
- </Text>
473
- </Group>
474
-
475
- { /* Render SVG children from composition API */ }
476
- { ! allSegmentsHidden && svgChildren }
477
- </>
478
- ) }
479
- </Group>
480
- </svg>
481
- </div>
482
-
483
- { legendPosition === 'bottom' && legendElement }
484
- { renderLegendSlot( legendChildren, 'bottom' ) }
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 children from composition API */ }
493
- { 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>
494
487
 
495
- { /* Render any other children that aren't compound components */ }
496
- { otherChildren }
497
- </Stack>
488
+ { /* Render SVG children from composition API */ }
489
+ { ! allSegmentsHidden && svgChildren }
490
+ </>
491
+ ) }
492
+ </Group>
493
+ </svg>
494
+ </Stack>
495
+ );
496
+ } }
497
+ </ChartLayout>
498
498
  </SingleChartContext.Provider>
499
499
  );
500
500
  };