@automattic/charts 0.56.4 → 0.56.6

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 (179) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/charts/bar-chart/index.cjs +6 -6
  3. package/dist/charts/bar-chart/index.css +1 -4
  4. package/dist/charts/bar-chart/index.css.map +1 -1
  5. package/dist/charts/bar-chart/index.d.cts +2 -8
  6. package/dist/charts/bar-chart/index.d.ts +2 -8
  7. package/dist/charts/bar-chart/index.js +5 -5
  8. package/dist/charts/bar-list-chart/index.cjs +7 -7
  9. package/dist/charts/bar-list-chart/index.css +1 -4
  10. package/dist/charts/bar-list-chart/index.css.map +1 -1
  11. package/dist/charts/bar-list-chart/index.d.cts +2 -2
  12. package/dist/charts/bar-list-chart/index.d.ts +2 -2
  13. package/dist/charts/bar-list-chart/index.js +6 -6
  14. package/dist/charts/conversion-funnel-chart/index.cjs +5 -5
  15. package/dist/charts/conversion-funnel-chart/index.css +1 -4
  16. package/dist/charts/conversion-funnel-chart/index.css.map +1 -1
  17. package/dist/charts/conversion-funnel-chart/index.d.cts +2 -1
  18. package/dist/charts/conversion-funnel-chart/index.d.ts +2 -1
  19. package/dist/charts/conversion-funnel-chart/index.js +4 -4
  20. package/dist/charts/geo-chart/index.cjs +4 -4
  21. package/dist/charts/geo-chart/index.css +1 -4
  22. package/dist/charts/geo-chart/index.css.map +1 -1
  23. package/dist/charts/geo-chart/index.d.cts +2 -1
  24. package/dist/charts/geo-chart/index.d.ts +2 -1
  25. package/dist/charts/geo-chart/index.js +3 -3
  26. package/dist/charts/leaderboard-chart/index.cjs +5 -5
  27. package/dist/charts/leaderboard-chart/index.css +1 -4
  28. package/dist/charts/leaderboard-chart/index.css.map +1 -1
  29. package/dist/charts/leaderboard-chart/index.d.cts +3 -2
  30. package/dist/charts/leaderboard-chart/index.d.ts +3 -2
  31. package/dist/charts/leaderboard-chart/index.js +4 -4
  32. package/dist/charts/line-chart/index.cjs +6 -6
  33. package/dist/charts/line-chart/index.css +1 -4
  34. package/dist/charts/line-chart/index.css.map +1 -1
  35. package/dist/charts/line-chart/index.d.cts +2 -8
  36. package/dist/charts/line-chart/index.d.ts +2 -8
  37. package/dist/charts/line-chart/index.js +5 -5
  38. package/dist/charts/pie-chart/index.cjs +6 -4
  39. package/dist/charts/pie-chart/index.cjs.map +1 -1
  40. package/dist/charts/pie-chart/index.css +13 -7
  41. package/dist/charts/pie-chart/index.css.map +1 -1
  42. package/dist/charts/pie-chart/index.d.cts +2 -1
  43. package/dist/charts/pie-chart/index.d.ts +2 -1
  44. package/dist/charts/pie-chart/index.js +5 -3
  45. package/dist/charts/pie-semi-circle-chart/index.cjs +6 -4
  46. package/dist/charts/pie-semi-circle-chart/index.cjs.map +1 -1
  47. package/dist/charts/pie-semi-circle-chart/index.css +12 -13
  48. package/dist/charts/pie-semi-circle-chart/index.css.map +1 -1
  49. package/dist/charts/pie-semi-circle-chart/index.d.cts +5 -2
  50. package/dist/charts/pie-semi-circle-chart/index.d.ts +5 -2
  51. package/dist/charts/pie-semi-circle-chart/index.js +5 -3
  52. package/dist/charts/sparkline/index.cjs +7 -7
  53. package/dist/charts/sparkline/index.css +1 -4
  54. package/dist/charts/sparkline/index.css.map +1 -1
  55. package/dist/charts/sparkline/index.js +6 -6
  56. package/dist/{chunk-ZXEFMKVP.cjs → chunk-3EXJP67N.cjs} +7 -7
  57. package/dist/{chunk-ZXEFMKVP.cjs.map → chunk-3EXJP67N.cjs.map} +1 -1
  58. package/dist/{chunk-HNEG3EFJ.cjs → chunk-55ZCOYDF.cjs} +117 -132
  59. package/dist/chunk-55ZCOYDF.cjs.map +1 -0
  60. package/dist/{chunk-KKPZ4MVF.js → chunk-7FDQGBY7.js} +145 -119
  61. package/dist/chunk-7FDQGBY7.js.map +1 -0
  62. package/dist/{chunk-7QDEU3KN.cjs → chunk-ASLARV7L.cjs} +6 -6
  63. package/dist/chunk-ASLARV7L.cjs.map +1 -0
  64. package/dist/chunk-BXFD7JIG.cjs +401 -0
  65. package/dist/chunk-BXFD7JIG.cjs.map +1 -0
  66. package/dist/{chunk-KMYJJTSR.cjs → chunk-CAFJRZPZ.cjs} +12 -12
  67. package/dist/{chunk-KMYJJTSR.cjs.map → chunk-CAFJRZPZ.cjs.map} +1 -1
  68. package/dist/{chunk-WMWAUOQ4.js → chunk-E62LCBGD.js} +4 -4
  69. package/dist/{chunk-ZY4FXLMM.js → chunk-GWBS65VC.js} +3 -3
  70. package/dist/{chunk-MEIVKY4K.js → chunk-IS5YYLTV.js} +18 -18
  71. package/dist/{chunk-MEIVKY4K.js.map → chunk-IS5YYLTV.js.map} +1 -1
  72. package/dist/{chunk-5N77S5N3.cjs → chunk-K6TGILHX.cjs} +8 -8
  73. package/dist/{chunk-5N77S5N3.cjs.map → chunk-K6TGILHX.cjs.map} +1 -1
  74. package/dist/{chunk-EBDUXL5K.js → chunk-KHQPN77E.js} +3 -3
  75. package/dist/{chunk-SEKPIG5K.js → chunk-KNIMXN6Z.js} +2 -2
  76. package/dist/{chunk-SEKPIG5K.js.map → chunk-KNIMXN6Z.js.map} +1 -1
  77. package/dist/{chunk-M7PRGJFE.js → chunk-MDRCAGKZ.js} +4 -4
  78. package/dist/{chunk-RSYD434G.cjs → chunk-NQJE2CC7.cjs} +120 -98
  79. package/dist/chunk-NQJE2CC7.cjs.map +1 -0
  80. package/dist/{chunk-66BXSWMW.cjs → chunk-O2JIANHK.cjs} +25 -25
  81. package/dist/chunk-O2JIANHK.cjs.map +1 -0
  82. package/dist/{chunk-R23BFDIW.js → chunk-OMS5QIJN.js} +6 -6
  83. package/dist/chunk-OMS5QIJN.js.map +1 -0
  84. package/dist/{chunk-TYIH5LMV.js → chunk-OP6PHB2U.js} +6 -6
  85. package/dist/chunk-OP6PHB2U.js.map +1 -0
  86. package/dist/{chunk-AWNCAKZY.js → chunk-RFSHE3HL.js} +60 -16
  87. package/dist/chunk-RFSHE3HL.js.map +1 -0
  88. package/dist/{chunk-FZYJM5PN.js → chunk-SSFFCBCF.js} +6 -6
  89. package/dist/chunk-SSFFCBCF.js.map +1 -0
  90. package/dist/{chunk-I5467ZJ5.cjs → chunk-SUDERBUA.cjs} +2 -2
  91. package/dist/{chunk-I5467ZJ5.cjs.map → chunk-SUDERBUA.cjs.map} +1 -1
  92. package/dist/{chunk-SH32YSZO.cjs → chunk-UFRBUT2D.cjs} +19 -19
  93. package/dist/{chunk-SH32YSZO.cjs.map → chunk-UFRBUT2D.cjs.map} +1 -1
  94. package/dist/{chunk-CMHPXSCI.js → chunk-VPAEBI2F.js} +109 -87
  95. package/dist/chunk-VPAEBI2F.js.map +1 -0
  96. package/dist/{chunk-CERFRCXD.cjs → chunk-X7JL2NYJ.cjs} +24 -24
  97. package/dist/chunk-X7JL2NYJ.cjs.map +1 -0
  98. package/dist/{chunk-PGJAZN2H.js → chunk-XD2HV7M5.js} +77 -92
  99. package/dist/chunk-XD2HV7M5.js.map +1 -0
  100. package/dist/{chunk-GBDFC74U.cjs → chunk-YAXY5L7I.cjs} +7 -7
  101. package/dist/{chunk-GBDFC74U.cjs.map → chunk-YAXY5L7I.cjs.map} +1 -1
  102. package/dist/{chunk-LSV7F26B.cjs → chunk-YDVHT7GS.cjs} +77 -33
  103. package/dist/chunk-YDVHT7GS.cjs.map +1 -0
  104. package/dist/components/legend/index.cjs +2 -2
  105. package/dist/components/legend/index.css +1 -4
  106. package/dist/components/legend/index.css.map +1 -1
  107. package/dist/components/legend/index.d.cts +2 -1
  108. package/dist/components/legend/index.d.ts +2 -1
  109. package/dist/components/legend/index.js +1 -1
  110. package/dist/components/tooltip/index.d.cts +2 -1
  111. package/dist/components/tooltip/index.d.ts +2 -1
  112. package/dist/hooks/index.cjs +2 -2
  113. package/dist/hooks/index.cjs.map +1 -1
  114. package/dist/hooks/index.css +1 -4
  115. package/dist/hooks/index.css.map +1 -1
  116. package/dist/hooks/index.d.cts +10 -7
  117. package/dist/hooks/index.d.ts +10 -7
  118. package/dist/hooks/index.js +3 -3
  119. package/dist/index.cjs +14 -14
  120. package/dist/index.css +24 -16
  121. package/dist/index.css.map +1 -1
  122. package/dist/index.d.cts +4 -4
  123. package/dist/index.d.ts +4 -4
  124. package/dist/index.js +13 -13
  125. package/dist/{leaderboard-chart-B5gWcqe7.d.ts → leaderboard-chart-BSgEw_Um.d.ts} +1 -1
  126. package/dist/{leaderboard-chart-C_6QDcqj.d.cts → leaderboard-chart-COtgamhe.d.cts} +1 -1
  127. package/dist/providers/index.cjs +2 -2
  128. package/dist/providers/index.css +1 -4
  129. package/dist/providers/index.css.map +1 -1
  130. package/dist/providers/index.d.cts +3 -2
  131. package/dist/providers/index.d.ts +3 -2
  132. package/dist/providers/index.js +1 -1
  133. package/dist/{themes-BDVaIfBz.d.cts → themes-CVR5rmIs.d.cts} +1 -1
  134. package/dist/{themes-mcS8QNkQ.d.ts → themes-DQzmaSze.d.ts} +1 -1
  135. package/dist/{types-BCFQlzTM.d.cts → types-CzdN7rUe.d.cts} +12 -3
  136. package/dist/{types-BCFQlzTM.d.ts → types-CzdN7rUe.d.ts} +12 -3
  137. package/dist/utils/index.d.cts +2 -1
  138. package/dist/utils/index.d.ts +2 -1
  139. package/package.json +10 -10
  140. package/src/charts/bar-chart/bar-chart.tsx +2 -9
  141. package/src/charts/bar-chart/test/bar-chart.test.tsx +3 -3
  142. package/src/charts/line-chart/line-chart.tsx +2 -2
  143. package/src/charts/line-chart/test/line-chart.test.tsx +3 -3
  144. package/src/charts/line-chart/types.ts +0 -7
  145. package/src/charts/pie-chart/pie-chart.module.scss +14 -3
  146. package/src/charts/pie-chart/pie-chart.tsx +172 -148
  147. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +17 -11
  148. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +147 -119
  149. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +46 -6
  150. package/src/charts/private/with-responsive/test/with-responsive.test.tsx +5 -5
  151. package/src/charts/private/with-responsive/with-responsive.tsx +8 -7
  152. package/src/hooks/index.ts +1 -1
  153. package/src/hooks/test/use-chart-margin.test.tsx +44 -0
  154. package/src/hooks/test/{use-element-height.test.tsx → use-element-size.test.tsx} +45 -36
  155. package/src/hooks/use-chart-margin.tsx +92 -6
  156. package/src/hooks/use-element-size.ts +43 -0
  157. package/src/hooks/use-tooltip-portal-relocator.module.scss +1 -4
  158. package/src/hooks/use-tooltip-portal-relocator.ts +11 -0
  159. package/src/types.ts +13 -3
  160. package/dist/chunk-4YYROZDJ.cjs +0 -375
  161. package/dist/chunk-4YYROZDJ.cjs.map +0 -1
  162. package/dist/chunk-66BXSWMW.cjs.map +0 -1
  163. package/dist/chunk-7QDEU3KN.cjs.map +0 -1
  164. package/dist/chunk-AWNCAKZY.js.map +0 -1
  165. package/dist/chunk-CERFRCXD.cjs.map +0 -1
  166. package/dist/chunk-CMHPXSCI.js.map +0 -1
  167. package/dist/chunk-FZYJM5PN.js.map +0 -1
  168. package/dist/chunk-HNEG3EFJ.cjs.map +0 -1
  169. package/dist/chunk-KKPZ4MVF.js.map +0 -1
  170. package/dist/chunk-LSV7F26B.cjs.map +0 -1
  171. package/dist/chunk-PGJAZN2H.js.map +0 -1
  172. package/dist/chunk-R23BFDIW.js.map +0 -1
  173. package/dist/chunk-RSYD434G.cjs.map +0 -1
  174. package/dist/chunk-TYIH5LMV.js.map +0 -1
  175. package/src/hooks/use-element-height.ts +0 -37
  176. /package/dist/{chunk-WMWAUOQ4.js.map → chunk-E62LCBGD.js.map} +0 -0
  177. /package/dist/{chunk-ZY4FXLMM.js.map → chunk-GWBS65VC.js.map} +0 -0
  178. /package/dist/{chunk-EBDUXL5K.js.map → chunk-KHQPN77E.js.map} +0 -0
  179. /package/dist/{chunk-M7PRGJFE.js.map → chunk-MDRCAGKZ.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { renderHook, waitFor } from '@testing-library/react';
2
- import { useElementHeight } from '../use-element-height';
2
+ import { useElementSize } from '../use-element-size';
3
3
 
4
4
  // Mock ResizeObserver
5
5
  class MockResizeObserver {
@@ -25,13 +25,12 @@ class MockResizeObserver {
25
25
  // Store original ResizeObserver
26
26
  const originalResizeObserver = globalThis.ResizeObserver;
27
27
 
28
- describe( 'useElementHeight', () => {
28
+ describe( 'useElementSize', () => {
29
29
  let mockResizeObserver;
30
30
 
31
31
  beforeEach( () => {
32
32
  mockResizeObserver = MockResizeObserver;
33
33
  globalThis.ResizeObserver = mockResizeObserver;
34
- globalThis.window.ResizeObserver = mockResizeObserver;
35
34
  } );
36
35
 
37
36
  afterEach( () => {
@@ -39,75 +38,82 @@ describe( 'useElementHeight', () => {
39
38
  jest.clearAllMocks();
40
39
  } );
41
40
 
42
- it( 'should return initial height of 0 by default', () => {
43
- const { result } = renderHook( () => useElementHeight() );
44
- const [ refCallback, height ] = result.current;
41
+ it( 'should return initial dimensions of 0 by default', () => {
42
+ const { result } = renderHook( () => useElementSize() );
43
+ const [ refCallback, width, height ] = result.current;
45
44
 
45
+ expect( width ).toBe( 0 );
46
46
  expect( height ).toBe( 0 );
47
47
  expect( typeof refCallback ).toBe( 'function' );
48
48
  } );
49
49
 
50
- it( 'should return custom initial height when provided', () => {
51
- const { result } = renderHook( () => useElementHeight( { initialHeight: 100 } ) );
52
- const [ , height ] = result.current;
50
+ it( 'should return custom initial dimensions when provided', () => {
51
+ const { result } = renderHook( () =>
52
+ useElementSize( { initialWidth: 200, initialHeight: 100 } )
53
+ );
54
+ const [ , width, height ] = result.current;
53
55
 
56
+ expect( width ).toBe( 200 );
54
57
  expect( height ).toBe( 100 );
55
58
  } );
56
59
 
57
- it( 'should update height when element is attached', async () => {
60
+ it( 'should update dimensions when element is attached', async () => {
58
61
  const mockElement = {
59
- getBoundingClientRect: jest.fn( () => ( { height: 150 } ) ),
62
+ getBoundingClientRect: jest.fn( () => ( { width: 300, height: 150 } ) ),
60
63
  };
61
64
 
62
- const { result } = renderHook( () => useElementHeight() );
65
+ const { result } = renderHook( () => useElementSize() );
63
66
  const [ refCallback ] = result.current;
64
67
 
65
68
  // Attach the element
66
69
  refCallback( mockElement as unknown as HTMLDivElement );
67
70
 
68
71
  await waitFor( () => {
69
- expect( result.current[ 1 ] ).toBe( 150 );
72
+ expect( result.current[ 1 ] ).toBe( 300 );
73
+ expect( result.current[ 2 ] ).toBe( 150 );
70
74
  } );
71
75
 
72
76
  expect( mockElement.getBoundingClientRect ).toHaveBeenCalled();
73
77
  } );
74
78
 
75
- it( 'should handle element with zero height', async () => {
79
+ it( 'should handle element with zero dimensions', async () => {
76
80
  const mockElement = {
77
- getBoundingClientRect: jest.fn( () => ( { height: 0 } ) ),
81
+ getBoundingClientRect: jest.fn( () => ( { width: 0, height: 0 } ) ),
78
82
  };
79
83
 
80
- const { result } = renderHook( () => useElementHeight() );
84
+ const { result } = renderHook( () => useElementSize() );
81
85
  const [ refCallback ] = result.current;
82
86
 
83
87
  refCallback( mockElement as unknown as HTMLDivElement );
84
88
 
85
89
  await waitFor( () => {
86
90
  expect( result.current[ 1 ] ).toBe( 0 );
91
+ expect( result.current[ 2 ] ).toBe( 0 );
87
92
  } );
88
93
  } );
89
94
 
90
- it( 'should handle getBoundingClientRect returning undefined height', async () => {
95
+ it( 'should handle getBoundingClientRect returning undefined dimensions', async () => {
91
96
  const mockElement = {
92
- getBoundingClientRect: jest.fn( () => ( { height: undefined } ) ),
97
+ getBoundingClientRect: jest.fn( () => ( { width: undefined, height: undefined } ) ),
93
98
  };
94
99
 
95
- const { result } = renderHook( () => useElementHeight() );
100
+ const { result } = renderHook( () => useElementSize() );
96
101
  const [ refCallback ] = result.current;
97
102
 
98
103
  refCallback( mockElement as unknown as HTMLDivElement );
99
104
 
100
105
  await waitFor( () => {
101
106
  expect( result.current[ 1 ] ).toBe( 0 );
107
+ expect( result.current[ 2 ] ).toBe( 0 );
102
108
  } );
103
109
  } );
104
110
 
105
111
  it( 'should disconnect previous observer when new element is attached', () => {
106
112
  const mockElement1 = {
107
- getBoundingClientRect: jest.fn( () => ( { height: 100 } ) ),
113
+ getBoundingClientRect: jest.fn( () => ( { width: 100, height: 100 } ) ),
108
114
  };
109
115
  const mockElement2 = {
110
- getBoundingClientRect: jest.fn( () => ( { height: 200 } ) ),
116
+ getBoundingClientRect: jest.fn( () => ( { width: 200, height: 200 } ) ),
111
117
  };
112
118
 
113
119
  const disconnectSpy = jest.fn();
@@ -119,7 +125,7 @@ describe( 'useElementHeight', () => {
119
125
 
120
126
  jest.spyOn( globalThis, 'ResizeObserver' ).mockImplementation( () => mockObserver );
121
127
 
122
- const { result } = renderHook( () => useElementHeight() );
128
+ const { result } = renderHook( () => useElementSize() );
123
129
  const [ refCallback ] = result.current;
124
130
 
125
131
  // Attach first element
@@ -133,7 +139,7 @@ describe( 'useElementHeight', () => {
133
139
 
134
140
  it( 'should disconnect observer when element is removed (null)', () => {
135
141
  const mockElement = {
136
- getBoundingClientRect: jest.fn( () => ( { height: 100 } ) ),
142
+ getBoundingClientRect: jest.fn( () => ( { width: 100, height: 100 } ) ),
137
143
  };
138
144
 
139
145
  const disconnectSpy = jest.fn();
@@ -145,7 +151,7 @@ describe( 'useElementHeight', () => {
145
151
 
146
152
  jest.spyOn( globalThis, 'ResizeObserver' ).mockImplementation( () => mockObserver );
147
153
 
148
- const { result } = renderHook( () => useElementHeight() );
154
+ const { result } = renderHook( () => useElementSize() );
149
155
  const [ refCallback ] = result.current;
150
156
 
151
157
  // Attach element
@@ -159,7 +165,7 @@ describe( 'useElementHeight', () => {
159
165
 
160
166
  it( 'should create ResizeObserver and observe element', () => {
161
167
  const mockElement = {
162
- getBoundingClientRect: jest.fn( () => ( { height: 100 } ) ),
168
+ getBoundingClientRect: jest.fn( () => ( { width: 100, height: 100 } ) ),
163
169
  };
164
170
 
165
171
  const observeSpy = jest.fn();
@@ -171,7 +177,7 @@ describe( 'useElementHeight', () => {
171
177
 
172
178
  jest.spyOn( globalThis, 'ResizeObserver' ).mockImplementation( () => mockObserver );
173
179
 
174
- const { result } = renderHook( () => useElementHeight() );
180
+ const { result } = renderHook( () => useElementSize() );
175
181
  const [ refCallback ] = result.current;
176
182
 
177
183
  refCallback( mockElement as unknown as HTMLDivElement );
@@ -181,7 +187,7 @@ describe( 'useElementHeight', () => {
181
187
  } );
182
188
 
183
189
  it( 'should maintain stable refCallback reference across re-renders', () => {
184
- const { result, rerender } = renderHook( () => useElementHeight() );
190
+ const { result, rerender } = renderHook( () => useElementSize() );
185
191
 
186
192
  const firstRefCallback = result.current[ 0 ];
187
193
 
@@ -195,26 +201,27 @@ describe( 'useElementHeight', () => {
195
201
 
196
202
  it( 'should work with different element types', async () => {
197
203
  const mockSpanElement = {
198
- getBoundingClientRect: jest.fn( () => ( { height: 50 } ) ),
204
+ getBoundingClientRect: jest.fn( () => ( { width: 75, height: 50 } ) ),
199
205
  };
200
206
 
201
- const { result } = renderHook( () => useElementHeight() );
207
+ const { result } = renderHook( () => useElementSize() );
202
208
  const [ refCallback ] = result.current;
203
209
 
204
210
  refCallback( mockSpanElement as unknown as HTMLDivElement );
205
211
 
206
212
  await waitFor( () => {
207
- expect( result.current[ 1 ] ).toBe( 50 );
213
+ expect( result.current[ 1 ] ).toBe( 75 );
214
+ expect( result.current[ 2 ] ).toBe( 50 );
208
215
  } );
209
216
  } );
210
217
 
211
- it( 'should update height when ResizeObserver callback is triggered', async () => {
218
+ it( 'should update dimensions when ResizeObserver callback is triggered', async () => {
212
219
  let resizeCallback;
213
220
  const mockElement = {
214
221
  getBoundingClientRect: jest
215
222
  .fn()
216
- .mockReturnValueOnce( { height: 100 } )
217
- .mockReturnValueOnce( { height: 200 } ),
223
+ .mockReturnValueOnce( { width: 100, height: 100 } )
224
+ .mockReturnValueOnce( { width: 200, height: 150 } ),
218
225
  };
219
226
 
220
227
  jest.spyOn( globalThis, 'ResizeObserver' ).mockImplementation( callback => {
@@ -226,13 +233,14 @@ describe( 'useElementHeight', () => {
226
233
  };
227
234
  } );
228
235
 
229
- const { result } = renderHook( () => useElementHeight() );
236
+ const { result } = renderHook( () => useElementSize() );
230
237
  const [ refCallback ] = result.current;
231
238
 
232
239
  refCallback( mockElement as unknown as HTMLDivElement );
233
240
 
234
241
  await waitFor( () => {
235
242
  expect( result.current[ 1 ] ).toBe( 100 );
243
+ expect( result.current[ 2 ] ).toBe( 100 );
236
244
  } );
237
245
 
238
246
  // Simulate resize
@@ -240,6 +248,7 @@ describe( 'useElementHeight', () => {
240
248
 
241
249
  await waitFor( () => {
242
250
  expect( result.current[ 1 ] ).toBe( 200 );
251
+ expect( result.current[ 2 ] ).toBe( 150 );
243
252
  } );
244
253
 
245
254
  expect( mockElement.getBoundingClientRect ).toHaveBeenCalledTimes( 2 );
@@ -247,7 +256,7 @@ describe( 'useElementHeight', () => {
247
256
 
248
257
  it( 'should handle unmount properly', () => {
249
258
  const mockElement = {
250
- getBoundingClientRect: jest.fn( () => ( { height: 100 } ) ),
259
+ getBoundingClientRect: jest.fn( () => ( { width: 100, height: 100 } ) ),
251
260
  };
252
261
 
253
262
  const disconnectSpy = jest.fn();
@@ -257,7 +266,7 @@ describe( 'useElementHeight', () => {
257
266
  unobserve: jest.fn(),
258
267
  } ) );
259
268
 
260
- const { result, unmount } = renderHook( () => useElementHeight() );
269
+ const { result, unmount } = renderHook( () => useElementSize() );
261
270
  const [ refCallback ] = result.current;
262
271
 
263
272
  refCallback( mockElement as unknown as HTMLDivElement );
@@ -4,6 +4,79 @@ import { getLongestTickWidth } from '../utils';
4
4
  import type { BaseChartProps, DataPointDate, SeriesData } from '../types';
5
5
  import type { XYChartTheme } from '@visx/xychart';
6
6
 
7
+ /**
8
+ * Base top margin used when no dynamic adjustments are necessary.
9
+ */
10
+ const DEFAULT_MARGIN_TOP = 10;
11
+
12
+ /**
13
+ * Base right margin used when no dynamic adjustments are necessary.
14
+ */
15
+ const DEFAULT_MARGIN_RIGHT = 20;
16
+
17
+ /**
18
+ * Base bottom margin used for charts with a bottom X-axis.
19
+ * This is large enough for typical font sizes and will be increased
20
+ * dynamically when tick labels require more space.
21
+ */
22
+ const DEFAULT_MARGIN_BOTTOM = 20;
23
+
24
+ /**
25
+ * Base left margin used when no dynamic adjustments are necessary.
26
+ */
27
+ const DEFAULT_MARGIN_LEFT = 20;
28
+
29
+ /**
30
+ * Bottom margin to use when the X-axis is rendered at the top.
31
+ * We only need a small buffer below the chart in that case.
32
+ */
33
+ const DEFAULT_BOTTOM_FOR_TOP_AXIS = 10;
34
+
35
+ /**
36
+ * Fallback font size used when we cannot derive a font size
37
+ * from the theme or axis styles for X-axis tick labels.
38
+ */
39
+ const DEFAULT_FONT_SIZE = 12;
40
+
41
+ /**
42
+ * Fallback tick length used when tickLength is not provided
43
+ * by the theme for either axis.
44
+ */
45
+ const DEFAULT_TICK_LENGTH = 8;
46
+
47
+ /**
48
+ * Fallback width used for Y-axis tick labels when we cannot
49
+ * measure them via getLongestTickWidth.
50
+ */
51
+ const DEFAULT_Y_TICK_WIDTH = 40;
52
+
53
+ const resolveFontSize = ( val?: number | string ): number | undefined => {
54
+ if ( typeof val === 'number' && ! isNaN( val ) ) {
55
+ return val;
56
+ }
57
+
58
+ if ( typeof val === 'string' ) {
59
+ const parsed = parseFloat( val );
60
+ return isNaN( parsed ) ? undefined : parsed;
61
+ }
62
+
63
+ return undefined;
64
+ };
65
+
66
+ const getXAxisLabelMetrics = ( theme: XYChartTheme, orientation: 'top' | 'bottom' ) => {
67
+ const xAxisStyles =
68
+ orientation === 'top' ? theme.axisStyles?.x?.top : theme.axisStyles?.x?.bottom;
69
+
70
+ const fontSize =
71
+ resolveFontSize( xAxisStyles?.axisLabel?.fontSize ) ||
72
+ resolveFontSize( theme.svgLabelSmall?.fontSize ) ||
73
+ DEFAULT_FONT_SIZE;
74
+
75
+ const tickLength = xAxisStyles?.tickLength ?? DEFAULT_TICK_LENGTH;
76
+
77
+ return { fontSize, tickLength };
78
+ };
79
+
7
80
  export const useChartMargin = (
8
81
  height: number,
9
82
  options: BaseChartProps[ 'options' ],
@@ -34,8 +107,12 @@ export const useChartMargin = (
34
107
 
35
108
  return useMemo( () => {
36
109
  // Default margin is for bottom axis labels.
37
- const defaultMargin = { top: 10, right: 20, bottom: 20, left: 20 };
38
- const defaultTickWidth = 40;
110
+ const defaultMargin = {
111
+ top: DEFAULT_MARGIN_TOP,
112
+ right: DEFAULT_MARGIN_RIGHT,
113
+ bottom: DEFAULT_MARGIN_BOTTOM,
114
+ left: DEFAULT_MARGIN_LEFT,
115
+ };
39
116
 
40
117
  // Auto-calculate margin for y axis labels based on orientation and tick width.
41
118
  const yAxisOrientation = options.axis?.y?.orientation;
@@ -46,7 +123,7 @@ export const useChartMargin = (
46
123
  options.axis?.y?.tickFormat,
47
124
  yAxisStyles.axisLabel
48
125
  );
49
- const yMarginValue = ( yTickWidth ?? defaultTickWidth ) + ( yAxisStyles?.tickLength ?? 0 );
126
+ const yMarginValue = ( yTickWidth ?? DEFAULT_Y_TICK_WIDTH ) + ( yAxisStyles?.tickLength ?? 0 );
50
127
 
51
128
  if ( yAxisOrientation === 'right' ) {
52
129
  defaultMargin.right = yMarginValue;
@@ -54,9 +131,18 @@ export const useChartMargin = (
54
131
  defaultMargin.left = yMarginValue;
55
132
  }
56
133
 
57
- if ( options.axis?.x?.orientation === 'top' ) {
58
- defaultMargin.top = 20;
59
- defaultMargin.bottom = 10;
134
+ // Dynamically compute X-axis margin (bottom by default, or top if orientation is 'top').
135
+ // This mirrors Y-axis behavior where margin is based on label size and tick length,
136
+ // but keeps the padding minimal so consumers can control container spacing themselves.
137
+ const xOrientation = options.axis?.x?.orientation === 'top' ? 'top' : 'bottom';
138
+ const { fontSize, tickLength } = getXAxisLabelMetrics( theme, xOrientation );
139
+ const computedXMargin = fontSize + tickLength;
140
+
141
+ if ( xOrientation === 'top' ) {
142
+ defaultMargin.top = Math.max( defaultMargin.top, computedXMargin );
143
+ defaultMargin.bottom = DEFAULT_BOTTOM_FOR_TOP_AXIS;
144
+ } else {
145
+ defaultMargin.bottom = Math.max( defaultMargin.bottom, computedXMargin );
60
146
  }
61
147
 
62
148
  return defaultMargin;
@@ -0,0 +1,43 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * Hook to measure the width and height of a DOM element.
5
+ * Returns a ref callback to attach to the element and the current dimensions in pixels.
6
+ *
7
+ * @param {object} props - Optional props.
8
+ * @param {number} props.initialWidth - The initial width to use.
9
+ * @param {number} props.initialHeight - The initial height to use.
10
+ *
11
+ * @return {[Function, number, number]} A tuple containing a ref callback, width, and height in pixels
12
+ */
13
+ export function useElementSize< T extends HTMLElement = HTMLDivElement >( {
14
+ initialWidth = 0,
15
+ initialHeight = 0,
16
+ }: {
17
+ initialWidth?: number;
18
+ initialHeight?: number;
19
+ } = {} ): [ ( node: T | null ) => void, number, number ] {
20
+ const [ width, setWidth ] = useState( initialWidth );
21
+ const [ height, setHeight ] = useState( initialHeight );
22
+ const observerRef = useRef< ResizeObserver | null >( null );
23
+
24
+ const refCallback = useCallback( ( node: T | null ) => {
25
+ if ( observerRef.current ) {
26
+ observerRef.current.disconnect();
27
+ observerRef.current = null;
28
+ }
29
+ if ( node ) {
30
+ const handleResize = () => {
31
+ const rect = node.getBoundingClientRect();
32
+ setWidth( rect.width || 0 );
33
+ setHeight( rect.height || 0 );
34
+ };
35
+ handleResize();
36
+ const resizeObserver = new ResizeObserver( handleResize );
37
+ resizeObserver.observe( node );
38
+ observerRef.current = resizeObserver;
39
+ }
40
+ }, [] );
41
+
42
+ return [ refCallback, width, height ];
43
+ }
@@ -1,9 +1,6 @@
1
1
  .relocatedPortal {
2
2
  position: fixed;
3
- top: 0;
4
- left: 0;
5
- width: 0;
6
- height: 0;
3
+ inset: 0;
7
4
  overflow: visible;
8
5
  z-index: 1;
9
6
  pointer-events: none;
@@ -107,6 +107,10 @@ export function useTooltipPortalRelocator(
107
107
  return;
108
108
  }
109
109
 
110
+ // Hide the portal immediately to prevent the tooltip from
111
+ // flashing at (0,0) before visx calculates the correct position.
112
+ node.style.opacity = '0';
113
+
110
114
  // Position the portal at the viewport origin so visx's
111
115
  // absolute-positioned tooltip coordinates remain correct.
112
116
  // Zero-size with overflow: visible so it doesn't affect layout
@@ -131,6 +135,13 @@ export function useTooltipPortalRelocator(
131
135
  if ( focusedElement ) {
132
136
  focusedElement.focus();
133
137
  }
138
+
139
+ // Reveal after two animation frames so visx has positioned the tooltip.
140
+ requestAnimationFrame( () => {
141
+ requestAnimationFrame( () => {
142
+ node.style.opacity = '';
143
+ } );
144
+ } );
134
145
  };
135
146
 
136
147
  // Patch document.body.removeChild so visx Portal unmount doesn't throw
package/src/types.ts CHANGED
@@ -7,6 +7,7 @@ import type { LegendShape } from '@visx/legend/lib/types';
7
7
  import type { ScaleInput, ScaleType } from '@visx/scale';
8
8
  import type { TextProps } from '@visx/text/lib/Text';
9
9
  import type { EventHandlerParams, GlyphProps, GridStyles, LineStyles } from '@visx/xychart';
10
+ import type { GapSize } from '@wordpress/theme';
10
11
  import type { CSSProperties, PointerEvent, ReactNode } from 'react';
11
12
  import type { GoogleDataTableColumn, GoogleDataTableRow } from 'react-google-charts';
12
13
 
@@ -364,15 +365,17 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
364
365
  */
365
366
  className?: string;
366
367
  /**
367
- * Width of the chart in pixels
368
+ * Width of the chart container in pixels. When omitted, the chart fills its parent's width.
368
369
  */
369
370
  width?: number;
370
371
  /**
371
- * Height of the chart in pixels
372
+ * Height of the chart container in pixels. When omitted, the chart fills its parent's height.
372
373
  */
373
374
  height?: number;
374
375
  /**
375
- * Size of the chart in pixels for pie and donut charts
376
+ * Maximum diameter of the pie in pixels (pie and donut charts only).
377
+ * The pie will shrink if the container is smaller than this value.
378
+ * When omitted, the pie fills the available space.
376
379
  */
377
380
  size?: number;
378
381
  /**
@@ -457,6 +460,13 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
457
460
  */
458
461
  animation?: boolean;
459
462
 
463
+ /**
464
+ * Gap between chart elements (SVG, legend, children).
465
+ * Uses WordPress design system tokens.
466
+ * @default 'md'
467
+ */
468
+ gap?: GapSize;
469
+
460
470
  /**
461
471
  * More options for the chart.
462
472
  */