@automattic/charts 0.56.5 → 0.56.7

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/AGENTS.md +135 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -1
  4. package/dist/charts/bar-chart/index.cjs +6 -6
  5. package/dist/charts/bar-chart/index.css +1 -4
  6. package/dist/charts/bar-chart/index.css.map +1 -1
  7. package/dist/charts/bar-chart/index.d.cts +2 -8
  8. package/dist/charts/bar-chart/index.d.ts +2 -8
  9. package/dist/charts/bar-chart/index.js +5 -5
  10. package/dist/charts/bar-list-chart/index.cjs +7 -7
  11. package/dist/charts/bar-list-chart/index.css +1 -4
  12. package/dist/charts/bar-list-chart/index.css.map +1 -1
  13. package/dist/charts/bar-list-chart/index.d.cts +2 -2
  14. package/dist/charts/bar-list-chart/index.d.ts +2 -2
  15. package/dist/charts/bar-list-chart/index.js +6 -6
  16. package/dist/charts/conversion-funnel-chart/index.cjs +5 -5
  17. package/dist/charts/conversion-funnel-chart/index.css +1 -4
  18. package/dist/charts/conversion-funnel-chart/index.css.map +1 -1
  19. package/dist/charts/conversion-funnel-chart/index.d.cts +2 -1
  20. package/dist/charts/conversion-funnel-chart/index.d.ts +2 -1
  21. package/dist/charts/conversion-funnel-chart/index.js +4 -4
  22. package/dist/charts/geo-chart/index.cjs +4 -4
  23. package/dist/charts/geo-chart/index.css +1 -4
  24. package/dist/charts/geo-chart/index.css.map +1 -1
  25. package/dist/charts/geo-chart/index.d.cts +2 -1
  26. package/dist/charts/geo-chart/index.d.ts +2 -1
  27. package/dist/charts/geo-chart/index.js +3 -3
  28. package/dist/charts/leaderboard-chart/index.cjs +5 -5
  29. package/dist/charts/leaderboard-chart/index.css +1 -4
  30. package/dist/charts/leaderboard-chart/index.css.map +1 -1
  31. package/dist/charts/leaderboard-chart/index.d.cts +3 -2
  32. package/dist/charts/leaderboard-chart/index.d.ts +3 -2
  33. package/dist/charts/leaderboard-chart/index.js +4 -4
  34. package/dist/charts/line-chart/index.cjs +6 -6
  35. package/dist/charts/line-chart/index.css +1 -4
  36. package/dist/charts/line-chart/index.css.map +1 -1
  37. package/dist/charts/line-chart/index.d.cts +2 -8
  38. package/dist/charts/line-chart/index.d.ts +2 -8
  39. package/dist/charts/line-chart/index.js +5 -5
  40. package/dist/charts/pie-chart/index.cjs +6 -4
  41. package/dist/charts/pie-chart/index.cjs.map +1 -1
  42. package/dist/charts/pie-chart/index.css +13 -7
  43. package/dist/charts/pie-chart/index.css.map +1 -1
  44. package/dist/charts/pie-chart/index.d.cts +2 -1
  45. package/dist/charts/pie-chart/index.d.ts +2 -1
  46. package/dist/charts/pie-chart/index.js +5 -3
  47. package/dist/charts/pie-semi-circle-chart/index.cjs +6 -4
  48. package/dist/charts/pie-semi-circle-chart/index.cjs.map +1 -1
  49. package/dist/charts/pie-semi-circle-chart/index.css +12 -13
  50. package/dist/charts/pie-semi-circle-chart/index.css.map +1 -1
  51. package/dist/charts/pie-semi-circle-chart/index.d.cts +5 -2
  52. package/dist/charts/pie-semi-circle-chart/index.d.ts +5 -2
  53. package/dist/charts/pie-semi-circle-chart/index.js +5 -3
  54. package/dist/charts/sparkline/index.cjs +7 -7
  55. package/dist/charts/sparkline/index.css +1 -4
  56. package/dist/charts/sparkline/index.css.map +1 -1
  57. package/dist/charts/sparkline/index.js +6 -6
  58. package/dist/{chunk-NGHXTIUE.cjs → chunk-3EXJP67N.cjs} +7 -7
  59. package/dist/{chunk-NGHXTIUE.cjs.map → chunk-3EXJP67N.cjs.map} +1 -1
  60. package/dist/{chunk-FIFSYVN6.cjs → chunk-55ZCOYDF.cjs} +117 -132
  61. package/dist/chunk-55ZCOYDF.cjs.map +1 -0
  62. package/dist/{chunk-LT4YOIMM.js → chunk-7FDQGBY7.js} +145 -119
  63. package/dist/chunk-7FDQGBY7.js.map +1 -0
  64. package/dist/{chunk-7QDEU3KN.cjs → chunk-ASLARV7L.cjs} +6 -6
  65. package/dist/chunk-ASLARV7L.cjs.map +1 -0
  66. package/dist/chunk-BXFD7JIG.cjs +401 -0
  67. package/dist/chunk-BXFD7JIG.cjs.map +1 -0
  68. package/dist/{chunk-XCXAWMJQ.cjs → chunk-CAFJRZPZ.cjs} +12 -12
  69. package/dist/{chunk-XCXAWMJQ.cjs.map → chunk-CAFJRZPZ.cjs.map} +1 -1
  70. package/dist/{chunk-KHRPRH4V.js → chunk-E62LCBGD.js} +4 -4
  71. package/dist/{chunk-PCOI2GT5.js → chunk-GWBS65VC.js} +3 -3
  72. package/dist/{chunk-MEIVKY4K.js → chunk-IS5YYLTV.js} +18 -18
  73. package/dist/{chunk-MEIVKY4K.js.map → chunk-IS5YYLTV.js.map} +1 -1
  74. package/dist/{chunk-Q6G3BGCL.cjs → chunk-K6TGILHX.cjs} +8 -8
  75. package/dist/{chunk-Q6G3BGCL.cjs.map → chunk-K6TGILHX.cjs.map} +1 -1
  76. package/dist/{chunk-X6GX4QUJ.js → chunk-KHQPN77E.js} +3 -3
  77. package/dist/{chunk-SEKPIG5K.js → chunk-KNIMXN6Z.js} +2 -2
  78. package/dist/{chunk-SEKPIG5K.js.map → chunk-KNIMXN6Z.js.map} +1 -1
  79. package/dist/{chunk-AFWQR3SM.js → chunk-MDRCAGKZ.js} +4 -4
  80. package/dist/{chunk-TKPK4RFS.cjs → chunk-NQJE2CC7.cjs} +120 -98
  81. package/dist/chunk-NQJE2CC7.cjs.map +1 -0
  82. package/dist/{chunk-FY325WQ4.cjs → chunk-O2JIANHK.cjs} +25 -25
  83. package/dist/chunk-O2JIANHK.cjs.map +1 -0
  84. package/dist/{chunk-DLSUC7RN.js → chunk-OMS5QIJN.js} +6 -6
  85. package/dist/chunk-OMS5QIJN.js.map +1 -0
  86. package/dist/{chunk-TYIH5LMV.js → chunk-OP6PHB2U.js} +6 -6
  87. package/dist/chunk-OP6PHB2U.js.map +1 -0
  88. package/dist/{chunk-32ESS4MV.js → chunk-RFSHE3HL.js} +17 -7
  89. package/dist/chunk-RFSHE3HL.js.map +1 -0
  90. package/dist/{chunk-KXSLMOW5.js → chunk-SSFFCBCF.js} +6 -6
  91. package/dist/chunk-SSFFCBCF.js.map +1 -0
  92. package/dist/{chunk-I5467ZJ5.cjs → chunk-SUDERBUA.cjs} +2 -2
  93. package/dist/{chunk-I5467ZJ5.cjs.map → chunk-SUDERBUA.cjs.map} +1 -1
  94. package/dist/{chunk-SH32YSZO.cjs → chunk-UFRBUT2D.cjs} +19 -19
  95. package/dist/{chunk-SH32YSZO.cjs.map → chunk-UFRBUT2D.cjs.map} +1 -1
  96. package/dist/{chunk-7TQSPLIN.js → chunk-VPAEBI2F.js} +109 -87
  97. package/dist/chunk-VPAEBI2F.js.map +1 -0
  98. package/dist/{chunk-IHESL7H5.cjs → chunk-X7JL2NYJ.cjs} +24 -24
  99. package/dist/chunk-X7JL2NYJ.cjs.map +1 -0
  100. package/dist/{chunk-DBY6C4O2.js → chunk-XD2HV7M5.js} +77 -92
  101. package/dist/chunk-XD2HV7M5.js.map +1 -0
  102. package/dist/{chunk-LTPJPIDP.cjs → chunk-YAXY5L7I.cjs} +7 -7
  103. package/dist/{chunk-LTPJPIDP.cjs.map → chunk-YAXY5L7I.cjs.map} +1 -1
  104. package/dist/{chunk-EJJO2QNB.cjs → chunk-YDVHT7GS.cjs} +17 -7
  105. package/dist/chunk-YDVHT7GS.cjs.map +1 -0
  106. package/dist/components/legend/index.cjs +2 -2
  107. package/dist/components/legend/index.css +1 -4
  108. package/dist/components/legend/index.css.map +1 -1
  109. package/dist/components/legend/index.d.cts +2 -1
  110. package/dist/components/legend/index.d.ts +2 -1
  111. package/dist/components/legend/index.js +1 -1
  112. package/dist/components/tooltip/index.d.cts +2 -1
  113. package/dist/components/tooltip/index.d.ts +2 -1
  114. package/dist/hooks/index.cjs +2 -2
  115. package/dist/hooks/index.cjs.map +1 -1
  116. package/dist/hooks/index.css +1 -4
  117. package/dist/hooks/index.css.map +1 -1
  118. package/dist/hooks/index.d.cts +10 -7
  119. package/dist/hooks/index.d.ts +10 -7
  120. package/dist/hooks/index.js +3 -3
  121. package/dist/index.cjs +14 -14
  122. package/dist/index.css +24 -16
  123. package/dist/index.css.map +1 -1
  124. package/dist/index.d.cts +4 -4
  125. package/dist/index.d.ts +4 -4
  126. package/dist/index.js +13 -13
  127. package/dist/{leaderboard-chart-B5gWcqe7.d.ts → leaderboard-chart-BSgEw_Um.d.ts} +1 -1
  128. package/dist/{leaderboard-chart-C_6QDcqj.d.cts → leaderboard-chart-COtgamhe.d.cts} +1 -1
  129. package/dist/providers/index.cjs +2 -2
  130. package/dist/providers/index.css +1 -4
  131. package/dist/providers/index.css.map +1 -1
  132. package/dist/providers/index.d.cts +3 -2
  133. package/dist/providers/index.d.ts +3 -2
  134. package/dist/providers/index.js +1 -1
  135. package/dist/{themes-BDVaIfBz.d.cts → themes-CVR5rmIs.d.cts} +1 -1
  136. package/dist/{themes-mcS8QNkQ.d.ts → themes-DQzmaSze.d.ts} +1 -1
  137. package/dist/{types-BCFQlzTM.d.ts → types-CzdN7rUe.d.cts} +12 -3
  138. package/dist/{types-BCFQlzTM.d.cts → types-CzdN7rUe.d.ts} +12 -3
  139. package/dist/utils/index.d.cts +2 -1
  140. package/dist/utils/index.d.ts +2 -1
  141. package/package.json +9 -9
  142. package/src/charts/bar-chart/bar-chart.tsx +2 -9
  143. package/src/charts/bar-chart/test/bar-chart.test.tsx +3 -3
  144. package/src/charts/line-chart/line-chart.tsx +2 -2
  145. package/src/charts/line-chart/test/line-chart.test.tsx +3 -3
  146. package/src/charts/line-chart/types.ts +0 -7
  147. package/src/charts/pie-chart/pie-chart.module.scss +14 -3
  148. package/src/charts/pie-chart/pie-chart.tsx +172 -148
  149. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +17 -11
  150. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +147 -119
  151. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +46 -6
  152. package/src/charts/private/with-responsive/test/with-responsive.test.tsx +5 -5
  153. package/src/charts/private/with-responsive/with-responsive.tsx +8 -7
  154. package/src/hooks/index.ts +1 -1
  155. package/src/hooks/test/{use-element-height.test.tsx → use-element-size.test.tsx} +45 -36
  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-32ESS4MV.js.map +0 -1
  161. package/dist/chunk-7QDEU3KN.cjs.map +0 -1
  162. package/dist/chunk-7TQSPLIN.js.map +0 -1
  163. package/dist/chunk-DBY6C4O2.js.map +0 -1
  164. package/dist/chunk-DLSUC7RN.js.map +0 -1
  165. package/dist/chunk-EJJO2QNB.cjs.map +0 -1
  166. package/dist/chunk-FIFSYVN6.cjs.map +0 -1
  167. package/dist/chunk-FY325WQ4.cjs.map +0 -1
  168. package/dist/chunk-IHESL7H5.cjs.map +0 -1
  169. package/dist/chunk-JL4ZKKZU.cjs +0 -375
  170. package/dist/chunk-JL4ZKKZU.cjs.map +0 -1
  171. package/dist/chunk-KXSLMOW5.js.map +0 -1
  172. package/dist/chunk-LT4YOIMM.js.map +0 -1
  173. package/dist/chunk-TKPK4RFS.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-KHRPRH4V.js.map → chunk-E62LCBGD.js.map} +0 -0
  177. /package/dist/{chunk-PCOI2GT5.js.map → chunk-GWBS65VC.js.map} +0 -0
  178. /package/dist/{chunk-X6GX4QUJ.js.map → chunk-KHQPN77E.js.map} +0 -0
  179. /package/dist/{chunk-AFWQR3SM.js.map → chunk-MDRCAGKZ.js.map} +0 -0
