@automattic/charts 4.8.0 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.cjs +624 -228
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +58 -8
  5. package/dist/index.d.cts +42 -8
  6. package/dist/index.d.ts +42 -8
  7. package/dist/index.js +628 -232
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/charts/area-chart/area-chart.tsx +5 -2
  11. package/src/charts/area-chart/test/area-chart.test.tsx +23 -14
  12. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
  13. package/src/charts/area-chart/types.ts +2 -0
  14. package/src/charts/bar-chart/bar-chart.tsx +5 -2
  15. package/src/charts/bar-chart/test/bar-chart.test.tsx +82 -63
  16. package/src/charts/bar-chart/types.ts +2 -0
  17. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
  18. package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -4
  19. package/src/charts/heatmap-chart/heatmap-chart.tsx +144 -37
  20. package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
  21. package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
  22. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +14 -2
  23. package/src/charts/heatmap-chart/private/index.ts +4 -2
  24. package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
  25. package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
  26. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +132 -1
  27. package/src/charts/heatmap-chart/types.ts +2 -0
  28. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
  29. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
  30. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
  31. package/src/charts/leaderboard-chart/types.ts +6 -1
  32. package/src/charts/line-chart/line-chart.tsx +5 -2
  33. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
  34. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
  35. package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
  36. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
  37. package/src/charts/line-chart/types.ts +5 -3
  38. package/src/charts/pie-chart/pie-chart.tsx +149 -139
  39. package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
  40. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +122 -120
  41. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +54 -0
  42. package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
  43. package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
  44. package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
  45. package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
  46. package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
  47. package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
  48. package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
  49. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
  50. package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
  51. package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
  52. package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
  53. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
  54. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +20 -19
  55. package/src/components/tooltip/xy-chart-tooltip.tsx +3 -3
  56. package/src/types.ts +18 -2
@@ -120,7 +120,7 @@ describe( 'LineChart', () => {
120
120
  tooltipPlacement,
121
121
  tooltipStyle: { backgroundColor: 'black', color: 'white', boxShadow: 'none' },
122
122
  } );
123
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
123
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
124
124
  await user.keyboard( '{ArrowRight}' );
125
125
 
126
126
  const vertical = screen.getByTestId( 'xy-chart-tooltip-crosshair-vertical' );
