@automattic/charts 0.44.0 → 0.46.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 (113) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/{chunk-2HB55BRH.js → chunk-4H3J2HCD.js} +102 -47
  3. package/dist/chunk-4H3J2HCD.js.map +1 -0
  4. package/dist/{chunk-G4FX5I3V.cjs → chunk-7AH76DXF.cjs} +119 -64
  5. package/dist/chunk-7AH76DXF.cjs.map +1 -0
  6. package/dist/{chunk-3O6FHD2T.js → chunk-A3PGOCJO.js} +46 -4
  7. package/dist/chunk-A3PGOCJO.js.map +1 -0
  8. package/dist/{chunk-G66WE3ON.js → chunk-BWEMZ72V.js} +41 -15
  9. package/dist/chunk-BWEMZ72V.js.map +1 -0
  10. package/dist/{chunk-BZ6UDD37.cjs → chunk-CNAKHZMW.cjs} +69 -31
  11. package/dist/chunk-CNAKHZMW.cjs.map +1 -0
  12. package/dist/{chunk-MAV6SE6L.cjs → chunk-GEB4GELE.cjs} +24 -24
  13. package/dist/{chunk-MAV6SE6L.cjs.map → chunk-GEB4GELE.cjs.map} +1 -1
  14. package/dist/{chunk-KM62I6SD.js → chunk-HVWETEEV.js} +53 -15
  15. package/dist/chunk-HVWETEEV.js.map +1 -0
  16. package/dist/{chunk-HYHBAHIU.js → chunk-JGX3ZNK5.js} +3 -3
  17. package/dist/{chunk-W5RFMC3A.js → chunk-JI6OGGGF.js} +3 -3
  18. package/dist/{chunk-SHADFB3T.js → chunk-KEBKTDOQ.js} +2 -2
  19. package/dist/{chunk-2HUX2CAT.cjs → chunk-LSGYIUQX.cjs} +44 -2
  20. package/dist/chunk-LSGYIUQX.cjs.map +1 -0
  21. package/dist/{chunk-UHESRL2F.cjs → chunk-N36WJKYM.cjs} +6 -6
  22. package/dist/{chunk-UHESRL2F.cjs.map → chunk-N36WJKYM.cjs.map} +1 -1
  23. package/dist/{chunk-Q2LDRQN7.js → chunk-PFT2X4OW.js} +2 -2
  24. package/dist/{chunk-GK3XEXVI.cjs → chunk-PNSMPZ3E.cjs} +8 -8
  25. package/dist/{chunk-GK3XEXVI.cjs.map → chunk-PNSMPZ3E.cjs.map} +1 -1
  26. package/dist/{chunk-SC462VDM.cjs → chunk-QPHNEQCK.cjs} +11 -11
  27. package/dist/{chunk-SC462VDM.cjs.map → chunk-QPHNEQCK.cjs.map} +1 -1
  28. package/dist/{chunk-ZA7OWPY7.cjs → chunk-VOMSG7KV.cjs} +50 -24
  29. package/dist/chunk-VOMSG7KV.cjs.map +1 -0
  30. package/dist/{chunk-QLLKOSJ6.cjs → chunk-YKVKFUV7.cjs} +50 -24
  31. package/dist/chunk-YKVKFUV7.cjs.map +1 -0
  32. package/dist/{chunk-XDIWMJZD.js → chunk-ZSNO2BYX.js} +39 -13
  33. package/dist/chunk-ZSNO2BYX.js.map +1 -0
  34. package/dist/components/bar-chart/index.cjs +4 -4
  35. package/dist/components/bar-chart/index.d.cts +2 -1
  36. package/dist/components/bar-chart/index.d.ts +2 -1
  37. package/dist/components/bar-chart/index.js +3 -3
  38. package/dist/components/bar-list-chart/index.cjs +5 -5
  39. package/dist/components/bar-list-chart/index.d.cts +1 -1
  40. package/dist/components/bar-list-chart/index.d.ts +1 -1
  41. package/dist/components/bar-list-chart/index.js +4 -4
  42. package/dist/components/conversion-funnel-chart/index.cjs +3 -3
  43. package/dist/components/conversion-funnel-chart/index.d.cts +1 -1
  44. package/dist/components/conversion-funnel-chart/index.d.ts +1 -1
  45. package/dist/components/conversion-funnel-chart/index.js +2 -2
  46. package/dist/components/leaderboard-chart/index.cjs +4 -4
  47. package/dist/components/leaderboard-chart/index.d.cts +2 -2
  48. package/dist/components/leaderboard-chart/index.d.ts +2 -2
  49. package/dist/components/leaderboard-chart/index.js +3 -3
  50. package/dist/components/legend/index.cjs +3 -3
  51. package/dist/components/legend/index.d.cts +1 -1
  52. package/dist/components/legend/index.d.ts +1 -1
  53. package/dist/components/legend/index.js +2 -2
  54. package/dist/components/line-chart/index.cjs +4 -4
  55. package/dist/components/line-chart/index.d.cts +1 -1
  56. package/dist/components/line-chart/index.d.ts +1 -1
  57. package/dist/components/line-chart/index.js +3 -3
  58. package/dist/components/pie-chart/index.cjs +4 -4
  59. package/dist/components/pie-chart/index.d.cts +7 -1
  60. package/dist/components/pie-chart/index.d.ts +7 -1
  61. package/dist/components/pie-chart/index.js +3 -3
  62. package/dist/components/pie-semi-circle-chart/index.cjs +4 -4
  63. package/dist/components/pie-semi-circle-chart/index.d.cts +7 -1
  64. package/dist/components/pie-semi-circle-chart/index.d.ts +7 -1
  65. package/dist/components/pie-semi-circle-chart/index.js +3 -3
  66. package/dist/components/tooltip/index.d.cts +1 -1
  67. package/dist/components/tooltip/index.d.ts +1 -1
  68. package/dist/hooks/index.cjs +4 -2
  69. package/dist/hooks/index.cjs.map +1 -1
  70. package/dist/hooks/index.d.cts +79 -2
  71. package/dist/hooks/index.d.ts +79 -2
  72. package/dist/hooks/index.js +3 -1
  73. package/dist/index.cjs +10 -10
  74. package/dist/index.d.cts +3 -3
  75. package/dist/index.d.ts +3 -3
  76. package/dist/index.js +9 -9
  77. package/dist/{leaderboard-chart-BWEheWCd.d.cts → leaderboard-chart-B5JRimc9.d.cts} +2 -2
  78. package/dist/{leaderboard-chart-rqyTz1m6.d.ts → leaderboard-chart-DQ8i8GMA.d.ts} +2 -2
  79. package/dist/providers/index.cjs +2 -2
  80. package/dist/providers/index.d.cts +2 -2
  81. package/dist/providers/index.d.ts +2 -2
  82. package/dist/providers/index.js +1 -1
  83. package/dist/{themes-CGUHFZ5g.d.ts → themes-CN85BQM1.d.ts} +1 -1
  84. package/dist/{themes-B4swlmql.d.cts → themes-TIJq1lG_.d.cts} +1 -1
  85. package/dist/{types-cEbX_Q2K.d.ts → types-73KOEWs9.d.cts} +3 -1
  86. package/dist/{types-cEbX_Q2K.d.cts → types-73KOEWs9.d.ts} +3 -1
  87. package/package.json +3 -3
  88. package/src/components/bar-chart/bar-chart.tsx +57 -11
  89. package/src/components/bar-chart/test/bar-chart.test.tsx +114 -0
  90. package/src/components/leaderboard-chart/leaderboard-chart.tsx +85 -38
  91. package/src/components/leaderboard-chart/test/leaderboard-chart.test.tsx +48 -0
  92. package/src/components/leaderboard-chart/types.ts +1 -0
  93. package/src/components/pie-chart/pie-chart.tsx +130 -93
  94. package/src/components/pie-chart/test/pie-chart.test.tsx +174 -0
  95. package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +96 -57
  96. package/src/components/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +84 -0
  97. package/src/hooks/index.ts +1 -0
  98. package/src/hooks/use-interactive-legend-data.ts +138 -0
  99. package/src/types.ts +3 -1
  100. package/dist/chunk-2HB55BRH.js.map +0 -1
  101. package/dist/chunk-2HUX2CAT.cjs.map +0 -1
  102. package/dist/chunk-3O6FHD2T.js.map +0 -1
  103. package/dist/chunk-BZ6UDD37.cjs.map +0 -1
  104. package/dist/chunk-G4FX5I3V.cjs.map +0 -1
  105. package/dist/chunk-G66WE3ON.js.map +0 -1
  106. package/dist/chunk-KM62I6SD.js.map +0 -1
  107. package/dist/chunk-QLLKOSJ6.cjs.map +0 -1
  108. package/dist/chunk-XDIWMJZD.js.map +0 -1
  109. package/dist/chunk-ZA7OWPY7.cjs.map +0 -1
  110. /package/dist/{chunk-HYHBAHIU.js.map → chunk-JGX3ZNK5.js.map} +0 -0
  111. /package/dist/{chunk-W5RFMC3A.js.map → chunk-JI6OGGGF.js.map} +0 -0
  112. /package/dist/{chunk-SHADFB3T.js.map → chunk-KEBKTDOQ.js.map} +0 -0
  113. /package/dist/{chunk-Q2LDRQN7.js.map → chunk-PFT2X4OW.js.map} +0 -0
