@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.
- package/CHANGELOG.md +13 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +235 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +23 -14
- package/dist/index.d.cts +51 -14
- package/dist/index.d.ts +51 -14
- package/dist/index.js +236 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +24 -24
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +301 -11
- package/src/charts/area-chart/types.ts +4 -3
- package/src/charts/bar-chart/bar-chart.tsx +21 -15
- package/src/charts/bar-chart/test/bar-chart.test.tsx +255 -2
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +6 -6
- package/src/charts/geo-chart/geo-chart.module.scss +8 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +31 -8
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +65 -5
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +125 -0
- package/src/charts/heatmap-chart/types.ts +8 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +17 -22
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +107 -1
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/line-chart/line-chart.tsx +31 -27
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +262 -5
- package/src/charts/line-chart/types.ts +4 -3
- package/src/charts/pie-chart/pie-chart.tsx +8 -13
- package/src/charts/pie-chart/test/pie-chart.test.tsx +73 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +8 -13
- package/src/charts/private/{single-chart-context/single-chart-context.tsx → chart-instance-context/chart-instance-context.ts} +1 -5
- package/src/charts/private/chart-instance-context/index.ts +3 -0
- package/src/charts/private/{single-chart-context/use-single-chart-context.ts → chart-instance-context/use-chart-instance-context.ts} +1 -3
- package/src/charts/private/svg-empty-state/get-all-hidden-message.ts +41 -0
- package/src/charts/private/svg-empty-state/index.ts +1 -0
- package/src/charts/private/svg-empty-state/test/get-all-hidden-message.test.ts +15 -0
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +2 -2
- package/src/charts/private/x-zoom/x-zoom.tsx +2 -2
- package/src/components/legend/legend.tsx +4 -4
- package/src/components/legend/private/base-legend.module.scss +1 -1
- package/src/components/legend/private/base-legend.tsx +67 -18
- package/src/components/legend/test/legend.test.tsx +100 -4
- package/src/components/tooltip/accessible-tooltip.tsx +13 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/{use-interactive-legend-data.ts → use-legend-visibility-data.ts} +29 -30
- package/src/index.ts +3 -1
- package/src/providers/chart-context/global-charts-provider.tsx +92 -18
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +47 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +142 -0
- package/src/providers/chart-context/test/use-default-hidden-series-paint.test.tsx +56 -0
- package/src/providers/chart-context/test/use-default-hidden-series.ssr.test.tsx +39 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +222 -0
- package/src/providers/chart-context/types.ts +7 -1
- package/src/styles/chart-scope.scss +2 -2
- package/src/styles/test/wpds-fallbacks.test.ts +131 -0
- package/src/types.ts +12 -0
- 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 {
|
|
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<
|
|
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<
|
|
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
|
|
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
|
|
57
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
222
|
-
const { visibleData, allSegmentsHidden, legendData } =
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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,
|
|
10
|
-
font-size: var(--wpds-typography-font-size-lg,
|
|
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
|
-
|
|
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 {
|
|
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
|
|
251
|
-
const { visibleData, allSegmentsHidden, legendData } =
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
|
-
import { ChartInstanceContext, type ChartInstanceContextValue } from './
|
|
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
|
+
};
|
|
@@ -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
|
+
} );
|