@automattic/charts 0.56.7 → 0.57.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 (176) hide show
  1. package/AGENTS.md +28 -98
  2. package/CHANGELOG.md +16 -0
  3. package/dist/charts/bar-chart/index.cjs +5 -6
  4. package/dist/charts/bar-chart/index.cjs.map +1 -1
  5. package/dist/charts/bar-chart/index.d.cts +3 -3
  6. package/dist/charts/bar-chart/index.d.ts +3 -3
  7. package/dist/charts/bar-chart/index.js +4 -5
  8. package/dist/charts/bar-list-chart/index.cjs +6 -7
  9. package/dist/charts/bar-list-chart/index.cjs.map +1 -1
  10. package/dist/charts/bar-list-chart/index.d.cts +3 -3
  11. package/dist/charts/bar-list-chart/index.d.ts +3 -3
  12. package/dist/charts/bar-list-chart/index.js +5 -6
  13. package/dist/charts/conversion-funnel-chart/index.cjs +5 -6
  14. package/dist/charts/conversion-funnel-chart/index.cjs.map +1 -1
  15. package/dist/charts/conversion-funnel-chart/index.d.cts +1 -1
  16. package/dist/charts/conversion-funnel-chart/index.d.ts +1 -1
  17. package/dist/charts/conversion-funnel-chart/index.js +4 -5
  18. package/dist/charts/geo-chart/index.cjs +4 -4
  19. package/dist/charts/geo-chart/index.d.cts +1 -1
  20. package/dist/charts/geo-chart/index.d.ts +1 -1
  21. package/dist/charts/geo-chart/index.js +3 -3
  22. package/dist/charts/leaderboard-chart/index.cjs +5 -5
  23. package/dist/charts/leaderboard-chart/index.css +8 -9
  24. package/dist/charts/leaderboard-chart/index.css.map +1 -1
  25. package/dist/charts/leaderboard-chart/index.d.cts +3 -3
  26. package/dist/charts/leaderboard-chart/index.d.ts +3 -3
  27. package/dist/charts/leaderboard-chart/index.js +4 -4
  28. package/dist/charts/line-chart/index.cjs +5 -6
  29. package/dist/charts/line-chart/index.cjs.map +1 -1
  30. package/dist/charts/line-chart/index.d.cts +3 -3
  31. package/dist/charts/line-chart/index.d.ts +3 -3
  32. package/dist/charts/line-chart/index.js +4 -5
  33. package/dist/charts/pie-chart/index.cjs +5 -6
  34. package/dist/charts/pie-chart/index.cjs.map +1 -1
  35. package/dist/charts/pie-chart/index.d.cts +4 -4
  36. package/dist/charts/pie-chart/index.d.ts +4 -4
  37. package/dist/charts/pie-chart/index.js +4 -5
  38. package/dist/charts/pie-semi-circle-chart/index.cjs +5 -6
  39. package/dist/charts/pie-semi-circle-chart/index.cjs.map +1 -1
  40. package/dist/charts/pie-semi-circle-chart/index.d.cts +4 -4
  41. package/dist/charts/pie-semi-circle-chart/index.d.ts +4 -4
  42. package/dist/charts/pie-semi-circle-chart/index.js +4 -5
  43. package/dist/charts/sparkline/index.cjs +6 -7
  44. package/dist/charts/sparkline/index.cjs.map +1 -1
  45. package/dist/charts/sparkline/index.js +5 -6
  46. package/dist/{chunk-XD2HV7M5.js → chunk-2NCY7R4G.js} +127 -762
  47. package/dist/chunk-2NCY7R4G.js.map +1 -0
  48. package/dist/{chunk-RFSHE3HL.js → chunk-32DH6JDF.js} +64 -43
  49. package/dist/chunk-32DH6JDF.js.map +1 -0
  50. package/dist/{chunk-SSFFCBCF.js → chunk-4OPFE4RM.js} +11 -8
  51. package/dist/chunk-4OPFE4RM.js.map +1 -0
  52. package/dist/{chunk-CAFJRZPZ.cjs → chunk-77OKCVQN.cjs} +17 -17
  53. package/dist/{chunk-CAFJRZPZ.cjs.map → chunk-77OKCVQN.cjs.map} +1 -1
  54. package/dist/{chunk-K6TGILHX.cjs → chunk-7FQX4ALL.cjs} +6 -6
  55. package/dist/{chunk-K6TGILHX.cjs.map → chunk-7FQX4ALL.cjs.map} +1 -1
  56. package/dist/{chunk-7FDQGBY7.js → chunk-BCX5THDQ.js} +9 -7
  57. package/dist/chunk-BCX5THDQ.js.map +1 -0
  58. package/dist/{chunk-KHQPN77E.js → chunk-CZGYJKG6.js} +4 -4
  59. package/dist/{chunk-3EXJP67N.cjs → chunk-D2UH4CFE.cjs} +9 -9
  60. package/dist/{chunk-3EXJP67N.cjs.map → chunk-D2UH4CFE.cjs.map} +1 -1
  61. package/dist/{chunk-TE63Y5PX.js → chunk-DAU3HNEG.js} +2 -2
  62. package/dist/chunk-DAU3HNEG.js.map +1 -0
  63. package/dist/{chunk-MDRCAGKZ.js → chunk-H2V4JMSA.js} +3 -3
  64. package/dist/{chunk-UFRBUT2D.cjs → chunk-I35UYJJR.cjs} +49 -6
  65. package/dist/chunk-I35UYJJR.cjs.map +1 -0
  66. package/dist/{chunk-GWBS65VC.js → chunk-IU4DYUAV.js} +3 -3
  67. package/dist/{chunk-E62LCBGD.js → chunk-PXLEMUGJ.js} +3 -3
  68. package/dist/{chunk-YDVHT7GS.cjs → chunk-RHHVEJHJ.cjs} +83 -62
  69. package/dist/chunk-RHHVEJHJ.cjs.map +1 -0
  70. package/dist/{chunk-YAXY5L7I.cjs → chunk-TO3OQBXG.cjs} +5 -5
  71. package/dist/{chunk-YAXY5L7I.cjs.map → chunk-TO3OQBXG.cjs.map} +1 -1
  72. package/dist/{chunk-VPAEBI2F.js → chunk-V36ERY7Y.js} +9 -7
  73. package/dist/chunk-V36ERY7Y.js.map +1 -0
  74. package/dist/{chunk-X7JL2NYJ.cjs → chunk-VJM5XCB4.cjs} +33 -30
  75. package/dist/chunk-VJM5XCB4.cjs.map +1 -0
  76. package/dist/{chunk-ZVGEDXDP.cjs → chunk-VTS3PNMS.cjs} +2 -2
  77. package/dist/{chunk-ZVGEDXDP.cjs.map → chunk-VTS3PNMS.cjs.map} +1 -1
  78. package/dist/{chunk-OMS5QIJN.js → chunk-WLODYNLB.js} +9 -7
  79. package/dist/chunk-WLODYNLB.js.map +1 -0
  80. package/dist/{chunk-NQJE2CC7.cjs → chunk-XKRJL2QT.cjs} +25 -23
  81. package/dist/chunk-XKRJL2QT.cjs.map +1 -0
  82. package/dist/{chunk-O2JIANHK.cjs → chunk-YE2T52VZ.cjs} +33 -31
  83. package/dist/chunk-YE2T52VZ.cjs.map +1 -0
  84. package/dist/{chunk-IS5YYLTV.js → chunk-Z26M4V2M.js} +46 -3
  85. package/dist/chunk-Z26M4V2M.js.map +1 -0
  86. package/dist/{chunk-55ZCOYDF.cjs → chunk-Z45KX47P.cjs} +153 -788
  87. package/dist/chunk-Z45KX47P.cjs.map +1 -0
  88. package/dist/{chunk-BXFD7JIG.cjs → chunk-ZH4F5RMG.cjs} +26 -24
  89. package/dist/chunk-ZH4F5RMG.cjs.map +1 -0
  90. package/dist/components/legend/index.cjs +3 -3
  91. package/dist/components/legend/index.d.cts +4 -4
  92. package/dist/components/legend/index.d.ts +4 -4
  93. package/dist/components/legend/index.js +2 -2
  94. package/dist/components/tooltip/index.d.cts +1 -1
  95. package/dist/components/tooltip/index.d.ts +1 -1
  96. package/dist/hooks/index.cjs +3 -3
  97. package/dist/hooks/index.d.cts +7 -3
  98. package/dist/hooks/index.d.ts +7 -3
  99. package/dist/hooks/index.js +2 -2
  100. package/dist/index.cjs +13 -14
  101. package/dist/index.cjs.map +1 -1
  102. package/dist/index.css +8 -9
  103. package/dist/index.css.map +1 -1
  104. package/dist/index.d.cts +7 -7
  105. package/dist/index.d.ts +7 -7
  106. package/dist/index.js +12 -13
  107. package/dist/{leaderboard-chart-BSgEw_Um.d.ts → leaderboard-chart-BKYYXcg2.d.ts} +5 -9
  108. package/dist/{leaderboard-chart-COtgamhe.d.cts → leaderboard-chart-DR7CGb0L.d.cts} +5 -9
  109. package/dist/{legend-C9ahiwOt.d.cts → legend-C2grwnWk.d.cts} +1 -1
  110. package/dist/{legend-jjMmhSg3.d.ts → legend-Cj0xM5dU.d.ts} +1 -1
  111. package/dist/providers/index.cjs +3 -3
  112. package/dist/providers/index.d.cts +3 -3
  113. package/dist/providers/index.d.ts +3 -3
  114. package/dist/providers/index.js +2 -2
  115. package/dist/{themes-DQzmaSze.d.ts → themes-BmVGrYnF.d.ts} +2 -2
  116. package/dist/{themes-CVR5rmIs.d.cts → themes-CyjKm-P_.d.cts} +2 -2
  117. package/dist/{types-DQNnq5Fr.d.ts → types-CuUEszrM.d.ts} +1 -1
  118. package/dist/{types-CzdN7rUe.d.cts → types-DZordNiO.d.cts} +11 -7
  119. package/dist/{types-CzdN7rUe.d.ts → types-DZordNiO.d.ts} +11 -7
  120. package/dist/types-I67mddpr.d.cts +78 -0
  121. package/dist/types-I67mddpr.d.ts +78 -0
  122. package/dist/{types-BBwg4Evw.d.cts → types-KtOPPzPX.d.cts} +1 -1
  123. package/dist/utils/index.cjs +2 -2
  124. package/dist/utils/index.d.cts +1 -1
  125. package/dist/utils/index.d.ts +1 -1
  126. package/dist/utils/index.js +1 -1
  127. package/package.json +6 -4
  128. package/src/charts/bar-chart/bar-chart.tsx +4 -3
  129. package/src/charts/bar-chart/test/bar-chart.test.tsx +30 -0
  130. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +2 -2
  131. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +0 -2
  132. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +9 -10
  133. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +95 -70
  134. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +58 -29
  135. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -5
  136. package/src/charts/leaderboard-chart/types.ts +4 -7
  137. package/src/charts/line-chart/line-chart.tsx +2 -3
  138. package/src/charts/pie-chart/pie-chart.tsx +2 -3
  139. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +2 -3
  140. package/src/components/legend/index.ts +8 -1
  141. package/src/components/legend/private/base-legend.tsx +32 -22
  142. package/src/components/legend/test/legend.test.tsx +148 -52
  143. package/src/components/legend/types.ts +42 -16
  144. package/src/hooks/test/use-zero-value-display.test.tsx +206 -0
  145. package/src/hooks/use-zero-value-display.ts +52 -23
  146. package/src/index.ts +7 -1
  147. package/src/providers/chart-context/test/chart-context.test.tsx +12 -6
  148. package/src/providers/chart-context/themes.ts +6 -4
  149. package/src/types.ts +11 -7
  150. package/src/utils/get-styles.ts +1 -1
  151. package/src/utils/test/get-styles.test.ts +12 -10
  152. package/dist/chunk-55ZCOYDF.cjs.map +0 -1
  153. package/dist/chunk-7FDQGBY7.js.map +0 -1
  154. package/dist/chunk-BXFD7JIG.cjs.map +0 -1
  155. package/dist/chunk-IS5YYLTV.js.map +0 -1
  156. package/dist/chunk-KNIMXN6Z.js +0 -51
  157. package/dist/chunk-KNIMXN6Z.js.map +0 -1
  158. package/dist/chunk-NQJE2CC7.cjs.map +0 -1
  159. package/dist/chunk-O2JIANHK.cjs.map +0 -1
  160. package/dist/chunk-OMS5QIJN.js.map +0 -1
  161. package/dist/chunk-RFSHE3HL.js.map +0 -1
  162. package/dist/chunk-SSFFCBCF.js.map +0 -1
  163. package/dist/chunk-SUDERBUA.cjs +0 -51
  164. package/dist/chunk-SUDERBUA.cjs.map +0 -1
  165. package/dist/chunk-TE63Y5PX.js.map +0 -1
  166. package/dist/chunk-UFRBUT2D.cjs.map +0 -1
  167. package/dist/chunk-VPAEBI2F.js.map +0 -1
  168. package/dist/chunk-X7JL2NYJ.cjs.map +0 -1
  169. package/dist/chunk-XD2HV7M5.js.map +0 -1
  170. package/dist/chunk-YDVHT7GS.cjs.map +0 -1
  171. package/dist/types-C05PdDJa.d.cts +0 -57
  172. package/dist/types-C05PdDJa.d.ts +0 -57
  173. /package/dist/{chunk-KHQPN77E.js.map → chunk-CZGYJKG6.js.map} +0 -0
  174. /package/dist/{chunk-MDRCAGKZ.js.map → chunk-H2V4JMSA.js.map} +0 -0
  175. /package/dist/{chunk-GWBS65VC.js.map → chunk-IU4DYUAV.js.map} +0 -0
  176. /package/dist/{chunk-E62LCBGD.js.map → chunk-PXLEMUGJ.js.map} +0 -0
