@automattic/charts 0.44.0 → 0.45.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 (110) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{chunk-3O6FHD2T.js → chunk-A3PGOCJO.js} +46 -4
  3. package/dist/chunk-A3PGOCJO.js.map +1 -0
  4. package/dist/{chunk-G66WE3ON.js → chunk-BWEMZ72V.js} +41 -15
  5. package/dist/chunk-BWEMZ72V.js.map +1 -0
  6. package/dist/{chunk-BZ6UDD37.cjs → chunk-CNAKHZMW.cjs} +69 -31
  7. package/dist/chunk-CNAKHZMW.cjs.map +1 -0
  8. package/dist/{chunk-G4FX5I3V.cjs → chunk-EPHDZVIG.cjs} +80 -56
  9. package/dist/chunk-EPHDZVIG.cjs.map +1 -0
  10. package/dist/{chunk-MAV6SE6L.cjs → chunk-GEB4GELE.cjs} +24 -24
  11. package/dist/{chunk-MAV6SE6L.cjs.map → chunk-GEB4GELE.cjs.map} +1 -1
  12. package/dist/{chunk-KM62I6SD.js → chunk-HVWETEEV.js} +53 -15
  13. package/dist/chunk-HVWETEEV.js.map +1 -0
  14. package/dist/{chunk-HYHBAHIU.js → chunk-JGX3ZNK5.js} +3 -3
  15. package/dist/{chunk-W5RFMC3A.js → chunk-JI6OGGGF.js} +3 -3
  16. package/dist/{chunk-SHADFB3T.js → chunk-KEBKTDOQ.js} +2 -2
  17. package/dist/{chunk-2HUX2CAT.cjs → chunk-LSGYIUQX.cjs} +44 -2
  18. package/dist/chunk-LSGYIUQX.cjs.map +1 -0
  19. package/dist/{chunk-UHESRL2F.cjs → chunk-N36WJKYM.cjs} +6 -6
  20. package/dist/{chunk-UHESRL2F.cjs.map → chunk-N36WJKYM.cjs.map} +1 -1
  21. package/dist/{chunk-Q2LDRQN7.js → chunk-PFT2X4OW.js} +2 -2
  22. package/dist/{chunk-GK3XEXVI.cjs → chunk-PNSMPZ3E.cjs} +8 -8
  23. package/dist/{chunk-GK3XEXVI.cjs.map → chunk-PNSMPZ3E.cjs.map} +1 -1
  24. package/dist/{chunk-SC462VDM.cjs → chunk-QPHNEQCK.cjs} +11 -11
  25. package/dist/{chunk-SC462VDM.cjs.map → chunk-QPHNEQCK.cjs.map} +1 -1
  26. package/dist/{chunk-2HB55BRH.js → chunk-VM3CHO3G.js} +62 -38
  27. package/dist/chunk-VM3CHO3G.js.map +1 -0
  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-rqyTz1m6.d.ts → leaderboard-chart-Cpg_k_Vg.d.ts} +1 -1
  78. package/dist/{leaderboard-chart-BWEheWCd.d.cts → leaderboard-chart-DOaY0V1U.d.cts} +1 -1
  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 +1 -1
  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/pie-chart/pie-chart.tsx +130 -93
  91. package/src/components/pie-chart/test/pie-chart.test.tsx +174 -0
  92. package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +96 -57
  93. package/src/components/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +84 -0
  94. package/src/hooks/index.ts +1 -0
  95. package/src/hooks/use-interactive-legend-data.ts +138 -0
  96. package/src/types.ts +3 -1
  97. package/dist/chunk-2HB55BRH.js.map +0 -1
  98. package/dist/chunk-2HUX2CAT.cjs.map +0 -1
  99. package/dist/chunk-3O6FHD2T.js.map +0 -1
  100. package/dist/chunk-BZ6UDD37.cjs.map +0 -1
  101. package/dist/chunk-G4FX5I3V.cjs.map +0 -1
  102. package/dist/chunk-G66WE3ON.js.map +0 -1
  103. package/dist/chunk-KM62I6SD.js.map +0 -1
  104. package/dist/chunk-QLLKOSJ6.cjs.map +0 -1
  105. package/dist/chunk-XDIWMJZD.js.map +0 -1
  106. package/dist/chunk-ZA7OWPY7.cjs.map +0 -1
  107. /package/dist/{chunk-HYHBAHIU.js.map → chunk-JGX3ZNK5.js.map} +0 -0
  108. /package/dist/{chunk-W5RFMC3A.js.map → chunk-JI6OGGGF.js.map} +0 -0
  109. /package/dist/{chunk-SHADFB3T.js.map → chunk-KEBKTDOQ.js.map} +0 -0
  110. /package/dist/{chunk-Q2LDRQN7.js.map → chunk-PFT2X4OW.js.map} +0 -0
