@automattic/charts 4.9.0 → 4.11.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 +28 -0
- package/TOKENS.md +7 -3
- package/dist/index.cjs +496 -774
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +66 -3
- package/dist/index.d.cts +65 -60
- package/dist/index.d.ts +65 -60
- package/dist/index.js +467 -726
- package/dist/index.js.map +1 -1
- package/dist/visx/group/index.js +1 -2
- package/dist/visx/legend/index.js +1 -2
- package/dist/visx/text/index.js +1 -2
- package/package.json +13 -12
- package/src/charts/area-chart/area-chart.tsx +2 -1
- package/src/charts/area-chart/test/area-chart.test.tsx +0 -8
- package/src/charts/area-chart/types.ts +0 -2
- package/src/charts/bar-chart/bar-chart.tsx +7 -2
- package/src/charts/bar-chart/test/bar-chart.test.tsx +0 -8
- package/src/charts/bar-chart/types.ts +0 -2
- package/src/charts/bar-list-chart/bar-list-chart.tsx +3 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +5 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +23 -7
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +22 -0
- package/src/charts/conversion-funnel-chart/types.ts +1 -1
- package/src/charts/geo-chart/geo-chart.module.scss +27 -0
- package/src/charts/geo-chart/geo-chart.tsx +85 -45
- package/src/charts/geo-chart/private/use-tooltip-position.ts +106 -0
- package/src/charts/geo-chart/test/geo-chart.test.tsx +77 -46
- package/src/charts/geo-chart/test/use-tooltip-position.test.ts +43 -0
- package/src/charts/geo-chart/types.ts +1 -1
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +3 -3
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +48 -43
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +5 -11
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +55 -27
- package/src/charts/heatmap-chart/types.ts +1 -3
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +6 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +34 -19
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +35 -6
- package/src/charts/leaderboard-chart/types.ts +1 -0
- package/src/charts/line-chart/line-chart.tsx +3 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +0 -8
- package/src/charts/line-chart/types.ts +0 -2
- package/src/charts/pie-chart/pie-chart.module.scss +3 -0
- package/src/charts/pie-chart/pie-chart.tsx +0 -5
- package/src/charts/pie-chart/test/pie-chart.test.tsx +4 -8
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +3 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +0 -5
- package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +8 -3
- package/src/charts/test/accessible-name.test.tsx +181 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +63 -3
- package/src/components/legend/hooks/use-chart-legend-items.ts +63 -1
- package/src/components/legend/private/base-legend.module.scss +5 -0
- package/src/components/legend/private/base-legend.tsx +35 -17
- package/src/components/legend/test/legend.test.tsx +42 -0
- package/src/components/legend/types.ts +2 -0
- package/src/components/tooltip/_surface.scss +19 -0
- package/src/components/tooltip/private/bounded-tooltip.tsx +2 -2
- package/src/components/tooltip/tooltip-box.module.scss +3 -17
- package/src/hooks/test/use-xychart-theme.test.tsx +22 -0
- package/src/hooks/use-xychart-theme.ts +33 -2
- package/src/index.ts +1 -0
- package/src/styles/chart-scope.scss +6 -4
- package/src/styles/test/chart-scope.test.ts +6 -2
- package/src/types.ts +19 -1
- package/src/utils/resolve-font-size.ts +9 -10
- package/src/utils/test/resolve-font-size.test.ts +11 -2
|
@@ -74,8 +74,6 @@ export interface HeatmapChartProps extends Omit<
|
|
|
74
74
|
* group past the edge, Page Up/Down by one group. Default `grid`.
|
|
75
75
|
*/
|
|
76
76
|
keyboardNavigation?: 'grid' | 'calendar';
|
|
77
|
-
/** Accessible name of the grid. Defaults to a localized "Heatmap chart". */
|
|
78
|
-
ariaLabel?: string;
|
|
79
77
|
/** Compact mode: hide in-cell values, tighten gap, thin axis labels. Default false. */
|
|
80
78
|
compact?: boolean;
|
|
81
79
|
/** Compact mode only: grow the cells to fill the chart's box, wrapping the column groups onto more rows when that makes them larger. Default false. */
|
|
@@ -104,7 +102,7 @@ export interface HeatmapChartProps extends Omit<
|
|
|
104
102
|
/** Floor a cell's height (px) in non-compact mode; see `minCellWidth`. */
|
|
105
103
|
minCellHeight?: number;
|
|
106
104
|
/**
|
|
107
|
-
* Color the cell scale is built from, deepened at the highest value toward
|
|
105
|
+
* Color the cell scale is built from, deepened at the highest value toward 6:1
|
|
108
106
|
* against the background. Defaults to the first series palette slot,
|
|
109
107
|
* `--a8c-charts-color-series-1`.
|
|
110
108
|
*/
|
|
@@ -16,8 +16,8 @@ export const SUBPIXEL_TOLERANCE = 0.5;
|
|
|
16
16
|
* depends on `--a8c-charts-dimension-leaderboard-row-gap`, label wrapping, and
|
|
17
17
|
* whatever a caller renders as a label, so any restated number would drift.
|
|
18
18
|
*
|
|
19
|
-
* Every row is a direct grid child marked with `data-row-index`;
|
|
20
|
-
*
|
|
19
|
+
* Every row is a `listitem` div, a direct grid child marked with `data-row-index`;
|
|
20
|
+
* interactive rows hold a button inside it.
|
|
21
21
|
*
|
|
22
22
|
* @param enabled - Whether to measure at all. When false every row fits.
|
|
23
23
|
* @param rowCount - Total number of rows rendered.
|
|
@@ -147,6 +147,12 @@
|
|
|
147
147
|
// needs no runtime cascade and never lands on the DOM as an unprefixed var.
|
|
148
148
|
$focus-ring-width: var(--wpds-border-width-focus);
|
|
149
149
|
|
|
150
|
+
.rowItem {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-column: 1 / -1;
|
|
153
|
+
grid-template-columns: subgrid;
|
|
154
|
+
}
|
|
155
|
+
|
|
150
156
|
.row {
|
|
151
157
|
display: grid;
|
|
152
158
|
grid-column: 1 / -1;
|
|
@@ -188,6 +188,7 @@ const BarWithLabel = ( {
|
|
|
188
188
|
* @param props.children - Child components for composition API
|
|
189
189
|
* @param props.className - Additional CSS class name
|
|
190
190
|
* @param props.style - Custom styling for the chart container
|
|
191
|
+
* @param props.ariaLabel - Accessible name for the list, defaulting to "Leaderboard chart"
|
|
191
192
|
* @return JSX element representing the leaderboard chart
|
|
192
193
|
*/
|
|
193
194
|
const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
@@ -208,6 +209,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
208
209
|
legend = {},
|
|
209
210
|
legendLabels,
|
|
210
211
|
gap = 'md',
|
|
212
|
+
ariaLabel,
|
|
211
213
|
className,
|
|
212
214
|
style,
|
|
213
215
|
children,
|
|
@@ -389,6 +391,8 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
389
391
|
</div>
|
|
390
392
|
) : (
|
|
391
393
|
<Grid
|
|
394
|
+
role="list"
|
|
395
|
+
aria-label={ ariaLabel ?? __( 'Leaderboard chart', 'jetpack-charts' ) }
|
|
392
396
|
templateColumns="minmax(0, 1fr) auto"
|
|
393
397
|
rowGap={ ROW_GAP }
|
|
394
398
|
columnGap={ COLUMN_GAP }
|
|
@@ -465,46 +469,57 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
465
469
|
|
|
466
470
|
if ( entry.onClick && entry.hasInteractiveLabel ) {
|
|
467
471
|
return (
|
|
468
|
-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Mouse-only fallback; the row button carries keyboard access.
|
|
469
472
|
<div
|
|
470
473
|
key={ entry.id }
|
|
474
|
+
role="listitem"
|
|
471
475
|
data-row-index={ rowIndex }
|
|
472
476
|
style={ rowStyle }
|
|
473
|
-
className={
|
|
474
|
-
onClick={ forwardCellClickToRowButton }
|
|
477
|
+
className={ styles.rowItem }
|
|
475
478
|
>
|
|
476
|
-
{
|
|
477
|
-
<
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
479
|
+
{ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Mouse-only fallback; the row button carries keyboard access. */ }
|
|
480
|
+
<div
|
|
481
|
+
className={ clsx( styles.row, styles.interactiveRow, styles.layeredRow ) }
|
|
482
|
+
onClick={ forwardCellClickToRowButton }
|
|
483
|
+
>
|
|
484
|
+
{ rowCells }
|
|
485
|
+
<button
|
|
486
|
+
type="button"
|
|
487
|
+
className={ styles.rowButton }
|
|
488
|
+
onClick={ entry.onClick }
|
|
489
|
+
aria-label={ entry.ariaLabel }
|
|
490
|
+
/>
|
|
491
|
+
<Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
|
|
492
|
+
</div>
|
|
484
493
|
</div>
|
|
485
494
|
);
|
|
486
495
|
}
|
|
487
496
|
|
|
488
497
|
if ( entry.onClick ) {
|
|
489
498
|
return (
|
|
490
|
-
<
|
|
499
|
+
<div
|
|
491
500
|
key={ entry.id }
|
|
492
|
-
|
|
501
|
+
role="listitem"
|
|
493
502
|
data-row-index={ rowIndex }
|
|
494
503
|
style={ rowStyle }
|
|
495
|
-
className={
|
|
496
|
-
onClick={ entry.onClick }
|
|
497
|
-
aria-label={ entry.ariaLabel }
|
|
504
|
+
className={ styles.rowItem }
|
|
498
505
|
>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
506
|
+
<button
|
|
507
|
+
type="button"
|
|
508
|
+
className={ clsx( styles.row, styles.interactiveRow ) }
|
|
509
|
+
onClick={ entry.onClick }
|
|
510
|
+
aria-label={ entry.ariaLabel }
|
|
511
|
+
>
|
|
512
|
+
{ rowCells }
|
|
513
|
+
<Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
502
516
|
);
|
|
503
517
|
}
|
|
504
518
|
|
|
505
519
|
return (
|
|
506
520
|
<div
|
|
507
521
|
key={ entry.id }
|
|
522
|
+
role="listitem"
|
|
508
523
|
data-row-index={ rowIndex }
|
|
509
524
|
style={ rowStyle }
|
|
510
525
|
className={ styles.row }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen, act } from '@testing-library/react';
|
|
1
|
+
import { render, screen, act, within } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { GlobalChartsProvider } from '../../../providers';
|
|
4
4
|
import { useGlobalChartsContext } from '../../../providers/chart-context/hooks/use-global-charts-context';
|
|
@@ -706,6 +706,32 @@ describe( 'LeaderboardChart', () => {
|
|
|
706
706
|
);
|
|
707
707
|
} );
|
|
708
708
|
|
|
709
|
+
it( 'renders each entry as a list item, keeping clickable rows as named buttons', () => {
|
|
710
|
+
render(
|
|
711
|
+
<LeaderboardChart
|
|
712
|
+
data={ [
|
|
713
|
+
mockData[ 0 ],
|
|
714
|
+
{ ...mockData[ 1 ], ariaLabel: 'Open Home', onClick: jest.fn() },
|
|
715
|
+
{
|
|
716
|
+
...mockData[ 1 ],
|
|
717
|
+
id: 'about',
|
|
718
|
+
ariaLabel: 'Open About',
|
|
719
|
+
onClick: jest.fn(),
|
|
720
|
+
hasInteractiveLabel: true,
|
|
721
|
+
},
|
|
722
|
+
] }
|
|
723
|
+
/>
|
|
724
|
+
);
|
|
725
|
+
const items = screen.getAllByRole( 'listitem' );
|
|
726
|
+
expect( items ).toHaveLength( 3 );
|
|
727
|
+
expect(
|
|
728
|
+
within( items[ 1 ] ).getByRole( 'button', { name: 'Open Home' } )
|
|
729
|
+
).toBeInTheDocument();
|
|
730
|
+
expect(
|
|
731
|
+
within( items[ 2 ] ).getByRole( 'button', { name: 'Open About' } )
|
|
732
|
+
).toBeInTheDocument();
|
|
733
|
+
} );
|
|
734
|
+
|
|
709
735
|
it( 'does not render a button for entries without onClick', () => {
|
|
710
736
|
render( <LeaderboardChart data={ [ mockData[ 0 ] ] } /> );
|
|
711
737
|
expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
|
|
@@ -721,15 +747,18 @@ describe( 'LeaderboardChart', () => {
|
|
|
721
747
|
|
|
722
748
|
describe( 'grid gaps', () => {
|
|
723
749
|
// eslint-disable-next-line testing-library/no-node-access -- The grid is rendered by `Grid` from @wordpress/components, which takes no test id.
|
|
724
|
-
const grid = () => document.querySelector( '[data-leaderboard-grid]' );
|
|
750
|
+
const grid = () => document.querySelector< HTMLElement >( '[data-leaderboard-grid]' );
|
|
725
751
|
|
|
726
752
|
it( 'hands the catalog role to the grid', () => {
|
|
727
753
|
render( <LeaderboardChart data={ mockData } /> );
|
|
728
754
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
755
|
+
// Grid writes its gaps into custom properties and reads them from a stylesheet.
|
|
756
|
+
expect( grid().style.getPropertyValue( '--wp-components-grid-row-gap' ) ).toBe(
|
|
757
|
+
'var(--a8c-charts-dimension-leaderboard-row-gap, 12px)'
|
|
758
|
+
);
|
|
759
|
+
expect( grid().style.getPropertyValue( '--wp-components-grid-column-gap' ) ).toBe(
|
|
760
|
+
'var(--a8c-charts-dimension-leaderboard-column-gap, 4px)'
|
|
761
|
+
);
|
|
733
762
|
} );
|
|
734
763
|
} );
|
|
735
764
|
} );
|
|
@@ -272,6 +272,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
272
272
|
const legendInteractive = legend.interactive ?? false;
|
|
273
273
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
274
274
|
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
275
|
+
const legendComparisonSeriesItems = legend.comparisonSeriesItems;
|
|
275
276
|
const legendShape = legend.shape ?? 'line';
|
|
276
277
|
const legendPosition = legend.position ?? 'bottom';
|
|
277
278
|
|
|
@@ -523,6 +524,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
523
524
|
glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ),
|
|
524
525
|
collapseGroups: legendCollapseGroups,
|
|
525
526
|
comparisonItem: legendComparisonItem,
|
|
527
|
+
comparisonSeriesItems: legendComparisonSeriesItems,
|
|
526
528
|
renderGlyph,
|
|
527
529
|
} ),
|
|
528
530
|
[
|
|
@@ -530,6 +532,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
530
532
|
glyphStyle?.radius,
|
|
531
533
|
legendCollapseGroups,
|
|
532
534
|
legendComparisonItem,
|
|
535
|
+
legendComparisonSeriesItems,
|
|
533
536
|
renderGlyph,
|
|
534
537
|
]
|
|
535
538
|
);
|
|
@@ -1610,14 +1610,6 @@ describe( 'LineChart', () => {
|
|
|
1610
1610
|
expect( chart ).toHaveAttribute( 'tabIndex', '0' );
|
|
1611
1611
|
expect( chart ).toHaveAttribute( 'role', 'application' );
|
|
1612
1612
|
expect( chart ).toHaveAttribute( 'aria-label', 'Line chart' );
|
|
1613
|
-
} );
|
|
1614
|
-
|
|
1615
|
-
test.each( [
|
|
1616
|
-
[ undefined, 'Line chart' ],
|
|
1617
|
-
[ 'Views over time', 'Views over time' ],
|
|
1618
|
-
] )( 'names the chart container from ariaLabel %p', ( ariaLabel, name ) => {
|
|
1619
|
-
renderWithTheme( { ariaLabel } );
|
|
1620
|
-
expect( screen.getByRole( 'application', { name } ) ).toBeInTheDocument();
|
|
1621
1613
|
expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
|
|
1622
1614
|
} );
|
|
1623
1615
|
|
|
@@ -31,8 +31,6 @@ export type RenderLineGlyphProps< Datum extends object > = GlyphProps< Datum > &
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
|
|
34
|
-
/** Accessible name of the chart. Defaults to a localized "Line chart". */
|
|
35
|
-
ariaLabel?: string;
|
|
36
34
|
/**
|
|
37
35
|
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
38
36
|
*/
|
|
@@ -138,11 +138,6 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
|
|
|
138
138
|
* When provided, replaces the default `label: value` tooltip with custom content.
|
|
139
139
|
*/
|
|
140
140
|
renderTooltip?: ( params: PieChartRenderTooltipParams ) => ReactNode;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Accessible name of the chart. Defaults to a localized "Pie chart".
|
|
144
|
-
*/
|
|
145
|
-
ariaLabel?: string;
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
// Base props type with optional responsive properties
|
|
@@ -780,14 +780,6 @@ describe( 'PieChart', () => {
|
|
|
780
780
|
{ label: 'Linux', value: 22000, valueDisplay: '22K' },
|
|
781
781
|
];
|
|
782
782
|
|
|
783
|
-
test.each( [
|
|
784
|
-
[ undefined, 'Pie chart' ],
|
|
785
|
-
[ 'Devices', 'Devices' ],
|
|
786
|
-
] )( 'names the focusable chart from ariaLabel %p', ( ariaLabel, name ) => {
|
|
787
|
-
render( <PieChart data={ data } ariaLabel={ ariaLabel } /> );
|
|
788
|
-
expect( screen.getByRole( 'application', { name } ) ).toHaveAttribute( 'tabindex', '0' );
|
|
789
|
-
} );
|
|
790
|
-
|
|
791
783
|
const expectRingOn = ( segment: HTMLElement ) =>
|
|
792
784
|
expect( screen.getByTestId( 'pie-selected-ring' ) ).toHaveAttribute(
|
|
793
785
|
'd',
|
|
@@ -799,6 +791,10 @@ describe( 'PieChart', () => {
|
|
|
799
791
|
render( <PieChart data={ data } withTooltips /> );
|
|
800
792
|
const [ macos, linux ] = screen.getAllByTestId( 'pie-segment' );
|
|
801
793
|
expect( screen.queryByTestId( 'pie-selected-ring' ) ).not.toBeInTheDocument();
|
|
794
|
+
expect( screen.getByRole( 'application', { name: 'Pie chart' } ) ).toHaveAttribute(
|
|
795
|
+
'tabindex',
|
|
796
|
+
'0'
|
|
797
|
+
);
|
|
802
798
|
|
|
803
799
|
await user.tab();
|
|
804
800
|
await user.keyboard( '{ArrowRight}' );
|
|
@@ -129,11 +129,6 @@ export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercen
|
|
|
129
129
|
* When provided, replaces the default `label: value` tooltip with custom content.
|
|
130
130
|
*/
|
|
131
131
|
renderTooltip?: ( params: PieSemiCircleChartRenderTooltipParams ) => ReactNode;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Accessible name of the chart. Defaults to a localized "Semi-circle chart".
|
|
135
|
-
*/
|
|
136
|
-
ariaLabel?: string;
|
|
137
132
|
}
|
|
138
133
|
|
|
139
134
|
// Base props type with optional responsive properties
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BoundedTooltip,
|
|
3
|
+
TOOLTIP_Z_INDEX,
|
|
4
|
+
} from '../../../components/tooltip/private/bounded-tooltip';
|
|
2
5
|
import { useStandaloneScopeClass } from '../../../providers/chart-scope';
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
6
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
const TOOLTIP_BOX_STYLE: CSSProperties = { zIndex: TOOLTIP_Z_INDEX };
|
|
4
9
|
|
|
5
10
|
interface PieTooltipProps {
|
|
6
11
|
top: number;
|
|
@@ -21,7 +26,7 @@ export const PieTooltip = ( {
|
|
|
21
26
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
22
27
|
|
|
23
28
|
return (
|
|
24
|
-
<BoundedTooltip top={ top } left={ left }>
|
|
29
|
+
<BoundedTooltip top={ top } left={ left } style={ TOOLTIP_BOX_STYLE }>
|
|
25
30
|
{ selectedIndex === undefined ? (
|
|
26
31
|
<div className={ standaloneScopeClass } role="tooltip">
|
|
27
32
|
{ children }
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { GlobalChartsProvider } from '../../providers';
|
|
3
|
+
import { salesByProduct } from '../../stories/sample-data';
|
|
4
|
+
import { AreaChartUnresponsive } from '../area-chart';
|
|
5
|
+
import { BarChartUnresponsive } from '../bar-chart';
|
|
6
|
+
import { BarListChartUnresponsive } from '../bar-list-chart';
|
|
7
|
+
import { ConversionFunnelChart } from '../conversion-funnel-chart';
|
|
8
|
+
import { GeoChartUnresponsive } from '../geo-chart';
|
|
9
|
+
import { HeatmapChart } from '../heatmap-chart';
|
|
10
|
+
import { LeaderboardChartUnresponsive } from '../leaderboard-chart';
|
|
11
|
+
import { LineChartUnresponsive } from '../line-chart';
|
|
12
|
+
import { PieChartUnresponsive } from '../pie-chart';
|
|
13
|
+
import { PieSemiCircleChartUnresponsive } from '../pie-semi-circle-chart';
|
|
14
|
+
import type { GeoData } from '../../types';
|
|
15
|
+
import type { ReactElement } from 'react';
|
|
16
|
+
|
|
17
|
+
jest.mock( 'react-google-charts', () => ( { Chart: () => <div /> } ) );
|
|
18
|
+
|
|
19
|
+
const series = [
|
|
20
|
+
{
|
|
21
|
+
label: 'Series A',
|
|
22
|
+
data: [
|
|
23
|
+
{ date: new Date( '2024-01-01' ), value: 10 },
|
|
24
|
+
{ date: new Date( '2024-01-02' ), value: 20 },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const slices = [
|
|
30
|
+
{ label: 'MacOS', value: 30, valueDisplay: '30', percentage: 60 },
|
|
31
|
+
{ label: 'Linux', value: 20, valueDisplay: '20', percentage: 40 },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const geoData: GeoData = [
|
|
35
|
+
[ 'Country', 'Value' ],
|
|
36
|
+
[ 'US', 100 ],
|
|
37
|
+
[ 'CA', 50 ],
|
|
38
|
+
[ 'GB', 25 ],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const funnelSteps = [
|
|
42
|
+
{ id: 'sessions', label: 'Sessions', rate: 100, count: 10000 },
|
|
43
|
+
{ id: 'cart', label: 'Cart', rate: 71.1, count: 7110 },
|
|
44
|
+
{ id: 'checkout', label: 'Checkout', rate: 52.5, count: 5250 },
|
|
45
|
+
{ id: 'purchase', label: 'Purchase', rate: 10.3, count: 1030 },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const heatmapData = [
|
|
49
|
+
{ label: 'W1', data: [ { value: 1 }, { value: 2 }, { value: null } ] },
|
|
50
|
+
{ label: 'W2', data: [ { value: 3 }, { value: 0 }, { value: 4 } ] },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const leaderboardData = [
|
|
54
|
+
{
|
|
55
|
+
id: 'direct',
|
|
56
|
+
label: 'Direct',
|
|
57
|
+
currentValue: 12500,
|
|
58
|
+
previousValue: 10000,
|
|
59
|
+
currentShare: 100,
|
|
60
|
+
previousShare: 80,
|
|
61
|
+
delta: 25,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'social',
|
|
65
|
+
label: 'Social Media',
|
|
66
|
+
currentValue: 8750,
|
|
67
|
+
previousValue: 9500,
|
|
68
|
+
currentShare: 70,
|
|
69
|
+
previousShare: 76,
|
|
70
|
+
delta: -8,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const CHARTS: [ string, string, string, ( ariaLabel?: string ) => ReactElement ][] = [
|
|
75
|
+
[
|
|
76
|
+
'line',
|
|
77
|
+
'application',
|
|
78
|
+
'Line chart',
|
|
79
|
+
ariaLabel => (
|
|
80
|
+
<LineChartUnresponsive
|
|
81
|
+
data={ series }
|
|
82
|
+
width={ 400 }
|
|
83
|
+
height={ 300 }
|
|
84
|
+
withGradientFill={ false }
|
|
85
|
+
ariaLabel={ ariaLabel }
|
|
86
|
+
/>
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
'area',
|
|
91
|
+
'application',
|
|
92
|
+
'Area chart',
|
|
93
|
+
ariaLabel => (
|
|
94
|
+
<AreaChartUnresponsive data={ series } width={ 400 } height={ 300 } ariaLabel={ ariaLabel } />
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
'bar',
|
|
99
|
+
'application',
|
|
100
|
+
'Bar chart',
|
|
101
|
+
ariaLabel => (
|
|
102
|
+
<BarChartUnresponsive data={ series } width={ 400 } height={ 300 } ariaLabel={ ariaLabel } />
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
'bar list',
|
|
107
|
+
'application',
|
|
108
|
+
'Bar list chart',
|
|
109
|
+
ariaLabel => (
|
|
110
|
+
<BarListChartUnresponsive
|
|
111
|
+
data={ salesByProduct }
|
|
112
|
+
width={ 400 }
|
|
113
|
+
height={ 300 }
|
|
114
|
+
ariaLabel={ ariaLabel }
|
|
115
|
+
/>
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
'pie',
|
|
120
|
+
'application',
|
|
121
|
+
'Pie chart',
|
|
122
|
+
ariaLabel => <PieChartUnresponsive data={ slices } size={ 300 } ariaLabel={ ariaLabel } />,
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
'semi-circle',
|
|
126
|
+
'application',
|
|
127
|
+
'Semi-circle chart',
|
|
128
|
+
ariaLabel => (
|
|
129
|
+
<PieSemiCircleChartUnresponsive data={ slices } width={ 300 } ariaLabel={ ariaLabel } />
|
|
130
|
+
),
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
'geo',
|
|
134
|
+
'group',
|
|
135
|
+
'Geo chart',
|
|
136
|
+
ariaLabel => (
|
|
137
|
+
<GeoChartUnresponsive data={ geoData } width={ 800 } height={ 400 } ariaLabel={ ariaLabel } />
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
'conversion funnel',
|
|
142
|
+
'group',
|
|
143
|
+
'Conversion funnel chart',
|
|
144
|
+
ariaLabel => (
|
|
145
|
+
<ConversionFunnelChart mainRate={ 25 } steps={ funnelSteps } ariaLabel={ ariaLabel } />
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
'heatmap',
|
|
150
|
+
'grid',
|
|
151
|
+
'Heatmap chart',
|
|
152
|
+
ariaLabel => (
|
|
153
|
+
<HeatmapChart data={ heatmapData } width={ 500 } height={ 300 } ariaLabel={ ariaLabel } />
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
'leaderboard',
|
|
158
|
+
'list',
|
|
159
|
+
'Leaderboard chart',
|
|
160
|
+
ariaLabel => (
|
|
161
|
+
<LeaderboardChartUnresponsive
|
|
162
|
+
data={ leaderboardData }
|
|
163
|
+
width={ 400 }
|
|
164
|
+
height={ 300 }
|
|
165
|
+
ariaLabel={ ariaLabel }
|
|
166
|
+
/>
|
|
167
|
+
),
|
|
168
|
+
],
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
it.each( CHARTS )(
|
|
172
|
+
'names the %s chart from ariaLabel, or its default',
|
|
173
|
+
( _chart, role, defaultName, renderChart ) => {
|
|
174
|
+
const { unmount } = render( <GlobalChartsProvider>{ renderChart() }</GlobalChartsProvider> );
|
|
175
|
+
expect( screen.getByRole( role, { name: defaultName } ) ).toBeInTheDocument();
|
|
176
|
+
unmount();
|
|
177
|
+
|
|
178
|
+
render( <GlobalChartsProvider>{ renderChart( 'Views by day' ) }</GlobalChartsProvider> );
|
|
179
|
+
expect( screen.getByRole( role, { name: 'Views by day' } ) ).toBeInTheDocument();
|
|
180
|
+
}
|
|
181
|
+
);
|
|
@@ -257,6 +257,23 @@ describe( 'useChartLegendItems', () => {
|
|
|
257
257
|
data: [ { label: 'Mon', value: 90 } ],
|
|
258
258
|
},
|
|
259
259
|
];
|
|
260
|
+
const twoMetricData: SeriesData[] = [
|
|
261
|
+
{ label: 'Views', group: 'views', data: [ { label: 'Mon', value: 100 } ] },
|
|
262
|
+
{
|
|
263
|
+
label: 'Views — previous',
|
|
264
|
+
group: 'views',
|
|
265
|
+
options: { type: 'comparison' as const },
|
|
266
|
+
data: [ { label: 'Mon', value: 90 } ],
|
|
267
|
+
},
|
|
268
|
+
{ label: 'Visitors', group: 'visitors', data: [ { label: 'Mon', value: 50 } ] },
|
|
269
|
+
{
|
|
270
|
+
label: 'Visitors — previous',
|
|
271
|
+
group: 'visitors',
|
|
272
|
+
options: { type: 'comparison' as const },
|
|
273
|
+
data: [ { label: 'Mon', value: 40 } ],
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
const perMetricLabel = ( metric: string ) => `${ metric } (Prev. 7 days)`;
|
|
260
277
|
|
|
261
278
|
test( 'is off by default', () => {
|
|
262
279
|
const { result } = renderHook(
|
|
@@ -300,15 +317,58 @@ describe( 'useChartLegendItems', () => {
|
|
|
300
317
|
expect( result.current.map( item => item.label ) ).toEqual( [ 'Views', 'Previous period' ] );
|
|
301
318
|
} );
|
|
302
319
|
|
|
303
|
-
test( '
|
|
320
|
+
test( 'names one static item per compared metric with comparisonSeriesItems', () => {
|
|
304
321
|
const { result } = renderHook(
|
|
305
|
-
() =>
|
|
322
|
+
() =>
|
|
323
|
+
useChartLegendItems(
|
|
324
|
+
twoMetricData,
|
|
325
|
+
{ collapseGroups: true, comparisonSeriesItems: perMetricLabel },
|
|
326
|
+
'line'
|
|
327
|
+
),
|
|
306
328
|
{ wrapper }
|
|
307
329
|
);
|
|
308
330
|
|
|
309
|
-
expect( result.current.map( item => item.label ) ).toEqual( [
|
|
331
|
+
expect( result.current.map( item => item.label ) ).toEqual( [
|
|
332
|
+
'Views',
|
|
333
|
+
'Visitors',
|
|
334
|
+
'Views (Prev. 7 days)',
|
|
335
|
+
'Visitors (Prev. 7 days)',
|
|
336
|
+
] );
|
|
337
|
+
expect(
|
|
338
|
+
result.current.slice( 2 ).map( ( { interactive, describesSeries } ) => ( {
|
|
339
|
+
interactive,
|
|
340
|
+
describesSeries,
|
|
341
|
+
} ) )
|
|
342
|
+
).toEqual( [
|
|
343
|
+
{ interactive: false, describesSeries: 'Views — previous' },
|
|
344
|
+
{ interactive: false, describesSeries: 'Visitors — previous' },
|
|
345
|
+
] );
|
|
346
|
+
expect( result.current[ 2 ].color ).toBe( result.current[ 0 ].color );
|
|
347
|
+
expect( result.current[ 3 ].color ).toBe( result.current[ 1 ].color );
|
|
310
348
|
} );
|
|
311
349
|
|
|
350
|
+
test.each( [
|
|
351
|
+
[
|
|
352
|
+
'comparisonItem',
|
|
353
|
+
comparisonData,
|
|
354
|
+
{ comparisonItem: true },
|
|
355
|
+
[ 'Views', 'Views — previous' ],
|
|
356
|
+
],
|
|
357
|
+
[
|
|
358
|
+
'comparisonSeriesItems',
|
|
359
|
+
twoMetricData,
|
|
360
|
+
{ comparisonSeriesItems: perMetricLabel },
|
|
361
|
+
[ 'Views', 'Views — previous', 'Visitors', 'Visitors — previous' ],
|
|
362
|
+
],
|
|
363
|
+
] )(
|
|
364
|
+
'adds nothing for %s when the comparison series already has its own item',
|
|
365
|
+
( _name, data, options, expected ) => {
|
|
366
|
+
const { result } = renderHook( () => useChartLegendItems( data, options ), { wrapper } );
|
|
367
|
+
|
|
368
|
+
expect( result.current.map( item => item.label ) ).toEqual( expected );
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
|
|
312
372
|
test( 'styles a current-period series by its own line styles, not the theme index', () => {
|
|
313
373
|
const theme = {
|
|
314
374
|
seriesLineStyles: [ { strokeWidth: 2 } ],
|