@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.
- package/CHANGELOG.md +14 -0
- package/dist/{chunk-2HB55BRH.js → chunk-4H3J2HCD.js} +102 -47
- package/dist/chunk-4H3J2HCD.js.map +1 -0
- package/dist/{chunk-G4FX5I3V.cjs → chunk-7AH76DXF.cjs} +119 -64
- package/dist/chunk-7AH76DXF.cjs.map +1 -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-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-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-BWEheWCd.d.cts → leaderboard-chart-B5JRimc9.d.cts} +2 -2
- package/dist/{leaderboard-chart-rqyTz1m6.d.ts → leaderboard-chart-DQ8i8GMA.d.ts} +2 -2
- 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 +3 -3
- 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/leaderboard-chart/leaderboard-chart.tsx +85 -38
- package/src/components/leaderboard-chart/test/leaderboard-chart.test.tsx +48 -0
- package/src/components/leaderboard-chart/types.ts +1 -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
|
@@ -2,9 +2,10 @@ import { localPoint } from '@visx/event';
|
|
|
2
2
|
import { Group } from '@visx/group';
|
|
3
3
|
import { Pie } from '@visx/shape';
|
|
4
4
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
5
6
|
import clsx from 'clsx';
|
|
6
7
|
import { useCallback, useContext, useMemo } from 'react';
|
|
7
|
-
import { useElementHeight } from '../../hooks';
|
|
8
|
+
import { useElementHeight, useInteractiveLegendData } from '../../hooks';
|
|
8
9
|
import {
|
|
9
10
|
GlobalChartsProvider,
|
|
10
11
|
useChartId,
|
|
@@ -70,6 +71,13 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
|
|
|
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
|
* Use the children prop to render additional elements on the chart.
|
|
75
83
|
*/
|
|
@@ -147,6 +155,7 @@ const PieChartInternal = ( {
|
|
|
147
155
|
cornerScale = 0,
|
|
148
156
|
showLabels = true,
|
|
149
157
|
legendValueDisplay = 'percentage',
|
|
158
|
+
legendInteractive = false,
|
|
150
159
|
children = null,
|
|
151
160
|
tooltipOffsetX = 0,
|
|
152
161
|
tooltipOffsetY = -15,
|
|
@@ -171,14 +180,24 @@ const PieChartInternal = ( {
|
|
|
171
180
|
hideTooltip();
|
|
172
181
|
}, [ withTooltips, hideTooltip ] );
|
|
173
182
|
|
|
183
|
+
const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
|
|
184
|
+
|
|
185
|
+
// Filter and recalculate data for interactive legends
|
|
186
|
+
const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
|
|
187
|
+
data,
|
|
188
|
+
chartId,
|
|
189
|
+
legendInteractive,
|
|
190
|
+
isSeriesVisible,
|
|
191
|
+
} );
|
|
192
|
+
|
|
174
193
|
// Memoize legend options to prevent unnecessary re-calculations
|
|
175
194
|
const legendOptions = useMemo(
|
|
176
195
|
() => ( { showValues: true, legendValueDisplay } ),
|
|
177
196
|
[ legendValueDisplay ]
|
|
178
197
|
);
|
|
179
198
|
|
|
180
|
-
// Create legend items using
|
|
181
|
-
const legendItems = useChartLegendItems(
|
|
199
|
+
// Create legend items using legendData (has recalculated percentages for visible items)
|
|
200
|
+
const legendItems = useChartLegendItems( legendData, legendOptions );
|
|
182
201
|
|
|
183
202
|
const { isValid, message } = validateData( data );
|
|
184
203
|
|
|
@@ -204,8 +223,6 @@ const PieChartInternal = ( {
|
|
|
204
223
|
metadata: chartMetadata,
|
|
205
224
|
} );
|
|
206
225
|
|
|
207
|
-
const { getElementStyles } = useGlobalChartsContext();
|
|
208
|
-
|
|
209
226
|
if ( ! isValid ) {
|
|
210
227
|
return (
|
|
211
228
|
<div className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) }>
|
|
@@ -225,7 +242,7 @@ const PieChartInternal = ( {
|
|
|
225
242
|
const centerX = width / 2;
|
|
226
243
|
const centerY = adjustedHeight / 2;
|
|
227
244
|
|
|
228
|
-
// Calculate the angle between each
|
|
245
|
+
// Calculate the angle between each (use original data length for consistent spacing)
|
|
229
246
|
const padAngle = gapScale * ( ( 2 * Math.PI ) / data.length );
|
|
230
247
|
|
|
231
248
|
const outerRadius = radius - padding;
|
|
@@ -235,10 +252,14 @@ const PieChartInternal = ( {
|
|
|
235
252
|
const cornerRadius = cornerScale ? Math.min( cornerScale * outerRadius, maxCornerRadius ) : 0;
|
|
236
253
|
|
|
237
254
|
// Map the data to include index for color assignment
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
255
|
+
// When interactive, we need to find the original index to maintain consistent colors
|
|
256
|
+
const dataWithIndex = visibleData.map( d => {
|
|
257
|
+
const originalIndex = data.findIndex( item => item.label === d.label );
|
|
258
|
+
return {
|
|
259
|
+
...d,
|
|
260
|
+
index: originalIndex >= 0 ? originalIndex : 0,
|
|
261
|
+
};
|
|
262
|
+
} );
|
|
242
263
|
|
|
243
264
|
const accessors = {
|
|
244
265
|
value: ( d: DataPointPercentage ) => d.value,
|
|
@@ -270,94 +291,109 @@ const PieChartInternal = ( {
|
|
|
270
291
|
height={ adjustedHeight }
|
|
271
292
|
>
|
|
272
293
|
<Group top={ centerY } left={ centerX }>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
294
|
+
{ allSegmentsHidden ? (
|
|
295
|
+
<text
|
|
296
|
+
textAnchor="middle"
|
|
297
|
+
dy=".33em"
|
|
298
|
+
fill={ providerTheme.gridColor || '#ccc' }
|
|
299
|
+
fontSize="14"
|
|
300
|
+
fontFamily="-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,sans-serif"
|
|
301
|
+
>
|
|
302
|
+
{ __(
|
|
303
|
+
'All segments are hidden. Click legend items to show data.',
|
|
304
|
+
'jetpack-charts'
|
|
305
|
+
) }
|
|
306
|
+
</text>
|
|
307
|
+
) : (
|
|
308
|
+
<Pie< DataPointPercentage & { index: number } >
|
|
309
|
+
data={ dataWithIndex }
|
|
310
|
+
pieValue={ accessors.value }
|
|
311
|
+
outerRadius={ outerRadius }
|
|
312
|
+
innerRadius={ innerRadius }
|
|
313
|
+
padAngle={ padAngle }
|
|
314
|
+
cornerRadius={ cornerRadius }
|
|
315
|
+
>
|
|
316
|
+
{ pie => {
|
|
317
|
+
return pie.arcs.map( ( arc, index ) => {
|
|
318
|
+
const [ centroidX, centroidY ] = pie.path.centroid( arc );
|
|
319
|
+
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
|
|
320
|
+
const handleMouseMove = ( event: MouseEvent< SVGElement > ) => {
|
|
321
|
+
if ( ! withTooltips ) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Get coordinates relative to the current target element
|
|
326
|
+
const coords = localPoint( event );
|
|
327
|
+
if ( coords ) {
|
|
328
|
+
// Account for legend offset when legend is on top
|
|
329
|
+
const legendOffset =
|
|
330
|
+
showLegend && legendPosition === 'top' ? legendHeight : 0;
|
|
331
|
+
showTooltip( {
|
|
332
|
+
tooltipData: arc.data,
|
|
333
|
+
tooltipLeft: coords.x + tooltipOffsetX,
|
|
334
|
+
tooltipTop: coords.y + legendOffset + tooltipOffsetY,
|
|
335
|
+
} );
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const pathProps: SVGProps< SVGPathElement > & { 'data-testid'?: string } = {
|
|
340
|
+
d: pie.path( arc ) || '',
|
|
341
|
+
fill: accessors.fill( arc.data ),
|
|
342
|
+
'data-testid': 'pie-segment',
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const groupProps: SVGProps< SVGGElement > = {};
|
|
346
|
+
if ( withTooltips ) {
|
|
347
|
+
groupProps.onMouseMove = handleMouseMove;
|
|
348
|
+
groupProps.onMouseLeave = onMouseLeave;
|
|
288
349
|
}
|
|
289
350
|
|
|
290
|
-
//
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const backgroundHeight = fontSize + labelPadding * 2;
|
|
322
|
-
|
|
323
|
-
return (
|
|
324
|
-
<g key={ `arc-${ index }` } { ...groupProps }>
|
|
325
|
-
<path { ...pathProps } />
|
|
326
|
-
{ showLabels && hasSpaceForLabel && (
|
|
327
|
-
<g>
|
|
328
|
-
{ providerTheme.labelBackgroundColor && (
|
|
329
|
-
<rect
|
|
330
|
-
x={ centroidX - backgroundWidth / 2 }
|
|
331
|
-
y={ centroidY - backgroundHeight / 2 }
|
|
332
|
-
width={ backgroundWidth }
|
|
333
|
-
height={ backgroundHeight }
|
|
334
|
-
fill={ providerTheme.labelBackgroundColor }
|
|
335
|
-
rx={ 4 }
|
|
336
|
-
ry={ 4 }
|
|
351
|
+
// Estimate text width more accurately for background sizing
|
|
352
|
+
const fontSize = 12;
|
|
353
|
+
const estimatedTextWidth = getStringWidth( arc.data.label, { fontSize } );
|
|
354
|
+
const labelPadding = 6;
|
|
355
|
+
const backgroundWidth = estimatedTextWidth + labelPadding * 2;
|
|
356
|
+
const backgroundHeight = fontSize + labelPadding * 2;
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<g key={ `arc-${ index }` } { ...groupProps }>
|
|
360
|
+
<path { ...pathProps } />
|
|
361
|
+
{ showLabels && hasSpaceForLabel && (
|
|
362
|
+
<g>
|
|
363
|
+
{ providerTheme.labelBackgroundColor && (
|
|
364
|
+
<rect
|
|
365
|
+
x={ centroidX - backgroundWidth / 2 }
|
|
366
|
+
y={ centroidY - backgroundHeight / 2 }
|
|
367
|
+
width={ backgroundWidth }
|
|
368
|
+
height={ backgroundHeight }
|
|
369
|
+
fill={ providerTheme.labelBackgroundColor }
|
|
370
|
+
rx={ 4 }
|
|
371
|
+
ry={ 4 }
|
|
372
|
+
pointerEvents="none"
|
|
373
|
+
/>
|
|
374
|
+
) }
|
|
375
|
+
<text
|
|
376
|
+
x={ centroidX }
|
|
377
|
+
y={ centroidY }
|
|
378
|
+
dy=".33em"
|
|
379
|
+
fill={ providerTheme.labelTextColor || '#333' }
|
|
380
|
+
fontSize={ fontSize }
|
|
381
|
+
textAnchor="middle"
|
|
337
382
|
pointerEvents="none"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
{ arc.data.label }
|
|
350
|
-
</text>
|
|
351
|
-
</g>
|
|
352
|
-
) }
|
|
353
|
-
</g>
|
|
354
|
-
);
|
|
355
|
-
} );
|
|
356
|
-
} }
|
|
357
|
-
</Pie>
|
|
383
|
+
>
|
|
384
|
+
{ arc.data.label }
|
|
385
|
+
</text>
|
|
386
|
+
</g>
|
|
387
|
+
) }
|
|
388
|
+
</g>
|
|
389
|
+
);
|
|
390
|
+
} );
|
|
391
|
+
} }
|
|
392
|
+
</Pie>
|
|
393
|
+
) }
|
|
358
394
|
|
|
359
395
|
{ /* Render SVG children (like Group, Text) inside the SVG */ }
|
|
360
|
-
{ svgChildren }
|
|
396
|
+
{ ! allSegmentsHidden && svgChildren }
|
|
361
397
|
</Group>
|
|
362
398
|
</svg>
|
|
363
399
|
|
|
@@ -373,6 +409,7 @@ const PieChartInternal = ( {
|
|
|
373
409
|
shape={ legendShape }
|
|
374
410
|
ref={ legendRef }
|
|
375
411
|
chartId={ chartId }
|
|
412
|
+
interactive={ legendInteractive }
|
|
376
413
|
/>
|
|
377
414
|
) }
|
|
378
415
|
|
|
@@ -334,4 +334,178 @@ describe( 'PieChart', () => {
|
|
|
334
334
|
expect( screen.getByRole( 'tooltip' ) ).toHaveTextContent( 'Test: 42' );
|
|
335
335
|
} );
|
|
336
336
|
} );
|
|
337
|
+
|
|
338
|
+
describe( 'Interactive Legend', () => {
|
|
339
|
+
test( 'filters segments when interactive legend is enabled and segment is toggled', async () => {
|
|
340
|
+
const user = userEvent.setup();
|
|
341
|
+
const testData = [
|
|
342
|
+
{ label: 'Segment A', value: 50, percentage: 50 },
|
|
343
|
+
{ label: 'Segment B', value: 50, percentage: 50 },
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
renderWithTheme( {
|
|
347
|
+
data: testData,
|
|
348
|
+
showLegend: true,
|
|
349
|
+
legendInteractive: true,
|
|
350
|
+
chartId: 'test-interactive-pie-chart',
|
|
351
|
+
} );
|
|
352
|
+
|
|
353
|
+
// Initially both segments should be visible
|
|
354
|
+
let segments = screen.getAllByTestId( 'pie-segment' );
|
|
355
|
+
expect( segments ).toHaveLength( 2 );
|
|
356
|
+
|
|
357
|
+
// Click first legend item to hide segment A
|
|
358
|
+
const legendItem = screen.getByRole( 'button', { name: /Segment A/i } );
|
|
359
|
+
await user.click( legendItem );
|
|
360
|
+
|
|
361
|
+
// Only one segment should remain
|
|
362
|
+
await waitFor( () => {
|
|
363
|
+
segments = screen.getAllByTestId( 'pie-segment' );
|
|
364
|
+
expect( segments ).toHaveLength( 1 );
|
|
365
|
+
} );
|
|
366
|
+
|
|
367
|
+
// Legend item should be marked as hidden
|
|
368
|
+
expect( legendItem ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
369
|
+
} );
|
|
370
|
+
|
|
371
|
+
test( 'shows empty state when all segments are hidden', async () => {
|
|
372
|
+
const user = userEvent.setup();
|
|
373
|
+
const testData = [
|
|
374
|
+
{ label: 'Segment A', value: 50, percentage: 50 },
|
|
375
|
+
{ label: 'Segment B', value: 50, percentage: 50 },
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
renderWithTheme( {
|
|
379
|
+
data: testData,
|
|
380
|
+
showLegend: true,
|
|
381
|
+
legendInteractive: true,
|
|
382
|
+
chartId: 'test-all-hidden-pie-chart',
|
|
383
|
+
} );
|
|
384
|
+
|
|
385
|
+
// Initially should have 2 segments
|
|
386
|
+
expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 2 );
|
|
387
|
+
|
|
388
|
+
// Hide both segments by clicking legend items
|
|
389
|
+
const legendItems = screen.getAllByRole( 'button' );
|
|
390
|
+
await user.click( legendItems[ 0 ] );
|
|
391
|
+
|
|
392
|
+
// Wait for first segment to be hidden
|
|
393
|
+
await waitFor( () => {
|
|
394
|
+
expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 1 );
|
|
395
|
+
} );
|
|
396
|
+
|
|
397
|
+
await user.click( legendItems[ 1 ] );
|
|
398
|
+
|
|
399
|
+
// Wait for all segments to be hidden
|
|
400
|
+
await waitFor( () => {
|
|
401
|
+
expect( screen.queryAllByTestId( 'pie-segment' ) ).toHaveLength( 0 );
|
|
402
|
+
} );
|
|
403
|
+
|
|
404
|
+
// Empty state should appear
|
|
405
|
+
expect( screen.getByText( /all segments are hidden/i ) ).toBeInTheDocument();
|
|
406
|
+
|
|
407
|
+
// Legend items should still be present (just marked inactive)
|
|
408
|
+
expect( screen.getAllByRole( 'button' ) ).toHaveLength( 2 );
|
|
409
|
+
} );
|
|
410
|
+
|
|
411
|
+
test( 'does not filter segments when legendInteractive is false', () => {
|
|
412
|
+
const testData = [
|
|
413
|
+
{ label: 'Segment A', value: 50, percentage: 50 },
|
|
414
|
+
{ label: 'Segment B', value: 50, percentage: 50 },
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
renderWithTheme( {
|
|
418
|
+
data: testData,
|
|
419
|
+
showLegend: true,
|
|
420
|
+
legendInteractive: false,
|
|
421
|
+
chartId: 'test-non-interactive-pie-chart',
|
|
422
|
+
} );
|
|
423
|
+
|
|
424
|
+
// Legend items should not be buttons
|
|
425
|
+
const buttons = screen.queryAllByRole( 'button' );
|
|
426
|
+
expect( buttons ).toHaveLength( 0 );
|
|
427
|
+
|
|
428
|
+
// All segments should be visible
|
|
429
|
+
const segments = screen.getAllByTestId( 'pie-segment' );
|
|
430
|
+
expect( segments ).toHaveLength( 2 );
|
|
431
|
+
} );
|
|
432
|
+
|
|
433
|
+
test( 'maintains consistent colors when segments are hidden', async () => {
|
|
434
|
+
const user = userEvent.setup();
|
|
435
|
+
const testData = [
|
|
436
|
+
{ label: 'Segment A', value: 30, percentage: 30 },
|
|
437
|
+
{ label: 'Segment B', value: 40, percentage: 40 },
|
|
438
|
+
{ label: 'Segment C', value: 30, percentage: 30 },
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
renderWithTheme( {
|
|
442
|
+
data: testData,
|
|
443
|
+
showLegend: true,
|
|
444
|
+
legendInteractive: true,
|
|
445
|
+
chartId: 'test-color-consistency-pie-chart',
|
|
446
|
+
} );
|
|
447
|
+
|
|
448
|
+
// Get initial segment colors
|
|
449
|
+
const initialSegments = screen.getAllByTestId( 'pie-segment' );
|
|
450
|
+
const segmentBColor = initialSegments[ 1 ].getAttribute( 'fill' );
|
|
451
|
+
|
|
452
|
+
// Hide Segment A
|
|
453
|
+
const legendItemA = screen.getByRole( 'button', { name: /Segment A/i } );
|
|
454
|
+
await user.click( legendItemA );
|
|
455
|
+
|
|
456
|
+
// Segment B should maintain its color (now it's the first visible segment)
|
|
457
|
+
await waitFor( () => {
|
|
458
|
+
expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 2 );
|
|
459
|
+
} );
|
|
460
|
+
|
|
461
|
+
const remainingSegments = screen.getAllByTestId( 'pie-segment' );
|
|
462
|
+
expect( remainingSegments[ 0 ] ).toHaveAttribute( 'fill', segmentBColor );
|
|
463
|
+
} );
|
|
464
|
+
|
|
465
|
+
test( 'recalculates legend percentages when segments are hidden', async () => {
|
|
466
|
+
const user = userEvent.setup();
|
|
467
|
+
const testData = [
|
|
468
|
+
{ label: 'Segment A', value: 25, percentage: 25 },
|
|
469
|
+
{ label: 'Segment B', value: 50, percentage: 50 },
|
|
470
|
+
{ label: 'Segment C', value: 25, percentage: 25 },
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
renderWithTheme( {
|
|
474
|
+
data: testData,
|
|
475
|
+
showLegend: true,
|
|
476
|
+
legendInteractive: true,
|
|
477
|
+
legendValueDisplay: 'percentage',
|
|
478
|
+
chartId: 'test-percentage-recalc-pie-chart',
|
|
479
|
+
} );
|
|
480
|
+
|
|
481
|
+
// Initially, legend should show original percentages
|
|
482
|
+
const legendItems = screen.getAllByTestId( 'legend-item' );
|
|
483
|
+
expect( legendItems ).toHaveLength( 3 );
|
|
484
|
+
expect( screen.getAllByText( '25%' ) ).toHaveLength( 2 ); // A and C both 25%
|
|
485
|
+
expect( screen.getByText( '50%' ) ).toBeInTheDocument();
|
|
486
|
+
|
|
487
|
+
// Hide Segment A (25%)
|
|
488
|
+
const legendItemA = screen.getByRole( 'button', { name: /Segment A/i } );
|
|
489
|
+
await user.click( legendItemA );
|
|
490
|
+
|
|
491
|
+
// Now B and C should recalculate: B = 50/75 = 66.67%, C = 25/75 = 33.33%
|
|
492
|
+
await waitFor( () => {
|
|
493
|
+
expect( screen.getByText( /66\.6/ ) ).toBeInTheDocument();
|
|
494
|
+
} );
|
|
495
|
+
|
|
496
|
+
// All 3 legend items should remain (hidden items stay in legend)
|
|
497
|
+
const remainingItems = screen.getAllByTestId( 'legend-item' );
|
|
498
|
+
expect( remainingItems ).toHaveLength( 3 );
|
|
499
|
+
|
|
500
|
+
// Segment A should still show original 25% (hidden items don't recalculate)
|
|
501
|
+
expect( legendItemA ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
502
|
+
expect( screen.getAllByText( '25%' ) ).toHaveLength( 1 ); // Only A shows 25%
|
|
503
|
+
|
|
504
|
+
// Segment B should now show ~67% (50 out of remaining 75)
|
|
505
|
+
expect( screen.getByText( /66\.6/ ) ).toBeInTheDocument();
|
|
506
|
+
|
|
507
|
+
// Segment C should now show ~33% (25 out of remaining 75)
|
|
508
|
+
expect( screen.getByText( /33\.3/ ) ).toBeInTheDocument();
|
|
509
|
+
} );
|
|
510
|
+
} );
|
|
337
511
|
} );
|
|
@@ -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
|
|