@@ -3,9 +3,10 @@ import { Group } from '@visx/group';
3
3
  import { Pie } from '@visx/shape';
4
4
  import { Text } from '@visx/text';
5
5
  import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
6
+ import { __ } from '@wordpress/i18n';
6
7
  import clsx from 'clsx';
7
8
  import { useCallback, useContext, useMemo } from 'react';
8
- import { useElementHeight } from '../../hooks';
9
+ import { useElementHeight, useInteractiveLegendData } from '../../hooks';
9
10
  import {
10
11
  GlobalChartsProvider,
11
12
  useChartId,
@@ -70,6 +71,13 @@ export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercen
70
71
  */
71
72
  legendValueDisplay?: LegendValueDisplay;
72
73
 
74
+ /**
75
+ * Enable interactive legend items that can toggle segment visibility.
76
+ * Requires chartId and GlobalChartsProvider.
77
+ * When segments are hidden, percentages are recalculated so visible segments total 100%.
78
+ */
79
+ legendInteractive?: boolean;
80
+
73
81
  /**
74
82
  * Horizontal offset for tooltip positioning in pixels (default: 0)
75
83
  */
@@ -133,6 +141,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
133
141
  legendItemClassName,
134
142
  legendShape = 'circle',
135
143
  legendValueDisplay = 'percentage',
144
+ legendInteractive = false,
136
145
  label,
137
146
  note,
138
147
  className,
@@ -183,7 +192,15 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
183
192
  // Validate data first to get validation result
184
193
  const { isValid, message } = validateData( data );
185
194
 
186
- const { getElementStyles } = useGlobalChartsContext();
195
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
196
+
197
+ // Filter and recalculate data for interactive legends
198
+ const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
199
+ data,
200
+ chartId,
201
+ legendInteractive,
202
+ isSeriesVisible,
203
+ } );
187
204
 
188
205
  // Define accessors with useMemo to avoid changing dependencies
189
206
  const accessors = useMemo(
@@ -205,8 +222,8 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
205
222
  [ legendValueDisplay ]
206
223
  );
207
224
 
208
- // Create legend items using the reusable hook
209
- const legendItems = useChartLegendItems( data, legendOptions );
225
+ // Create legend items using legendData (has recalculated percentages for visible items)
226
+ const legendItems = useChartLegendItems( legendData, legendOptions );
210
227
 
211
228
  // Process children to extract compound components
