@automattic/charts 4.1.0 → 4.2.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 (80) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/TOKENS.md +7 -5
  3. package/dist/index.cjs +1115 -471
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +39 -1
  6. package/dist/index.d.cts +116 -16
  7. package/dist/index.d.ts +116 -16
  8. package/dist/index.js +1115 -473
  9. package/dist/index.js.map +1 -1
  10. package/package.json +11 -11
  11. package/src/charts/area-chart/area-chart.tsx +2 -1
  12. package/src/charts/area-chart/private/overlays.tsx +1 -2
  13. package/src/charts/area-chart/test/area-chart.test.tsx +31 -0
  14. package/src/charts/bar-chart/bar-chart.tsx +67 -17
  15. package/src/charts/bar-chart/index.ts +1 -1
  16. package/src/charts/bar-chart/private/band-highlight.tsx +85 -0
  17. package/src/charts/bar-chart/private/band-scale.ts +13 -0
  18. package/src/charts/bar-chart/private/band-tooltip.tsx +138 -0
  19. package/src/charts/bar-chart/private/comparison-bars.tsx +5 -10
  20. package/src/charts/bar-chart/private/index.ts +2 -0
  21. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +109 -0
  22. package/src/charts/bar-chart/private/test/band-tooltip.test.ts +26 -0
  23. package/src/charts/bar-chart/private/truncated-tick-component.tsx +1 -1
  24. package/src/charts/bar-chart/private/use-bar-chart-options.ts +1 -1
  25. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +11 -23
  26. package/src/charts/bar-chart/test/bar-chart.test.tsx +565 -1
  27. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -3
  28. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  29. package/src/charts/conversion-funnel-chart/types.ts +4 -2
  30. package/src/charts/geo-chart/types.ts +4 -2
  31. package/src/charts/heatmap-chart/{private/build-calendar-data.ts → build-calendar-data.ts} +15 -45
  32. package/src/charts/heatmap-chart/build-month-calendar-data.ts +124 -0
  33. package/src/charts/heatmap-chart/heatmap-chart.module.scss +32 -0
  34. package/src/charts/heatmap-chart/heatmap-chart.tsx +191 -114
  35. package/src/charts/heatmap-chart/index.ts +15 -3
  36. package/src/charts/heatmap-chart/private/civil-day.ts +26 -0
  37. package/src/charts/heatmap-chart/private/column-groups.ts +71 -0
  38. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +19 -3
  39. package/src/charts/heatmap-chart/private/index.ts +2 -2
  40. package/src/charts/heatmap-chart/private/keyboard-navigation.ts +201 -0
  41. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +37 -1
  42. package/src/charts/heatmap-chart/test/build-month-calendar-data.test.ts +194 -0
  43. package/src/charts/heatmap-chart/test/column-groups.test.ts +83 -0
  44. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +428 -2
  45. package/src/charts/heatmap-chart/test/keyboard-navigation.test.ts +25 -0
  46. package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +1 -1
  47. package/src/charts/heatmap-chart/test/use-month-calendar-heatmap-data.test.tsx +90 -0
  48. package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +1 -1
  49. package/src/charts/heatmap-chart/test/zoned-month-calendar-data.test.ts +23 -0
  50. package/src/charts/heatmap-chart/types.ts +88 -3
  51. package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +2 -2
  52. package/src/charts/heatmap-chart/use-month-calendar-heatmap-data.ts +34 -0
  53. package/src/charts/leaderboard-chart/types.ts +13 -14
  54. package/src/charts/line-chart/line-chart.tsx +3 -2
  55. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  56. package/src/charts/line-chart/test/line-chart.test.tsx +31 -0
  57. package/src/charts/line-chart/types.ts +2 -2
  58. package/src/charts/private/time-axis-options.ts +1 -1
  59. package/src/charts/private/with-responsive/with-responsive.tsx +2 -2
  60. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +1 -3
  61. package/src/charts/private/x-zoom/x-zoom.tsx +1 -2
  62. package/src/components/legend/private/base-legend.tsx +1 -1
  63. package/src/components/tooltip/accessible-tooltip.tsx +146 -37
  64. package/src/components/tooltip/base-tooltip.module.scss +6 -1
  65. package/src/components/tooltip/private/bounded-tooltip.tsx +12 -6
  66. package/src/components/tooltip/test/accessible-tooltip.test.tsx +160 -0
  67. package/src/components/tooltip/test/bounded-tooltip.test.tsx +17 -0
  68. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +29 -3
  69. package/src/components/tooltip/xy-chart-tooltip.tsx +11 -11
  70. package/src/hooks/test/use-element-size.test.tsx +13 -13
  71. package/src/hooks/use-xychart-theme.ts +1 -1
  72. package/src/index.ts +7 -1
  73. package/src/providers/chart-context/themes.ts +1 -0
  74. package/src/styles/chart-scope.scss +11 -3
  75. package/src/styles/test/chart-scope.test.ts +36 -2
  76. package/src/types.ts +6 -13
  77. package/src/utils/format-metric-value.ts +4 -4
  78. package/src/utils/get-edge-tick-widths.ts +1 -1
  79. package/src/utils/get-styles.ts +1 -1
  80. package/src/visx/types.ts +7 -10