@@ -143,7 +143,7 @@ describe( 'LineChart', () => {
143
143
  test( 'passes a color-only tooltipStyle through with no background added', async () => {
144
144
  const user = userEvent.setup();
145
145
  renderWithTheme( { tooltipStyle: { color: 'white' } } );
146
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
146
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
147
147
  await user.keyboard( '{ArrowRight}' );
148
148
 
149
149
  const box = screen.getByTestId( 'bounded-tooltip' );
@@ -154,7 +154,7 @@ describe( 'LineChart', () => {
154
154
  test( 'paints a tooltipStyle background on the box, not on the default content', async () => {
155
155
  const user = userEvent.setup();
156
156
  renderWithTheme( { tooltipStyle: { background: 'black' } } );
157
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
157
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
158
158
  await user.keyboard( '{ArrowRight}' );
159
159
 
160
160
  expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( { background: 'black' } );
@@ -164,7 +164,7 @@ describe( 'LineChart', () => {
164
164
  test( 'preserves the existing tooltip colors for noncolor overrides', async () => {
165
165
  const user = userEvent.setup();
166
166
  renderWithTheme( { tooltipStyle: { boxShadow: 'none' } } );
167
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
167
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
168
168
  await user.keyboard( '{ArrowRight}' );
169
169
 
170
170
  const content = screen.getByTestId( 'line-chart-tooltip-content' );
@@ -179,13 +179,13 @@ describe( 'LineChart', () => {
179
179
  } );
180
180
 
181
181
  test.each( [ [ 'Escape', '{Escape}' ] ] )(
182
- 'returns focus to the grid after %s',
182
+ 'returns focus to the chart after %s',
183
183
  async ( _name, keys ) => {
184
184
  jest.useFakeTimers();
185
185
  try {
186
186
  const user = userEvent.setup( { advanceTimers: jest.advanceTimersByTime } );
187
187
  renderWithTheme();
188
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
188
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
189
189
 
190
190
  await user.tab();
191
191
  expect( chart ).toHaveFocus();
@@ -225,7 +225,7 @@ describe( 'LineChart', () => {
225
225
  ],
226
226
  } );
227
227
  // Should render without crashing and show the single point
228
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
228
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
229
229
  } );
230
230
 
231
231
  test( 'renders a bucket with no reading instead of failing the whole chart', () => {
@@ -241,7 +241,7 @@ describe( 'LineChart', () => {
241
241
  ],
242
242
  } );
243
243
  expect( screen.queryByText( /invalid data/i ) ).not.toBeInTheDocument();
244
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
244
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
245
245
  } );
246
246
 
247
247
  test( 'tooltip reads No data for a bucket with no reading', async () => {
@@ -258,7 +258,7 @@ describe( 'LineChart', () => {
258
258
  ],
259
259
  } );
260
260
 
261
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
261
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
262
262
  await user.keyboard( '{ArrowRight}' );
263
263
 
264
264
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -282,7 +282,7 @@ describe( 'LineChart', () => {
282
282
 
283
283
  expect( screen.queryByText( /invalid data/i ) ).not.toBeInTheDocument();
284
284
 
285
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
285
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
286
286
  const ticks = within( chart )
287
287
  .getAllByText( /^-?[\d.,]+$/ )
288
288
  .map( el => el.textContent ?? '' );
@@ -305,7 +305,7 @@ describe( 'LineChart', () => {
305
305
  ],
306
306
  } );
307
307
 
308
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
308
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
309
309
  const ticks = within( chart )
310
310
  .getAllByText( /^-?[\d.,]+$/ )
311
311
  .map( el => el.textContent );
@@ -333,7 +333,7 @@ describe( 'LineChart', () => {
333
333
  ],
334
334
  } );
335
335
 
336
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
336
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
337
337
  const ticks = within( chart )
338
338
  .getAllByText( /^-?[\d.,]+$/ )
339
339
  .map( el => el.textContent );
@@ -353,7 +353,7 @@ describe( 'LineChart', () => {
353
353
  ],
354
354
  } );
355
355
 
356
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
356
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
357
357
  expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ).length ).toBeGreaterThan( 2 );
358
358
  } );
359
359
 
@@ -368,7 +368,7 @@ describe( 'LineChart', () => {
368
368
  options: { axis: { y: { tickValues: [ 0, 0.5, 1 ] } } },
369
369
  } );
370
370
 
371
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
371
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
372
372
  expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ) ).toHaveLength( 3 );
373
373
  } );
374
374
 
@@ -389,7 +389,7 @@ describe( 'LineChart', () => {
389
389
  },
390
390
  } );
391
391
 
392
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
392
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
393
393
  expect( within( chart ).getAllByText( /^\d+%$/ ) ).toHaveLength( 6 );
394
394
  } );
395
395
 
@@ -544,9 +544,9 @@ describe( 'LineChart', () => {
544
544
  ],
545
545
  } );
546
546
 
547
- const grid = screen.getByRole( 'grid', { name: /line chart/i } );
547
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
548
548
  // eslint-disable-next-line testing-library/no-node-access -- visx's stroke is a <path class="visx-line">; the axis and grid lines are plain <line> elements sharing that class.
549
- const linePath = grid.querySelector( 'path.visx-line' );
549
+ const linePath = chart.querySelector( 'path.visx-line' );
550
550
  const gapCount = linePath?.getAttribute( 'd' )?.match( /M/g )?.length;
551
551
  expect( gapCount ).toBe( 2 );
552
552
  } );
@@ -615,7 +615,7 @@ describe( 'LineChart', () => {
615
615
  expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
616
616
  const html = document.body.innerHTML;
617
617
  expect( html.indexOf( 'data-testid="legend-horizontal"' ) ).toBeLessThan(
618
- html.indexOf( 'role="grid"' )
618
+ html.indexOf( 'role="application"' )
619
619
  );
620
620
  } );
621
621
  } );
@@ -805,7 +805,7 @@ describe( 'LineChart', () => {
805
805
  expect( gradient ).toBeInTheDocument();
806
806
 
807
807
  // Verify that the chart container exists
808
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
808
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
809
809
  expect( chart ).toBeInTheDocument();
810
810
  } );
811
811
  } );
@@ -821,7 +821,7 @@ describe( 'LineChart', () => {
821
821
  },
822
822
  } );
823
823
  // The chart should render with the custom axis configuration
824
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
824
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
825
825
  } );
826
826
  } );
827
827
 
