@automattic/charts 4.11.1 → 4.13.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 +24 -0
- package/dist/index.cjs +1412 -787
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +59 -25
- package/dist/index.d.cts +64 -120
- package/dist/index.d.ts +64 -120
- package/dist/index.js +1400 -765
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/charts/area-chart/area-chart.tsx +16 -6
- package/src/charts/area-chart/test/area-chart.test.tsx +42 -9
- package/src/charts/bar-chart/bar-chart.tsx +143 -142
- package/src/charts/bar-chart/index.ts +3 -0
- package/src/charts/bar-chart/private/band-tooltip.tsx +41 -15
- package/src/charts/bar-chart/private/bar-layer.tsx +132 -0
- package/src/charts/bar-chart/private/comparison-bars-geometry.ts +22 -34
- package/src/charts/bar-chart/private/comparison-bars.tsx +9 -18
- package/src/charts/bar-chart/private/comparison-constants.ts +4 -10
- package/src/charts/bar-chart/private/grouped-bars.ts +68 -0
- package/src/charts/bar-chart/private/index.ts +7 -1
- package/src/charts/bar-chart/private/overlap-bars.ts +183 -0
- package/src/charts/bar-chart/private/selected-bar-ring.tsx +84 -0
- package/src/charts/bar-chart/private/slot-tooltip-members.tsx +28 -0
- package/src/charts/bar-chart/private/slotted-bar-series.tsx +84 -0
- package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +4 -11
- package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +2 -3
- package/src/charts/bar-chart/private/test/grouped-bars.test.ts +87 -0
- package/src/charts/bar-chart/private/test/overlap-bars.test.ts +159 -0
- package/src/charts/bar-chart/private/use-bar-stops.ts +87 -0
- package/src/charts/bar-chart/private/use-overlap-slots.ts +213 -0
- package/src/charts/bar-chart/private/value-domain.ts +0 -10
- package/src/charts/bar-chart/test/bar-chart-overlap.test.tsx +347 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +181 -19
- package/src/charts/bar-chart/test/use-bar-legend-items.test.tsx +81 -0
- package/src/charts/bar-chart/types.ts +3 -0
- package/src/charts/bar-chart/use-bar-colors.ts +46 -0
- package/src/charts/bar-chart/use-bar-legend-items.ts +87 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +24 -11
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +235 -182
- package/src/charts/conversion-funnel-chart/index.ts +2 -0
- package/src/charts/conversion-funnel-chart/private/index.ts +1 -0
- package/src/charts/conversion-funnel-chart/private/use-funnel-selection.ts +9 -47
- package/src/charts/conversion-funnel-chart/private/use-roving-step-focus.ts +83 -0
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +312 -92
- package/src/charts/conversion-funnel-chart/test/use-funnel-selection.test.ts +23 -188
- package/src/charts/conversion-funnel-chart/types.ts +22 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +41 -1
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +101 -30
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +47 -0
- package/src/charts/line-chart/line-chart.tsx +25 -18
- package/src/charts/line-chart/test/line-chart.test.tsx +240 -1
- package/src/charts/pie-chart/pie-chart.tsx +7 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +7 -2
- package/src/charts/private/pie-keyboard-navigation/index.ts +0 -1
- package/src/charts/private/{pie-keyboard-navigation/pie-keyboard-navigation.module.scss → selected-ring/selected-ring.module.scss} +11 -6
- package/src/charts/private/selected-ring/selected-ring.tsx +73 -0
- package/src/charts/private/{pie-keyboard-navigation/test/selected-segment-ring.test.ts → selected-ring/test/selected-ring.test.ts} +1 -1
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +20 -0
- package/src/components/legend/hooks/use-chart-legend-items.ts +27 -3
- package/src/components/tooltip/private/at-nearest-date.ts +49 -0
- package/src/components/tooltip/private/keyboard-stops.ts +67 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +10 -2
- package/src/components/tooltip/test/keyboard-stops.test.ts +36 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +135 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +11 -14
- package/src/index.ts +10 -2
- package/src/providers/chart-context/global-charts-provider.tsx +2 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +0 -13
- package/src/providers/chart-context/themes.ts +0 -1
- package/src/providers/chart-context/types.ts +2 -0
- package/src/test-utils/selected-bar-ring.ts +14 -0
- package/src/types.ts +14 -5
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +0 -47
|
@@ -11,10 +11,13 @@ import userEvent from '@testing-library/user-event';
|
|
|
11
11
|
import { GlobalChartsProvider } from '../../../providers';
|
|
12
12
|
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
13
13
|
import { steadyTrafficData } from '../../../stories/sample-data';
|
|
14
|
+
import { selectedBarRingBounds } from '../../../test-utils/selected-bar-ring';
|
|
14
15
|
import BarChart, { BarChartUnresponsive } from '../bar-chart';
|
|
15
16
|
import { useBarChartOptions } from '../private';
|
|
17
|
+
import { useBarColors } from '../use-bar-colors';
|
|
16
18
|
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
17
19
|
import type { DataPointDate, SeriesData } from '../../../types';
|
|
20
|
+
import type { RenderTooltipParams } from '../../../visx/types';
|
|
18
21
|
|
|
19
22
|
// Mock useElementSize to return non-zero dimensions in jsdom so charts render
|
|
20
23
|
const mockRefCallback = jest.fn();
|
|
@@ -22,6 +25,17 @@ jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
|
22
25
|
useElementSize: () => [ mockRefCallback, 500, 300 ],
|
|
23
26
|
} ) );
|
|
24
27
|
|
|
28
|
+
const geometryOf = ( element: Element ) =>
|
|
29
|
+
[ 'x', 'y', 'width', 'height' ].map( name =>
|
|
30
|
+
Number( element.getAttribute( name ) ).toFixed( 2 )
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const selectedRingGeometry = () =>
|
|
34
|
+
Object.values( selectedBarRingBounds() ).map( value => value.toFixed( 2 ) );
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line testing-library/no-node-access -- visx renders the bars and takes no test id.
|
|
37
|
+
const visxBars = () => document.querySelectorAll( '.visx-bar' );
|
|
38
|
+
|
|
25
39
|
describe( 'BarChart', () => {
|
|
26
40
|
const defaultProps = {
|
|
27
41
|
width: 500,
|
|
@@ -1644,7 +1658,7 @@ describe( 'BarChart', () => {
|
|
|
1644
1658
|
expect( screen.queryByTestId( 'bar-chart-patterns' ) ).not.toBeInTheDocument();
|
|
1645
1659
|
} );
|
|
1646
1660
|
|
|
1647
|
-
test( 'comparison
|
|
1661
|
+
test( 'fills the comparison bar with its own pattern when patterns are enabled', () => {
|
|
1648
1662
|
renderWithTheme( {
|
|
1649
1663
|
withPatterns: true,
|
|
1650
1664
|
data: [
|
|
@@ -1670,9 +1684,7 @@ describe( 'BarChart', () => {
|
|
|
1670
1684
|
|
|
1671
1685
|
const shadow = screen.getAllByTestId( /^bar-chart-comparison-\d+-\d+$/ )[ 0 ];
|
|
1672
1686
|
const shadowFill = shadow.getAttribute( 'fill' );
|
|
1673
|
-
|
|
1674
|
-
// series' pattern (index 0) rather than the comparison series' own index.
|
|
1675
|
-
expect( shadowFill ).toMatch( /^url\(#bar-pattern-.+-0\)$/ );
|
|
1687
|
+
expect( shadowFill ).toMatch( /^url\(#bar-pattern-.+-1\)$/ );
|
|
1676
1688
|
} );
|
|
1677
1689
|
} );
|
|
1678
1690
|
|
|
@@ -2014,6 +2026,39 @@ describe( 'BarChart', () => {
|
|
|
2014
2026
|
expect( tooltip ).toHaveTextContent( 'Previous period: 15' );
|
|
2015
2027
|
} );
|
|
2016
2028
|
|
|
2029
|
+
test( 'pairs the comparison value by date when the bars have empty labels', async () => {
|
|
2030
|
+
const user = userEvent.setup();
|
|
2031
|
+
renderWithTheme( {
|
|
2032
|
+
withTooltips: true,
|
|
2033
|
+
data: [
|
|
2034
|
+
{
|
|
2035
|
+
label: 'This period',
|
|
2036
|
+
group: 'views',
|
|
2037
|
+
data: [
|
|
2038
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: '' },
|
|
2039
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: '' },
|
|
2040
|
+
],
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
label: 'Previous period',
|
|
2044
|
+
group: 'views',
|
|
2045
|
+
options: { type: 'comparison' as const },
|
|
2046
|
+
data: [
|
|
2047
|
+
{ date: new Date( '2024-01-01' ), value: 15, label: '' },
|
|
2048
|
+
{ date: new Date( '2024-01-02' ), value: 25, label: '' },
|
|
2049
|
+
],
|
|
2050
|
+
},
|
|
2051
|
+
],
|
|
2052
|
+
} );
|
|
2053
|
+
|
|
2054
|
+
screen.getByRole( 'application', { name: /bar chart/i } ).focus();
|
|
2055
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
2056
|
+
|
|
2057
|
+
const tooltip = screen.getByTestId( 'chart-tooltip-1' );
|
|
2058
|
+
expect( tooltip ).toHaveTextContent( 'This period: 20' );
|
|
2059
|
+
expect( tooltip ).toHaveTextContent( 'Previous period: 25' );
|
|
2060
|
+
} );
|
|
2061
|
+
|
|
2017
2062
|
test( 'keyboard navigation past the first slot stays on the primary bar, not the comparison series', async () => {
|
|
2018
2063
|
const user = userEvent.setup();
|
|
2019
2064
|
renderWithTheme( {
|
|
@@ -2275,7 +2320,7 @@ describe( 'BarChart', () => {
|
|
|
2275
2320
|
await user.keyboard( '{ArrowRight}' );
|
|
2276
2321
|
|
|
2277
2322
|
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'No data' );
|
|
2278
|
-
expect( screen.queryByTestId( 'bar-chart-
|
|
2323
|
+
expect( screen.queryByTestId( 'bar-chart-selected-ring' ) ).not.toBeInTheDocument();
|
|
2279
2324
|
} );
|
|
2280
2325
|
|
|
2281
2326
|
test( 'outlines the first rendered bar for the first bucket with a reading', async () => {
|
|
@@ -2286,9 +2331,7 @@ describe( 'BarChart', () => {
|
|
|
2286
2331
|
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
2287
2332
|
|
|
2288
2333
|
expect( screen.getByTestId( 'chart-tooltip-1' ) ).toHaveTextContent( '20' );
|
|
2289
|
-
expect(
|
|
2290
|
-
/\.visx-bar:nth-child\(1\) \{/
|
|
2291
|
-
);
|
|
2334
|
+
expect( selectedRingGeometry() ).toEqual( geometryOf( visxBars()[ 0 ] ) );
|
|
2292
2335
|
} );
|
|
2293
2336
|
|
|
2294
2337
|
test( 'skips the bars an earlier series left out when outlining a later series', async () => {
|
|
@@ -2319,9 +2362,7 @@ describe( 'BarChart', () => {
|
|
|
2319
2362
|
await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}{ArrowRight}' );
|
|
2320
2363
|
|
|
2321
2364
|
expect( screen.getByTestId( 'chart-tooltip-3' ) ).toHaveTextContent( '15' );
|
|
2322
|
-
expect(
|
|
2323
|
-
/\.visx-bar:nth-child\(4\) \{/
|
|
2324
|
-
);
|
|
2365
|
+
expect( selectedRingGeometry() ).toEqual( geometryOf( visxBars()[ 3 ] ) );
|
|
2325
2366
|
} );
|
|
2326
2367
|
} );
|
|
2327
2368
|
} );
|
|
@@ -2705,8 +2746,11 @@ describe( 'BarChart', () => {
|
|
|
2705
2746
|
} );
|
|
2706
2747
|
|
|
2707
2748
|
describe( 'Comparison Series', () => {
|
|
2708
|
-
it(
|
|
2709
|
-
|
|
2749
|
+
it.each( [
|
|
2750
|
+
[ 'opaque, in the colors the charts context reports', undefined ],
|
|
2751
|
+
[ 'translucent, in the series colors, under the deprecated theme opacity', 0.5 ],
|
|
2752
|
+
] )( 'draws a comparison bar behind the primary bar, %s', ( _case, opacity ) => {
|
|
2753
|
+
const data: SeriesData[] = [
|
|
2710
2754
|
{
|
|
2711
2755
|
label: 'This year',
|
|
2712
2756
|
group: 'views',
|
|
@@ -2725,18 +2769,56 @@ describe( 'BarChart', () => {
|
|
|
2725
2769
|
],
|
|
2726
2770
|
},
|
|
2727
2771
|
];
|
|
2728
|
-
|
|
2772
|
+
let barColors: string[] = [];
|
|
2773
|
+
const CaptureBarColors = () => {
|
|
2774
|
+
barColors = useBarColors()( data );
|
|
2775
|
+
return null;
|
|
2776
|
+
};
|
|
2777
|
+
render(
|
|
2778
|
+
<GlobalChartsProvider
|
|
2779
|
+
theme={ opacity ? { barChart: { barStyles: { comparison: { opacity } } } } : undefined }
|
|
2780
|
+
>
|
|
2781
|
+
<CaptureBarColors />
|
|
2782
|
+
<BarChart data={ data } width={ 400 } height={ 300 } />
|
|
2783
|
+
</GlobalChartsProvider>
|
|
2784
|
+
);
|
|
2729
2785
|
|
|
2730
|
-
|
|
2731
|
-
// Match individual rect testids like bar-chart-comparison-1-0 (not the group wrapper)
|
|
2786
|
+
const [ primaryColor, comparisonColor ] = barColors;
|
|
2732
2787
|
const shadows = screen.getAllByTestId( /^bar-chart-comparison-\d+-\d+$/ );
|
|
2733
2788
|
expect( shadows ).toHaveLength( 2 );
|
|
2734
|
-
expect( shadows[ 0 ] ).toHaveAttribute( '
|
|
2789
|
+
expect( shadows[ 0 ] ).toHaveAttribute( 'fill', comparisonColor );
|
|
2790
|
+
expect( shadows[ 0 ].attributes.getNamedItem( 'opacity' )?.value ).toBe(
|
|
2791
|
+
opacity ? String( opacity ) : undefined
|
|
2792
|
+
);
|
|
2793
|
+
expect( comparisonColor === primaryColor ).toBe( !! opacity );
|
|
2735
2794
|
|
|
2736
|
-
// primary bars still render for the single primary series (visx .visx-bar)
|
|
2737
2795
|
// eslint-disable-next-line testing-library/no-node-access
|
|
2738
2796
|
const bars = document.querySelectorAll( '.visx-bar' );
|
|
2739
2797
|
expect( bars.length ).toBeGreaterThanOrEqual( 2 );
|
|
2798
|
+
expect( bars[ 0 ] ).toHaveAttribute( 'fill', primaryColor );
|
|
2799
|
+
} );
|
|
2800
|
+
|
|
2801
|
+
it( "fills a comparison bar in its datum's own color, as a primary bar does", () => {
|
|
2802
|
+
render(
|
|
2803
|
+
<BarChart
|
|
2804
|
+
width={ 400 }
|
|
2805
|
+
height={ 300 }
|
|
2806
|
+
data={ [
|
|
2807
|
+
{ label: 'This year', group: 'views', data: [ { label: 'Jan', value: 100 } ] },
|
|
2808
|
+
{
|
|
2809
|
+
label: 'Last year',
|
|
2810
|
+
group: 'views',
|
|
2811
|
+
options: { type: 'comparison' as const },
|
|
2812
|
+
data: [ { label: 'Jan', value: 80, color: '#123456' } ],
|
|
2813
|
+
},
|
|
2814
|
+
] }
|
|
2815
|
+
/>
|
|
2816
|
+
);
|
|
2817
|
+
|
|
2818
|
+
expect( screen.getByTestId( 'bar-chart-comparison-1-0' ) ).toHaveAttribute(
|
|
2819
|
+
'fill',
|
|
2820
|
+
'#123456'
|
|
2821
|
+
);
|
|
2740
2822
|
} );
|
|
2741
2823
|
|
|
2742
2824
|
it( 'hides the comparison shadow group from assistive technology', () => {
|
|
@@ -2787,7 +2869,6 @@ describe( 'BarChart', () => {
|
|
|
2787
2869
|
|
|
2788
2870
|
const shadows = screen.getAllByTestId( /^bar-chart-comparison-\d+-\d+$/ );
|
|
2789
2871
|
expect( shadows ).toHaveLength( 2 );
|
|
2790
|
-
expect( shadows[ 0 ] ).toHaveAttribute( 'opacity', '0.5' );
|
|
2791
2872
|
} );
|
|
2792
2873
|
|
|
2793
2874
|
it( 'expands value-axis domain to include comparison values exceeding the primary max', () => {
|
|
@@ -3507,6 +3588,87 @@ describe( 'BarChart', () => {
|
|
|
3507
3588
|
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
3508
3589
|
} );
|
|
3509
3590
|
|
|
3591
|
+
const labelledViews = [
|
|
3592
|
+
{ label: 'Apr', value: 29 },
|
|
3593
|
+
{ label: 'May', value: 19 },
|
|
3594
|
+
];
|
|
3595
|
+
const april = new Date( '2025-04-01T00:00:00Z' );
|
|
3596
|
+
const may = new Date( '2025-05-01T00:00:00Z' );
|
|
3597
|
+
|
|
3598
|
+
it.each( [
|
|
3599
|
+
{
|
|
3600
|
+
name: 'aligned series',
|
|
3601
|
+
views: labelledViews,
|
|
3602
|
+
visitors: [
|
|
3603
|
+
{ label: 'Apr', value: 112 },
|
|
3604
|
+
{ label: 'May', value: 80 },
|
|
3605
|
+
],
|
|
3606
|
+
presses: 3,
|
|
3607
|
+
rows: [ 'Views=19@1', 'Visitors=80@1' ],
|
|
3608
|
+
nearest: 'Views',
|
|
3609
|
+
},
|
|
3610
|
+
{
|
|
3611
|
+
name: 'ragged series',
|
|
3612
|
+
views: labelledViews,
|
|
3613
|
+
visitors: [ { label: 'May', value: 80 } ],
|
|
3614
|
+
presses: 2,
|
|
3615
|
+
rows: [ 'Views=19@1', 'Visitors=80@0' ],
|
|
3616
|
+
nearest: 'Visitors',
|
|
3617
|
+
},
|
|
3618
|
+
{
|
|
3619
|
+
name: 'dated points with empty labels',
|
|
3620
|
+
views: [
|
|
3621
|
+
{ date: april, label: '', value: 29 },
|
|
3622
|
+
{ date: may, label: '', value: 19 },
|
|
3623
|
+
],
|
|
3624
|
+
visitors: [
|
|
3625
|
+
{ date: april, label: '', value: 112 },
|
|
3626
|
+
{ date: may, label: '', value: 80 },
|
|
3627
|
+
],
|
|
3628
|
+
presses: 3,
|
|
3629
|
+
rows: [ 'Views=19@1', 'Visitors=80@1' ],
|
|
3630
|
+
nearest: 'Views',
|
|
3631
|
+
},
|
|
3632
|
+
] )(
|
|
3633
|
+
'gives the keyboard tooltip only the bars at the selected category: $name',
|
|
3634
|
+
async ( { views, visitors, presses, rows, nearest } ) => {
|
|
3635
|
+
const user = userEvent.setup();
|
|
3636
|
+
const renderTooltip = ( { tooltipData }: RenderTooltipParams< DataPointDate > ) => (
|
|
3637
|
+
<div>
|
|
3638
|
+
<span data-testid="nearest">{ tooltipData?.nearestDatum?.key }</span>
|
|
3639
|
+
{ Object.entries( tooltipData?.datumByKey ?? {} ).map(
|
|
3640
|
+
( [ key, { datum, index } ] ) => (
|
|
3641
|
+
<span
|
|
3642
|
+
key={ key }
|
|
3643
|
+
data-testid="row"
|
|
3644
|
+
>{ `${ key }=${ datum.value }@${ index }` }</span>
|
|
3645
|
+
)
|
|
3646
|
+
) }
|
|
3647
|
+
</div>
|
|
3648
|
+
);
|
|
3649
|
+
|
|
3650
|
+
renderWithTheme( {
|
|
3651
|
+
withTooltips: true,
|
|
3652
|
+
renderTooltip,
|
|
3653
|
+
data: [
|
|
3654
|
+
{ label: 'Views', data: views, options: {} },
|
|
3655
|
+
{ label: 'Visitors', data: visitors, options: {} },
|
|
3656
|
+
],
|
|
3657
|
+
} );
|
|
3658
|
+
|
|
3659
|
+
screen.getByRole( 'application', { name: /bar chart/i } ).focus();
|
|
3660
|
+
for ( let i = 0; i < presses; i++ ) {
|
|
3661
|
+
await user.keyboard( '{ArrowRight}' );
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
expect( screen.getAllByTestId( 'row' ).map( row => row.textContent ) ).toEqual(
|
|
3665
|
+
expect.arrayContaining( rows )
|
|
3666
|
+
);
|
|
3667
|
+
expect( screen.getAllByTestId( 'row' ) ).toHaveLength( rows.length );
|
|
3668
|
+
expect( screen.getByTestId( 'nearest' ) ).toHaveTextContent( nearest );
|
|
3669
|
+
}
|
|
3670
|
+
);
|
|
3671
|
+
|
|
3510
3672
|
it( 'stops at the last visible slot on a standard chart', async () => {
|
|
3511
3673
|
const user = userEvent.setup();
|
|
3512
3674
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { useChartLegendItems } from '../../../components/legend';
|
|
3
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
4
|
+
import { useBarColors } from '../use-bar-colors';
|
|
5
|
+
import { useBarLegendItems } from '../use-bar-legend-items';
|
|
6
|
+
import type { SeriesData } from '../../../types';
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
const wrapper = ( { children }: { children: ReactNode } ) => (
|
|
10
|
+
<GlobalChartsProvider>{ children }</GlobalChartsProvider>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const twoMetricData: SeriesData[] = [
|
|
14
|
+
{ label: 'Views', group: 'views', data: [ { label: 'Mon', value: 100 } ] },
|
|
15
|
+
{
|
|
16
|
+
label: 'Views — previous',
|
|
17
|
+
group: 'views',
|
|
18
|
+
options: { type: 'comparison' },
|
|
19
|
+
data: [ { label: 'Mon', value: 90 } ],
|
|
20
|
+
},
|
|
21
|
+
{ label: 'Visitors', group: 'visitors', data: [ { label: 'Mon', value: 50 } ] },
|
|
22
|
+
{
|
|
23
|
+
label: 'Visitors — previous',
|
|
24
|
+
group: 'visitors',
|
|
25
|
+
options: { type: 'comparison' },
|
|
26
|
+
data: [ { label: 'Mon', value: 40 } ],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const perMetricLabel = ( metric: string ) => `${ metric } (Prev. 7 days)`;
|
|
30
|
+
|
|
31
|
+
describe( 'useBarLegendItems', () => {
|
|
32
|
+
test.each( [
|
|
33
|
+
[
|
|
34
|
+
'takes the grouped comparison tints, a collapsed group in its comparison tint,',
|
|
35
|
+
'grouped',
|
|
36
|
+
[ 1, 3, 1, 3 ],
|
|
37
|
+
],
|
|
38
|
+
[ 'takes the opaque overlap bar colors', 'overlap', [ 0, 2, 1, 3 ] ],
|
|
39
|
+
] as const )( '%s with layout %s', ( _case, layout, colorOrder ) => {
|
|
40
|
+
const { result } = renderHook(
|
|
41
|
+
() => ( {
|
|
42
|
+
items: useBarLegendItems( twoMetricData, {
|
|
43
|
+
collapseGroups: true,
|
|
44
|
+
comparisonSeriesItems: perMetricLabel,
|
|
45
|
+
layout,
|
|
46
|
+
} ),
|
|
47
|
+
getBarColors: useBarColors(),
|
|
48
|
+
} ),
|
|
49
|
+
{ wrapper }
|
|
50
|
+
);
|
|
51
|
+
const colors = result.current.getBarColors( twoMetricData, { layout } );
|
|
52
|
+
|
|
53
|
+
expect( result.current.items.map( item => item.color ) ).toEqual(
|
|
54
|
+
colorOrder.map( index => colors[ index ] )
|
|
55
|
+
);
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
test( 'lists one item per series in slot order, primaries first, with layout overlap', () => {
|
|
59
|
+
const { result } = renderHook(
|
|
60
|
+
() => useBarLegendItems( twoMetricData, { layout: 'overlap' } ),
|
|
61
|
+
{ wrapper }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect( result.current.map( item => item.label ) ).toEqual(
|
|
65
|
+
[ 0, 2, 1, 3 ].map( index => twoMetricData[ index ].label )
|
|
66
|
+
);
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
test( "returns the chart legend's own items without a layout", () => {
|
|
70
|
+
const options = { collapseGroups: true };
|
|
71
|
+
const { result } = renderHook(
|
|
72
|
+
() => ( {
|
|
73
|
+
bar: useBarLegendItems( twoMetricData, options ),
|
|
74
|
+
chart: useChartLegendItems( twoMetricData, options ),
|
|
75
|
+
} ),
|
|
76
|
+
{ wrapper }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect( result.current.bar ).toEqual( result.current.chart );
|
|
80
|
+
} );
|
|
81
|
+
} );
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BarChartLayout,
|
|
2
3
|
BaseChartProps,
|
|
3
4
|
DataPointDate,
|
|
4
5
|
SeriesData,
|
|
@@ -33,6 +34,8 @@ export interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVis
|
|
|
33
34
|
/** CSS class for each primary bar, in addition to visx-bar. */
|
|
34
35
|
barClassName?: ( datum: DataPointDate ) => string | undefined;
|
|
35
36
|
orientation?: 'horizontal' | 'vertical';
|
|
37
|
+
/** Defaults to `grouped`. */
|
|
38
|
+
layout?: BarChartLayout;
|
|
36
39
|
withPatterns?: boolean;
|
|
37
40
|
showZeroValues?: boolean;
|
|
38
41
|
/** Highlight the active band across the plot when tooltips are enabled. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useGlobalChartsContext } from '../../providers/chart-context/hooks/use-global-charts-context';
|
|
3
|
+
import { BACKGROUND_FALLBACK } from '../../providers/chart-context/private/catalog-pointers';
|
|
4
|
+
import { getGroupedBarColors } from './private/grouped-bars';
|
|
5
|
+
import { getOverlapBarColors } from './private/overlap-bars';
|
|
6
|
+
import type { BarChartLayout, SeriesData } from '../../types';
|
|
7
|
+
|
|
8
|
+
export type GetBarColors = (
|
|
9
|
+
series: SeriesData[],
|
|
10
|
+
options?: { layout?: BarChartLayout }
|
|
11
|
+
) => string[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads the colors `BarChart` fills series with, so a custom tooltip or legend can match the bars.
|
|
15
|
+
*
|
|
16
|
+
* @return A function from series and a layout to each series' bar color, by index.
|
|
17
|
+
*/
|
|
18
|
+
export function useBarColors(): GetBarColors {
|
|
19
|
+
const {
|
|
20
|
+
getElementStyles,
|
|
21
|
+
backgroundColor = BACKGROUND_FALLBACK,
|
|
22
|
+
theme,
|
|
23
|
+
} = useGlobalChartsContext();
|
|
24
|
+
const comparisonOpacity = theme.barChart?.barStyles?.comparison?.opacity;
|
|
25
|
+
|
|
26
|
+
return useCallback< GetBarColors >(
|
|
27
|
+
( series, { layout = 'grouped' } = {} ) => {
|
|
28
|
+
const colors = series.map( ( data, index ) => getElementStyles( { data, index } ).color );
|
|
29
|
+
|
|
30
|
+
if ( layout !== 'overlap' ) {
|
|
31
|
+
// The deprecated comparison opacity keeps the translucent bars in their series colors.
|
|
32
|
+
return comparisonOpacity === undefined
|
|
33
|
+
? getGroupedBarColors( series, colors, backgroundColor )
|
|
34
|
+
: colors;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return getOverlapBarColors(
|
|
38
|
+
series,
|
|
39
|
+
colors,
|
|
40
|
+
[ getElementStyles( { index: 0 } ).color, getElementStyles( { index: 1 } ).color ],
|
|
41
|
+
backgroundColor
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
[ getElementStyles, backgroundColor, comparisonOpacity ]
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useChartLegendItems } from '../../components/legend/hooks/use-chart-legend-items';
|
|
3
|
+
import { useBarColors } from './use-bar-colors';
|
|
4
|
+
import type { ChartLegendOptions } from '../../components/legend/hooks/use-chart-legend-items';
|
|
5
|
+
import type { BaseLegendItem } from '../../components/legend/types';
|
|
6
|
+
import type { BarChartLayout, LegendShape, SeriesData } from '../../types';
|
|
7
|
+
|
|
8
|
+
export type BarLegendOptions = Omit< ChartLegendOptions, 'seriesColors' > & {
|
|
9
|
+
/** The layout the bars are drawn in. Unset, the items are the chart legend's own. */
|
|
10
|
+
layout?: BarChartLayout;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isComparison = ( data: SeriesData[], label: string | undefined ) =>
|
|
14
|
+
data.some( series => series.label === label && series.options?.type === 'comparison' );
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Applies what a bar layout changes about a legend: overlap lists every primary before every comparison, as it draws their slots, with every swatch opaque like its bar; a collapsed grouped item takes its comparison's tint, the wider bar that frames the group.
|
|
18
|
+
*
|
|
19
|
+
* @param data - The series the items were built from.
|
|
20
|
+
* @param items - The legend items.
|
|
21
|
+
* @param layout - The layout the bars are drawn in.
|
|
22
|
+
* @param seriesColors - Each series' bar color, by index.
|
|
23
|
+
* @return The legend items.
|
|
24
|
+
*/
|
|
25
|
+
function forBarLayout(
|
|
26
|
+
data: SeriesData[],
|
|
27
|
+
items: BaseLegendItem[],
|
|
28
|
+
layout: BarChartLayout,
|
|
29
|
+
seriesColors: string[]
|
|
30
|
+
): BaseLegendItem[] {
|
|
31
|
+
if ( layout === 'overlap' ) {
|
|
32
|
+
const opaque = items.map( item =>
|
|
33
|
+
item.shapeStyle ? { ...item, shapeStyle: { ...item.shapeStyle, opacity: undefined } } : item
|
|
34
|
+
);
|
|
35
|
+
// Static items, such as a comparison item, follow the series items and keep their place.
|
|
36
|
+
const seriesItems = opaque.filter( item => item.seriesLabels );
|
|
37
|
+
const firstLabel = ( item: BaseLegendItem ) => item.seriesLabels?.[ 0 ];
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
...seriesItems.filter( item => ! isComparison( data, firstLabel( item ) ) ),
|
|
41
|
+
...seriesItems.filter( item => isComparison( data, firstLabel( item ) ) ),
|
|
42
|
+
...opaque.filter( item => ! item.seriesLabels ),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return items.map( item => {
|
|
47
|
+
if ( ( item.seriesLabels?.length ?? 0 ) < 2 ) {
|
|
48
|
+
return item;
|
|
49
|
+
}
|
|
50
|
+
const comparisonIndex = data.findIndex(
|
|
51
|
+
series => series.options?.type === 'comparison' && item.seriesLabels?.includes( series.label )
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return comparisonIndex === -1 ? item : { ...item, color: seriesColors[ comparisonIndex ] };
|
|
55
|
+
} );
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds legend items that match `BarChart`'s bars in a layout, for a custom legend or a chart's own.
|
|
60
|
+
*
|
|
61
|
+
* @param data - The series the chart draws.
|
|
62
|
+
* @param options - The legend options, and the layout the bars are drawn in.
|
|
63
|
+
* @param legendShape - The shape of the legend swatches.
|
|
64
|
+
* @return The legend items.
|
|
65
|
+
*/
|
|
66
|
+
export function useBarLegendItems(
|
|
67
|
+
data: SeriesData[],
|
|
68
|
+
options: BarLegendOptions = {},
|
|
69
|
+
legendShape?: LegendShape< SeriesData[], number >
|
|
70
|
+
): BaseLegendItem[] {
|
|
71
|
+
const { layout } = options;
|
|
72
|
+
const getBarColors = useBarColors();
|
|
73
|
+
const seriesColors = useMemo(
|
|
74
|
+
() => ( layout ? getBarColors( data, { layout } ) : undefined ),
|
|
75
|
+
[ data, layout, getBarColors ]
|
|
76
|
+
);
|
|
77
|
+
const chartLegendOptions = useMemo(
|
|
78
|
+
(): ChartLegendOptions => ( { ...options, seriesColors } ),
|
|
79
|
+
[ options, seriesColors ]
|
|
80
|
+
);
|
|
81
|
+
const items = useChartLegendItems( data, chartLegendOptions, legendShape );
|
|
82
|
+
|
|
83
|
+
return useMemo(
|
|
84
|
+
() => ( layout && seriesColors ? forBarLayout( data, items, layout, seriesColors ) : items ),
|
|
85
|
+
[ data, items, layout, seriesColors ]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -52,13 +52,28 @@
|
|
|
52
52
|
height: 100%;
|
|
53
53
|
|
|
54
54
|
&--animated {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
|
|
56
|
+
.step-text,
|
|
57
|
+
.funnel-bar,
|
|
58
|
+
.bar-container {
|
|
59
|
+
transition:
|
|
60
|
+
opacity var(--wpds-motion-duration-lg) var(--wpds-motion-easing-subtle),
|
|
61
|
+
background-color var(--wpds-motion-duration-lg) var(--wpds-motion-easing-subtle);
|
|
62
|
+
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
// Dim the contents, not the step: opacity on an ancestor would also fade
|
|
66
|
+
// the bar's focus ring.
|
|
60
67
|
&--blurred {
|
|
61
|
-
|
|
68
|
+
|
|
69
|
+
.step-text,
|
|
70
|
+
.funnel-bar {
|
|
71
|
+
opacity: 0.3;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.bar-container {
|
|
75
|
+
background-color: color-mix(in sRGB, var(--a8c-charts-color-surface-secondary) 30%, transparent);
|
|
76
|
+
}
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
|
|
@@ -119,20 +134,18 @@
|
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
.tooltip-title
|
|
137
|
+
.tooltip-title,
|
|
138
|
+
.tooltip-label,
|
|
139
|
+
.tooltip-value {
|
|
123
140
|
color: var(--a8c-charts-color-label);
|
|
124
141
|
font-size: var(--wpds-typography-font-size-sm);
|
|
125
142
|
font-style: normal;
|
|
126
143
|
font-weight: var(--wpds-typography-font-weight-default);
|
|
127
|
-
line-height: var(--wpds-typography-line-height-
|
|
144
|
+
line-height: var(--wpds-typography-line-height-sm);
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
.tooltip-
|
|
131
|
-
color: var(--a8c-charts-color-label);
|
|
132
|
-
font-size: var(--wpds-typography-font-size-md);
|
|
133
|
-
font-style: normal;
|
|
147
|
+
.tooltip-value {
|
|
134
148
|
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
135
|
-
line-height: var(--wpds-typography-line-height-sm);
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
.empty-state {
|