@@ -0,0 +1,109 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { scaleBand } from '@visx/scale';
4
+ import { DataContext, TooltipContext } from '@visx/xychart';
5
+ import { useState } from 'react';
6
+ import { GlobalChartsProvider } from '../../../../providers';
7
+ import { CATALOG_POINTERS } from '../../../../providers/chart-context/private/catalog-pointers';
8
+ import { BarChartUnresponsive } from '../../bar-chart';
9
+ import { BandHighlight } from '../band-highlight';
10
+ import type { BandHighlightSelection } from '../../bar-chart';
11
+
12
+ const xScale = scaleBand( { domain: [ 'Jan', 'Feb' ], range: [ 10, 210 ] } );
13
+ const dataContext = {
14
+ xScale,
15
+ dataRegistry: { get: () => ( { xAccessor: datum => datum.label } ) },
16
+ margin: { top: 20, left: 10 },
17
+ innerWidth: 200,
18
+ innerHeight: 150,
19
+ };
20
+
21
+ const Fixture = ( { label = 'Jan', onChange } ) => {
22
+ return (
23
+ <DataContext.Provider value={ dataContext as never }>
24
+ <TooltipContext.Provider
25
+ value={
26
+ {
27
+ tooltipOpen: true,
28
+ tooltipData: { nearestDatum: { key: 'scores', datum: { label, value: 50 } } },
29
+ } as never
30
+ }
31
+ >
32
+ <svg>
33
+ <BandHighlight visible horizontal={ false } onChange={ onChange } />
34
+ </svg>
35
+ </TooltipContext.Provider>
36
+ </DataContext.Provider>
37
+ );
38
+ };
39
+
40
+ describe( 'BandHighlight', () => {
41
+ test( 'uses the catalog surface fill', () => {
42
+ render( <Fixture onChange={ undefined } /> );
43
+ expect( screen.getByTestId( 'bar-chart-band-highlight' ) ).toHaveAttribute(
44
+ 'fill',
45
+ CATALOG_POINTERS.surfaceSecondary
46
+ );
47
+ } );
48
+
49
+ test( 'settles inline derived-state callbacks and notifies the latest callback on selection changes', () => {
50
+ const notify = jest.fn();
51
+ const DerivedState = ( { label } ) => {
52
+ const [ band, setBand ] = useState< { x: number } | null >( null );
53
+ return (
54
+ <>
55
+ <output>{ band?.x }</output>
56
+ <Fixture
57
+ label={ label }
58
+ // eslint-disable-next-line react/jsx-no-bind -- An inline callback reproduces parent state updates changing its identity.
59
+ onChange={ ( selection: BandHighlightSelection | null ) => {
60
+ notify( label );
61
+ if ( notify.mock.calls.length > 4 ) {
62
+ throw new Error( 'Highlight notifications did not settle' );
63
+ }
64
+ setBand( selection && { x: selection.x } );
65
+ } }
66
+ />
67
+ </>
68
+ );
69
+ };
70
+ const { rerender } = render( <DerivedState label="Jan" /> );
71
+ expect( screen.getByRole( 'status' ) ).toHaveTextContent( '10' );
72
+ expect( notify ).toHaveBeenCalledTimes( 1 );
73
+ rerender( <DerivedState label="Feb" /> );
74
+ expect( screen.getByRole( 'status' ) ).toHaveTextContent( '110' );
75
+ expect( notify ).toHaveBeenCalledTimes( 2 );
76
+ expect( notify ).toHaveBeenLastCalledWith( 'Feb' );
77
+ } );
78
+
79
+ test.each( [ { withTooltips: false }, { data: [] } ] )(
80
+ 'clears the selected band when the highlight unmounts with %j',
81
+ async props => {
82
+ const user = userEvent.setup();
83
+ const onChange = jest.fn();
84
+ const chartProps = {
85
+ width: 500,
86
+ height: 300,
87
+ withTooltips: true,
88
+ onBandHighlightChange: onChange,
89
+ data: [ { label: 'Scores', data: [ { label: 'Jan', value: 50 } ] } ],
90
+ };
91
+ const { rerender } = render(
92
+ <GlobalChartsProvider>
93
+ <BarChartUnresponsive { ...chartProps } />
94
+ </GlobalChartsProvider>
95
+ );
96
+ await user.tab();
97
+ await user.keyboard( '{ArrowRight}' );
98
+ expect( onChange ).toHaveBeenLastCalledWith(
99
+ expect.objectContaining( { datum: expect.objectContaining( { label: 'Jan' } ) } )
100
+ );
101
+ rerender(
102
+ <GlobalChartsProvider>
103
+ <BarChartUnresponsive { ...chartProps } { ...props } />
104
+ </GlobalChartsProvider>
105
+ );
106
+ expect( onChange ).toHaveBeenLastCalledWith( null );
107
+ }
108
+ );
109
+ } );
@@ -0,0 +1,26 @@
1
+ import { scaleBand } from '@visx/scale';
2
+ import { nearestBandIndex } from '../band-tooltip';
3
+
4
+ describe( 'nearestBandIndex', () => {
5
+ test.each( [
6
+ [ 25, 688 ],
7
+ [ 688, 25 ],
8
+ ] )( 'matches every painted center with outer padding in range %s to %s', ( start, end ) => {
9
+ const data = Array.from( { length: 30 }, ( _, index ) => String( index ) );
10
+ const scale = scaleBand( {
11
+ domain: data,
12
+ range: [ start, end ],
13
+ paddingInner: 0.024,
14
+ paddingOuter: 0.7,
15
+ } );
16
+ data.forEach( ( datum, index ) => {
17
+ expect(
18
+ nearestBandIndex( data, value => value, scale, scale( datum )! + scale.bandwidth() / 2 )
19
+ ).toBe( index );
20
+ } );
21
+ } );
22
+ test( 'ignores data outside the band domain', () => {
23
+ const scale = scaleBand( { domain: [ 'recorded' ], range: [ 0, 100 ] } );
24
+ expect( nearestBandIndex( [ 'missing' ], value => value, scale, 50 ) ).toBe( -1 );
25
+ } );
26
+ } );
@@ -11,7 +11,7 @@ import type { FC, CSSProperties } from 'react';
11
11
  * @return The bandwidth of the scale
