@automattic/charts 2.0.0 → 3.0.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 (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/TOKENS.md +107 -48
  3. package/dist/index.cjs +584 -209
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +97 -43
  6. package/dist/index.d.cts +46 -41
  7. package/dist/index.d.ts +46 -41
  8. package/dist/index.js +582 -210
  9. package/dist/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/charts/area-chart/area-chart.module.scss +8 -3
  12. package/src/charts/area-chart/area-chart.tsx +3 -5
  13. package/src/charts/bar-chart/bar-chart.module.scss +14 -6
  14. package/src/charts/bar-chart/bar-chart.tsx +24 -15
  15. package/src/charts/bar-chart/private/use-bar-chart-options.ts +197 -30
  16. package/src/charts/bar-chart/test/bar-chart.test.tsx +685 -1
  17. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +17 -12
  18. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
  19. package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
  20. package/src/charts/geo-chart/geo-chart.tsx +12 -4
  21. package/src/charts/heatmap-chart/heatmap-chart.module.scss +13 -9
  22. package/src/charts/heatmap-chart/heatmap-chart.tsx +12 -5
  23. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
  24. package/src/charts/leaderboard-chart/types.ts +0 -2
  25. package/src/charts/line-chart/line-chart.module.scss +10 -5
  26. package/src/charts/line-chart/line-chart.tsx +3 -5
  27. package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
  28. package/src/charts/pie-chart/pie-chart.tsx +4 -1
  29. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -1
  30. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  31. package/src/charts/private/test/time-axis.test.ts +247 -2
  32. package/src/charts/private/time-axis.ts +252 -63
  33. package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
  34. package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
  35. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  36. package/src/components/legend/private/base-legend.tsx +3 -1
  37. package/src/components/legend/test/legend.test.tsx +2 -1
  38. package/src/components/tooltip/accessible-tooltip.tsx +4 -2
  39. package/src/components/tooltip/base-tooltip.tsx +8 -1
  40. package/src/components/trend-indicator/test/trend-indicator.test.tsx +22 -1
  41. package/src/components/trend-indicator/trend-indicator.module.scss +3 -6
  42. package/src/components/trend-indicator/trend-indicator.tsx +3 -0
  43. package/src/hooks/test/use-chart-margin.test.tsx +60 -0
  44. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
  45. package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +89 -0
  46. package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
  47. package/src/hooks/use-chart-margin.tsx +3 -4
  48. package/src/hooks/use-xychart-theme.ts +19 -11
  49. package/src/index.ts +9 -2
  50. package/src/providers/chart-context/global-charts-provider.tsx +35 -12
  51. package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
  52. package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
  53. package/src/providers/chart-context/test/chart-context.test.tsx +0 -40
  54. package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
  55. package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
  56. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
  57. package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
  58. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
  59. package/src/providers/chart-context/themes.ts +18 -27
  60. package/src/providers/chart-context/types.ts +0 -3
  61. package/src/providers/chart-scope/chart-scope-context.ts +8 -0
  62. package/src/providers/chart-scope/index.ts +3 -0
  63. package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
  64. package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
  65. package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
  66. package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
  67. package/src/providers/index.ts +1 -0
  68. package/src/styles/chart-scope-class.ts +6 -0
  69. package/src/styles/chart-scope.scss +83 -0
  70. package/src/styles/test/chart-scope.test.ts +165 -0
  71. package/src/types.ts +6 -40
  72. package/src/utils/index.ts +1 -1
  73. package/src/utils/resolve-css-var.ts +69 -28
  74. package/src/utils/test/create-css-variable-resolver.test.ts +83 -0
  75. package/src/charts/private/grid-control/grid-control.module.scss +0 -8
  76. package/src/charts/private/grid-control/grid-control.tsx +0 -26
  77. package/src/charts/private/grid-control/index.ts +0 -1
  78. package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
package/src/types.ts CHANGED
@@ -482,12 +482,12 @@ export type AxisOptions = {
482
482
  tickClassName?: string;
483
483
  tickFormat?: TickFormatter< ScaleInput< AxisScale > >;
484
484
  /**
485
- * Bucket resolution of the data on a time x-axis. When set, the automatic
486
- * tick formatter derives tick formats from it directly instead of
487
- * inferring the resolution from point spacing. The overall time span still
488
- * constrains the choice — e.g. hourly buckets spanning more than a week
489
- * get date ticks, since hour ticks would be unreadable at that span.
490
- * Ignored when `tickFormat` is set.
485
+ * Bucket resolution of the data, set on whichever axis carries the dates.
486
+ * When set, the automatic tick formatter derives tick formats from it
487
+ * directly instead of inferring the resolution from point spacing. For
488
+ * daily-or-finer buckets the overall time span still constrains the choice —
489
+ * e.g. hourly buckets spanning more than a week get date ticks, since hour
490
+ * ticks would be unreadable at that span. Ignored when `tickFormat` is set.
491
491
  */
492
492
  tickResolution?: TickResolution;
493
493
  /**
@@ -736,40 +736,6 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > =
736
736
  };
737
737
  };
738
738
 
739
- /**
740
- * Properties for grid components
741
- */
742
- export type GridProps = {
743
- /**
744
- * Width of the grid in pixels
745
- */
746
- width: number;
747
- /**
748
- * Height of the grid in pixels
749
- */
750
- height: number;
751
- /**
752
- * Grid visibility. x is default.
753
- */
754
- gridVisibility?: 'x' | 'y' | 'xy' | 'none';
755
- /**
756
- * X-axis scale for the grid
757
- * TODO: Fix any type after resolving visx scale type issues
758
- */
759
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
760
- xScale: any;
761
- /**
762
- * Y-axis scale for the grid
763
- * TODO: Fix any type after resolving visx scale type issues
764
- */
765
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
766
- yScale: any;
767
- /**
768
- * Top offset for the grid
769
- */
770
- top?: number;
771
- };
772
-
773
739
  /**
774
740
  * Local type definitions for Popover API attributes and events
775
741
  * These are used to avoid extending React module types while still getting type safety
@@ -30,7 +30,7 @@ export { mergeThemes } from './merge-themes';
30
30
  export * from './color-utils';
31
31
 
32
32
  // CSS utilities
33
- export { resolveCssVariable } from './resolve-css-var';
33
+ export { resolveCssVariable, createCssVariableResolver } from './resolve-css-var';
34
34
 
35
35
  // Font sizing utilities
36
36
  export { resolveFontSize } from './resolve-font-size';
@@ -15,34 +15,57 @@ const CSS_VAR_NAME_PATTERN = /^--[\w-]+$/;
15
15
  * @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)
16
16
  * @return The resolved value, fallback value, or null if unresolvable
17
17
  */
18
- export const resolveCssVariable = (
19
- value: string,
18
+ export const resolveCssVariable = ( value: string, element?: HTMLElement | null ): string | null =>
19
+ createCssVariableResolver( element )( value );
20
+
21
+ /**
22
+ * Builds a resolver that reads every value from one `getComputedStyle` snapshot.
23
+ *
24
+ * `getComputedStyle` is the expensive half of resolving a token: each call can force the browser to flush pending style, and a chart theme resolves five roles at once. Taking the snapshot once per build turns that into a single query. The reads that share a snapshot happen synchronously inside one memo body, so no style change can land between them — the resolver is single-pass by design and is not meant to be held across renders.
25
+ *
26
+ * @param element - The element to resolve against, or null/undefined for the document root.
27
+ * @return A resolver that resolves each value exactly as `resolveCssVariable` does.
28
+ */
29
+ export function createCssVariableResolver(
20
30
  element?: HTMLElement | null
21
- ): string | null => {
22
- if ( ! value ) {
23
- return null;
24
- }
31
+ ): ( value: string ) => string | null {
32
+ let styles: CSSStyleDeclaration | null = null;
25
33
 
26
- // Check if it's a var() expression: var(--name) or var(--name, fallback)
27
- // Parse manually to avoid regex backtracking vulnerabilities
28
- if ( value.startsWith( 'var(' ) && value.endsWith( ')' ) ) {
29
- const parsed = parseVarExpression( value );
34
+ // Deferred so a resolver for a theme that turns out to hold only literals costs nothing. Only a successful read is kept: caching the null would leave a resolver built before its element is in the document dead for the rest of its life.
35
+ const getStyles = () => {
36
+ if ( ! styles ) {
37
+ styles = computedStyleFor( element );
38
+ }
30
39
 
31
- if ( parsed ) {
32
- const resolved = resolveVariableName( parsed.varName, element );
40
+ return styles;
41
+ };
33
42
 
34
- return resolved || parsed.fallback;
43
+ return ( value: string ): string | null => {
44
+ if ( ! value ) {
45
+ return null;
35
46
  }
36
- }
37
47
 
38
- // Check if it's a plain variable name (starts with --)
39
- if ( value.startsWith( '--' ) ) {
40
- return resolveVariableName( value, element );
41
- }
48
+ // Check if it's a var() expression: var(--name) or var(--name, fallback)
49
+ // Parse manually to avoid regex backtracking vulnerabilities
50
+ if ( value.startsWith( 'var(' ) && value.endsWith( ')' ) ) {
51
+ const parsed = parseVarExpression( value );
52
+
53
+ if ( parsed ) {
54
+ const resolved = readCustomProperty( parsed.varName, getStyles() );
42
55
 
43
- // Return regular values as-is (e.g., '#ffffff', 'red')
44
- return value;
45
- };
56
+ return resolved || parsed.fallback;
57
+ }
58
+ }
59
+
60
+ // Check if it's a plain variable name (starts with --)
61
+ if ( value.startsWith( '--' ) ) {
62
+ return readCustomProperty( value, getStyles() );
63
+ }
64
+
65
+ // Return regular values as-is (e.g., '#ffffff', 'red')
66
+ return value;
67
+ };
68
+ }
46
69
 
47
70
  /**
48
71
  * Parses a var() expression into its variable name and optional fallback.
@@ -87,24 +110,42 @@ function parseVarExpression( expr: string ): { varName: string; fallback: string
87
110
  }
88
111
 
89
112
  /**
90
- * Resolves a plain CSS variable name to its computed value.
113
+ * Takes one computed-style snapshot, or null where there is nothing to read (SSR, or a detached element).
114
+ *
115
+ * @param element - The element to read from, or null/undefined for the document root.
116
+ * @return The computed styles, or null.
117
+ */
118
+ function computedStyleFor( element?: HTMLElement | null ): CSSStyleDeclaration | null {
119
+ if ( typeof window === 'undefined' || typeof document === 'undefined' ) {
120
+ return null;
121
+ }
122
+
123
+ try {
124
+ return getComputedStyle( element || document.documentElement );
125
+ } catch {
126
+ // getComputedStyle throws on a detached element.
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Reads one custom property from a computed-style snapshot.
91
133
  *
92
134
  * @param varName - A CSS variable name like '--my-color'
93
- * @param element - Optional DOM element to resolve from
135
+ * @param styles - The snapshot to read from, or null
94
136
  * @return The computed value or null
95
137
  */
96
- function resolveVariableName( varName: string, element?: HTMLElement | null ): string | null {
97
- if ( typeof window === 'undefined' || typeof document === 'undefined' ) {
138
+ function readCustomProperty( varName: string, styles: CSSStyleDeclaration | null ): string | null {
139
+ if ( ! styles ) {
98
140
  return null;
99
141
  }
100
142
 
101
143
  try {
102
- const targetElement = element || document.documentElement;
103
- const computedValue = getComputedStyle( targetElement ).getPropertyValue( varName ).trim();
144
+ const computedValue = styles.getPropertyValue( varName ).trim();
104
145
 
105
146
  return computedValue || null;
106
147
  } catch {
107
- // Return null if getComputedStyle throws (e.g., detached element)
148
+ // Belt and braces — no engine is known to throw here. Kept because a caller may hand in a stand-in for CSSStyleDeclaration; `resolve-css-var.test.ts` covers exactly that.
108
149
  return null;
109
150
  }
110
151
  }
@@ -0,0 +1,83 @@
1
+ import { createCssVariableResolver } from '../resolve-css-var';
2
+
3
+ describe( 'createCssVariableResolver', () => {
4
+ let originalGetComputedStyle: typeof window.getComputedStyle;
5
+
6
+ beforeEach( () => {
7
+ originalGetComputedStyle = window.getComputedStyle;
8
+ } );
9
+
10
+ afterEach( () => {
11
+ window.getComputedStyle = originalGetComputedStyle;
12
+ } );
13
+
14
+ const mockStyles = ( values: Record< string, string > ) => {
15
+ const spy = jest.fn( () => ( {
16
+ getPropertyValue: ( prop: string ) => values[ prop ] ?? '',
17
+ } ) );
18
+ window.getComputedStyle = spy as unknown as typeof window.getComputedStyle;
19
+
20
+ return spy;
21
+ };
22
+
23
+ it( 'reads many values from a single snapshot', () => {
24
+ const spy = mockStyles( { '--a': '#111', '--b': '#222' } );
25
+ const resolve = createCssVariableResolver();
26
+
27
+ expect( resolve( '--a' ) ).toBe( '#111' );
28
+ expect( resolve( 'var(--b)' ) ).toBe( '#222' );
29
+ expect( spy ).toHaveBeenCalledTimes( 1 );
30
+ } );
31
+
32
+ it( 'takes no snapshot at all for values that need no lookup', () => {
33
+ const spy = mockStyles( {} );
34
+ const resolve = createCssVariableResolver();
35
+
36
+ expect( resolve( '#ffffff' ) ).toBe( '#ffffff' );
37
+ expect( resolve( '' ) ).toBeNull();
38
+ expect( spy ).not.toHaveBeenCalled();
39
+ } );
40
+
41
+ it( 'resolves against the element it was built for', () => {
42
+ const element = document.createElement( 'div' );
43
+ const spy = jest.fn( ( target: Element ) => ( {
44
+ getPropertyValue: () => ( target === element ? '#c029dc' : '#000000' ),
45
+ } ) );
46
+ window.getComputedStyle = spy as unknown as typeof window.getComputedStyle;
47
+
48
+ expect( createCssVariableResolver( element )( '--accent' ) ).toBe( '#c029dc' );
49
+ expect( spy ).toHaveBeenCalledWith( element );
50
+ } );
51
+
52
+ it( 'falls back to the var() fallback when the property is unset', () => {
53
+ mockStyles( {} );
54
+
55
+ expect( createCssVariableResolver()( 'var(--missing, #dbdbdb)' ) ).toBe( '#dbdbdb' );
56
+ } );
57
+
58
+ // A failed read is not cached: a resolver built before its element is in the document would otherwise stay dead for the rest of its life, which is the shape CHARTS-255 introduces.
59
+ it( 'retries after a snapshot that could not be taken', () => {
60
+ const failing = jest.fn( () => {
61
+ throw new Error( 'not attached' );
62
+ } );
63
+ window.getComputedStyle = failing as unknown as typeof window.getComputedStyle;
64
+
65
+ const resolve = createCssVariableResolver();
66
+ expect( resolve( '--a' ) ).toBeNull();
67
+
68
+ mockStyles( { '--a': '#111' } );
69
+ expect( resolve( '--a' ) ).toBe( '#111' );
70
+ } );
71
+
72
+ it( 'matches resolveCssVariable for the values it is given', () => {
73
+ mockStyles( { '--a': '#111' } );
74
+ const resolve = createCssVariableResolver();
75
+
76
+ expect( [
77
+ resolve( '--a' ),
78
+ resolve( 'var(--a)' ),
79
+ resolve( 'var(--z, red)' ),
80
+ resolve( 'red' ),
81
+ ] ).toEqual( [ '#111', '#111', 'red', 'red' ] );
82
+ } );
83
+ } );
@@ -1,8 +0,0 @@
1
- .grid-control {
2
-
3
- :global(.visx-line) {
4
- stroke: var(--a8c-charts-color-grid, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
5
- stroke-width: 1px;
6
- shape-rendering: crispEdges;
7
- }
8
- }
@@ -1,26 +0,0 @@
1
- import { GridRows, GridColumns } from '@visx/grid';
2
- import styles from './grid-control.module.scss';
3
- import type { GridProps } from '../../../types';
4
- import type { FC } from 'react';
5
-
6
- const GridControl: FC< GridProps > = ( {
7
- width,
8
- height,
9
- xScale,
10
- yScale,
11
- gridVisibility = 'x',
12
- top = 0,
13
- } ) => {
14
- return (
15
- <g transform={ `translate(0, ${ top })` } className={ styles[ 'grid-control' ] }>
16
- { gridVisibility.includes( 'x' ) && (
17
- <GridRows scale={ xScale } width={ width } data-testid="x-grid" />
18
- ) }
19
- { gridVisibility.includes( 'y' ) && (
20
- <GridColumns scale={ yScale } height={ height } data-testid="y-grid" />
21
- ) }
22
- </g>
23
- );
24
- };
25
-
26
- export default GridControl;
@@ -1 +0,0 @@
1
- export { default as GridControl } from './grid-control';
@@ -1,56 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import { scaleBand, scaleLinear } from '@visx/scale';
3
- import GridControl from '../grid-control';
4
-
5
- describe( 'GridControl', () => {
6
- const defaultProps = {
7
- width: 200,
8
- height: 200,
9
- xScale: scaleBand( { domain: [ 'A', 'B', 'C' ], range: [ 0, 100 ] } ),
10
- yScale: scaleLinear( { domain: [ 0, 100 ], range: [ 100, 0 ] } ),
11
- };
12
-
13
- test( 'renders x-axis grid lines', () => {
14
- render(
15
- <svg>
16
- <GridControl { ...defaultProps } gridVisibility="x" />
17
- </svg>
18
- );
19
- const xGridLines = screen.getAllByTestId( 'x-grid' );
20
- expect( xGridLines.length ).toBeGreaterThan( 0 );
21
- expect( screen.queryAllByTestId( 'y-grid' ) ).toHaveLength( 0 );
22
- } );
23
-
24
- test( 'renders y-axis grid lines', () => {
25
- render(
26
- <svg>
27
- <GridControl { ...defaultProps } gridVisibility="y" />
28
- </svg>
29
- );
30
- expect( screen.queryAllByTestId( 'x-grid' ) ).toHaveLength( 0 );
31
- const yGridLines = screen.getAllByTestId( 'y-grid' );
32
- expect( yGridLines.length ).toBeGreaterThan( 0 );
33
- } );
34
-
35
- test( 'renders both axes grid lines', () => {
36
- render(
37
- <svg>
38
- <GridControl { ...defaultProps } gridVisibility="xy" />
39
- </svg>
40
- );
41
- const xGridLines = screen.getAllByTestId( 'x-grid' );
42
- const yGridLines = screen.getAllByTestId( 'y-grid' );
43
- expect( xGridLines.length ).toBeGreaterThan( 0 );
44
- expect( yGridLines.length ).toBeGreaterThan( 0 );
45
- } );
46
-
47
- test( 'renders no grid lines when visibility is none', () => {
48
- render(
49
- <svg>
50
- <GridControl { ...defaultProps } gridVisibility="none" />
51
- </svg>
52
- );
53
- expect( screen.queryAllByTestId( 'x-grid' ) ).toHaveLength( 0 );
54
- expect( screen.queryAllByTestId( 'y-grid' ) ).toHaveLength( 0 );
55
- } );
56
- } );