@@ -3,11 +3,12 @@ import { Pie } from '@visx/shape';
3
3
  import { Text } from '@visx/text';
4
4
  import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
5
5
  import { __ } from '@wordpress/i18n';
6
+ import { Stack } from '@wordpress/ui';
6
7
  import clsx from 'clsx';
7
8
  import { useCallback, useContext, useMemo } from 'react';
8
9
  import { Legend, useChartLegendItems } from '../../components/legend';
9
10
  import { BaseTooltip } from '../../components/tooltip';
10
- import { useElementHeight, useInteractiveLegendData, usePrefersReducedMotion } from '../../hooks';
11
+ import { useElementSize, useInteractiveLegendData, usePrefersReducedMotion } from '../../hooks';
11
12
  import {
12
13
  GlobalChartsProvider,
13
14
  useChartId,
@@ -52,10 +53,13 @@ const renderDefaultPieSemiCircleTooltip = ( {
52
53
  };
53
54
 
54
55
  const PAD_ANGLE = 0.03; // Padding between segments
56
+ const DEFAULT_WIDTH = 400;
55
57
 
56
58
  export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercentage[] > {
57
59
  /**
58
- * Width of the chart in pixels; height would be half of this value calculated automatically.
60
+ * Explicit width of the chart container in pixels.
61
+ * When omitted, the chart fills its parent container's width.
62
+ * The chart always maintains a 2:1 width-to-height ratio, constrained by available space.
59
63
  */
60
64
  width?: number;
61
65
 
@@ -157,7 +161,8 @@ const validateData = ( data: DataPointPercentage[] ) => {
157
161
  const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
158
162
  data,
159
163
  chartId: providedChartId,
160
- width = 400,
164
+ width: propWidth,
165
+ height: propHeight,
161
166
  thickness = 0.4,
162
167
  clockwise = true,
163
168
  withTooltips = false,
@@ -179,9 +184,11 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
179
184
  tooltipOffsetX = 0,
180
185
  tooltipOffsetY = -15,
181
186
  renderTooltip = renderDefaultPieSemiCircleTooltip,
187
+ gap = 'md',
182
188
  } ) => {
183
189
  const chartId = useChartId( providedChartId );
184
- const [ legendRef, legendHeight ] = useElementHeight< HTMLDivElement >();
190
+ // Measure the SVG wrapper to calculate constrained dimensions
191
+ const [ svgWrapperRef, svgWrapperWidth, svgWrapperHeight ] = useElementSize< HTMLDivElement >();
185
192
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
186
193
  useTooltip< DataPointPercentage >();
187
194
 
@@ -295,10 +302,17 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
295
302
 
296
303
  const prefersReducedMotion = usePrefersReducedMotion();
297
304
 
305
+ const effectiveWidth = propWidth || DEFAULT_WIDTH;
306
+
298
307
  if ( ! isValid ) {
308
+ const errorWidth = propHeight
309
+ ? Math.min( propWidth || propHeight * 2, propHeight * 2 )
310
+ : effectiveWidth;
311
+ const errorHeight = errorWidth / 2;
312
+
299
313
  return (
300
314
  <div className={ styles[ 'pie-semi-circle-chart' ] }>
301
- <svg width={ width } height={ width / 2 } data-testid="pie-chart-svg">
315
+ <svg width={ errorWidth } height={ errorHeight } data-testid="pie-chart-svg">
302
316
  <text x="50%" y="50%" textAnchor="middle" className={ styles.error }>
303
317
  { message }
304
318
  </text>
@@ -307,12 +321,16 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
307
321
  );
308
322
  }
309
323
 
310
- // Calculate chart dimensions
311
- // TODO: we might want to accept height as a prop in the future, because the height of container might not always be enough.
324
+ // Calculate chart dimensions maintaining the 2:1 width-to-height ratio.
325
+ // Use measured SVG wrapper dimensions to respect height constraints, falling back
326
+ // to explicit props during initial render before measurement is available.
327
+ const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : effectiveWidth;
328
+ const availableHeight =
329
+ svgWrapperHeight > 0 ? svgWrapperHeight : propHeight || effectiveWidth / 2;
330
+ // Constrain width so that height (= width / 2) never exceeds the available height
331
+ const width = Math.min( availableWidth, availableHeight * 2 );
312
332
  const height = width / 2;
313
- // The chart only takes the height minus the legend height.
314
- const chartHeight = height - ( showLegend && legendPosition === 'top' ? legendHeight : 0 );
315
- const radius = Math.min( width / 2, chartHeight );
333
+ const radius = height; // For a semi-circle, radius equals the SVG height
316
334
  const innerRadius = radius * ( 1 - thickness );
317
335
 
318
336
  // Map data with index for color assignment
@@ -329,119 +347,144 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
329
347
  const startAngle = clockwise ? -Math.PI / 2 : Math.PI / 2;
330
348
  const endAngle = clockwise ? Math.PI / 2 : -Math.PI / 2;
331
349
 
350
+ const legendElement = showLegend && (
351
+ <Legend
352
+ orientation={ legendOrientation }
353
+ position={ legendPosition }
354
+ alignment={ legendAlignment }
355
+ maxWidth={ legendMaxWidth }
356
+ textOverflow={ legendTextOverflow }
357
+ legendItemClassName={ legendItemClassName }
358
+ shape={ legendShape }
359
+ chartId={ chartId }
360
+ interactive={ legendInteractive }
361
+ />
362
+ );
363
+
332
364
  return (
333
365
  <SingleChartContext.Provider
334
366
  value={ {
335
367
  chartId,
336
368
  chartWidth: width,
337
- chartHeight: radius,
369
+ chartHeight: height,
338
370
  } }
339
371
  >
340
- <div
372
+ <Stack
341
373
  ref={ containerRef }
374
+ direction="column"
375
+ gap={ gap }
342
376
  className={ clsx(
343
377
  'pie-semi-circle-chart',
344
378
  styles[ 'pie-semi-circle-chart' ],
345
379
  {
346
- [ styles[ 'pie-semi-circle-chart--legend-top' ] ]:
347
- showLegend && legendPosition === 'top',
380
+ [ styles[ 'pie-semi-circle-chart--responsive' ] ]: ! propWidth && ! propHeight,
348
381
  },
349
382
  className
350
383
  ) }
384
+ style={ {
385
+ width: propWidth || undefined,
386
+ height: propHeight || undefined,
387
+ } }
351
388
  data-testid="pie-chart-container"
352
389
  >
353
- <svg
354
- width={ width }
355
- height={ radius }
356
- viewBox={ `0 0 ${ width } ${ chartHeight }` }
357
- data-testid="pie-chart-svg"
358
- >
359
- <defs>
360
- <RadialWipeAnimation
361
- id={ `radial-wipe-${ chartId }` }
362
- radius={ radius }
363
- innerRadius={ innerRadius }
364
- startAngle="-180deg"
365
- wipePercentage={ 50 }
366
- />
367
- </defs>
368
-
369
- { /* Main chart group centered horizontally and positioned at bottom */ }
370
- <Group
371
- top={ chartHeight }
372
- left={ width / 2 }
373
- mask={ animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null }
390
+ { legendPosition === 'top' && legendElement }
391
+
392
+ <div ref={ svgWrapperRef } className={ styles[ 'pie-semi-circle-chart__svg-wrapper' ] }>
393
+ <svg
394
+ width={ width }
395
+ height={ height }
396
+ viewBox={ `0 0 ${ width } ${ height }` }
397
+ data-testid="pie-chart-svg"
374
398
  >
375
- { allSegmentsHidden ? (
376
- <text
377
- textAnchor="middle"
378
- y={ -radius / 2 }
379
- fill="#ccc"
380
- fontSize="14"
381
- fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
382
- >
383
- { __(
384
- 'All segments are hidden. Click legend items to show data.',
385
- 'jetpack-charts'
386
- ) }
387
- </text>
388
- ) : (
389
- <>
390
- { /* Pie chart */ }
391
- <Pie< DataPointPercentage & { index: number } >
392
- data={ dataWithIndex }
393
- pieValue={ accessors.value }
394
- outerRadius={ radius }
395
- innerRadius={ innerRadius }
396
- cornerRadius={ 3 }
397
- padAngle={ PAD_ANGLE }
398
- startAngle={ startAngle }
399
- endAngle={ endAngle }
400
- pieSort={ accessors.sort }
399
+ <defs>
400
+ <RadialWipeAnimation
401
+ id={ `radial-wipe-${ chartId }` }
402
+ radius={ radius }
403
+ innerRadius={ innerRadius }
404
+ startAngle="-180deg"
405
+ wipePercentage={ 50 }
406
+ />
407
+ </defs>
408
+
409
+ { /* Main chart group centered horizontally and positioned at bottom */ }
410
+ <Group
411
+ top={ height }
412
+ left={ width / 2 }
413
+ mask={ animation && ! prefersReducedMotion ? `url(#radial-wipe-${ chartId })` : null }
414
+ >
415
+ { allSegmentsHidden ? (
416
+ <text
417
+ textAnchor="middle"
418
+ y={ -radius / 2 }
419
+ fill="#ccc"
420
+ fontSize="14"
421
+ fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
401
422
  >
402
- { pie => {
403
- return pie.arcs.map( arc => (
404
- <g
405
- key={ arc.data.label }
406
- onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
407
- onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
408
- >
409
- <path
410
- d={ pie.path( arc ) || '' }
411
- fill={ accessors.fill( arc.data ) }
412
- data-testid="pie-segment"
413
- />
414
- </g>
415
- ) );
416
- } }
417
- </Pie>
418
-
419
- { /* Label and note text */ }
420
- <Group>
421
- <Text
422
- textAnchor="middle"
423
- verticalAnchor="start"
424
- y={ -40 } // Position above the chart with space for note
425
- className={ styles.label }
426
- >
427
- { label }
428
- </Text>
429
- <Text
430
- textAnchor="middle"
431
- verticalAnchor="start"
432
- y={ -20 } // Position between label and chart
433
- className={ styles.note }
423
+ { __(
424
+ 'All segments are hidden. Click legend items to show data.',
425
+ 'jetpack-charts'
426
+ ) }
427
+ </text>
428
+ ) : (
429
+ <>
430
+ { /* Pie chart */ }
431
+ <Pie< DataPointPercentage & { index: number } >
432
+ data={ dataWithIndex }
433
+ pieValue={ accessors.value }
434
+ outerRadius={ radius }
435
+ innerRadius={ innerRadius }
436
+ cornerRadius={ 3 }
437
+ padAngle={ PAD_ANGLE }
438
+ startAngle={ startAngle }
439
+ endAngle={ endAngle }
440
+ pieSort={ accessors.sort }
434
441
  >
435
- { note }
436
- </Text>
437
- </Group>
438
-
439
- { /* Render SVG children from composition API */ }
440
- { ! allSegmentsHidden && svgChildren }
441
- </>
442
- ) }
443
- </Group>
444
- </svg>
442
+ { pie => {
443
+ return pie.arcs.map( arc => (
444
+ <g
445
+ key={ arc.data.label }
446
+ onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
447
+ onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
448
+ >
449
+ <path
450
+ d={ pie.path( arc ) || '' }
451
+ fill={ accessors.fill( arc.data ) }
452
+ data-testid="pie-segment"
453
+ />
454
+ </g>
455
+ ) );
456
+ } }
457
+ </Pie>
458
+
459
+ { /* Label and note text */ }
460
+ <Group>
461
+ <Text
462
+ textAnchor="middle"
463
+ verticalAnchor="start"
464
+ y={ -40 } // Position above the chart with space for note
465
+ className={ styles.label }
466
+ >
467
+ { label }
468
+ </Text>
469
+ <Text
470
+ textAnchor="middle"
471
+ verticalAnchor="start"
472
+ y={ -20 } // Position between label and chart
473
+ className={ styles.note }
474
+ >
475
+ { note }
476
+ </Text>
477
+ </Group>
478
+
479
+ { /* Render SVG children from composition API */ }
480
+ { ! allSegmentsHidden && svgChildren }
481
+ </>
482
+ ) }
483
+ </Group>
484
+ </svg>
485
+ </div>
486
+
487
+ { legendPosition !== 'top' && legendElement }
445
488
 
446
489
  { withTooltips && tooltipOpen && tooltipData && (
447
490
  <TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
@@ -449,27 +492,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
449
492
  </TooltipInPortal>
450
493
  ) }
451
494
 
452
- { showLegend && (
453
- <Legend
454
- orientation={ legendOrientation }
455
- position={ legendPosition }
456
- alignment={ legendAlignment }
457
- maxWidth={ legendMaxWidth }
458
- textOverflow={ legendTextOverflow }
459
- legendItemClassName={ legendItemClassName }
460
- shape={ legendShape }
461
- ref={ legendRef }
462
- chartId={ chartId }
463
- interactive={ legendInteractive }
464
- />
465
- ) }
466
-
467
495
  { /* Render HTML children from composition API */ }
468
496
  { htmlChildren }
469
497
 
470
498
  { /* Render any other children that aren't compound components */ }
471
499
  { otherChildren }
472
- </div>
500
+ </Stack>
473
501
  </SingleChartContext.Provider>
474
502
  );
475
503
  };
@@ -3,6 +3,15 @@ import userEvent from '@testing-library/user-event';
3
3
  import { GlobalChartsProvider } from '../../../providers';
4
4
  import PieSemiCircleChart from '../pie-semi-circle-chart';
5
5
 
6
+ // Mock useParentSize so the responsive wrapper returns predictable dimensions in tests
7
+ jest.mock( '@visx/responsive', () => ( {
8
+ useParentSize: jest.fn( () => ( {
9
+ parentRef: { current: null },
10
+ width: 400,
11
+ height: 200,
12
+ } ) ),
13
+ } ) );
14
+
6
15
  // Mock data for testing
7
16
  const mockData = [
8
17
  {
@@ -167,15 +176,15 @@ describe( 'PieSemiCircleChart', () => {
167
176
  expect( thinPathD ).not.toBe( thickPathD );
168
177
  } );
169
178
 
170
- it( 'renders with correct dimensions', () => {
171
- const width = 400;
172
- render( <PieSemiCircleChart data={ mockData } width={ width } /> );
179
+ it( 'renders with correct dimensions from measured container', () => {
180
+ // Mock returns width:400, height:200 — chart should render at 400×200 (2:1 ratio)
181
+ render( <PieSemiCircleChart data={ mockData } /> );
173
182
 
174
183
  const svg = screen.getByTestId( 'pie-chart-svg' );
175
184
 
176
- expect( svg ).toHaveAttribute( 'width', width.toString() );
177
- expect( svg ).toHaveAttribute( 'height', ( width / 2 ).toString() );
178
- expect( svg ).toHaveAttribute( 'viewBox', `0 0 ${ width } ${ width / 2 }` );
185
+ expect( svg ).toHaveAttribute( 'width', '400' );
186
+ expect( svg ).toHaveAttribute( 'height', '200' );
187
+ expect( svg ).toHaveAttribute( 'viewBox', '0 0 400 200' );
179
188
  } );
180
189
 
181
190
  describe( 'Data Validation', () => {
@@ -216,6 +225,37 @@ describe( 'PieSemiCircleChart', () => {
216
225
  } );
217
226
  } );
218
227
 
228
+ describe( 'Responsive wrapper', () => {
229
+ it( 'fills parent container (height:100%) by default', () => {
230
+ render( <PieSemiCircleChart data={ mockData } /> );
231
+ const wrapper = screen.getByTestId( 'responsive-wrapper' );
232
+ expect( wrapper ).toHaveStyle( { height: '100%' } );
233
+ } );
234
+
235
+ it( 'constrains chart to 2:1 ratio from measured dimensions', () => {
236
+ // Mock returns width:400, height:200, so chart renders at 400×200 (2:1 ratio)
237
+ render( <PieSemiCircleChart data={ mockData } /> );
238
+ const svg = screen.getByTestId( 'pie-chart-svg' );
239
+ expect( svg ).toHaveAttribute( 'width', '400' );
240
+ expect( svg ).toHaveAttribute( 'height', '200' );
241
+ } );
242
+
243
+ it( 'constrains chart width when container height is shorter than 2:1 ratio', () => {
244
+ // If parent height is 100px, chart should be at most 200×100 (not 400×200)
245
+ const { useParentSize } = jest.requireMock( '@visx/responsive' );
246
+ useParentSize.mockReturnValueOnce( {
247
+ parentRef: { current: null },
248
+ width: 400,
249
+ height: 100,
250
+ } );
251
+ render( <PieSemiCircleChart data={ mockData } /> );
252
+ const svg = screen.getByTestId( 'pie-chart-svg' );
253
+ // chartWidth = min(400, 100*2) = 200, chartHeight = 100
254
+ expect( svg ).toHaveAttribute( 'width', '200' );
255
+ expect( svg ).toHaveAttribute( 'height', '100' );
256
+ } );
257
+ } );
258
+
219
259
  describe( 'Interactive Legend', () => {
220
260
  test( 'filters segments when interactive legend is enabled and segment is toggled', async () => {
221
261
  const user = userEvent.setup();
@@ -47,10 +47,10 @@ describe( 'withResponsive', () => {
47
47
  expect( component ).toHaveStyle( { width: '400px' } );
48
48
  } );
49
49
 
50
- test( 'passes size prop equal to container width', () => {
51
- render( <ResponsiveComponent data={ [] } /> );
50
+ test( 'passes explicit size prop through to component', () => {
51
+ render( <ResponsiveComponent data={ [] } size={ 200 } /> );
52
52
  const component = screen.getByTestId( 'mock-component' );
53
- expect( component ).toHaveAttribute( 'data-size', '600' );
53
+ expect( component ).toHaveAttribute( 'data-size', '200' );
54
54
  } );
55
55
  } );
56
56
 
@@ -61,8 +61,8 @@ describe( 'withResponsive', () => {
61
61
  expect( wrapper ).toHaveStyle( { width: '100%', height: '100%' } );
62
62
  } );
63
63
 
64
- test( 'wrapper uses size prop for dimensions when provided', () => {
65
- render( <ResponsiveComponent data={ [] } size={ 200 } /> );
64
+ test( 'wrapper uses explicit width/height for dimensions when provided', () => {
65
+ render( <ResponsiveComponent data={ [] } width={ 200 } height={ 200 } /> );
66
66
  const wrapper = screen.getByTestId( 'responsive-wrapper' );
67
67
  expect( wrapper ).toHaveStyle( { width: '200px', height: '200px' } );
68
68
  } );
@@ -86,10 +86,11 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
86
86
  aspectRatio,
87
87
  } );
88
88
 
89
- // Use measured dimensions, but fall back to explicit props if measurement returns 0
90
- // (e.g., during initial render or in test environments without DOM measurement)
91
- const effectiveWidth = measuredWidth || size || width || 0;
92
- const effectiveHeight = measuredHeight || size || height || 0;
89
+ // Use measured dimensions, but fall back to explicit width/height props if measurement returns 0
90
+ // (e.g., during initial render or in test environments without DOM measurement).
91
+ // Do not use size here — size controls chart element dimensions (e.g. pie diameter), not container dimensions.
92
+ const effectiveWidth = measuredWidth || width || 0;
93
+ const effectiveHeight = measuredHeight || height || 0;
93
94
 
94
95
  const defaultHeight = hasAspectRatio ? 'auto' : '100%';
95
96
 
@@ -99,14 +100,14 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
99
100
  data-testid="responsive-wrapper"
100
101
  className={ styles.container }
101
102
  style={ {
102
- width: size ?? width ?? '100%',
103
- height: size ?? height ?? defaultHeight,
103
+ width: width ?? '100%',
104
+ height: height ?? defaultHeight,
104
105
  } }
105
106
  >
106
107
  <WrappedComponent
107
108
  width={ effectiveWidth }
108
109
  height={ effectiveHeight }
109
- size={ effectiveWidth }
110
+ size={ size }
110
111
  { ...( chartProps as T ) }
111
112
  />
112
113
  </div>
@@ -3,7 +3,7 @@ export { useChartMouseHandler } from './use-chart-mouse-handler';
3
3
  export { useXYChartTheme } from './use-xychart-theme';
4
4
  export { useChartDataTransform } from './use-chart-data-transform';
5
5
  export { useChartMargin } from './use-chart-margin';
6
- export { useElementHeight } from './use-element-height';
6
+ export { useElementSize } from './use-element-size';
7
7
  export { useHasLegendChild } from './use-has-legend-child';
8
8
  export { useTextTruncation } from './use-text-truncation';
9
9
  export { useZeroValueDisplay } from './use-zero-value-display';