212
229
  const { svgChildren, htmlChildren, otherChildren } = useChartChildren(
@@ -253,10 +270,14 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
253
270
  const innerRadius = radius * ( 1 - thickness );
254
271
 
255
272
  // Map data with index for color assignment
256
- const dataWithIndex = data.map( ( d, index ) => ( {
257
- ...d,
258
- index,
259
- } ) );
273
+ // When interactive, we need to find the original index to maintain consistent colors
274
+ const dataWithIndex = visibleData.map( d => {
275
+ const originalIndex = data.findIndex( item => item.label === d.label );
276
+ return {
277
+ ...d,
278
+ index: originalIndex >= 0 ? originalIndex : 0,
279
+ };
280
+ } );
260
281
 
261
282
  // Configure pie angles based on clockwise direction
262
283
  const startAngle = clockwise ? -Math.PI / 2 : Math.PI / 2;
@@ -287,57 +308,74 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
287
308
  >
288
309
  { /* Main chart group centered horizontally and positioned at bottom */ }
289
310
  <Group top={ chartHeight } left={ width / 2 }>
290
- { /* Pie chart */ }
291
- <Pie< DataPointPercentage & { index: number } >
292
- data={ dataWithIndex }
293
- pieValue={ accessors.value }
294
- outerRadius={ radius }
295
- innerRadius={ innerRadius }
296
- cornerRadius={ 3 }
297
- padAngle={ PAD_ANGLE }
298
- startAngle={ startAngle }
299
- endAngle={ endAngle }
300
- pieSort={ accessors.sort }
301
- >
302
- { pie => {
303
- return pie.arcs.map( arc => (
304
- <g
305
- key={ arc.data.label }
306
- onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
307
- onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
308
- >
309
- <path
310
- d={ pie.path( arc ) || '' }
311
- fill={ accessors.fill( arc.data ) }
312
- data-testid="pie-segment"
313
- />
314
- </g>
315
- ) );
316
- } }
317
- </Pie>
318
-
319
- { /* Label and note text */ }
320
- <Group>
321
- <Text
322
- textAnchor="middle"
323
- verticalAnchor="start"
324
- y={ -40 } // Position above the chart with space for note
325
- className={ styles.label }
326
- >
327
- { label }
328
- </Text>
329
- <Text
311
+ { allSegmentsHidden ? (
312
+ <text
330
313
  textAnchor="middle"
331
- verticalAnchor="start"
332
- y={ -20 } // Position between label and chart
333
- className={ styles.note }
314
+ y={ -radius / 2 }
315
+ fill="#ccc"
316
+ fontSize="14"
317
+ fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
334
318
  >
335
- { note }
336
- </Text>
337
- </Group>
338
-
339
- { /* Render SVG children from composition API */ }
340
- { svgChildren }
319
+ { __(
320
+ 'All segments are hidden. Click legend items to show data.',
321
+ 'jetpack-charts'
322
+ ) }
323
+ </text>
324
+ ) : (
325
+ <>
326
+ { /* Pie chart */ }
327
+ <Pie< DataPointPercentage & { index: number } >
328
+ data={ dataWithIndex }
329
+ pieValue={ accessors.value }
330
+ outerRadius={ radius }
331
+ innerRadius={ innerRadius }
332
+ cornerRadius={ 3 }
333
+ padAngle={ PAD_ANGLE }
334
+ startAngle={ startAngle }
335
+ endAngle={ endAngle }
336
+ pieSort={ accessors.sort }
337
+ >
338
+ { pie => {
339
+ return pie.arcs.map( arc => (
340
+ <g
341
+ key={ arc.data.label }
342
+ onMouseMove={ withTooltips ? handleArcMouseMove( arc ) : undefined }
343
+ onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
344
+ >
345
+ <path
346
+ d={ pie.path( arc ) || '' }
347
+ fill={ accessors.fill( arc.data ) }
348
+ data-testid="pie-segment"
349
+ />
350
+ </g>
351
+ ) );
352
+ } }
353
+ </Pie>
354
+
355
+ { /* Label and note text */ }
356
+ <Group>
357
+ <Text
358
+ textAnchor="middle"
359
+ verticalAnchor="start"
360
+ y={ -40 } // Position above the chart with space for note
361
+ className={ styles.label }
362
+ >
363
+ { label }
364
+ </Text>
365
+ <Text
366
+ textAnchor="middle"
367
+ verticalAnchor="start"
368
+ y={ -20 } // Position between label and chart
369
+ className={ styles.note }
370
+ >
371
+ { note }
372
+ </Text>
373
+ </Group>
374
+
375
+ { /* Render SVG children from composition API */ }
376
+ { ! allSegmentsHidden && svgChildren }
377
+ </>
378
+ ) }
341
379
  </Group>
342
380
  </svg>
343
381
 
@@ -360,6 +398,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
360
398
  shape={ legendShape }
361
399
  ref={ legendRef }
362
400
  chartId={ chartId }
401
+ interactive={ legendInteractive }
363
402
  />
364
403
  ) }
365
404
 
@@ -178,4 +178,88 @@ describe( 'PieSemiCircleChart', () => {
178
178
  ).toBeInTheDocument();
179
179
  } );
180
180
  } );