@@ -562,6 +562,36 @@ describe( 'BarChart', () => {
562
562
  expect( width ).toBeGreaterThan( 0 );
563
563
  } );
564
564
  } );
565
+
566
+ test( 'ensures minimum pixel height for zero values in small charts', () => {
567
+ // With a small chart height (100px) and large data range, zero-value bars
568
+ // should still be visible (at least 3px based on MIN_PIXEL_HEIGHT)
569
+ renderWithTheme( {
570
+ showZeroValues: true,
571
+ height: 100,
572
+ data: [
573
+ {
574
+ label: 'Test Series',
575
+ data: [
576
+ { label: 'Zero', value: 0 },
577
+ { label: 'Large', value: 10000 },
578
+ ],
579
+ options: {},
580
+ },
581
+ ],
582
+ } );
583
+
584
+ const svgElement = screen.getByRole( 'grid', { name: /bar chart/i } ).querySelector( 'svg' );
585
+ const bars = svgElement?.querySelectorAll( '.visx-bar-group rect' );
586
+
587
+ expect( bars?.length ).toBe( 2 );
588
+
589
+ // The zero-value bar (first bar) should have a minimum visible height.
590
+ // We check for >= 2px to allow for rounding in the pixel calculation.
591
+ const zeroBar = bars?.[ 0 ];
592
+ const zeroBarHeight = parseFloat( zeroBar?.getAttribute( 'height' ) || '0' );
593
+ expect( zeroBarHeight ).toBeGreaterThanOrEqual( 2 );
594
+ } );
565
595
  } );
