@automattic/charts 3.0.0 → 3.1.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 (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/TOKENS.md +3 -3
  3. package/dist/index.cjs +235 -123
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +23 -14
  6. package/dist/index.d.cts +51 -14
  7. package/dist/index.d.ts +51 -14
  8. package/dist/index.js +236 -125
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/area-chart/area-chart.tsx +24 -24
  12. package/src/charts/area-chart/private/overlays.tsx +3 -3
  13. package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
  14. package/src/charts/area-chart/types.ts +4 -3
  15. package/src/charts/bar-chart/bar-chart.tsx +21 -15
  16. package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
  17. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +6 -6
  18. package/src/charts/geo-chart/geo-chart.module.scss +8 -0
  19. package/src/charts/heatmap-chart/heatmap-chart.module.scss +9 -0
  20. package/src/charts/heatmap-chart/heatmap-chart.tsx +31 -8
  21. package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
  22. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
  23. package/src/charts/heatmap-chart/types.ts +8 -0
  24. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
  25. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
  26. package/src/charts/line-chart/line-chart.module.scss +2 -2
  27. package/src/charts/line-chart/line-chart.tsx +31 -27
  28. package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
  29. package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
  30. package/src/charts/line-chart/types.ts +4 -3
  31. package/src/charts/pie-chart/pie-chart.tsx +8 -13
  32. package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
  33. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
  34. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +8 -13
  35. package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
  36. package/src/charts/private/chart-instance-context/index.ts +3 -0
  37. package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
  38. package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
  39. package/src/charts/private/svg-empty-state/index.ts +1 -0
  40. package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
  41. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
  42. package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
  43. package/src/components/legend/legend.tsx +4 -4
  44. package/src/components/legend/private/base-legend.module.scss +1 -1
  45. package/src/components/legend/private/base-legend.tsx +67 -18
  46. package/src/components/legend/test/legend.test.tsx +100 -4
  47. package/src/components/tooltip/accessible-tooltip.tsx +13 -2
  48. package/src/components/trend-indicator/trend-indicator.module.scss +1 -1
  49. package/src/hooks/index.ts +1 -1
  50. package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
  51. package/src/index.ts +3 -1
  52. package/src/providers/chart-context/global-charts-provider.tsx +92 -18
  53. package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
  54. package/src/providers/chart-context/test/chart-context.test.tsx +142 -0
  55. package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
  56. package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
  57. package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
  58. package/src/providers/chart-context/types.ts +7 -1
  59. package/src/styles/chart-scope.scss +2 -2
  60. package/src/styles/test/wpds-fallbacks.test.ts +131 -0
  61. package/src/types.ts +12 -0
  62. package/src/charts/private/single-chart-context/index.ts +0 -8
@@ -1,12 +1,14 @@
1
1
  /* eslint-disable react/jsx-no-bind */
2
2
 
3
- import { render, screen, waitFor, within } from '@testing-library/react';
3
+ import { render, screen, waitFor, within, act } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import { GlyphDiamond } from '@visx/glyph';
6
6
  import { createElement, createRef } from 'react';
7
7
  import { GlobalChartsProvider, defaultTheme } from '../../../providers';
8
+ import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
8
9
  import LineChart, { LineChartUnresponsive } from '../line-chart';
9
- import type { SingleChartRef } from '../../private/single-chart-context';
10
+ import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
11
+ import type { ChartInstanceRef } from '../../private/chart-instance-context';
10
12
 
11
13
  // Mock useElementSize to return non-zero dimensions in jsdom so charts render
12
14
  const mockRefCallback = jest.fn();
@@ -902,7 +904,7 @@ describe( 'LineChart', () => {
902
904
 
903
905
  describe( 'Chart Ref Interface', () => {
904
906
  test( 'exposes getScales method via ref', () => {
905
- const ref = createRef< SingleChartRef >();
907
+ const ref = createRef< ChartInstanceRef >();
906
908
  renderUnwrappedWithTheme( {}, 'default', ref );
907
909
 
908
910
  expect( ref.current?.getScales() ).toBeDefined();
@@ -911,7 +913,7 @@ describe( 'LineChart', () => {
911
913
  } );
912
914
 
913
915
  test( 'exposes getChartDimensions method via ref', () => {
914
- const ref = createRef< SingleChartRef >();
916
+ const ref = createRef< ChartInstanceRef >();
915
917
  renderUnwrappedWithTheme( { width: 800, height: 400 }, 'default', ref );
916
918
 
917
919
  const dimensions = ref.current?.getChartDimensions();
@@ -1298,7 +1300,7 @@ describe( 'LineChart', () => {
1298
1300
  expect( buttons ).toHaveLength( 0 );
1299
1301
  } );
1300
1302
 
1301
- it( 'shows all series when chartId is missing even if legendInteractive is true', () => {
1303
+ it( 'shows all series when nothing has been hidden', () => {
1302
1304
  render(
1303
1305
  <GlobalChartsProvider>
1304
1306
  <LineChartUnresponsive
@@ -1317,6 +1319,136 @@ describe( 'LineChart', () => {
1317
1319
  expect( item ).toHaveAttribute( 'aria-pressed', 'true' );
1318
1320
  } );
1319
1321
  } );
1322
+
1323
+ it( 'hides a series programmatically when the legend is not interactive', () => {
1324
+ let context: GlobalChartsContextValue;
1325
+ const Grab = () => {
1326
+ context = useGlobalChartsContext();
1327
+ return null;
1328
+ };
1329
+
1330
+ render(
1331
+ <GlobalChartsProvider>
1332
+ <Grab />
1333
+ <LineChartUnresponsive
1334
+ width={ 500 }
1335
+ height={ 300 }
1336
+ withGradientFill={ false }
1337
+ showLegend={ true }
1338
+ legend={ { interactive: false } }
1339
+ chartId="test-programmatic-line"
1340
+ data={ [
1341
+ {
1342
+ label: 'Series A',
1343
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
1344
+ options: {},
1345
+ },
1346
+ {
1347
+ label: 'Series B',
1348
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
1349
+ options: {},
1350
+ },
1351
+ ] }
1352
+ />
1353
+ </GlobalChartsProvider>
1354
+ );
1355
+
1356
+ expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
1357
+
1358
+ act( () => {
1359
+ context.toggleSeriesVisibility( 'test-programmatic-line', 'Series A' );
1360
+ context.toggleSeriesVisibility( 'test-programmatic-line', 'Series B' );
1361
+ } );
1362
+
1363
+ expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
1364
+ } );
1365
+
1366
+ it( 'omits the click instruction when the legend cannot be clicked', () => {
1367
+ let context: GlobalChartsContextValue;
1368
+ const Grab = () => {
1369
+ context = useGlobalChartsContext();
1370
+ return null;
1371
+ };
1372
+
1373
+ render(
1374
+ <GlobalChartsProvider>
1375
+ <Grab />
1376
+ <LineChartUnresponsive
1377
+ width={ 500 }
1378
+ height={ 300 }
1379
+ withGradientFill={ false }
1380
+ showLegend={ true }
1381
+ legend={ { interactive: false } }
1382
+ chartId="test-empty-copy-line"
1383
+ data={ [
1384
+ {
1385
+ label: 'Series A',
1386
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
1387
+ options: {},
1388
+ },
1389
+ ] }
1390
+ />
1391
+ </GlobalChartsProvider>
1392
+ );
1393
+
1394
+ act( () => {
1395
+ context.toggleSeriesVisibility( 'test-empty-copy-line', 'Series A' );
1396
+ } );
1397
+
1398
+ expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
1399
+ expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
1400
+ } );
1401
+
1402
+ it( 'pins the value axis across a programmatic hide when rescaleYOnVisibilityChange is false', () => {
1403
+ let context: GlobalChartsContextValue;
1404
+ const Grab = () => {
1405
+ context = useGlobalChartsContext();
1406
+ return null;
1407
+ };
1408
+ const ref = createRef< ChartInstanceRef >();
1409
+
1410
+ render(
1411
+ <GlobalChartsProvider>
1412
+ <Grab />
1413
+ <LineChartUnresponsive
1414
+ width={ 500 }
1415
+ height={ 300 }
1416
+ withGradientFill={ false }
1417
+ showLegend={ true }
1418
+ legend={ { interactive: false } }
1419
+ chartId="test-programmatic-pin-line"
1420
+ rescaleYOnVisibilityChange={ false }
1421
+ ref={ ref }
1422
+ data={ [
1423
+ {
1424
+ label: 'Series A',
1425
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
1426
+ options: {},
1427
+ },
1428
+ {
1429
+ label: 'Series B',
1430
+ data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
1431
+ options: {},
1432
+ },
1433
+ ] }
1434
+ />
1435
+ </GlobalChartsProvider>
1436
+ );
1437
+
1438
+ const before = (
1439
+ ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
1440
+ )?.domain();
1441
+ expect( before ).toBeDefined();
1442
+
1443
+ act( () => {
1444
+ context.toggleSeriesVisibility( 'test-programmatic-pin-line', 'Series B' );
1445
+ } );
1446
+
1447
+ const after = (
1448
+ ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
1449
+ )?.domain();
1450
+ expect( after ).toEqual( before );
1451
+ } );
1320
1452
  } );
1321
1453
 
1322
1454
  // The line is not animated, so it clips only while actually zoomed.
@@ -1523,4 +1655,129 @@ describe( 'LineChart', () => {
1523
1655
  ).toBeInTheDocument();
1524
1656
  } );
1525
1657
  } );
1658
+
1659
+ describe( 'defaultHiddenSeries', () => {
1660
+ it( 'renders a series hidden when named in defaultHiddenSeries', () => {
1661
+ render(
1662
+ <GlobalChartsProvider>
1663
+ <LineChartUnresponsive
1664
+ width={ 500 }
1665
+ height={ 300 }
1666
+ withGradientFill={ false }
1667
+ showLegend={ true }
1668
+ legend={ { interactive: true } }
1669
+ chartId="test-default-hidden-line"
1670
+ defaultHiddenSeries={ [ 'Series B' ] }
1671
+ data={ [
1672
+ {
1673
+ label: 'Series A',
1674
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
1675
+ options: {},
1676
+ },
1677
+ {
1678
+ label: 'Series B',
1679
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
1680
+ options: {},
1681
+ },
1682
+ ] }
1683
+ />
1684
+ </GlobalChartsProvider>
1685
+ );
1686
+
1687
+ const items = screen.getAllByRole( 'button' );
1688
+ expect( items[ 0 ] ).toHaveAttribute( 'aria-pressed', 'true' );
1689
+ expect( items[ 1 ] ).toHaveAttribute( 'aria-pressed', 'false' );
1690
+ } );
1691
+
1692
+ it( 'lets the user reveal a series seeded hidden', async () => {
1693
+ const user = userEvent.setup();
1694
+
1695
+ render(
1696
+ <GlobalChartsProvider>
1697
+ <LineChartUnresponsive
1698
+ width={ 500 }
1699
+ height={ 300 }
1700
+ withGradientFill={ false }
1701
+ showLegend={ true }
1702
+ legend={ { interactive: true } }
1703
+ chartId="test-default-hidden-reveal-line"
1704
+ defaultHiddenSeries={ [ 'Series B' ] }
1705
+ data={ [
1706
+ {
1707
+ label: 'Series A',
1708
+ data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
1709
+ options: {},
1710
+ },
1711
+ {
1712
+ label: 'Series B',
1713
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
1714
+ options: {},
1715
+ },
1716
+ ] }
1717
+ />
1718
+ </GlobalChartsProvider>
1719
+ );
1720
+
1721
+ await user.click( screen.getAllByRole( 'button' )[ 1 ] );
1722
+
1723
+ expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
1724
+ } );
1725
+
1726
+ it( 'keeps a revealed series visible after a data change', async () => {
1727
+ // Matters specifically at chart level: useChartRegistration unregisters and
1728
+ // re-registers whenever legendItems change, which a data prop change triggers.
1729
+ // A hook-level harness has no registration at all, so it can't catch a
1730
+ // regression where re-registration re-triggers the seeding effect.
1731
+ const user = userEvent.setup();
1732
+ const chartId = 'test-default-hidden-data-change-line';
1733
+
1734
+ const makeData = ( seriesAValue: number ) => [
1735
+ {
1736
+ label: 'Series A',
1737
+ data: [ { date: new Date( '2024-01-01' ), value: seriesAValue, label: 'Jan 1' } ],
1738
+ options: {},
1739
+ },
1740
+ {
1741
+ label: 'Series B',
1742
+ data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
1743
+ options: {},
1744
+ },
1745
+ ];
1746
+
1747
+ const { rerender } = render(
1748
+ <GlobalChartsProvider>
1749
+ <LineChartUnresponsive
1750
+ width={ 500 }
1751
+ height={ 300 }
1752
+ withGradientFill={ false }
1753
+ showLegend={ true }
1754
+ legend={ { interactive: true } }
1755
+ chartId={ chartId }
1756
+ defaultHiddenSeries={ [ 'Series B' ] }
1757
+ data={ makeData( 10 ) }
1758
+ />
1759
+ </GlobalChartsProvider>
1760
+ );
1761
+
1762
+ await user.click( screen.getAllByRole( 'button' )[ 1 ] );
1763
+ expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
1764
+
1765
+ rerender(
1766
+ <GlobalChartsProvider>
1767
+ <LineChartUnresponsive
1768
+ width={ 500 }
1769
+ height={ 300 }
1770
+ withGradientFill={ false }
1771
+ showLegend={ true }
1772
+ legend={ { interactive: true } }
1773
+ chartId={ chartId }
1774
+ defaultHiddenSeries={ [ 'Series B' ] }
1775
+ data={ makeData( 30 ) }
1776
+ />
1777
+ </GlobalChartsProvider>
1778
+ );
1779
+
1780
+ expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
1781
+ } );
1782
+ } );
1526
1783
  } );
@@ -3,6 +3,7 @@ import type {
3
3
  DataPointDate,
4
4
  SeriesData,
5
5
  SeriesChartLegendConfig,
6
+ SeriesVisibilityProps,
6
7
  AnnotationStyles,
7
8
  DataPoint,
8
9
  } from '../../types';
@@ -28,7 +29,7 @@ export type RenderLineGlyphProps< Datum extends object > = GlyphProps< Datum > &
28
29
  position?: 'start' | 'end';
29
30
  };
30
31
 
31
- export interface LineChartProps extends BaseChartProps< SeriesData[] > {
32
+ export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
32
33
  /**
33
34
  * Legend configuration. Supports `collapseGroups` on top of the shared options.
34
35
  */
@@ -53,8 +54,8 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] > {
53
54
  */
54
55
  zoomable?: boolean;
55
56
  /**
56
- * Whether the Y axis rescales to fit only the visible series when series are
57
- * hidden or shown through the interactive legend.
57
+ * Whether the Y axis rescales to fit only the visible series when the set of
58
+ * visible series changes — via the interactive legend or otherwise.
58
59
  * Defaults to `true` (the pre-existing behaviour). Set to `false` to pin the Y
59
60
  * axis to the full data extent so hiding series does not move the chart's
60
61
  * baseline — useful for comparison charts. Matches `AreaChart`.
@@ -1,14 +1,13 @@
1
1
  import { Group } from '@visx/group';
2
2
  import { Pie } from '@visx/shape';
3
3
  import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
4
- import { __ } from '@wordpress/i18n';
5
4
  import clsx from 'clsx';
6
5
  import { useCallback, useContext, useMemo } from 'react';
7
6
  import { Legend, useChartLegendItems } from '../../components/legend';
8
7
  import { BaseTooltip } from '../../components/tooltip';
9
8
  import {
10
9
  useDataWithPercentages,
11
- useInteractiveLegendData,
10
+ useLegendVisibilityData,
12
11
  usePrefersReducedMotion,
13
12
  } from '../../hooks';
14
13
  import {
@@ -24,10 +23,10 @@ import { attachSubComponents, resolveFontSize } from '../../utils';
24
23
  import { getStringWidth } from '../../visx/text';
25
24
  import { Center } from '../private/center';
26
25
  import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
26
+ import { ChartInstanceContext } from '../private/chart-instance-context';
27
27
  import { ChartLayout } from '../private/chart-layout';
28
28
  import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
29
- import { SingleChartContext } from '../private/single-chart-context';
30
- import { SvgEmptyState } from '../private/svg-empty-state';
29
+ import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
31
30
  import { withResponsive, ResponsiveConfig } from '../private/with-responsive';
32
31
  import styles from './pie-chart.module.scss';
33
32
  import type { LegendValueDisplay } from '../../components/legend';
@@ -218,11 +217,10 @@ const PieChartInternal = ( {
218
217
  // Calculate percentages from values (single source of truth)
219
218
  const dataWithPercentages = useDataWithPercentages( data );
220
219
 
221
- // Filter and recalculate data for interactive legends
222
- const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
220
+ // Filter and recalculate data from the shared legend visibility state.
221
+ const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData( {
223
222
  data: dataWithPercentages,
224
223
  chartId,
225
- legendInteractive,
226
224
  isSeriesVisible,
227
225
  } );
228
226
 
@@ -308,7 +306,7 @@ const PieChartInternal = ( {
308
306
  );
309
307
 
310
308
  return (
311
- <SingleChartContext.Provider value={ { chartId } }>
309
+ <ChartInstanceContext.Provider value={ { chartId } }>
312
310
  <ChartLayout
313
311
  legendPosition={ legendPosition }
314
312
  legendElement={ legendElement }
@@ -385,10 +383,7 @@ const PieChartInternal = ( {
385
383
  >
386
384
  { allSegmentsHidden ? (
387
385
  <SvgEmptyState x={ 0 } y={ 0 } width={ width } height={ height }>
388
- { __(
389
- 'All segments are hidden. Click legend items to show data.',
390
- 'jetpack-charts'
391
- ) }
386
+ { getAllHiddenMessage( legendInteractive, 'segments' ) }
392
387
  </SvgEmptyState>
393
388
  ) : (
394
389
  <Pie< DataPointPercentageCalculated & { index: number } >
@@ -495,7 +490,7 @@ const PieChartInternal = ( {
495
490
  );
496
491
  } }
497
492
  </ChartLayout>
498
- </SingleChartContext.Provider>
493
+ </ChartInstanceContext.Provider>
499
494
  );
500
495
  };
501
496
 
@@ -1,6 +1,8 @@
1
+ /* eslint-disable react/jsx-no-bind */
2
+
1
3
  import { render, screen, waitFor } from '@testing-library/react';
2
4
  import userEvent from '@testing-library/user-event';
3
- import { GlobalChartsProvider } from '../../../providers';
5
+ import { GlobalChartsProvider, useGlobalChartsContext } from '../../../providers';
4
6
  import PieChart from '../pie-chart';
5
7
 
6
8
  describe( 'PieChart', () => {
@@ -548,4 +550,74 @@ describe( 'PieChart', () => {
548
550
  expect( screen.getByText( /33\.3/ ) ).toBeInTheDocument();
549
551
  } );
550
552
  } );
553
+
554
+ describe( 'Programmatic visibility (non-interactive legend)', () => {
555
+ const chartId = 'test-programmatic-hide-pie-chart';
556
+ const testData = [
557
+ { label: 'Segment A', value: 50 },
558
+ { label: 'Segment B', value: 50 },
559
+ ];
560
+
561
+ const Harness = () => {
562
+ const { toggleSeriesVisibility } = useGlobalChartsContext();
563
+ return (
564
+ <>
565
+ <button onClick={ () => toggleSeriesVisibility( chartId, 'Segment A' ) }>
566
+ Hide Segment A
567
+ </button>
568
+ <button onClick={ () => toggleSeriesVisibility( chartId, 'Segment B' ) }>
569
+ Hide Segment B
570
+ </button>
571
+ <PieChart
572
+ data={ testData }
573
+ size={ 500 }
574
+ showLegend
575
+ legend={ { interactive: false } }
576
+ chartId={ chartId }
577
+ />
578
+ </>
579
+ );
580
+ };
581
+
582
+ test( 'removes a segment hidden programmatically even though the legend is not clickable', async () => {
583
+ const user = userEvent.setup();
584
+
585
+ render(
586
+ <GlobalChartsProvider>
587
+ <Harness />
588
+ </GlobalChartsProvider>
589
+ );
590
+
591
+ expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 2 );
592
+
593
+ await user.click( screen.getByRole( 'button', { name: 'Hide Segment A' } ) );
594
+
595
+ await waitFor( () => {
596
+ expect( screen.getAllByTestId( 'pie-segment' ) ).toHaveLength( 1 );
597
+ } );
598
+
599
+ // The legend itself stays non-interactive (no toggle button for the segment).
600
+ expect( screen.queryAllByRole( 'button', { name: /Segment A: /i } ) ).toHaveLength( 0 );
601
+ } );
602
+
603
+ test( 'empty-state copy omits the click instruction when the legend is not interactive', async () => {
604
+ const user = userEvent.setup();
605
+
606
+ render(
607
+ <GlobalChartsProvider>
608
+ <Harness />
609
+ </GlobalChartsProvider>
610
+ );
611
+
612
+ await user.click( screen.getByRole( 'button', { name: 'Hide Segment A' } ) );
613
+ await user.click( screen.getByRole( 'button', { name: 'Hide Segment B' } ) );
614
+
615
+ await waitFor( () => {
616
+ expect( screen.queryAllByTestId( 'pie-segment' ) ).toHaveLength( 0 );
617
+ } );
618
+
619
+ expect( screen.getByText( 'All segments are hidden.' ) ).toBeInTheDocument();
620
+ expect( screen.queryByText( /Click legend items to show data/i ) ).not.toBeInTheDocument();
621
+ } );
622
+ } );
551
623
  } );
@@ -6,8 +6,8 @@
6
6
  }
7
7
 
8
8
  .label {
9
- font-weight: var(--wpds-typography-font-weight-emphasis, 499);
10
- font-size: var(--wpds-typography-font-size-lg, 16px);
9
+ font-weight: var(--wpds-typography-font-weight-emphasis, 600);
10
+ font-size: var(--wpds-typography-font-size-lg, 15px);
11
11
  }
12
12
 
13
13
  .note {
@@ -2,14 +2,13 @@ import { Group } from '@visx/group';
2
2
  import { Pie } from '@visx/shape';
3
3
  import { Text } from '@visx/text';
4
4
  import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
5
- import { __ } from '@wordpress/i18n';
6
5
  import clsx from 'clsx';
7
6
  import { useCallback, useContext, useMemo } from 'react';
8
7
  import { Legend, useChartLegendItems } from '../../components/legend';
9
8
  import { BaseTooltip } from '../../components/tooltip';
10
9
  import {
11
10
  useDataWithPercentages,
12
- useInteractiveLegendData,
11
+ useLegendVisibilityData,
13
12
  usePrefersReducedMotion,
14
13
  } from '../../hooks';
15
14
  import {
@@ -23,10 +22,10 @@ import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
23
22
  import { attachSubComponents } from '../../utils';
24
23
  import { Center } from '../private/center';
25
24
  import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
25
+ import { ChartInstanceContext } from '../private/chart-instance-context';
26
26
  import { ChartLayout } from '../private/chart-layout';
27
27
  import { RadialWipeAnimation } from '../private/radial-wipe-animation';
28
- import { SingleChartContext } from '../private/single-chart-context';
29
- import { SvgEmptyState } from '../private/svg-empty-state';
28
+ import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
30
29
  import { withResponsive } from '../private/with-responsive';
31
30
  import styles from './pie-semi-circle-chart.module.scss';
32
31
  import type { LegendValueDisplay } from '../../components/legend';
@@ -247,11 +246,10 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
247
246
  // Calculate percentages from values (single source of truth)
248
247
  const dataWithPercentages = useDataWithPercentages( data );
249
248
 
250
- // Filter and recalculate data for interactive legends
251
- const { visibleData, allSegmentsHidden, legendData } = useInteractiveLegendData( {
249
+ // Filter and recalculate data from the shared legend visibility state.
250
+ const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData( {
252
251
  data: dataWithPercentages,
253
252
  chartId,
254
- legendInteractive,
255
253
  isSeriesVisible,
256
254
  } );
257
255
 
@@ -353,7 +351,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
353
351
  );
354
352
 
355
353
  return (
356
- <SingleChartContext.Provider value={ { chartId } }>
354
+ <ChartInstanceContext.Provider value={ { chartId } }>
357
355
  <ChartLayout
358
356
  legendPosition={ legendPosition }
359
357
  legendElement={ legendElement }
@@ -427,10 +425,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
427
425
  >
428
426
  { allSegmentsHidden ? (
429
427
  <SvgEmptyState x={ 0 } y={ -radius / 2 } width={ width } height={ height }>
430
- { __(
431
- 'All segments are hidden. Click legend items to show data.',
432
- 'jetpack-charts'
433
- ) }
428
+ { getAllHiddenMessage( legendInteractive, 'segments' ) }
434
429
  </SvgEmptyState>
435
430
  ) : (
436
431
  <>
@@ -493,7 +488,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
493
488
  );
494
489
  } }
495
490
  </ChartLayout>
496
- </SingleChartContext.Provider>
491
+ </ChartInstanceContext.Provider>
497
492
  );
498
493
  };
499
494
 
@@ -15,11 +15,7 @@ export interface ChartInstanceContextValue {
15
15
  chartRef?: React.RefObject< ChartInstanceRef >;
16
16
  chartWidth?: number;
17
17
  chartHeight?: number;
18
+ isSeriesVisible?: ( seriesLabel: string ) => boolean;
18
19
  }
19
20
 
20
21
  export const ChartInstanceContext = createContext< ChartInstanceContextValue | null >( null );
21
-
22
- // Backward compatibility exports
23
- export const SingleChartContext = ChartInstanceContext;
24
- export type SingleChartContextValue = ChartInstanceContextValue;
25
- export type SingleChartRef = ChartInstanceRef;
@@ -0,0 +1,3 @@
1
+ export { ChartInstanceContext } from './chart-instance-context';
2
+ export { useChartInstanceContext } from './use-chart-instance-context';
3
+ export type { ChartInstanceContextValue, ChartInstanceRef } from './chart-instance-context';
@@ -1,5 +1,5 @@
1
1
  import { useContext } from 'react';
2
- import { ChartInstanceContext, type ChartInstanceContextValue } from './single-chart-context';
2
+ import { ChartInstanceContext, type ChartInstanceContextValue } from './chart-instance-context';
3
3
 
4
4
  export const useChartInstanceContext = (): ChartInstanceContextValue => {
5
5
  const context = useContext( ChartInstanceContext );
@@ -8,5 +8,3 @@ export const useChartInstanceContext = (): ChartInstanceContextValue => {
8
8
  }
9
9
  return context;
10
10
  };
11
-
12
- export const useSingleChartContext = useChartInstanceContext;
@@ -0,0 +1,41 @@
1
+ import { _x } from '@wordpress/i18n';
2
+
3
+ export type HiddenChartItemType = 'series' | 'segments';
4
+
5
+ /**
6
+ * Return the empty-state message for a chart whose items are all hidden.
7
+ *
8
+ * Each translation call has a distinct gettext context. This prevents Terser
9
+ * from folding conditional calls into one call with a conditional msgid, which
10
+ * would make the strings unavailable to static translation extraction.
11
+ *
12
+ * @param interactive - Whether the legend can be used to show hidden items.
13
+ * @param itemType - The chart-specific name for its data items.
14
+ * @return The translated empty-state message.
15
+ */
16
+ export const getAllHiddenMessage = (
17
+ interactive: boolean,
18
+ itemType: HiddenChartItemType
19
+ ): string => {
20
+ if ( itemType === 'segments' ) {
21
+ if ( interactive ) {
22
+ return _x(
23
+ 'All segments are hidden. Click legend items to show data.',
24
+ 'chart empty state: interactive segments',
25
+ 'jetpack-charts'
26
+ );
27
+ }
28
+
29
+ return _x( 'All segments are hidden.', 'chart empty state: segments', 'jetpack-charts' );
30
+ }
31
+
32
+ if ( interactive ) {
33
+ return _x(
34
+ 'All series are hidden. Click legend items to show data.',
35
+ 'chart empty state: interactive series',
36
+ 'jetpack-charts'
37
+ );
38
+ }
39
+
40
+ return _x( 'All series are hidden.', 'chart empty state: series', 'jetpack-charts' );
41
+ };
@@ -1 +1,2 @@
1
1
  export { SvgEmptyState } from './svg-empty-state';
2
+ export { getAllHiddenMessage } from './get-all-hidden-message';
@@ -0,0 +1,15 @@
1
+ import { getAllHiddenMessage } from '../get-all-hidden-message';
2
+
3
+ describe( 'getAllHiddenMessage', () => {
4
+ it.each( [
5
+ [ false, 'series', 'All series are hidden.' ],
6
+ [ true, 'series', 'All series are hidden. Click legend items to show data.' ],
7
+ [ false, 'segments', 'All segments are hidden.' ],
8
+ [ true, 'segments', 'All segments are hidden. Click legend items to show data.' ],
9
+ ] as const )(
10
+ 'returns the expected message for interactive=%s %s',
11
+ ( interactive, type, message ) => {
12
+ expect( getAllHiddenMessage( interactive, type ) ).toBe( message );
13
+ }
14
+ );
15
+ } );