181
+
182
+ describe( 'Interactive Legend', () => {
183
+ test( 'filters segments when interactive legend is enabled and segment is toggled', async () => {
184
+ const user = userEvent.setup();
185
+ const testData = [
186
+ { label: 'Segment A', value: 50, percentage: 50 },
187
+ { label: 'Segment B', value: 50, percentage: 50 },
188
+ ];
189
+
190
+ renderPieChart( {
191
+ data: testData,
192
+ showLegend: true,
193
+ legendInteractive: true,
194
+ chartId: 'test-interactive-semi-circle-chart',
195
+ } );
196
+
197
+ // Initially both segments should be visible
198
+ let segments = screen.getAllByTestId( 'pie-segment' );
199
+ expect( segments ).toHaveLength( 2 );
200
+
201
+ // Click first legend item to hide segment A
202
+ const legendItem = screen.getByRole( 'button', { name: /Segment A/i } );
203
+ await user.click( legendItem );
204
+
205
+ // Only one segment should remain
206
+ await waitFor( () => {
207
+ segments = screen.getAllByTestId( 'pie-segment' );
208
+ expect( segments ).toHaveLength( 1 );
209
+ } );
210
+
211
+ // Legend item should be marked as hidden
212
+ expect( legendItem ).toHaveAttribute( 'aria-pressed', 'false' );
213
+ } );
214
+
215
+ test( 'shows empty state when all segments are hidden', async () => {
216
+ const user = userEvent.setup();
217
+ const testData = [
218
+ { label: 'Segment A', value: 50, percentage: 50 },
219
+ { label: 'Segment B', value: 50, percentage: 50 },
220
+ ];
221
+
222
+ renderPieChart( {
223
+ data: testData,
224
+ showLegend: true,
225
+ legendInteractive: true,
226
+ chartId: 'test-all-hidden-semi-circle-chart',
227
+ } );
228
+
229
+ // Hide both segments
230
+ const legendItems = screen.getAllByRole( 'button' );
231
+ await user.click( legendItems[ 0 ] );
232
+ await user.click( legendItems[ 1 ] );
233
+
234
+ // Should show empty state message
235
+ await waitFor( () => {
236
+ expect( screen.getByText( /all segments are hidden/i ) ).toBeInTheDocument();
237
+ } );
238
+
239
+ // Should not render any segments
240
+ expect( screen.queryAllByTestId( 'pie-segment' ) ).toHaveLength( 0 );
241
+ } );
242
+
243
+ test( 'does not filter segments when legendInteractive is false', () => {
244
+ const testData = [
245
+ { label: 'Segment A', value: 50, percentage: 50 },
246
+ { label: 'Segment B', value: 50, percentage: 50 },
247
+ ];
248
+
249
+ renderPieChart( {
250
+ data: testData,
251
+ showLegend: true,
252
+ legendInteractive: false,
253
+ chartId: 'test-non-interactive-semi-circle-chart',
254
+ } );
255
+
256
+ // Legend items should not be buttons
257
+ const buttons = screen.queryAllByRole( 'button' );
258
+ expect( buttons ).toHaveLength( 0 );
259
+
260
+ // All segments should be visible
261
+ const segments = screen.getAllByTestId( 'pie-segment' );
262
+ expect( segments ).toHaveLength( 2 );
263
+ } );
264
+ } );
181
265
  } );
@@ -6,3 +6,4 @@ export { useChartMargin } from './use-chart-margin';
6
6
  export { useElementHeight } from './use-element-height';
7
7
  export { useTextTruncation } from './use-text-truncation';
8
8
  export { useZeroValueDisplay } from './use-zero-value-display';