566
596
 
567
597
  /* eslint-enable testing-library/no-node-access */
@@ -372,7 +372,7 @@ describe( 'ConversionFunnelChart', () => {
372
372
  expect( customRenderMainMetric ).toHaveBeenCalledWith( {
373
373
  mainRate: 10.3,
374
374
  changeIndicator: undefined,
375
- className: undefined,
375
+ className: 'main-metric',
376
376
  changeColor: expect.any( String ),
377
377
  } );
378
378
  } );
@@ -427,7 +427,7 @@ describe( 'ConversionFunnelChart', () => {
427
427
  index: 1,
428
428
  top: expect.any( Number ),
429
429
  left: expect.any( Number ),
430
- className: undefined,
430
+ className: 'tooltip-wrapper',
431
431
  } );
432
432
  } );
433
433
 
@@ -53,7 +53,6 @@ export function useLeaderboardLegendItems( {
53
53
 
54
54
  items.push( {
55
55
  label: legendLabels?.primary || __( 'Current period', 'jetpack-charts' ),
56
- value: '',
57
56
  color: resolvedPrimaryColor,
58
57
  } );
59
58
 
@@ -66,7 +65,6 @@ export function useLeaderboardLegendItems( {
66
65
 
67
66
  items.push( {
68
67
  label: legendLabels?.comparison || __( 'Previous period', 'jetpack-charts' ),
69
- value: '',
70
68
  color: resolvedSecondaryColor,
71
69
  } );
72
70
  }
@@ -1,19 +1,20 @@
1
1
  .leaderboardChart {
2
- display: flex;
3
- flex-direction: column;
4
2
  transition: opacity 0.3s ease-in-out;
5
3
 
6
- &--legend-top {
7
- flex-direction: column-reverse;
8
- }
9
-
10
- &--with-legend {
11
- gap: 16px;
4
+ &--responsive {
5
+ height: 100%;
6
+ width: 100%;
12
7
  }
13
8
 
14
9
  &--loading {
15
10
  opacity: 0.5;
16
11
  }
12
+
13
+ &__content {
14
+ flex: 1;
15
+ min-height: 0;
16
+ overflow: auto;
17
+ }
17
18
  }
18
19
 
19
20
  .barWithLabelContainer {
@@ -61,8 +62,6 @@
61
62
  }
62
63
 
63
64
  .valueContainer {
64
- display: flex;
65
- gap: 4px;
66
65
  justify-content: flex-end;
67
66
  }
68
67
 
@@ -1,11 +1,8 @@
1
1
  /* eslint-disable @wordpress/no-unsafe-wp-apis */
2
- import {
3
- __experimentalVStack as VStack,
4
- __experimentalGrid as Grid,
5
- __experimentalText as Text,
6
- } from '@wordpress/components';
2
+ import { __experimentalGrid as Grid, __experimentalText as Text } from '@wordpress/components';
7
3
  import { Fragment } from '@wordpress/element';
8
4
  import { __ } from '@wordpress/i18n';
5
+ import { Stack } from '@wordpress/ui';
9
6
  import clsx from 'clsx';
10
7
  import { useContext, useMemo, type FC } from 'react';
11
8
  import { Legend } from '../../components/legend';
@@ -116,6 +113,8 @@ const BarWithLabel = ( {
116
113
  * @param props - Component props
117
114
  * @param props.data - Array of leaderboard entries to display
118
115
  * @param props.chartId - Optional unique identifier for the chart
116
+ * @param props.width - Optional width of the chart container in pixels
117
+ * @param props.height - Optional height of the chart container in pixels
119
118
  * @param props.withComparison - Whether to show comparison data
120
119
  * @param props.withOverlayLabel - Whether to overlay the label on top of the bar
121
120
  * @param props.primaryColor - Primary color for current period bars
@@ -129,10 +128,10 @@ const BarWithLabel = ( {
129
128
  * @param props.legendPosition - Legend position
130
129
  * @param props.legendAlignment - Legend alignment
131
130
  * @param props.legendShape - Legend shape
132
- * @param props.legendShapeWidth - Width of legend shapes in pixels
133
- * @param props.legendShapeHeight - Height of legend shapes in pixels
131
+ * @param props.legendShapeStyles - Styles for legend shapes (width, height, margin)
134
132
  * @param props.legendLabels - Custom labels for legend items
135
133
  * @param props.legendInteractive - Whether legend items are interactive (clickable to toggle series visibility)
134
+ * @param props.gap - Spacing between legend and chart content
136
135
  * @param props.children - Child components for composition API
137
136
  * @param props.className - Additional CSS class name
138
137
  * @param props.style - Custom styling for the chart container
@@ -141,6 +140,8 @@ const BarWithLabel = ( {
141
140
  const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
142
141
  data,
143
142
  chartId: providedChartId,
143
+ width: propWidth,
144
+ height: propHeight,
144
145
  withComparison = false,
145
146
  withOverlayLabel = false,
146
147
  primaryColor,
@@ -154,16 +155,17 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
154
155
  legendPosition = 'bottom',
155
156
  legendAlignment = 'center',
156
157
  legendShape = 'circle',
157
- legendShapeWidth = 8,
158
- legendShapeHeight = 8,
158
+ legendShapeStyles: legendShapeStylesProp,
159
159
  legendLabels,
160
160
  legendInteractive = false,
161
+ gap = 'md',
161
162
  className,
162
163
  style,
163
164
  children,
164
165
  } ) => {
165
166
  const chartId = useChartId( providedChartId );
166
167
  const { leaderboardChart: leaderboardChartSettings } = useGlobalChartsTheme();
168
+ const legendShapeStyles = { width: 8, height: 8, ...legendShapeStylesProp };
167
169
 
168
170
  // Process children to extract compound components
169
171
  const { otherChildren } = useChartChildren( children, 'LeaderboardChart' );
@@ -258,13 +260,21 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
258
260
  chartHeight: 0,
259
261
  } }
260
262
  >
261
- <div
263
+ <Stack
264
+ direction="column"
265
+ data-testid="leaderboard-chart-container"
262
266
  className={ clsx(
263
267
  styles.leaderboardChart,
268
+ { [ styles[ 'leaderboardChart--responsive' ] ]: ! propWidth && ! propHeight },
264
269
  { [ styles[ 'leaderboardChart--loading' ] ]: loading },
265
270
  className
266
271
  ) }
267
- style={ style }
272
+ gap={ gap }
273
+ style={ {
274
+ ...style,
275
+ width: propWidth || undefined,
276
+ height: propHeight || undefined,
277
+ } }
268
278
  >
269
279
  <div className={ styles.emptyState }>
270
280
  { loading
@@ -273,11 +283,23 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
273
283
  </div>
274
284
  { /* Render children from composition API */ }
275
285
  { otherChildren }
276
- </div>
286
+ </Stack>
277
287
  </SingleChartContext.Provider>
278
288
  );
279
289
  }
280
290
 
291
+ const legendElement = showLegend && (
292
+ <Legend
293
+ orientation={ legendOrientation }
294
+ position={ legendPosition }
295
+ alignment={ legendAlignment }
296
+ shape={ legendShape }
297
+ shapeStyles={ legendShapeStyles }
298
+ chartId={ chartId }
299
+ interactive={ legendInteractive }
300
+ />
301
+ );
302
+
281
303
  return (
282
304
  <SingleChartContext.Provider
283
305
  value={ {
@@ -286,76 +308,79 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
286
308
  chartHeight: 0,
287
309
  } }
288
310
  >
289
- <div
311
+ <Stack
312
+ direction="column"
313
+ data-testid="leaderboard-chart-container"
290
314
  className={ clsx(
291
315
  styles.leaderboardChart,
292
316
  {
317
+ [ styles[ 'leaderboardChart--responsive' ] ]: ! propWidth && ! propHeight,
293
318
  [ styles[ 'leaderboardChart--loading' ] ]: loading,
294
- [ styles[ 'leaderboardChart--with-legend' ] ]: showLegend,
295
- [ styles[ 'leaderboardChart--legend-top' ] ]: showLegend && legendPosition === 'top',
296
319
  },
297
320
  className
298
321
  ) }
299
- style={ style }
322
+ gap={ gap }
323
+ style={ {
324
+ ...style,
325
+ width: propWidth || undefined,
326
+ height: propHeight || undefined,
327
+ } }
300
328
  >
301
- { allSeriesHidden ? (
302
- <div className={ styles.emptyState }>
303
- { __( 'All series are hidden. Click legend items to show data.', 'jetpack-charts' ) }
304
- </div>
305
- ) : (
306
- <Grid templateColumns="minmax(0, 1fr) auto" rowGap={ rowGap } columnGap={ columnGap }>
307
- { data.map( entry => {
308
- const colorIndex = Math.sign( entry.delta ) + 1;
309
- const deltaColor = deltaColors[ colorIndex ];
310
-
311
- return (
312
- <Fragment key={ entry.id }>
313
- <VStack spacing={ labelSpacing }>
314
- <BarWithLabel
315
- entry={ entry }
316
- withComparison={ withComparison }
317
- withOverlayLabel={ withOverlayLabel }
318
- primaryColor={ resolvedPrimaryColor }
319
- secondaryColor={ resolvedSecondaryColor }
320
- isPrimaryVisible={ isPrimaryVisible }
321
- isComparisonVisible={ isComparisonVisible }
322
- animation={ animation && ! loading && ! prefersReducedMotion }
323
- />
324
- </VStack>
325
-
326
- <div
327
- className={ clsx( styles.valueContainer, {
328
- [ styles.overlayLabel ]: withOverlayLabel,
329
- } ) }
330
- >
331
- { isPrimaryVisible && <Text>{ valueFormatter( entry.currentValue ) }</Text> }
332
-
333
- { withComparison && isComparisonVisible && (
334
- <Text style={ { color: deltaColor } }>{ deltaFormatter( entry.delta ) }</Text>
335
- ) }
336
- </div>
337
- </Fragment>
338
- );
339
- } ) }
340
- </Grid>
341
- ) }
329
+ { legendPosition === 'top' && legendElement }
342
330
 
343
- { showLegend && (
344
- <Legend
345
- orientation={ legendOrientation }
346
- position={ legendPosition }
347
- alignment={ legendAlignment }
348
- shape={ legendShape }
349
- shapeWidth={ legendShapeWidth }
350
- shapeHeight={ legendShapeHeight }
351
- chartId={ chartId }
352
- interactive={ legendInteractive }
353
- />
354
- ) }
331
+ <div className={ styles.leaderboardChart__content }>
332
+ { allSeriesHidden ? (
333
+ <div className={ styles.emptyState }>
334
+ { __( 'All series are hidden. Click legend items to show data.', 'jetpack-charts' ) }
335
+ </div>
336
+ ) : (
337
+ <Grid templateColumns="minmax(0, 1fr) auto" rowGap={ rowGap } columnGap={ columnGap }>
338
+ { data.map( entry => {
339
+ const colorIndex = Math.sign( entry.delta ) + 1;
340
+ const deltaColor = deltaColors[ colorIndex ];
341
+
342
+ return (
343
+ <Fragment key={ entry.id }>
344
+ <Stack direction="column" gap={ labelSpacing }>
345
+ <BarWithLabel
346
+ entry={ entry }
347
+ withComparison={ withComparison }
348
+ withOverlayLabel={ withOverlayLabel }
349
+ primaryColor={ resolvedPrimaryColor }
350
+ secondaryColor={ resolvedSecondaryColor }
351
+ isPrimaryVisible={ isPrimaryVisible }
352
+ isComparisonVisible={ isComparisonVisible }
353
+ animation={ animation && ! loading && ! prefersReducedMotion }
354
+ />
355
+ </Stack>
356
+
357
+ <Stack
358
+ direction="row"
359
+ gap="xs"
360
+ className={ clsx( styles.valueContainer, {
361
+ [ styles.overlayLabel ]: withOverlayLabel,
362
+ } ) }
363
+ >
364
+ { isPrimaryVisible && <Text>{ valueFormatter( entry.currentValue ) }</Text> }
365
+
366
+ { withComparison && isComparisonVisible && (
367
+ <Text style={ { color: deltaColor } }>
368
+ { deltaFormatter( entry.delta ) }
369
+ </Text>
370
+ ) }
371
+ </Stack>
372
+ </Fragment>
373
+ );
374
+ } ) }
375
+ </Grid>
376
+ ) }
377
+ </div>
378
+
379
+ { legendPosition === 'bottom' && legendElement }
355
380
 
356
381
  { /* Render children from composition API */ }
357
382
  { otherChildren }
358
- </div>
383
+ </Stack>
359
384
  </SingleChartContext.Provider>
360
385
  );
361
386
  };
@@ -2,6 +2,17 @@ import { render, screen } from '@testing-library/react';
2
2
  import LeaderboardChart from '../leaderboard-chart';
3
3
  import type { LeaderboardEntry } from '../../../types';
4
4
 
5
+ const mockDefaultParentSize = () => ( {
6
+ parentRef: { current: null },
7
+ width: 400,
8
+ height: 300,
9
+ } );
10
+
11
+ // Mock useParentSize so the responsive wrapper returns predictable dimensions in tests
12
+ jest.mock( '@visx/responsive', () => ( {
13
+ useParentSize: jest.fn( () => mockDefaultParentSize() ),
14
+ } ) );
15
+
5
16
  const mockData: LeaderboardEntry[] = [
6
17
  {
7
18
  id: 'direct',
@@ -40,6 +51,11 @@ const testValueFormatter = ( value: number ) => `${ value }$`;
40
51
  const testDeltaFormatter = ( value: number ) => `${ value }delta`;
41
52
 
42
53
  describe( 'LeaderboardChart', () => {
54
+ afterEach( () => {
55
+ const { useParentSize } = jest.requireMock( '@visx/responsive' );
56
+ useParentSize.mockImplementation( () => mockDefaultParentSize() );
57
+ } );
58
+
43
59
  it( 'renders leaderboard entries', () => {
44
60
  render( <LeaderboardChart data={ mockData } /> );
45
61
 
@@ -154,8 +170,7 @@ describe( 'LeaderboardChart', () => {
154
170
  withComparison={ true }
155
171
  showLegend={ true }
156
172
  legendShape="rect"
157
- legendShapeWidth={ 10 }
158
- legendShapeHeight={ 6 }
173
+ legendShapeStyles={ { width: 10, height: 6 } }
159
174
  />
160
175
  );
161
176
 
@@ -204,7 +219,7 @@ describe( 'LeaderboardChart', () => {
204
219
  it( 'renders LeaderboardChart.Legend as child component', () => {
205
220
  render(
206
221
  <LeaderboardChart data={ mockData } withComparison={ true }>
207
- <LeaderboardChart.Legend data-testid="composition-legend-item" />
222
+ <LeaderboardChart.Legend />
208
223
  </LeaderboardChart>
209
224
  );
210
225
 
@@ -212,8 +227,8 @@ describe( 'LeaderboardChart', () => {
212
227
  expect( screen.getByText( 'Direct' ) ).toBeInTheDocument();
213
228
  expect( screen.getByText( 'Social Media' ) ).toBeInTheDocument();
214
229
 
215
- // Composition legend should render - each legend item gets its own element
216
- expect( screen.getAllByTestId( 'composition-legend-item' ) ).toHaveLength( 2 );
230
+ // Composition legend should render
231
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
217
232
  expect( screen.getByText( 'Current period' ) ).toBeInTheDocument();
218
233
  expect( screen.getByText( 'Previous period' ) ).toBeInTheDocument();
219
234
  } );
@@ -221,15 +236,12 @@ describe( 'LeaderboardChart', () => {
221
236
  it( 'renders composition legend regardless of showLegend value', () => {
222
237
  render(
223
238
  <LeaderboardChart data={ mockData } withComparison={ true } showLegend={ false }>
224
- <LeaderboardChart.Legend data-testid="composition-legend-item" />
239
+ <LeaderboardChart.Legend />
225
240
  </LeaderboardChart>
226
241
  );
227
242
 
228
- // No built-in legend should be rendered when showLegend is false
229
- expect( screen.queryByTestId( 'legend-item' ) ).not.toBeInTheDocument();
230
-
231
- // Composition legend should still render regardless of showLegend value
232
- expect( screen.getAllByTestId( 'composition-legend-item' ) ).toHaveLength( 2 );
243
+ // Composition legend should render regardless of showLegend value
244
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
233
245
  expect( screen.getByText( 'Current period' ) ).toBeInTheDocument();
234
246
  expect( screen.getByText( 'Previous period' ) ).toBeInTheDocument();
235
247
  } );
@@ -237,41 +249,36 @@ describe( 'LeaderboardChart', () => {
237
249
  it( 'supports both built-in and composition legends simultaneously', () => {
238
250
  render(
239
251
  <LeaderboardChart data={ mockData } withComparison={ true } showLegend={ true }>
240
- <LeaderboardChart.Legend data-testid="composition-legend-item" />
252
+ <LeaderboardChart.Legend />
241
253
  </LeaderboardChart>
242
254
  );
243
255
 
244
- // Built-in legend should render (with legend-item test IDs)
245
- expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
246
-
247
- // Composition legend should also render
248
- expect( screen.getAllByTestId( 'composition-legend-item' ) ).toHaveLength( 2 );
256
+ // Both built-in and composition legends should render (2 items each = 4 total)
257
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 4 );
249
258
 
250
259
  // Should have legend items from both legends
251
260
  const currentPeriodItems = screen.getAllByText( 'Current period' );
252
261
  const previousPeriodItems = screen.getAllByText( 'Previous period' );
253
- expect( currentPeriodItems ).toHaveLength( 2 ); // One from each legend
254
- expect( previousPeriodItems ).toHaveLength( 2 ); // One from each legend
262
+ expect( currentPeriodItems ).toHaveLength( 2 );
263
+ expect( previousPeriodItems ).toHaveLength( 2 );
255
264
  } );
256
265
 
257
266
  it( 'passes props correctly to composition legend', () => {
258
267
  render(
259
268
  <LeaderboardChart data={ mockData } withComparison={ true }>
260
- <LeaderboardChart.Legend
261
- data-testid="composition-legend-item"
262
- shape="circle"
263
- shapeWidth={ 12 }
264
- shapeHeight={ 12 }
265
- style={ { marginTop: '20px' } }
266
- />
269
+ <LeaderboardChart.Legend shape="circle" shapeStyles={ { margin: '4px 8px' } } />
267
270
  </LeaderboardChart>
268
271
  );
269
272
 
270
- const legendItems = screen.getAllByTestId( 'composition-legend-item' );
273
+ const legendItems = screen.getAllByTestId( 'legend-item' );
271
274
  expect( legendItems ).toHaveLength( 2 );
272
- // Check that each legend item has the custom style applied
275
+
276
+ // Verify custom shape styles are applied within each legend item.
277
+ // Direct DOM access is needed because visx legend shapes lack accessible attributes and we cannot pass a test id to them.
273
278
  legendItems.forEach( item => {
274
- expect( item ).toHaveStyle( { marginTop: '20px' } );
279
+ // eslint-disable-next-line testing-library/no-node-access
280
+ const shape = item.querySelector( '.visx-legend-shape' );
281
+ expect( shape ).toHaveStyle( { margin: '4px 8px' } );
275
282
  } );
276
283
  } );
277
284
 
@@ -339,4 +346,26 @@ describe( 'LeaderboardChart', () => {
339
346
  expect( screen.getByText( '-8%' ) ).toBeInTheDocument();
340
347
  } );
341
348
  } );
349
+
350
+ describe( 'Responsive wrapper', () => {
351
+ it( 'fills parent container (height:100%) by default', () => {
352
+ render( <LeaderboardChart data={ mockData } /> );
353
+ const wrapper = screen.getByTestId( 'responsive-wrapper' );
354
+ expect( wrapper ).toHaveStyle( { height: '100%' } );
355
+ } );
356
+
357
+ it( 'applies explicit width and height to chart container', () => {
358
+ const { useParentSize } = jest.requireMock( '@visx/responsive' );
359
+ useParentSize.mockReturnValue( {
360
+ parentRef: { current: null },
361
+ width: 0,
362
+ height: 0,
363
+ } );
364
+
365
+ render( <LeaderboardChart data={ mockData } width={ 500 } height={ 240 } /> );
366
+ const chartContainer = screen.getByTestId( 'leaderboard-chart-container' );
367
+
368
+ expect( chartContainer ).toHaveStyle( { width: '500px', height: '240px' } );
369
+ } );
370
+ } );
342
371
  } );
@@ -80,7 +80,6 @@ describe( 'useLeaderboardLegendItems', () => {
80
80
  expect( result.current ).toHaveLength( 1 );
81
81
  expect( result.current[ 0 ] ).toEqual( {
82
82
  label: 'Current period',
83
- value: '',
84
83
  color: expect.any( String ),
85
84
  } );
86
85
  } );
@@ -102,14 +101,12 @@ describe( 'useLeaderboardLegendItems', () => {
102
101
  // Current period item
103
102
  expect( result.current[ 0 ] ).toEqual( {
104
103
  label: 'Current period',
105
- value: '',
106
104
  color: expect.any( String ),
107
105
  } );
108
106
 
109
107
  // Previous period item
110
108
  expect( result.current[ 1 ] ).toEqual( {
111
109
  label: 'Previous period',
112
- value: '',
113
110
  color: expect.any( String ),
114
111
  } );
115
112
  } );
@@ -585,7 +582,7 @@ describe( 'useLeaderboardLegendItems', () => {
585
582
  expect( result.current[ 1 ].label ).toBe( 'Previous period' );
586
583
  } );
587
584
 
588
- it( 'should have empty value strings for all items', () => {
585
+ it( 'should not include value property for legend items', () => {
589
586
  const wrapper = createWrapper();
590
587
  const { result } = renderHook(
591
588
  () =>
@@ -598,7 +595,7 @@ describe( 'useLeaderboardLegendItems', () => {
598
595
  );
599
596
 
600
597
  result.current.forEach( item => {
601
- expect( item.value ).toBe( '' );
598
+ expect( item ).not.toHaveProperty( 'value' );
602
599
  } );
603
600
  } );
604
601
 
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { BaseChartProps, LeaderboardEntry } from '../../types';
3
+ import type { LegendShapeStyles } from '../../components/legend';
3
4
 
4
5
  export interface LeaderboardChartProps
5
6
  extends Pick<
@@ -15,6 +16,7 @@ export interface LeaderboardChartProps
15
16
  | 'width'
16
17
  | 'height'
17
18
  | 'size'
19
+ | 'gap'
18
20
  | 'legendInteractive'
19
21
  | 'animation'
20
22
  > {
@@ -61,14 +63,9 @@ export interface LeaderboardChartProps
61
63
  };
62
64
 
63
65
  /**
64
- * Width of legend shapes in pixels
66
+ * Styles for legend shapes (width, height, margin).
65
67
  */
66
- legendShapeWidth?: number;
67
-
68
- /**
69
- * Height of legend shapes in pixels
70
- */
71
- legendShapeHeight?: number;
68
+ legendShapeStyles?: LegendShapeStyles;
72
69
 
73
70
  /**
74
71
  * Custom labels for legend items
@@ -457,9 +457,8 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
457
457
  orientation={ legendOrientation }
458
458
  alignment={ legendAlignment }
459
459
  position={ legendPosition }
460
- maxWidth={ legendMaxWidth }
461
- textOverflow={ legendTextOverflow }
462
- legendItemClassName={ legendItemClassName }
460
+ labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
461
+ itemClassName={ legendItemClassName }
463
462
  className={ styles[ 'line-chart__legend' ] }
464
463
  shape={ legendShape }
465
464
  chartId={ chartId }
@@ -317,9 +317,8 @@ const PieChartInternal = ( {
317
317
  orientation={ legendOrientation }
318
318
  position={ legendPosition }
319
319
  alignment={ legendAlignment }
320
- maxWidth={ legendMaxWidth }
321
- textOverflow={ legendTextOverflow }
322
- legendItemClassName={ legendItemClassName }
320
+ labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
321
+ itemClassName={ legendItemClassName }
323
322
  shape={ legendShape }
324
323
  chartId={ chartId }
325
324
  interactive={ legendInteractive }
@@ -352,9 +352,8 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
352
352
  orientation={ legendOrientation }
353
353
  position={ legendPosition }
354
354
  alignment={ legendAlignment }
355
- maxWidth={ legendMaxWidth }
356
- textOverflow={ legendTextOverflow }
357
- legendItemClassName={ legendItemClassName }
355
+ labelStyles={ { maxWidth: legendMaxWidth, textOverflow: legendTextOverflow } }
356
+ itemClassName={ legendItemClassName }
358
357
  shape={ legendShape }
359
358
  chartId={ chartId }
360
359
  interactive={ legendInteractive }
@@ -1,4 +1,11 @@
1
1
  export { Legend } from './legend';
2
2
  export { useChartLegendItems } from './hooks/use-chart-legend-items';
3
- export type { LegendProps, BaseLegendProps, BaseLegendItem } from './types';
3
+ export type {
4
+ LegendProps,
5
+ BaseLegendProps,
6
+ BaseLegendItem,
7
+ LegendItemStyles,
8
+ LegendLabelStyles,
9
+ LegendShapeStyles,
10
+ } from './types';
4
11
  export type { ChartLegendOptions, LegendValueDisplay } from './hooks/use-chart-legend-items';