@@ -344,7 +344,9 @@ type BaseChartProps<T = DataPoint | DataPointDate | LeaderboardEntry> = {
344
344
  legendItemClassName?: string;
345
345
  /**
346
346
  * Enable interactive legend items that can toggle series visibility.
347
- * Currently only supported for LineChart. Requires chartId and GlobalChartsProvider.
347
+ * Supported for LineChart, PieChart, and PieSemiCircleChart.
348
+ * Requires chartId and GlobalChartsProvider.
349
+ * For pie charts, percentages are recalculated so visible segments total 100%.
348
350
  */
349
351
  legendInteractive?: boolean;
350
352
  /**
@@ -344,7 +344,9 @@ type BaseChartProps<T = DataPoint | DataPointDate | LeaderboardEntry> = {
344
344
  legendItemClassName?: string;
345
345
  /**
346
346
  * Enable interactive legend items that can toggle series visibility.
347
- * Currently only supported for LineChart. Requires chartId and GlobalChartsProvider.
347
+ * Supported for LineChart, PieChart, and PieSemiCircleChart.
348
+ * Requires chartId and GlobalChartsProvider.
349
+ * For pie charts, percentages are recalculated so visible segments total 100%.
348
350
  */
349
351
  legendInteractive?: boolean;
350
352
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -150,7 +150,7 @@
150
150
  "typecheck": "tsc --noEmit"
151
151
  },