@@ -1067,7 +1067,7 @@ describe( 'LineChart', () => {
1067
1067
 
1068
1068
  // Instead of checking styles, verify the chart renders
1069
1069
  expect( screen.getByTestId( 'line-chart' ) ).toBeInTheDocument();
1070
- expect( screen.getByRole( 'grid', { name: /line chart/i } ) ).toBeInTheDocument();
1070
+ expect( screen.getByRole( 'application', { name: /line chart/i } ) ).toBeInTheDocument();
1071
1071
  } );
1072
1072
  } );
1073
1073
 
@@ -1605,17 +1605,26 @@ describe( 'LineChart', () => {
1605
1605
  describe( 'Chart Focus and Accessibility Attributes', () => {
1606
1606
  test( 'chart container has proper accessibility attributes', () => {
1607
1607
  renderWithTheme();
1608
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1608
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1609
1609
 
1610
1610
  expect( chart ).toHaveAttribute( 'tabIndex', '0' );
1611
- expect( chart ).toHaveAttribute( 'role', 'grid' );
1611
+ expect( chart ).toHaveAttribute( 'role', 'application' );
1612
1612
  expect( chart ).toHaveAttribute( 'aria-label', 'Line chart' );
1613
1613
  } );
1614
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
+ expect( screen.queryByLabelText( 'XYChart' ) ).not.toBeInTheDocument();
1622
+ } );
1623
+
1615
1624
  test( 'chart container can receive focus', async () => {
1616
1625
  const user = userEvent.setup();
1617
1626
  renderWithTheme();
1618
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1627
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1619
1628
 
1620
1629
  await user.tab();
1621
1630
  expect( chart ).toHaveFocus();
@@ -1646,7 +1655,7 @@ describe( 'LineChart', () => {
1646
1655
  const onDatumActivate = jest.fn();
1647
1656
  renderWithTheme( { data: [ SERIES_A, SERIES_B ], onDatumActivate } );
1648
1657
 
1649
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
1658
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
1650
1659
  await user.keyboard( '{ArrowRight}{ArrowRight}{Enter}' );
1651
1660
 
1652
1661
  expect( onDatumActivate ).toHaveBeenCalledTimes( 1 );
@@ -1662,7 +1671,7 @@ describe( 'LineChart', () => {
1662
1671
  const onDatumActivate = jest.fn();
1663
1672
  renderWithTheme( { data: [ SERIES_A ], onDatumActivate } );
1664
1673
 
1665
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
1674
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
1666
1675
  await user.keyboard( '{Enter}' );
1667
1676
 
1668
1677
  expect( onDatumActivate ).not.toHaveBeenCalled();
@@ -1696,7 +1705,7 @@ describe( 'LineChart', () => {
1696
1705
  ],
1697
1706
  } );
1698
1707
 
1699
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1708
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1700
1709
  chart.focus();
1701
1710
 
1702
1711
  // Single tab should focus on the first tooltip.
@@ -1741,7 +1750,7 @@ describe( 'LineChart', () => {
1741
1750
  ],
1742
1751
  } );
1743
1752
 
1744
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1753
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1745
1754
  chart.focus();
1746
1755
 
1747
1756
  // Right arrow key should focus on the first tooltip.
@@ -1788,7 +1797,7 @@ describe( 'LineChart', () => {
1788
1797
  ],
1789
1798
  } );
1790
1799
 
1791
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1800
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1792
1801
  chart.focus();
1793
1802
 
1794
1803
  // Chart should be in focus.
@@ -1819,7 +1828,7 @@ describe( 'LineChart', () => {
1819
1828
 
1820
1829
  renderWithTheme( { renderTooltip: customTooltipRenderer } );
1821
1830
 
1822
- const chart = screen.getByRole( 'grid', { name: /line chart/i } );
1831
+ const chart = screen.getByRole( 'application', { name: /line chart/i } );
1823
1832
  chart.focus();
1824
1833
 
1825
1834
  // Click on right arrow key to focus on the first tooltip.
@@ -2049,8 +2058,9 @@ describe( 'LineChart', () => {
2049
2058
 
2050
2059
  const pointer = ( type: string, clientX: number, clientY: number ) => {
2051
2060
  // visx owns the pointer capture rect and does not expose an attribute prop for it.
2061
+ const chart = screen.getByRole( 'application' );
2052
2062
  // eslint-disable-next-line testing-library/no-node-access
2053
- const target = screen.getByRole( 'grid' ).querySelector( 'svg > rect[fill="transparent"]' );
2063
+ const target = chart.querySelector( 'svg > rect[fill="transparent"]' );
2054
2064
  fireEvent( target, new MouseEvent( type, { bubbles: true, clientX, clientY } ) );
2055
2065
  };
2056
2066
 
@@ -2398,7 +2408,7 @@ describe( 'LineChart', () => {
2398
2408
  options: {},
2399
2409
  },
2400
2410
  ];
2401
- // Bare numbers inside the plot (scoped via the chart's grid role, away from visx's
2411
+ // Bare numbers inside the plot (scoped via the chart's application role, away from visx's
2402
2412
  // off-screen text-measurement SVGs) are value-axis ticks: the series labels carry no digits
2403
2413
  // and the empty-state message is text-only.
2404
2414
  const numericTick = /^[\d,]+$/;
@@ -2414,7 +2424,7 @@ describe( 'LineChart', () => {
2414
2424
  data: twoSeries,
2415
2425
  } );
2416
2426
 
2417
- const chart = screen.getByRole( 'grid' );
2427
+ const chart = screen.getByRole( 'application' );
2418
2428
  const ticksBefore = within( chart )
2419
2429
  .getAllByText( numericTick )
2420
2430
  .map( el => el.textContent )
@@ -2441,7 +2451,7 @@ describe( 'LineChart', () => {
2441
2451
  data: twoSeries,
2442
2452
  } );
2443
2453
 
2444
- const chart = screen.getByRole( 'grid' );
2454
+ const chart = screen.getByRole( 'application' );
2445
2455
  const ticksBefore = within( chart )
2446
2456
  .getAllByText( numericTick )
2447
2457
  .map( el => el.textContent )
@@ -2469,7 +2479,7 @@ describe( 'LineChart', () => {
2469
2479
  data: twoSeries,
2470
2480
  } );
2471
2481
 
2472
- const chart = screen.getByRole( 'grid' );
2482
+ const chart = screen.getByRole( 'application' );
2473
2483
  expect( within( chart ).getAllByText( numericTick ).length ).toBeGreaterThan( 0 );
2474
2484
 
2475
2485
  const buttons = screen.getAllByRole( 'button' );
@@ -93,7 +93,7 @@ describe( 'default tooltip at hourly resolution', () => {
93
93
  </GlobalChartsProvider>
94
94
  );
95
95
 
96
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
96
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
97
97
  await user.keyboard( '{ArrowRight}' );
98
98
 
99
99
  expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( '2.8.2026, 09 Uhr' );
@@ -130,7 +130,7 @@ describe( 'default tooltip at hourly resolution', () => {
130
130
  </GlobalChartsProvider>
131
131
  );
132
132
 
133
- screen.getByRole( 'grid', { name: /line chart/i } ).focus();
133
+ screen.getByRole( 'application', { name: /line chart/i } ).focus();
134
134
  await user.keyboard( '{ArrowRight}' );
135
135
 
136
136
  const tooltip = screen.getByTestId( 'chart-tooltip-0' );
@@ -10,7 +10,7 @@ import type {
10
10
  } from '../../types';
11
11
  import type { CrosshairStyle, RenderTooltipParams } from '../../visx/types';
12
12
  import type { GlyphProps } from '@visx/xychart';
13
- import type { ReactNode, SVGProps, FC, CSSProperties } from 'react';
13
+ import type { ReactNode, SVGProps, CSSProperties } from 'react';
14
14
 
15
15
  export type LineChartAnnotationProps = {
16
16
  datum: DataPointDate;
@@ -19,8 +19,8 @@ export type LineChartAnnotationProps = {
19
19
  subjectType?: 'circle' | 'line-vertical' | 'line-horizontal';
20
20
  styles?: AnnotationStyles;
21
21
  testId?: string;
22
- renderLabel?: FC< { title: string; subtitle?: string } >;
23
- renderLabelPopover?: FC< { title: string; subtitle?: string } >;
22
+ renderLabel?: ( props: { title: string; subtitle?: string } ) => ReactNode;
23
+ renderLabelPopover?: ( props: { title: string; subtitle?: string } ) => ReactNode;
24
24
  };
25
25
 
26
26
  export type CurveType = 'smooth' | 'linear' | 'monotone';
@@ -31,6 +31,8 @@ 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;
34
36
  /**
35
37
  * Legend configuration. Supports `collapseGroups` on top of the shared options.
36
38
  */