12
12
  */
13
13
  const getScaleBandwidth = < Scale extends AxisScale >( scale?: Scale ) => {
14
- return scale && 'bandwidth' in scale ? scale.bandwidth() ?? 0 : 0;
14
+ return scale && 'bandwidth' in scale ? ( scale.bandwidth() ?? 0 ) : 0;
15
15
  };
16
16
  interface TruncatedTickComponentProps extends TickRendererProps {
17
17
  /** Which axis this tick belongs to */
@@ -330,7 +330,7 @@ export function useBarChartOptions(
330
330
  timeAxis.domain,
331
331
  timeAxis.tickFormatter,
332
332
  dateAxisOptions.numTicks ?? DEFAULT_NUM_TICKS
333
- )
333
+ )
334
334
  : null;
335
335
  const dateAxisTickValues = bandTickValues ? { tickValues: bandTickValues } : {};
336
336
 
@@ -1,25 +1,13 @@
1
1
  import { render } from '@testing-library/react';
2
2
  import { GlobalChartsProvider } from '../../../providers';
3
3
  import BarChart from '../bar-chart';
4
- import type { ReactNode } from 'react';
5
-
6
- // jsdom gives every element a zero-sized box, so visx can never resolve a
7
- // pointer to a datum here. Standing `XYChart` in for a prop recorder keeps this
8
- // test on the part jsdom can answer: that the handlers reach the chart at all.
9
- // Whether a bar then reports the datum under the pointer is a browser question.
10
- const mockXYChart = jest.fn();
11
- jest.mock( '@visx/xychart', () => {
12
- const actual = jest.requireActual( '@visx/xychart' );
13
-
14
- return {
15
- __esModule: true,
16
- ...actual,
17
- XYChart: ( props: { children?: ReactNode } ) => {
18
- mockXYChart( props );
19
- return <svg />;
20
- },
21
- };
22
- } );
4
+ const mockBandTooltip = jest.fn();
5
+ jest.mock( '../private/band-tooltip', () => ( {
6
+ BandTooltip: props => {
7
+ mockBandTooltip( props );
8
+ return null;
9
+ },
10
+ } ) );
23
11
 
24
12
  const mockRefCallback = jest.fn();
25
13
  jest.mock( '../../../hooks/use-element-size', () => ( {
@@ -38,7 +26,7 @@ const DATA = [
38
26
  ];
39
27
 
40
28
  /**
41
- * Render the chart with the recorder standing in for `XYChart`.
29
+ * Render the chart with the recorder standing in for band pointer handling.
42
30
  *
43
31
  * @param props - Props layered onto the defaults.
44
32
  * @return The render result.
@@ -52,15 +40,15 @@ function renderBarChart( props = {} ) {
52
40
  }
53
41
 
54
42
  describe( 'BarChart pointer events', () => {
55
- beforeEach( () => mockXYChart.mockClear() );
43
+ beforeEach( () => mockBandTooltip.mockClear() );
56
44
 
57
- test( 'hands the pointer handlers to the chart, so a bar can be clicked', () => {
45
+ test( 'routes pointer handlers through band correction without requiring tooltips', () => {
58
46
  const onPointerDown = jest.fn();
59
47
  const onPointerUp = jest.fn();
60
48
 
61
49
  renderBarChart( { onPointerDown, onPointerUp } );
62
50
 
63
- const props = mockXYChart.mock.calls.at( -1 )[ 0 ];
51
+ const props = mockBandTooltip.mock.calls.at( -1 )[ 0 ];
64
52
  expect( props.onPointerDown ).toBe( onPointerDown );
65
53
  expect( props.onPointerUp ).toBe( onPointerUp );
66
54
  } );