152
152
  "dependencies": {
153
- "@automattic/number-formatters": "^1.0.13",
153
+ "@automattic/number-formatters": "^1.0.14",
154
154
  "@babel/runtime": "7.28.4",
155
155
  "@react-spring/web": "9.7.5",
156
156
  "@visx/annotation": "^3.12.0",
@@ -216,7 +216,7 @@
216
216
  "sass-loader": "^16.0.0",
217
217
  "storybook": "9.0.15",
218
218
  "tsup": "8.5.0",
219
- "typescript": "5.9.2",
219
+ "typescript": "5.9.3",
220
220
  "webpack": "^5.88.0",
221
221
  "webpack-cli": "^6.0.0"
222
222
  },
@@ -15,6 +15,7 @@ import {
15
15
  useChartId,
16
16
  useChartRegistration,
17
17
  useGlobalChartsContext,
18
+ useGlobalChartsTheme,
18
19
  GlobalChartsContext,
19
20
  } from '../../providers';
20
21
  import { attachSubComponents } from '../../utils';
@@ -34,6 +35,7 @@ export interface BarChartProps extends BaseChartProps< SeriesData[] > {
34
35
  orientation?: 'horizontal' | 'vertical';
35
36
  withPatterns?: boolean;
36
37
  showZeroValues?: boolean;
38
+ legendInteractive?: boolean;
37
39
  children?: ReactNode;
38
40
  }
39
41
 
@@ -92,6 +94,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
92
94
  orientation = 'vertical',
93
95
  withPatterns = false,
94
96
  showZeroValues = false,
97
+ legendInteractive = false,
95
98
  children,
