@automattic/charts 3.0.0 → 3.1.1
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 +18 -0
- package/TOKENS.md +3 -3
- package/dist/index.cjs +250 -136
- 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 +251 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +26 -26
- package/src/charts/area-chart/private/overlays.tsx +3 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +346 -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/private/use-bar-chart-options.ts +19 -12
- package/src/charts/bar-chart/test/bar-chart.test.tsx +277 -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 +33 -29
- package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +2 -2
- package/src/charts/line-chart/test/line-chart.test.tsx +313 -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,6 +1,6 @@
|
|
|
1
1
|
import { DataContext } from '@visx/xychart';
|
|
2
2
|
import { useEffect, useState, useCallback } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useChartInstanceContext } from '../../private/chart-instance-context';
|
|
4
4
|
import styles from '../line-chart.module.scss';
|
|
5
5
|
import type { AxisScale } from '@visx/axis';
|
|
6
6
|
import type { FC, ReactNode } from 'react';
|
|
@@ -15,7 +15,7 @@ interface ScaleData {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const LineChartAnnotationsOverlay: FC< LineChartAnnotationsProps > = ( { children } ) => {
|
|
18
|
-
const { chartRef, chartWidth, chartHeight } =
|
|
18
|
+
const { chartRef, chartWidth, chartHeight } = useChartInstanceContext();
|
|
19
19
|
|
|
20
20
|
const [ scales, setScales ] = useState< ScaleData | null >( null );
|
|
21
21
|
const [ scalesStable, setScalesStable ] = useState< boolean >( false );
|
|
@@ -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();
|
|
@@ -498,6 +500,57 @@ describe( 'LineChart', () => {
|
|
|
498
500
|
expect( ticks.length ).toBeGreaterThan( 1 );
|
|
499
501
|
} );
|
|
500
502
|
|
|
503
|
+
test( 'keeps the derived month formatter when tickFormat is passed as undefined.', () => {
|
|
504
|
+
renderWithTheme( {
|
|
505
|
+
width: 800,
|
|
506
|
+
options: { axis: { x: { tickFormat: undefined } } },
|
|
507
|
+
data: [
|
|
508
|
+
{
|
|
509
|
+
label: 'Series A',
|
|
510
|
+
data: [
|
|
511
|
+
{ date: new Date( '2024-01-01' ), value: 10 },
|
|
512
|
+
{ date: new Date( '2024-04-01' ), value: 20 },
|
|
513
|
+
{ date: new Date( '2024-07-01' ), value: 30 },
|
|
514
|
+
{ date: new Date( '2024-10-01' ), value: 40 },
|
|
515
|
+
{ date: new Date( '2025-03-01' ), value: 50 },
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
} );
|
|
520
|
+
|
|
521
|
+
// January is absent: formatMonthOrYearTick renders it as the year instead.
|
|
522
|
+
const ticks = screen.getAllByText( /^(Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/ );
|
|
523
|
+
expect( ticks.length ).toBeGreaterThan( 1 );
|
|
524
|
+
expect(
|
|
525
|
+
screen.queryByText( /^(February|March|April|June|July|August|September|October)$/ )
|
|
526
|
+
).not.toBeInTheDocument();
|
|
527
|
+
} );
|
|
528
|
+
|
|
529
|
+
test( 'honors an explicit tickFormat over the derived formatter.', () => {
|
|
530
|
+
renderWithTheme( {
|
|
531
|
+
width: 800,
|
|
532
|
+
options: {
|
|
533
|
+
axis: {
|
|
534
|
+
x: {
|
|
535
|
+
tickFormat: ( date: Date | number ) =>
|
|
536
|
+
`tick-${ new Date( Number( date ) ).getUTCMonth() }`,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
data: [
|
|
541
|
+
{
|
|
542
|
+
label: 'Series A',
|
|
543
|
+
data: [
|
|
544
|
+
{ date: new Date( '2024-01-01' ), value: 10 },
|
|
545
|
+
{ date: new Date( '2024-07-01' ), value: 30 },
|
|
546
|
+
],
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
} );
|
|
550
|
+
|
|
551
|
+
expect( screen.getAllByText( /^tick-\d+$/ ).length ).toBeGreaterThan( 0 );
|
|
552
|
+
} );
|
|
553
|
+
|
|
501
554
|
test( 'renders ticks in year format.', () => {
|
|
502
555
|
renderWithTheme( {
|
|
503
556
|
width: 800,
|
|
@@ -902,7 +955,7 @@ describe( 'LineChart', () => {
|
|
|
902
955
|
|
|
903
956
|
describe( 'Chart Ref Interface', () => {
|
|
904
957
|
test( 'exposes getScales method via ref', () => {
|
|
905
|
-
const ref = createRef<
|
|
958
|
+
const ref = createRef< ChartInstanceRef >();
|
|
906
959
|
renderUnwrappedWithTheme( {}, 'default', ref );
|
|
907
960
|
|
|
908
961
|
expect( ref.current?.getScales() ).toBeDefined();
|
|
@@ -911,7 +964,7 @@ describe( 'LineChart', () => {
|
|
|
911
964
|
} );
|
|
912
965
|
|
|
913
966
|
test( 'exposes getChartDimensions method via ref', () => {
|
|
914
|
-
const ref = createRef<
|
|
967
|
+
const ref = createRef< ChartInstanceRef >();
|
|
915
968
|
renderUnwrappedWithTheme( { width: 800, height: 400 }, 'default', ref );
|
|
916
969
|
|
|
917
970
|
const dimensions = ref.current?.getChartDimensions();
|
|
@@ -1298,7 +1351,7 @@ describe( 'LineChart', () => {
|
|
|
1298
1351
|
expect( buttons ).toHaveLength( 0 );
|
|
1299
1352
|
} );
|
|
1300
1353
|
|
|
1301
|
-
it( 'shows all series when
|
|
1354
|
+
it( 'shows all series when nothing has been hidden', () => {
|
|
1302
1355
|
render(
|
|
1303
1356
|
<GlobalChartsProvider>
|
|
1304
1357
|
<LineChartUnresponsive
|
|
@@ -1317,6 +1370,136 @@ describe( 'LineChart', () => {
|
|
|
1317
1370
|
expect( item ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1318
1371
|
} );
|
|
1319
1372
|
} );
|
|
1373
|
+
|
|
1374
|
+
it( 'hides a series programmatically when the legend is not interactive', () => {
|
|
1375
|
+
let context: GlobalChartsContextValue;
|
|
1376
|
+
const Grab = () => {
|
|
1377
|
+
context = useGlobalChartsContext();
|
|
1378
|
+
return null;
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
render(
|
|
1382
|
+
<GlobalChartsProvider>
|
|
1383
|
+
<Grab />
|
|
1384
|
+
<LineChartUnresponsive
|
|
1385
|
+
width={ 500 }
|
|
1386
|
+
height={ 300 }
|
|
1387
|
+
withGradientFill={ false }
|
|
1388
|
+
showLegend={ true }
|
|
1389
|
+
legend={ { interactive: false } }
|
|
1390
|
+
chartId="test-programmatic-line"
|
|
1391
|
+
data={ [
|
|
1392
|
+
{
|
|
1393
|
+
label: 'Series A',
|
|
1394
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1395
|
+
options: {},
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
label: 'Series B',
|
|
1399
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1400
|
+
options: {},
|
|
1401
|
+
},
|
|
1402
|
+
] }
|
|
1403
|
+
/>
|
|
1404
|
+
</GlobalChartsProvider>
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
expect( screen.queryByText( /all series are hidden/i ) ).not.toBeInTheDocument();
|
|
1408
|
+
|
|
1409
|
+
act( () => {
|
|
1410
|
+
context.toggleSeriesVisibility( 'test-programmatic-line', 'Series A' );
|
|
1411
|
+
context.toggleSeriesVisibility( 'test-programmatic-line', 'Series B' );
|
|
1412
|
+
} );
|
|
1413
|
+
|
|
1414
|
+
expect( screen.getByText( /all series are hidden/i ) ).toBeInTheDocument();
|
|
1415
|
+
} );
|
|
1416
|
+
|
|
1417
|
+
it( 'omits the click instruction when the legend cannot be clicked', () => {
|
|
1418
|
+
let context: GlobalChartsContextValue;
|
|
1419
|
+
const Grab = () => {
|
|
1420
|
+
context = useGlobalChartsContext();
|
|
1421
|
+
return null;
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
render(
|
|
1425
|
+
<GlobalChartsProvider>
|
|
1426
|
+
<Grab />
|
|
1427
|
+
<LineChartUnresponsive
|
|
1428
|
+
width={ 500 }
|
|
1429
|
+
height={ 300 }
|
|
1430
|
+
withGradientFill={ false }
|
|
1431
|
+
showLegend={ true }
|
|
1432
|
+
legend={ { interactive: false } }
|
|
1433
|
+
chartId="test-empty-copy-line"
|
|
1434
|
+
data={ [
|
|
1435
|
+
{
|
|
1436
|
+
label: 'Series A',
|
|
1437
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1438
|
+
options: {},
|
|
1439
|
+
},
|
|
1440
|
+
] }
|
|
1441
|
+
/>
|
|
1442
|
+
</GlobalChartsProvider>
|
|
1443
|
+
);
|
|
1444
|
+
|
|
1445
|
+
act( () => {
|
|
1446
|
+
context.toggleSeriesVisibility( 'test-empty-copy-line', 'Series A' );
|
|
1447
|
+
} );
|
|
1448
|
+
|
|
1449
|
+
expect( screen.getByText( 'All series are hidden.' ) ).toBeInTheDocument();
|
|
1450
|
+
expect( screen.queryByText( /click legend items/i ) ).not.toBeInTheDocument();
|
|
1451
|
+
} );
|
|
1452
|
+
|
|
1453
|
+
it( 'pins the value axis across a programmatic hide when rescaleYOnVisibilityChange is false', () => {
|
|
1454
|
+
let context: GlobalChartsContextValue;
|
|
1455
|
+
const Grab = () => {
|
|
1456
|
+
context = useGlobalChartsContext();
|
|
1457
|
+
return null;
|
|
1458
|
+
};
|
|
1459
|
+
const ref = createRef< ChartInstanceRef >();
|
|
1460
|
+
|
|
1461
|
+
render(
|
|
1462
|
+
<GlobalChartsProvider>
|
|
1463
|
+
<Grab />
|
|
1464
|
+
<LineChartUnresponsive
|
|
1465
|
+
width={ 500 }
|
|
1466
|
+
height={ 300 }
|
|
1467
|
+
withGradientFill={ false }
|
|
1468
|
+
showLegend={ true }
|
|
1469
|
+
legend={ { interactive: false } }
|
|
1470
|
+
chartId="test-programmatic-pin-line"
|
|
1471
|
+
rescaleYOnVisibilityChange={ false }
|
|
1472
|
+
ref={ ref }
|
|
1473
|
+
data={ [
|
|
1474
|
+
{
|
|
1475
|
+
label: 'Series A',
|
|
1476
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1477
|
+
options: {},
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
label: 'Series B',
|
|
1481
|
+
data: [ { date: new Date( '2024-01-01' ), value: 200, label: 'Jan 1' } ],
|
|
1482
|
+
options: {},
|
|
1483
|
+
},
|
|
1484
|
+
] }
|
|
1485
|
+
/>
|
|
1486
|
+
</GlobalChartsProvider>
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
const before = (
|
|
1490
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
1491
|
+
)?.domain();
|
|
1492
|
+
expect( before ).toBeDefined();
|
|
1493
|
+
|
|
1494
|
+
act( () => {
|
|
1495
|
+
context.toggleSeriesVisibility( 'test-programmatic-pin-line', 'Series B' );
|
|
1496
|
+
} );
|
|
1497
|
+
|
|
1498
|
+
const after = (
|
|
1499
|
+
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
1500
|
+
)?.domain();
|
|
1501
|
+
expect( after ).toEqual( before );
|
|
1502
|
+
} );
|
|
1320
1503
|
} );
|
|
1321
1504
|
|
|
1322
1505
|
// The line is not animated, so it clips only while actually zoomed.
|
|
@@ -1523,4 +1706,129 @@ describe( 'LineChart', () => {
|
|
|
1523
1706
|
).toBeInTheDocument();
|
|
1524
1707
|
} );
|
|
1525
1708
|
} );
|
|
1709
|
+
|
|
1710
|
+
describe( 'defaultHiddenSeries', () => {
|
|
1711
|
+
it( 'renders a series hidden when named in defaultHiddenSeries', () => {
|
|
1712
|
+
render(
|
|
1713
|
+
<GlobalChartsProvider>
|
|
1714
|
+
<LineChartUnresponsive
|
|
1715
|
+
width={ 500 }
|
|
1716
|
+
height={ 300 }
|
|
1717
|
+
withGradientFill={ false }
|
|
1718
|
+
showLegend={ true }
|
|
1719
|
+
legend={ { interactive: true } }
|
|
1720
|
+
chartId="test-default-hidden-line"
|
|
1721
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1722
|
+
data={ [
|
|
1723
|
+
{
|
|
1724
|
+
label: 'Series A',
|
|
1725
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1726
|
+
options: {},
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
label: 'Series B',
|
|
1730
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1731
|
+
options: {},
|
|
1732
|
+
},
|
|
1733
|
+
] }
|
|
1734
|
+
/>
|
|
1735
|
+
</GlobalChartsProvider>
|
|
1736
|
+
);
|
|
1737
|
+
|
|
1738
|
+
const items = screen.getAllByRole( 'button' );
|
|
1739
|
+
expect( items[ 0 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1740
|
+
expect( items[ 1 ] ).toHaveAttribute( 'aria-pressed', 'false' );
|
|
1741
|
+
} );
|
|
1742
|
+
|
|
1743
|
+
it( 'lets the user reveal a series seeded hidden', async () => {
|
|
1744
|
+
const user = userEvent.setup();
|
|
1745
|
+
|
|
1746
|
+
render(
|
|
1747
|
+
<GlobalChartsProvider>
|
|
1748
|
+
<LineChartUnresponsive
|
|
1749
|
+
width={ 500 }
|
|
1750
|
+
height={ 300 }
|
|
1751
|
+
withGradientFill={ false }
|
|
1752
|
+
showLegend={ true }
|
|
1753
|
+
legend={ { interactive: true } }
|
|
1754
|
+
chartId="test-default-hidden-reveal-line"
|
|
1755
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1756
|
+
data={ [
|
|
1757
|
+
{
|
|
1758
|
+
label: 'Series A',
|
|
1759
|
+
data: [ { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' } ],
|
|
1760
|
+
options: {},
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
label: 'Series B',
|
|
1764
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1765
|
+
options: {},
|
|
1766
|
+
},
|
|
1767
|
+
] }
|
|
1768
|
+
/>
|
|
1769
|
+
</GlobalChartsProvider>
|
|
1770
|
+
);
|
|
1771
|
+
|
|
1772
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
1773
|
+
|
|
1774
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1775
|
+
} );
|
|
1776
|
+
|
|
1777
|
+
it( 'keeps a revealed series visible after a data change', async () => {
|
|
1778
|
+
// Matters specifically at chart level: useChartRegistration unregisters and
|
|
1779
|
+
// re-registers whenever legendItems change, which a data prop change triggers.
|
|
1780
|
+
// A hook-level harness has no registration at all, so it can't catch a
|
|
1781
|
+
// regression where re-registration re-triggers the seeding effect.
|
|
1782
|
+
const user = userEvent.setup();
|
|
1783
|
+
const chartId = 'test-default-hidden-data-change-line';
|
|
1784
|
+
|
|
1785
|
+
const makeData = ( seriesAValue: number ) => [
|
|
1786
|
+
{
|
|
1787
|
+
label: 'Series A',
|
|
1788
|
+
data: [ { date: new Date( '2024-01-01' ), value: seriesAValue, label: 'Jan 1' } ],
|
|
1789
|
+
options: {},
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
label: 'Series B',
|
|
1793
|
+
data: [ { date: new Date( '2024-01-01' ), value: 20, label: 'Jan 1' } ],
|
|
1794
|
+
options: {},
|
|
1795
|
+
},
|
|
1796
|
+
];
|
|
1797
|
+
|
|
1798
|
+
const { rerender } = render(
|
|
1799
|
+
<GlobalChartsProvider>
|
|
1800
|
+
<LineChartUnresponsive
|
|
1801
|
+
width={ 500 }
|
|
1802
|
+
height={ 300 }
|
|
1803
|
+
withGradientFill={ false }
|
|
1804
|
+
showLegend={ true }
|
|
1805
|
+
legend={ { interactive: true } }
|
|
1806
|
+
chartId={ chartId }
|
|
1807
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1808
|
+
data={ makeData( 10 ) }
|
|
1809
|
+
/>
|
|
1810
|
+
</GlobalChartsProvider>
|
|
1811
|
+
);
|
|
1812
|
+
|
|
1813
|
+
await user.click( screen.getAllByRole( 'button' )[ 1 ] );
|
|
1814
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1815
|
+
|
|
1816
|
+
rerender(
|
|
1817
|
+
<GlobalChartsProvider>
|
|
1818
|
+
<LineChartUnresponsive
|
|
1819
|
+
width={ 500 }
|
|
1820
|
+
height={ 300 }
|
|
1821
|
+
withGradientFill={ false }
|
|
1822
|
+
showLegend={ true }
|
|
1823
|
+
legend={ { interactive: true } }
|
|
1824
|
+
chartId={ chartId }
|
|
1825
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1826
|
+
data={ makeData( 30 ) }
|
|
1827
|
+
/>
|
|
1828
|
+
</GlobalChartsProvider>
|
|
1829
|
+
);
|
|
1830
|
+
|
|
1831
|
+
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1832
|
+
} );
|
|
1833
|
+
} );
|
|
1526
1834
|
} );
|
|
@@ -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;
|