@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.
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-3O6FHD2T.js → chunk-A3PGOCJO.js} +46 -4
- package/dist/chunk-A3PGOCJO.js.map +1 -0
- package/dist/{chunk-G66WE3ON.js → chunk-BWEMZ72V.js} +41 -15
- package/dist/chunk-BWEMZ72V.js.map +1 -0
- package/dist/{chunk-BZ6UDD37.cjs → chunk-CNAKHZMW.cjs} +69 -31
- package/dist/chunk-CNAKHZMW.cjs.map +1 -0
- package/dist/{chunk-G4FX5I3V.cjs → chunk-EPHDZVIG.cjs} +80 -56
- package/dist/chunk-EPHDZVIG.cjs.map +1 -0
- package/dist/{chunk-MAV6SE6L.cjs → chunk-GEB4GELE.cjs} +24 -24
- package/dist/{chunk-MAV6SE6L.cjs.map → chunk-GEB4GELE.cjs.map} +1 -1
- package/dist/{chunk-KM62I6SD.js → chunk-HVWETEEV.js} +53 -15
- package/dist/chunk-HVWETEEV.js.map +1 -0
- package/dist/{chunk-HYHBAHIU.js → chunk-JGX3ZNK5.js} +3 -3
- package/dist/{chunk-W5RFMC3A.js → chunk-JI6OGGGF.js} +3 -3
- package/dist/{chunk-SHADFB3T.js → chunk-KEBKTDOQ.js} +2 -2
- package/dist/{chunk-2HUX2CAT.cjs → chunk-LSGYIUQX.cjs} +44 -2
- package/dist/chunk-LSGYIUQX.cjs.map +1 -0
- package/dist/{chunk-UHESRL2F.cjs → chunk-N36WJKYM.cjs} +6 -6
- package/dist/{chunk-UHESRL2F.cjs.map → chunk-N36WJKYM.cjs.map} +1 -1
- package/dist/{chunk-Q2LDRQN7.js → chunk-PFT2X4OW.js} +2 -2
- package/dist/{chunk-GK3XEXVI.cjs → chunk-PNSMPZ3E.cjs} +8 -8
- package/dist/{chunk-GK3XEXVI.cjs.map → chunk-PNSMPZ3E.cjs.map} +1 -1
- package/dist/{chunk-SC462VDM.cjs → chunk-QPHNEQCK.cjs} +11 -11
- package/dist/{chunk-SC462VDM.cjs.map → chunk-QPHNEQCK.cjs.map} +1 -1
- package/dist/{chunk-2HB55BRH.js → chunk-VM3CHO3G.js} +62 -38
- package/dist/chunk-VM3CHO3G.js.map +1 -0
- package/dist/{chunk-ZA7OWPY7.cjs → chunk-VOMSG7KV.cjs} +50 -24
- package/dist/chunk-VOMSG7KV.cjs.map +1 -0
- package/dist/{chunk-QLLKOSJ6.cjs → chunk-YKVKFUV7.cjs} +50 -24
- package/dist/chunk-YKVKFUV7.cjs.map +1 -0
- package/dist/{chunk-XDIWMJZD.js → chunk-ZSNO2BYX.js} +39 -13
- package/dist/chunk-ZSNO2BYX.js.map +1 -0
- package/dist/components/bar-chart/index.cjs +4 -4
- package/dist/components/bar-chart/index.d.cts +2 -1
- package/dist/components/bar-chart/index.d.ts +2 -1
- package/dist/components/bar-chart/index.js +3 -3
- package/dist/components/bar-list-chart/index.cjs +5 -5
- package/dist/components/bar-list-chart/index.d.cts +1 -1
- package/dist/components/bar-list-chart/index.d.ts +1 -1
- package/dist/components/bar-list-chart/index.js +4 -4
- package/dist/components/conversion-funnel-chart/index.cjs +3 -3
- package/dist/components/conversion-funnel-chart/index.d.cts +1 -1
- package/dist/components/conversion-funnel-chart/index.d.ts +1 -1
- package/dist/components/conversion-funnel-chart/index.js +2 -2
- package/dist/components/leaderboard-chart/index.cjs +4 -4
- package/dist/components/leaderboard-chart/index.d.cts +2 -2
- package/dist/components/leaderboard-chart/index.d.ts +2 -2
- package/dist/components/leaderboard-chart/index.js +3 -3
- package/dist/components/legend/index.cjs +3 -3
- package/dist/components/legend/index.d.cts +1 -1
- package/dist/components/legend/index.d.ts +1 -1
- package/dist/components/legend/index.js +2 -2
- package/dist/components/line-chart/index.cjs +4 -4
- package/dist/components/line-chart/index.d.cts +1 -1
- package/dist/components/line-chart/index.d.ts +1 -1
- package/dist/components/line-chart/index.js +3 -3
- package/dist/components/pie-chart/index.cjs +4 -4
- package/dist/components/pie-chart/index.d.cts +7 -1
- package/dist/components/pie-chart/index.d.ts +7 -1
- package/dist/components/pie-chart/index.js +3 -3
- package/dist/components/pie-semi-circle-chart/index.cjs +4 -4
- package/dist/components/pie-semi-circle-chart/index.d.cts +7 -1
- package/dist/components/pie-semi-circle-chart/index.d.ts +7 -1
- package/dist/components/pie-semi-circle-chart/index.js +3 -3
- package/dist/components/tooltip/index.d.cts +1 -1
- package/dist/components/tooltip/index.d.ts +1 -1
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +79 -2
- package/dist/hooks/index.d.ts +79 -2
- package/dist/hooks/index.js +3 -1
- package/dist/index.cjs +10 -10
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -9
- package/dist/{leaderboard-chart-rqyTz1m6.d.ts → leaderboard-chart-Cpg_k_Vg.d.ts} +1 -1
- package/dist/{leaderboard-chart-BWEheWCd.d.cts → leaderboard-chart-DOaY0V1U.d.cts} +1 -1
- package/dist/providers/index.cjs +2 -2
- package/dist/providers/index.d.cts +2 -2
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js +1 -1
- package/dist/{themes-CGUHFZ5g.d.ts → themes-CN85BQM1.d.ts} +1 -1
- package/dist/{themes-B4swlmql.d.cts → themes-TIJq1lG_.d.cts} +1 -1
- package/dist/{types-cEbX_Q2K.d.ts → types-73KOEWs9.d.cts} +3 -1
- package/dist/{types-cEbX_Q2K.d.cts → types-73KOEWs9.d.ts} +3 -1
- package/package.json +1 -1
- package/src/components/bar-chart/bar-chart.tsx +57 -11
- package/src/components/bar-chart/test/bar-chart.test.tsx +114 -0
- package/src/components/pie-chart/pie-chart.tsx +130 -93
- package/src/components/pie-chart/test/pie-chart.test.tsx +174 -0
- package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +96 -57
- package/src/components/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +84 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-interactive-legend-data.ts +138 -0
- package/src/types.ts +3 -1
- package/dist/chunk-2HB55BRH.js.map +0 -1
- package/dist/chunk-2HUX2CAT.cjs.map +0 -1
- package/dist/chunk-3O6FHD2T.js.map +0 -1
- package/dist/chunk-BZ6UDD37.cjs.map +0 -1
- package/dist/chunk-G4FX5I3V.cjs.map +0 -1
- package/dist/chunk-G66WE3ON.js.map +0 -1
- package/dist/chunk-KM62I6SD.js.map +0 -1
- package/dist/chunk-QLLKOSJ6.cjs.map +0 -1
- package/dist/chunk-XDIWMJZD.js.map +0 -1
- package/dist/chunk-ZA7OWPY7.cjs.map +0 -1
- /package/dist/{chunk-HYHBAHIU.js.map → chunk-JGX3ZNK5.js.map} +0 -0
- /package/dist/{chunk-W5RFMC3A.js.map → chunk-JI6OGGGF.js.map} +0 -0
- /package/dist/{chunk-SHADFB3T.js.map → chunk-KEBKTDOQ.js.map} +0 -0
- /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
|
|
209
|
-
const legendItems = useChartLegendItems(
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
{
|
|
291
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
314
|
+
y={ -radius / 2 }
|
|
315
|
+
fill="#ccc"
|
|
316
|
+
fontSize="14"
|
|
317
|
+
fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
|
|
334
318
|
>
|
|
335
|
-
{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
} );
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|