96
99
  } ) => {
97
100
  const horizontal = orientation === 'horizontal';
@@ -127,7 +130,29 @@ const BarChartInternal: FC< BarChartProps > = ( {
127
130
  totalPoints,
128
131
  } );
129
132
 
130
- const { getElementStyles } = useGlobalChartsContext();
133
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
134
+ const providerTheme = useGlobalChartsTheme();
135
+
136
+ // Add visibility information to series when using interactive legends
137
+ const seriesWithVisibility = useMemo( () => {
138
+ if ( ! chartId || ! legendInteractive ) {
139
+ return dataWithVisibleZeros.map( ( series, index ) => ( {
140
+ series,
141
+ index,
142
+ isVisible: true,
143
+ } ) );
144
+ }
145
+ return dataWithVisibleZeros.map( ( series, index ) => ( {
146
+ series,
147
+ index,
148
+ isVisible: isSeriesVisible( chartId, series.label ),
149
+ } ) );
150
+ }, [ dataWithVisibleZeros, chartId, isSeriesVisible, legendInteractive ] );
151
+
152
+ // Check if all series are hidden
153
+ const allSeriesHidden = useMemo( () => {
154
+ return seriesWithVisibility.every( ( { isVisible } ) => ! isVisible );
155
+ }, [ seriesWithVisibility ] );
131
156
 
132
157
  const getBarBackground = useCallback(
133
158
  ( index: number ) => () =>
@@ -348,17 +373,37 @@ const BarChartInternal: FC< BarChartProps > = ( {
348
373
 
349
374
  { highlightedBarStyle && <style>{ highlightedBarStyle }</style> }
350
375
 
376
+ { allSeriesHidden ? (
377
+ <text
378
+ x={ width / 2 }
379
+ y={ ( height - ( showLegend ? legendHeight : 0 ) ) / 2 }
380
+ textAnchor="middle"
381
+ fill={ providerTheme.gridStyles?.stroke || '#ccc' }
382
+ fontSize="14"
383
+ fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
384
+ >
385
+ { __( 'All series are hidden. Click legend items to show data.', 'jetpack-charts' ) }
386
+ </text>
387
+ ) : null }
388
+
351
389
  <BarGroup padding={ chartOptions.barGroup.padding }>
352
- { dataWithVisibleZeros.map( ( seriesData, index ) => (
353
- <BarSeries
354
- key={ seriesData?.label }
355
- dataKey={ seriesData?.label }
356
- data={ seriesData.data as DataPointDate[] }
357
- yAccessor={ chartOptions.accessors.yAccessor }
358
- xAccessor={ chartOptions.accessors.xAccessor }
359
- colorAccessor={ getBarBackground( index ) }
360
- />
361
- ) ) }
390
+ { seriesWithVisibility.map( ( { series: seriesData, index, isVisible } ) => {
391
+ // Skip rendering invisible series
392
+ if ( ! isVisible ) {
393
+ return null;
394
+ }
395
+
396
+ return (
397
+ <BarSeries
398
+ key={ seriesData?.label }
399
+ dataKey={ seriesData?.label }
400
+ data={ seriesData.data as DataPointDate[] }
401
+ yAccessor={ chartOptions.accessors.yAccessor }
402
+ xAccessor={ chartOptions.accessors.xAccessor }
403
+ colorAccessor={ getBarBackground( index ) }
404
+ />
405
+ );
406
+ } ) }
362
407
  </BarGroup>
363
408
 
364
409
  <Axis { ...chartOptions.axis.x } />
@@ -391,6 +436,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
391
436
  shape={ legendShape }
392
437
  ref={ legendRef }
393
438
  chartId={ chartId }
439
+ interactive={ legendInteractive }
394
440
  />
395
441
  ) }
396
442
 
@@ -563,4 +563,118 @@ describe( 'BarChart', () => {
563
563
  } );
564
564
 
565
565
  /* eslint-enable testing-library/no-node-access */
566
+
567
+ describe( 'Interactive Legend', () => {
568
+ it( 'filters series when interactive legend is enabled and series is toggled', async () => {
569
+ const user = userEvent.setup();
570
+
571
+ renderWithTheme( {
572
+ showLegend: true,
573
+ legendInteractive: true,
574
+ chartId: 'test-interactive-bar-chart',
575
+ data: [
576
+ {
577
+ label: 'Series A',
578
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
579
+ options: {},
580
+ },
581
+ {
582
+ label: 'Series B',
583
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
584
+ options: {},
585
+ },
586
+ ],
587
+ } );
588
+
589
+ // Click on first legend item to hide it
590
+ const legendItems = screen.getAllByRole( 'button' );
591
+ await user.click( legendItems[ 0 ] );
592
+
593
+ // The series should now be hidden (aria-pressed = false)
594
+ const legendItem = screen.getAllByRole( 'button' )[ 0 ];
595
+ expect( legendItem ).toHaveAttribute( 'aria-pressed', 'false' );
596
+ } );
597
+
598
+ it( 'does not filter series when legendInteractive is false', () => {
599
+ renderWithTheme( {
600
+ showLegend: true,
601
+ legendInteractive: false,
602
+ chartId: 'test-non-interactive-bar-chart',
603
+ data: [
604
+ {
605
+ label: 'Series A',
606
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
607
+ options: {},
608
+ },
609
+ {
610
+ label: 'Series B',
611
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
612
+ options: {},
613
+ },
614
+ ],
615
+ } );
616
+
617
+ // Legend items should not be interactive
618
+ const buttons = screen.queryAllByRole( 'button' );
619
+ expect( buttons ).toHaveLength( 0 );
620
+ } );
621
+
622
+ it( 'shows all series when chartId is missing even if legendInteractive is true', () => {
623
+ renderWithTheme( {
624
+ showLegend: true,
625
+ legendInteractive: true,
626
+ // No chartId provided
627
+ data: [
628
+ {
629
+ label: 'Series A',
630
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
631
+ options: {},
632
+ },
633
+ {
634
+ label: 'Series B',
635
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
636
+ options: {},
637
+ },
638
+ ],
639
+ } );
640
+
641
+ // All legend items should be visible (not hidden)
642
+ const legendItems = screen.getAllByRole( 'button' );
643
+ legendItems.forEach( item => {
644
+ expect( item ).toHaveAttribute( 'aria-pressed', 'true' );
645
+ } );
646
+ } );
647
+
648
+ it( 'shows "All series are hidden" message when all series are toggled off', async () => {
649
+ const user = userEvent.setup();
650
+
651
+ renderWithTheme( {
652
+ showLegend: true,
653
+ legendInteractive: true,
654
+ chartId: 'test-all-hidden-bar-chart',
655
+ data: [
656
+ {
657
+ label: 'Series A',
658
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
659
+ options: {},
660
+ },
661
+ {
662
+ label: 'Series B',
663
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
664
+ options: {},
665
+ },
666
+ ],
667
+ } );
668
+
669
+ // Hide all series
670
+ const legendItems = screen.getAllByRole( 'button' );
671
+ await user.click( legendItems[ 0 ] );
672
+ await user.click( legendItems[ 1 ] );
673
+
674
+ // Check for the "all series hidden" message
675
+ expect(
676
+ screen.getByText( /all series are hidden.*click legend items to show data/i )
677
+ ).toBeInTheDocument();
678
+ } );
679
+ } );
566
680
  } );
