@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
@@ -18,13 +18,11 @@ 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
 
@@ -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(
@@ -288,8 +288,8 @@ describe( 'PieSemiCircleChart', () => {
288
288
  test( 'filters segments when interactive legend is enabled and segment is toggled', async () => {
289
289
  const user = userEvent.setup();
290
290
  const testData = [
291
- { label: 'Segment A', value: 50, percentage: 50 },
292
- { label: 'Segment B', value: 50, percentage: 50 },
291
+ { label: 'Segment A', value: 50 },
292
+ { label: 'Segment B', value: 50 },
293
293
  ];
294
294
 
295
295
  renderPieChart( {
@@ -320,8 +320,8 @@ describe( 'PieSemiCircleChart', () => {
320
320
  test( 'shows empty state when all segments are hidden', async () => {
321
321
  const user = userEvent.setup();
322
322
  const testData = [
323
- { label: 'Segment A', value: 50, percentage: 50 },
324
- { label: 'Segment B', value: 50, percentage: 50 },
323
+ { label: 'Segment A', value: 50 },
324
+ { label: 'Segment B', value: 50 },
325
325
  ];
326
326
 
327
327
  renderPieChart( {
@@ -347,8 +347,8 @@ describe( 'PieSemiCircleChart', () => {
347
347
 
348
348
  test( 'does not filter segments when legendInteractive is false', () => {
349
349
  const testData = [
350
- { label: 'Segment A', value: 50, percentage: 50 },
351
- { label: 'Segment B', value: 50, percentage: 50 },
350
+ { label: 'Segment A', value: 50 },
351
+ { label: 'Segment B', value: 50 },
352
352
  ];
353
353
 
354
354
  renderPieChart( {
@@ -0,0 +1,7 @@
1
+ // Shared content area for charts using the render prop.
2
+ // Wraps render prop output and handles measurement.
3
+ .chart-layout__content {
4
+ flex: 1;
5
+ min-height: 0;
6
+ min-width: 0;
7
+ }
@@ -0,0 +1,106 @@
1
+ import { Stack } from '@wordpress/ui';
2
+ import { useEffect } from 'react';
3
+ import { useElementSize } from '../../../hooks';
4
+ import { renderLegendSlot } from '../chart-composition';
5
+ import styles from './chart-layout.module.scss';
6
+ import type { LegendPosition } from '../../../types';
7
+ import type { LegendChild } from '../chart-composition/use-chart-children';
8
+ import type { GapSize } from '@wordpress/theme';
9
+ import type { CSSProperties, ReactNode } from 'react';
10
+
11
+ /**
12
+ * Measurements provided to the render prop when ChartLayout handles resize listening.
13
+ */
14
+ export interface ContentMeasurements {
15
+ /** Measured width of the content area in pixels */
16
+ contentWidth: number;
17
+ /** Measured height of the content area in pixels */
18
+ contentHeight: number;
19
+ /** True when a non-zero contentHeight measurement is available */
20
+ isMeasured: boolean;
21
+ }
22
+
23
+ export interface ChartLayoutProps {
24
+ /** Position for the prop-based legend element */
25
+ legendPosition: LegendPosition;
26
+ /** The legend element rendered via the showLegend prop (false when hidden) */
27
+ legendElement?: ReactNode;
28
+ /** Legend children from the composition API */
29
+ legendChildren: LegendChild[];
30
+ /** Chart content — either a ReactNode or a render prop receiving content measurements */
31
+ children: ReactNode | ( ( measurements: ContentMeasurements ) => ReactNode );
32
+ /** Content rendered after the bottom legend (e.g., nonLegendChildren, htmlChildren, tooltips) */
33
+ trailingContent?: ReactNode;
34
+ /** Called when the measured content height changes (for render-prop mode) */
35
+ onContentHeightChange?: ( height: number ) => void;
36
+ /** Gap between Stack items */
37
+ gap?: GapSize;
38
+ /** Additional class names */
39
+ className?: string;
40
+ /** Inline styles (width, height, etc.) */
41
+ style?: CSSProperties;
42
+ /** Test ID for the container */
43
+ 'data-testid'?: string;
44
+ /** Chart ID attribute */
45
+ 'data-chart-id'?: string;
46
+ }
47
+
48
+ export const ChartLayout = ( {
49
+ legendPosition,
50
+ legendElement,
51
+ legendChildren,
52
+ children,
53
+ trailingContent,
54
+ onContentHeightChange,
55
+ gap,
56
+ className,
57
+ style,
58
+ 'data-testid': dataTestId,
59
+ 'data-chart-id': dataChartId,
60
+ }: ChartLayoutProps ) => {
61
+ const [ contentRef, contentWidth, contentHeight ] = useElementSize< HTMLDivElement >();
62
+ const isRenderProp = typeof children === 'function';
63
+ const isMeasured = contentHeight > 0;
64
+
65
+ // When using render-prop children, hide the layout until measurement is available
66
+ // to prevent layout shift. Plain ReactNode children don't need this since they
67
+ // don't depend on measured dimensions.
68
+ const visibilityStyle: { visibility?: 'hidden' | 'visible' } =
69
+ isRenderProp && ! isMeasured ? { visibility: 'hidden' } : {};
70
+
71
+ useEffect( () => {
72
+ if ( isRenderProp && onContentHeightChange && isMeasured ) {
73
+ onContentHeightChange( contentHeight );
74
+ }
75
+ }, [ isRenderProp, contentHeight, isMeasured, onContentHeightChange ] );
76
+ const renderedChildren = isRenderProp
77
+ ? children( { contentWidth, contentHeight, isMeasured } )
78
+ : children;
79
+
80
+ return (
81
+ <Stack
82
+ direction="column"
83
+ gap={ gap }
84
+ className={ className }
85
+ style={ { ...style, ...visibilityStyle } }
86
+ data-testid={ dataTestId }
87
+ data-chart-id={ dataChartId }
88
+ >
89
+ { legendPosition === 'top' && legendElement }
90
+ { renderLegendSlot( legendChildren, 'top' ) }
91
+
92
+ { isRenderProp ? (
93
+ <div ref={ contentRef } className={ styles[ 'chart-layout__content' ] }>
94
+ { renderedChildren }
95
+ </div>
96
+ ) : (
97
+ renderedChildren
98
+ ) }
99
+
100
+ { legendPosition === 'bottom' && legendElement }
101
+ { renderLegendSlot( legendChildren, 'bottom' ) }
102
+
103
+ { trailingContent }
104
+ </Stack>
105
+ );
106
+ };
@@ -0,0 +1,2 @@
1
+ export { ChartLayout } from './chart-layout';
2
+ export type { ChartLayoutProps, ContentMeasurements } from './chart-layout';
@@ -0,0 +1,167 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { renderLegendSlot } from '../../chart-composition';
3
+ import { ChartLayout } from '../chart-layout';
4
+ import type { LegendChild } from '../../chart-composition/use-chart-children';
5
+
6
+ // Mock renderLegendSlot since we test it separately
7
+ jest.mock( '../../chart-composition', () => ( {
8
+ renderLegendSlot: jest.fn( () => [] ),
9
+ } ) );
10
+
11
+ const mockRenderLegendSlot = renderLegendSlot as jest.Mock;
12
+
13
+ describe( 'ChartLayout', () => {
14
+ beforeEach( () => {
15
+ mockRenderLegendSlot.mockReturnValue( [] );
16
+ } );
17
+
18
+ it( 'renders children inside a column Stack', () => {
19
+ render(
20
+ <ChartLayout legendPosition="bottom" legendChildren={ [] }>
21
+ <div data-testid="chart-content">Chart</div>
22
+ </ChartLayout>
23
+ );
24
+ expect( screen.getByTestId( 'chart-content' ) ).toBeInTheDocument();
25
+ } );
26
+
27
+ it( 'renders legend element at top when legendPosition is top', () => {
28
+ const legendElement = <div data-testid="legend">Legend</div>;
29
+ render(
30
+ <ChartLayout legendPosition="top" legendElement={ legendElement } legendChildren={ [] }>
31
+ <div data-testid="chart-content">Chart</div>
32
+ </ChartLayout>
33
+ );
34
+ const legend = screen.getByTestId( 'legend' );
35
+ const content = screen.getByTestId( 'chart-content' );
36
+ expect( legend.compareDocumentPosition( content ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING );
37
+ } );
38
+
39
+ it( 'renders legend element at bottom when legendPosition is bottom', () => {
40
+ const legendElement = <div data-testid="legend">Legend</div>;
41
+ render(
42
+ <ChartLayout legendPosition="bottom" legendElement={ legendElement } legendChildren={ [] }>
43
+ <div data-testid="chart-content">Chart</div>
44
+ </ChartLayout>
45
+ );
46
+ const content = screen.getByTestId( 'chart-content' );
47
+ const legend = screen.getByTestId( 'legend' );
48
+ expect( content.compareDocumentPosition( legend ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING );
49
+ } );
50
+
51
+ it( 'does not render legend element when it is false/null', () => {
52
+ render(
53
+ <ChartLayout legendPosition="top" legendElement={ false } legendChildren={ [] }>
54
+ <div data-testid="chart-content">Chart</div>
55
+ </ChartLayout>
56
+ );
57
+ expect( screen.queryByTestId( 'legend' ) ).not.toBeInTheDocument();
58
+ } );
59
+
60
+ it( 'calls renderLegendSlot for both positions', () => {
61
+ const legendChildren: LegendChild[] = [];
62
+ render(
63
+ <ChartLayout legendPosition="bottom" legendChildren={ legendChildren }>
64
+ <div>Chart</div>
65
+ </ChartLayout>
66
+ );
67
+ expect( mockRenderLegendSlot ).toHaveBeenCalledWith( legendChildren, 'top' );
68
+ expect( mockRenderLegendSlot ).toHaveBeenCalledWith( legendChildren, 'bottom' );
69
+ } );
70
+
71
+ it( 'hides layout until measured when using render-prop children', () => {
72
+ // Override the global getBoundingClientRect mock to return zero (unmeasured state)
73
+ // for elements inside this test. This simulates the initial state before
74
+ // ResizeObserver provides real dimensions in a browser.
75
+ const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
76
+ Element.prototype.getBoundingClientRect = function () {
77
+ return { width: 0, height: 0, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0 } as DOMRect;
78
+ };
79
+
80
+ try {
81
+ const childFn = jest.fn().mockReturnValue( <div>Chart</div> );
82
+ render(
83
+ <ChartLayout legendPosition="bottom" legendChildren={ [] } data-testid="layout">
84
+ { childFn }
85
+ </ChartLayout>
86
+ );
87
+ // When contentHeight is 0, layout should be hidden to prevent layout shift
88
+ expect( screen.getByTestId( 'layout' ) ).toHaveStyle( { visibility: 'hidden' } );
89
+ } finally {
90
+ Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
91
+ }
92
+ } );
93
+
94
+ it( 'does not hide layout when using plain ReactNode children', () => {
95
+ render(
96
+ <ChartLayout legendPosition="bottom" legendChildren={ [] } data-testid="layout">
97
+ <div>Chart</div>
98
+ </ChartLayout>
99
+ );
100
+ const layoutStyle = screen.getByTestId( 'layout' ).getAttribute( 'style' ) ?? '';
101
+ expect( layoutStyle ).not.toContain( 'visibility' );
102
+ } );
103
+
104
+ it( 'passes className and style to Stack', () => {
105
+ render(
106
+ <ChartLayout
107
+ legendPosition="bottom"
108
+ legendChildren={ [] }
109
+ className="my-chart"
110
+ style={ { width: 400, height: 300 } }
111
+ data-testid="layout"
112
+ >
113
+ <div>Chart</div>
114
+ </ChartLayout>
115
+ );
116
+ const layout = screen.getByTestId( 'layout' );
117
+ expect( layout ).toHaveClass( 'my-chart' );
118
+ expect( layout ).toHaveStyle( { width: '400px', height: '300px' } );
119
+ } );
120
+
121
+ it( 'passes gap to Stack', () => {
122
+ render(
123
+ <ChartLayout legendPosition="bottom" legendChildren={ [] } gap="lg" data-testid="layout">
124
+ <div>Chart</div>
125
+ </ChartLayout>
126
+ );
127
+ // Stack renders gap as a CSS class or style — just verify it renders without error
128
+ expect( screen.getByTestId( 'layout' ) ).toBeInTheDocument();
129
+ } );
130
+
131
+ it( 'calls function-as-children with measurement props', () => {
132
+ const childFn = jest.fn().mockReturnValue( <div data-testid="chart-content">Chart</div> );
133
+
134
+ render(
135
+ <ChartLayout legendPosition="bottom" legendChildren={ [] }>
136
+ { childFn }
137
+ </ChartLayout>
138
+ );
139
+
140
+ expect( childFn ).toHaveBeenCalled();
141
+ const firstCallArg = childFn.mock.calls[ 0 ][ 0 ];
142
+
143
+ expect( firstCallArg ).toEqual(
144
+ expect.objectContaining( {
145
+ contentWidth: expect.any( Number ),
146
+ contentHeight: expect.any( Number ),
147
+ isMeasured: expect.any( Boolean ),
148
+ } )
149
+ );
150
+ } );
151
+
152
+ it( 'renders trailing content after bottom legend', () => {
153
+ render(
154
+ <ChartLayout
155
+ legendPosition="bottom"
156
+ legendElement={ <div data-testid="legend">Legend</div> }
157
+ legendChildren={ [] }
158
+ trailingContent={ <div data-testid="trailing">Extra</div> }
159
+ >
160
+ <div data-testid="chart-content">Chart</div>
161
+ </ChartLayout>
162
+ );
163
+ const legend = screen.getByTestId( 'legend' );
164
+ const trailing = screen.getByTestId( 'trailing' );
165
+ expect( legend.compareDocumentPosition( trailing ) ).toBe( Node.DOCUMENT_POSITION_FOLLOWING );
166
+ } );
167
+ } );
@@ -13,8 +13,8 @@ export interface ChartInstanceRef {
13
13
  export interface ChartInstanceContextValue {
14
14
  chartId: string;
15
15
  chartRef?: React.RefObject< ChartInstanceRef >;
16
- chartWidth: number;
17
- chartHeight: number;
16
+ chartWidth?: number;
17
+ chartHeight?: number;
18
18
  }
19
19
 
20
20
  export const ChartInstanceContext = createContext< ChartInstanceContextValue | null >( null );
@@ -0,0 +1 @@
1
+ export { SvgEmptyState } from './svg-empty-state';
@@ -0,0 +1,7 @@
1
+ .svg-empty-state {
2
+ text-align: center;
3
+ width: 100%;
4
+ height: 100%;
5
+ font-size: var(--wpds-font-size-md, 13px);
6
+ color: var(--wpds-color-fg-content-neutral-weak, #6d6d6d);
7
+ }
@@ -0,0 +1,40 @@
1
+ import { Stack } from '@wordpress/ui';
2
+ import styles from './svg-empty-state.module.scss';
3
+ import type { FC, ReactNode } from 'react';
4
+
5
+ interface SvgEmptyStateProps {
6
+ /** X coordinate of the center point */
7
+ x: number;
8
+ /** Y coordinate of the center point */
9
+ y: number;
10
+ /** Available width for the text area */
11
+ width: number;
12
+ /** Available height for the text area */
13
+ height: number;
14
+ /** Text content */
15
+ children: ReactNode;
16
+ }
17
+
18
+ /**
19
+ * Renders empty-state text inside an SVG using foreignObject so that the
20
+ * message wraps onto multiple lines instead of being clipped.
21
+ *
22
+ * The component centers the text within the specified area.
23
+ *
24
+ * @param root0 - Component props
25
+ * @param root0.x - X coordinate of the center point
26
+ * @param root0.y - Y coordinate of the center point
27
+ * @param root0.width - Available width for the text area
28
+ * @param root0.height - Available height for the text area
29
+ * @param root0.children - Text content
30
+ * @return {JSX.Element} A foreignObject element containing the centered text.
31
+ */
32
+ export const SvgEmptyState: FC< SvgEmptyStateProps > = ( { x, y, width, height, children } ) => {
33
+ return (
34
+ <foreignObject x={ x - width / 2 } y={ y - height / 2 } width={ width } height={ height }>
35
+ <Stack align="center" justify="center" className={ styles[ 'svg-empty-state' ] }>
36
+ { children }
37
+ </Stack>
38
+ </foreignObject>
39
+ );
40
+ };
@@ -1,7 +1,7 @@
1
1
  import { renderHook } from '@testing-library/react';
2
2
  import { GlobalChartsProvider } from '../../../../providers';
3
3
  import { useChartLegendItems } from '../use-chart-legend-items';
4
- import type { DataPointPercentage, DataPointDate, SeriesData } from '../../../../types';
4
+ import type { DataPointPercentageCalculated, DataPointDate, SeriesData } from '../../../../types';
5
5
  import type { ReactNode } from 'react';
6
6
 
7
7
  // Wrapper component to provide GlobalChartsProvider context
@@ -11,8 +11,8 @@ const wrapper = ( { children }: { children: ReactNode } ) => (
11
11
 
12
12
  describe( 'useChartLegendItems', () => {
13
13
  describe( 'Number Formatting (i18n)', () => {
14
- describe( 'DataPointPercentage', () => {
15
- const percentageData: DataPointPercentage[] = [
14
+ describe( 'DataPointPercentageCalculated', () => {
15
+ const percentageData: DataPointPercentageCalculated[] = [
16
16
  { label: 'Item 1', value: 80000, percentage: 60.6 },
17
17
  { label: 'Item 2', value: 30000, percentage: 22.7 },
18
18
  { label: 'Item 3', value: 22000, percentage: 16.7 },
@@ -50,7 +50,7 @@ describe( 'useChartLegendItems', () => {
50
50
  } );
51
51
 
52
52
  test( 'uses valueDisplay when provided, falling back to formatted value', () => {
53
- const dataWithDisplay: DataPointPercentage[] = [
53
+ const dataWithDisplay: DataPointPercentageCalculated[] = [
54
54
  { label: 'Item 1', value: 80000, percentage: 60, valueDisplay: 'Custom 80K' },
55
55
  { label: 'Item 2', value: 30000, percentage: 30 },
56
56
  ];
@@ -194,7 +194,9 @@ describe( 'useChartLegendItems', () => {
194
194
  } );
195
195
 
196
196
  test( 'handles data with zero values', () => {
197
- const zeroData: DataPointPercentage[] = [ { label: 'Zero Value', value: 0, percentage: 0 } ];
197
+ const zeroData: DataPointPercentageCalculated[] = [
198
+ { label: 'Zero Value', value: 0, percentage: 0 },
199
+ ];
198
200
 
199
201
  const { result } = renderHook(
200
202
  () =>
@@ -209,7 +211,7 @@ describe( 'useChartLegendItems', () => {
209
211
  } );
210
212
 
211
213
  test( 'handles very large numbers', () => {
212
- const largeData: DataPointPercentage[] = [
214
+ const largeData: DataPointPercentageCalculated[] = [
213
215
  { label: 'Large', value: 1234567890, percentage: 100 },
214
216
  ];
215
217
 
@@ -227,7 +229,7 @@ describe( 'useChartLegendItems', () => {
227
229
  } );
228
230
 
229
231
  test( 'handles decimal values', () => {
230
- const decimalData: DataPointPercentage[] = [
232
+ const decimalData: DataPointPercentageCalculated[] = [
231
233
  { label: 'Decimal', value: 1234.5678, percentage: 100 },
232
234
  ];
233
235
 
@@ -247,7 +249,9 @@ describe( 'useChartLegendItems', () => {
247
249
 
248
250
  describe( 'Label and Color', () => {
249
251
  test( 'preserves label and color from data', () => {
250
- const data: DataPointPercentage[] = [ { label: 'Test Label', value: 100, percentage: 100 } ];
252
+ const data: DataPointPercentageCalculated[] = [
253
+ { label: 'Test Label', value: 100, percentage: 100 },
254
+ ];
251
255
 
252
256
  const { result } = renderHook(
253
257
  () =>
@@ -6,7 +6,7 @@ import {
6
6
  type ElementStyles,
7
7
  } from '../../../providers';
8
8
  import { formatPercentage } from '../../../utils';
9
- import type { SeriesData, DataPointDate, DataPointPercentage } from '../../../types';
9
+ import type { SeriesData, DataPointDate, DataPointPercentageCalculated } from '../../../types';
10
10
  import type { BaseLegendItem } from '../types';
11
11
  import type { LegendShape } from '@visx/legend/lib/types';
12
12
  import type { GlyphProps } from '@visx/xychart';
@@ -31,7 +31,7 @@ export interface ChartLegendOptions {
31
31
  * @return Formatted value string
32
32
  */
33
33
  function formatPointValue(
34
- point: DataPointDate | DataPointPercentage,
34
+ point: DataPointDate | DataPointPercentageCalculated,
35
35
  showValues: boolean,
36
36
  legendValueDisplay: LegendValueDisplay = 'percentage'
37
37
  ): string {
@@ -39,16 +39,15 @@ function formatPointValue(
39
39
  return '';
40
40
  }
41
41
 
42
- // Handle DataPointPercentage (pie chart data)
42
+ // Handle DataPointPercentageCalculated (pie chart data with calculated percentage)
43
43
  if ( 'percentage' in point ) {
44
- const percentagePoint = point as DataPointPercentage;
45
44
  switch ( legendValueDisplay ) {
46
45
  case 'percentage':
47
- return formatPercentage( percentagePoint.percentage );
46
+ return formatPercentage( point.percentage );
48
47
  case 'value':
49
- return formatNumber( percentagePoint.value );
48
+ return formatNumber( point.value );
50
49
  case 'valueDisplay':
51
- return percentagePoint.valueDisplay || formatNumber( percentagePoint.value );
50
+ return point.valueDisplay || formatNumber( point.value );
52
51
  default:
53
52
  return '';
54
53
  }
@@ -145,7 +144,7 @@ function processSeriesData(
145
144
  * @return Array of processed legend items
146
145
  */
147
146
  function processPointData(
148
- pointData: ( DataPointDate | DataPointPercentage )[],
147
+ pointData: ( DataPointDate | DataPointPercentageCalculated )[],
149
148
  getElementStyles: ( params: GetElementStylesParams ) => ElementStyles,
150
149
  showValues: boolean,
151
150
  legendValueDisplay: LegendValueDisplay,
@@ -154,9 +153,9 @@ function processPointData(
154
153
  renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode,
155
154
  legendShape?: LegendShape< SeriesData[], number >
156
155
  ): BaseLegendItem[] {
157
- const mapper = ( point: DataPointDate | DataPointPercentage, index: number ) => {
156
+ const mapper = ( point: DataPointDate | DataPointPercentageCalculated, index: number ) => {
158
157
  const { color, glyph, shapeStyles } = getElementStyles( {
159
- data: point as DataPointPercentage,
158
+ data: point as DataPointPercentageCalculated,
160
159
  index,
161
160
  legendShape,
162
161
  } );
@@ -182,7 +181,7 @@ function processPointData(
182
181
  * @return Array of legend items ready for display
183
182
  */
184
183
  export function useChartLegendItems<
185
- T extends SeriesData[] | DataPointDate[] | DataPointPercentage[],
184
+ T extends SeriesData[] | DataPointDate[] | DataPointPercentageCalculated[],
186
185
  >(
187
186
  data: T,
188
187
  options: ChartLegendOptions = {},
@@ -215,9 +214,9 @@ export function useChartLegendItems<
215
214
  );
216
215
  }
217
216
 
218
- // Handle DataPointDate or DataPointPercentage (single data points)
217
+ // Handle DataPointDate or DataPointPercentageCalculated (single data points)
219
218
  return processPointData(
220
- data as ( DataPointDate | DataPointPercentage )[],
219
+ data as ( DataPointDate | DataPointPercentageCalculated )[],
221
220
  getElementStyles,
222
221
  showValues,
223
222
  legendValueDisplay,