9
+ export { useInteractiveLegendData } from './use-interactive-legend-data';
@@ -0,0 +1,138 @@
1
+ import { useMemo } from 'react';
2
+
3
+ /**
4
+ * Data point interface for charts with interactive legends.
5
+ * Requires label for series identification, value for calculations, and percentage for display.
6
+ */
7
+ interface DataPointWithPercentage {
8
+ label: string;
9
+ value: number;
10
+ percentage: number;
11
+ }
12
+
13
+ /**
14
+ * Parameters for the useInteractiveLegendData hook.
15
+ */
16
+ interface UseInteractiveLegendDataParams< T extends DataPointWithPercentage > {
17
+ /** The chart data to filter based on legend visibility */
18
+ data: T[];
19
+ /** Unique chart identifier, required for interactive legends */
20
+ chartId: string | undefined;
21
+ /** Whether interactive legend filtering is enabled */
22
+ legendInteractive: boolean;
23
+ /** Function to check if a series is visible in the legend */
24
+ isSeriesVisible: ( chartId: string, label: string ) => boolean;
25
+ }
26
+
27
+ /**
28
+ * Return value from the useInteractiveLegendData hook.
29
+ */
30
+ interface UseInteractiveLegendDataResult< T extends DataPointWithPercentage > {
31
+ /** Filtered data array containing only visible segments with recalculated percentages */
32
+ visibleData: T[];
33
+ /** Boolean indicating if all segments are hidden */
34
+ allSegmentsHidden: boolean;
35
+ /**
36
+ * Legend data with recalculated percentages for visible items.
37
+ * Uses original data for hidden items, but shows recalculated percentages for visible ones.
38
+ * This ensures the legend displays accurate percentages while maintaining all entries.
39
+ */
40
+ legendData: T[];
41
+ }
42
+
43
+ /**
44
+ * Custom hook to filter and recalculate chart data for interactive legends.
45
+ *
46
+ * When interactive legends are enabled, this hook:
47
+ * 1. Filters data to show only visible series based on legend selection
48
+ * 2. Recalculates percentages so visible segments total 100%
49
+ * 3. Tracks whether all segments are hidden to show empty state
50
+ *
51
+ * This is particularly useful for pie charts, donut charts, and semi-circle charts
52
+ * where segment visibility and percentages need to be dynamically adjusted.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData({
57
+ * data: chartData,
58
+ * chartId: 'my-pie-chart',
59
+ * legendInteractive: true,
60
+ * isSeriesVisible: (id, label) => context.isSeriesVisible(id, label),
61
+ * });
62
+ *
63
+ * // Use legendData for creating legend items (shows recalculated percentages)
64
+ * const legendItems = useChartLegendItems(legendData, legendOptions);
65
+ *
66
+ * if (allSegmentsHidden) {
67
+ * return <EmptyState />;
68
+ * }
69
+ *
70
+ * // Use visibleData for rendering the chart (only visible segments)
71
+ * return <PieChart data={visibleData} />;
72
+ * ```
73
+ *
74
+ * @param params - Configuration object for the hook
75
+ * @param params.data - The chart data to filter
76
+ * @param params.chartId - Unique identifier for the chart (required for interactive mode)
77
+ * @param params.legendInteractive - Whether to enable interactive filtering
78
+ * @param params.isSeriesVisible - Function to check series visibility
79
+ * @return Object containing visibleData, allSegmentsHidden flag, and legendData with recalculated percentages
80
+ */
81
+ export const useInteractiveLegendData = < T extends DataPointWithPercentage >( {
82
+ data,
83
+ chartId,
84
+ legendInteractive,
85
+ isSeriesVisible,
86
+ }: UseInteractiveLegendDataParams< T > ): UseInteractiveLegendDataResult< T > => {
87
+ // Filter and recalculate data for interactive legends
88
+ const visibleData = useMemo( () => {
89
+ // If interactive mode is disabled or no chartId, return all data unchanged
90
+ if ( ! chartId || ! legendInteractive ) {
91
+ return data;
92
+ }
93
+
94
+ // Filter to only visible segments based on legend state
95
+ const filtered = data.filter( segment => isSeriesVisible( chartId, segment.label ) );
96
+
97
+ // If no segments are visible, return empty array
98
+ if ( filtered.length === 0 ) {
99
+ return [];
100
+ }
101
+
102
+ // Recalculate percentages so visible segments total 100%
103
+ const totalValue = filtered.reduce( ( sum, segment ) => sum + segment.value, 0 );
104
+
105
+ return filtered.map( segment => ( {
106
+ ...segment,
107
+ percentage: totalValue > 0 ? ( segment.value / totalValue ) * 100 : 0,
108
+ } ) );
109
+ }, [ data, chartId, isSeriesVisible, legendInteractive ] );
110
+
111
+ // Check if all segments are hidden (only relevant in interactive mode)
112
+ const allSegmentsHidden = useMemo( () => {
113
+ return legendInteractive && visibleData.length === 0;
114
+ }, [ legendInteractive, visibleData ] );
115
+
116
+ // Prepare legend data with recalculated percentages for visible items
117
+ // This maintains all legend entries but shows updated percentages for visible segments
118
+ const legendData = useMemo( () => {
119
+ if ( ! legendInteractive || ! chartId ) {
120
+ return data;
121
+ }
122
+
123
+ // Map original data to show recalculated percentages for visible items
124
+ return data.map( segment => {
125
+ const isVisible = isSeriesVisible( chartId, segment.label );
126
+ if ( ! isVisible ) {
127
+ // Return original data for hidden items
128
+ return segment;
129
+ }
130
+
131
+ // For visible items, find the recalculated percentage from visibleData
132
+ const recalculated = visibleData.find( d => d.label === segment.label );
133
+ return recalculated || segment;
134
+ } );
135
+ }, [ data, visibleData, legendInteractive, chartId, isSeriesVisible ] );
136
+
137
+ return { visibleData, allSegmentsHidden, legendData };
138
+ };
package/src/types.ts CHANGED
@@ -365,7 +365,9 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
365
365
  legendItemClassName?: string;
366
366
  /**
367
367
  * Enable interactive legend items that can toggle series visibility.
368
- * Currently only supported for LineChart. Requires chartId and GlobalChartsProvider.
368
+ * Supported for LineChart, PieChart, and PieSemiCircleChart.
369
+ * Requires chartId and GlobalChartsProvider.
370
+ * For pie charts, percentages are recalculated so visible segments total 100%.
369
371
  */
370
372
  legendInteractive?: boolean;
371
373
  /**