@@ -62,12 +62,16 @@ const BarWithLabel = ( {
62
62
  withOverlayLabel,
63
63
  primaryColor,
64
64
  secondaryColor,
65
+ isPrimaryVisible = true,
66
+ isComparisonVisible = true,
65
67
  }: {
66
68
  entry: LeaderboardEntry;
67
69
  withComparison?: boolean;
68
70
  withOverlayLabel?: boolean;
69
71
  primaryColor: string;
70
72
  secondaryColor: string;
73
+ isPrimaryVisible?: boolean;
74
+ isComparisonVisible?: boolean;
71
75
  } ) => (
72
76
  <div
73
77
  className={ clsx( styles.barWithLabelContainer, {
@@ -76,15 +80,17 @@ const BarWithLabel = ( {
76
80
  >
77
81
  <BarLabel label={ entry.label } />
78
82
 
79
- <div
80
- className={ styles.bar }
81
- style={ {
82
- width: entry.currentShare + '%',
83
- backgroundColor: primaryColor,
84
- } }
85
- ></div>
83
+ { isPrimaryVisible && (
84
+ <div
85
+ className={ styles.bar }
86
+ style={ {
87
+ width: entry.currentShare + '%',
88
+ backgroundColor: primaryColor,
89
+ } }
90
+ ></div>
91
+ ) }
86
92
 
87
- { withComparison && ! withOverlayLabel && (
93
+ { withComparison && ! withOverlayLabel && isComparisonVisible && (
88
94
  <div
89
95
  className={ styles.bar }
90
96
  style={ {
@@ -118,6 +124,7 @@ const BarWithLabel = ( {
118
124
  * @param props.legendShapeWidth - Width of legend shapes in pixels
119
125
  * @param props.legendShapeHeight - Height of legend shapes in pixels
120
126
  * @param props.legendLabels - Custom labels for legend items
127
+ * @param props.legendInteractive - Whether legend items are interactive (clickable to toggle series visibility)
121
128
  * @param props.children - Child components for composition API
122
129
  * @param props.className - Additional CSS class name
123
130
  * @param props.style - Custom styling for the chart container
@@ -141,6 +148,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
141
148
  legendShapeWidth = 8,
142
149
  legendShapeHeight = 8,
143
150
  legendLabels,
151
+ legendInteractive = false,
144
152
  className,
145
153
  style,
146
154
  children,
@@ -158,7 +166,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
158
166
  secondaryColor: settingsSecondaryColor,
159
167
  deltaColors,
160
168
  } = leaderboardChartSettings;
161
- const { getElementStyles } = useGlobalChartsContext();
169
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
162
170
  const { color: resolvedPrimaryColor } = getElementStyles( {
163
171
  index: 0,
164
172
  overrideColor: primaryColor || settingsPrimaryColor,
@@ -178,6 +186,36 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
178
186
  legendLabels,
179
187
  } );
180
188
 
189
+ // Track visibility of primary and comparison series for interactive legends
190
+ const isPrimaryVisible = useMemo( () => {
191
+ if ( ! chartId || ! legendInteractive || legendItems.length === 0 ) {
192
+ return true;
193
+ }
194
+ return isSeriesVisible( chartId, legendItems[ 0 ].label );
195
+ }, [ chartId, legendInteractive, legendItems, isSeriesVisible ] );
196
+
197
+ const isComparisonVisible = useMemo( () => {
198
+ if ( ! chartId || ! legendInteractive || legendItems.length < 2 ) {
199
+ return true;
200
+ }
201
+ return isSeriesVisible( chartId, legendItems[ 1 ].label );
202
+ }, [ chartId, legendInteractive, legendItems, isSeriesVisible ] );
203
+
204
+ // Check if all series are hidden
205
+ const allSeriesHidden = useMemo( () => {
206
+ if ( ! legendInteractive ) return false;
207
+ if ( withComparison && ! withOverlayLabel ) {
208
+ return ! isPrimaryVisible && ! isComparisonVisible;
209
+ }
210
+ return ! isPrimaryVisible;
211
+ }, [
212
+ legendInteractive,
213
+ isPrimaryVisible,
214
+ isComparisonVisible,
215
+ withComparison,
216
+ withOverlayLabel,
217
+ ] );
218
+
181
219
  // Validate data
182
220
  const isDataValid = Boolean( data && data.length > 0 );
183
221
 
@@ -242,38 +280,46 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
242
280
  gap: showLegend ? '16px' : '0',
243
281
  } }
244
282
  >
245
- <Grid templateColumns="minmax(0, 1fr) auto" rowGap={ rowGap } columnGap={ columnGap }>
246
- { data.map( entry => {
247
- const colorIndex = Math.sign( entry.delta ) + 1;
248
- const deltaColor = deltaColors[ colorIndex ];
283
+ { allSeriesHidden ? (
284
+ <div className={ styles.emptyState }>
285
+ { __( 'All series are hidden. Click legend items to show data.', 'jetpack-charts' ) }
286
+ </div>
287
+ ) : (
288
+ <Grid templateColumns="minmax(0, 1fr) auto" rowGap={ rowGap } columnGap={ columnGap }>
289
+ { data.map( entry => {
290
+ const colorIndex = Math.sign( entry.delta ) + 1;
291
+ const deltaColor = deltaColors[ colorIndex ];
249
292
 
250
- return (
251
- <Fragment key={ entry.id }>
252
- <VStack spacing={ labelSpacing }>
253
- <BarWithLabel
254
- entry={ entry }
255
- withComparison={ withComparison }
256
- withOverlayLabel={ withOverlayLabel }
257
- primaryColor={ resolvedPrimaryColor }
258
- secondaryColor={ resolvedSecondaryColor }
259
- />
260
- </VStack>
293
+ return (
294
+ <Fragment key={ entry.id }>
295
+ <VStack spacing={ labelSpacing }>
296
+ <BarWithLabel
297
+ entry={ entry }
298
+ withComparison={ withComparison }
299
+ withOverlayLabel={ withOverlayLabel }
300
+ primaryColor={ resolvedPrimaryColor }
301
+ secondaryColor={ resolvedSecondaryColor }
302
+ isPrimaryVisible={ isPrimaryVisible }
303
+ isComparisonVisible={ isComparisonVisible }
304
+ />
305
+ </VStack>
261
306
 
262
- <div
263
- className={ clsx( styles.valueContainer, {
264
- [ styles.overlayLabel ]: withOverlayLabel,
265
- } ) }
266
- >
267
- <Text>{ valueFormatter( entry.currentValue ) }</Text>
307
+ <div
308
+ className={ clsx( styles.valueContainer, {
309
+ [ styles.overlayLabel ]: withOverlayLabel,
310
+ } ) }
311
+ >
312
+ { isPrimaryVisible && <Text>{ valueFormatter( entry.currentValue ) }</Text> }
268
313
 
269
- { withComparison && (
270
- <Text style={ { color: deltaColor } }>{ deltaFormatter( entry.delta ) }</Text>
271
- ) }
272
- </div>
273
- </Fragment>
274
- );
275
- } ) }
276
- </Grid>
314
+ { withComparison && isComparisonVisible && (
315
+ <Text style={ { color: deltaColor } }>{ deltaFormatter( entry.delta ) }</Text>
316
+ ) }
317
+ </div>
318
+ </Fragment>
319
+ );
320
+ } ) }
321
+ </Grid>
322
+ ) }
277
323
 
278
324
  { showLegend && (
279
325
  <Legend
@@ -284,6 +330,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
284
330
  shapeWidth={ legendShapeWidth }
285
331
  shapeHeight={ legendShapeHeight }
286
332
  chartId={ chartId }
333
+ interactive={ legendInteractive }
287
334
  />
288
335
  ) }
289
336
 
@@ -291,4 +291,52 @@ describe( 'LeaderboardChart', () => {
291
291
  expect( screen.getByText( '-8%' ) ).toBeInTheDocument();
292
292
  } );
293
293
  } );
294
+
295
+ describe( 'Interactive Legend', () => {
296
+ it( 'renders legend as interactive when legendInteractive is true', () => {
297
+ render(
298
+ <LeaderboardChart
299
+ data={ mockData }
300
+ withComparison={ true }
301
+ showLegend={ true }
302
+ legendInteractive={ true }
303
+ />
304
+ );
305
+
306
+ const legendItems = screen.getAllByRole( 'button' );
307
+ expect( legendItems.length ).toBeGreaterThan( 0 );
308
+ } );
309
+
310
+ it( 'renders legend as non-interactive when legendInteractive is false', () => {
311
+ render(
312
+ <LeaderboardChart
313
+ data={ mockData }
314
+ withComparison={ true }
315
+ showLegend={ true }
316
+ legendInteractive={ false }
317
+ />
318
+ );
319
+
320
+ // Legend items should not have button role when not interactive
321
+ const legendItems = screen.queryAllByRole( 'button' );
322
+ expect( legendItems ).toHaveLength( 0 );
323
+ } );
324
+
325
+ it( 'shows all data when all series are visible', () => {
326
+ render(
327
+ <LeaderboardChart
328
+ data={ mockData }
329
+ withComparison={ true }
330
+ showLegend={ true }
331
+ legendInteractive={ true }
332
+ />
333
+ );
334
+
335
+ // All values should be visible
336
+ expect( screen.getByText( '12.5K' ) ).toBeInTheDocument();
337
+ expect( screen.getByText( '8.8K' ) ).toBeInTheDocument();
338
+ expect( screen.getByText( '+25%' ) ).toBeInTheDocument();
339
+ expect( screen.getByText( '-8%' ) ).toBeInTheDocument();
340
+ } );
341
+ } );
294
342
  } );
@@ -15,6 +15,7 @@ export interface LeaderboardChartProps
15
15
  | 'width'
16
16
  | 'height'
17
17
  | 'size'
18
+ | 'legendInteractive'
18
19
  > {
19
20
  /**
20
21
  * Whether to show comparison data