@automattic/charts 3.1.1 → 4.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 (116) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/TOKENS.md +80 -46
  3. package/dist/index.cjs +1224 -662
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +72 -23
  6. package/dist/index.d.cts +230 -162
  7. package/dist/index.d.ts +230 -162
  8. package/dist/index.js +1224 -663
  9. package/dist/index.js.map +1 -1
  10. package/package.json +10 -10
  11. package/src/charts/area-chart/area-chart.tsx +39 -25
  12. package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
  13. package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
  14. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
  15. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
  16. package/src/charts/area-chart/types.ts +4 -7
  17. package/src/charts/bar-chart/bar-chart.tsx +38 -11
  18. package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
  20. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +67 -0
  21. package/src/charts/bar-chart/test/bar-chart.test.tsx +79 -0
  22. package/src/charts/bar-list-chart/bar-list-chart.tsx +45 -2
  23. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -0
  24. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
  25. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
  26. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
  27. package/src/charts/geo-chart/geo-chart.module.scss +1 -0
  28. package/src/charts/geo-chart/geo-chart.tsx +5 -10
  29. package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
  30. package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
  31. package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
  32. package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
  33. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
  34. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
  35. package/src/charts/heatmap-chart/types.ts +2 -2
  36. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  37. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
  38. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
  39. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
  40. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
  41. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
  42. package/src/charts/line-chart/line-chart.module.scss +10 -18
  43. package/src/charts/line-chart/line-chart.tsx +109 -25
  44. package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
  45. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
  46. package/src/charts/line-chart/test/line-chart.test.tsx +74 -3
  47. package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
  48. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
  49. package/src/charts/line-chart/types.ts +4 -1
  50. package/src/charts/pie-chart/pie-chart.module.scss +13 -1
  51. package/src/charts/pie-chart/pie-chart.tsx +30 -40
  52. package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
  53. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
  54. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
  55. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
  56. package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
  57. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  58. package/src/charts/private/test/axis-formatting.test.tsx +85 -0
  59. package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
  60. package/src/charts/private/test/time-axis-options.test.ts +267 -0
  61. package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
  62. package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
  63. package/src/charts/private/test/time-axis.test.ts +9 -69
  64. package/src/charts/private/time-axis-options.ts +106 -0
  65. package/src/charts/private/time-axis.ts +318 -136
  66. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  67. package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
  68. package/src/charts/sparkline/sparkline.tsx +9 -3
  69. package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
  70. package/src/charts/sparkline/types.ts +2 -2
  71. package/src/components/legend/private/base-legend.module.scss +9 -5
  72. package/src/components/tooltip/accessible-tooltip.tsx +38 -10
  73. package/src/components/tooltip/base-tooltip.module.scss +3 -3
  74. package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
  75. package/src/components/tooltip/test/accessible-tooltip.test.tsx +67 -0
  76. package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
  77. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
  78. package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
  79. package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
  80. package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +17 -10
  81. package/src/hooks/test/use-xychart-theme.test.tsx +63 -5
  82. package/src/hooks/use-xychart-theme.ts +33 -23
  83. package/src/index.ts +4 -0
  84. package/src/providers/chart-context/global-charts-provider.tsx +64 -54
  85. package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
  86. package/src/providers/chart-context/index.ts +1 -0
  87. package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
  88. package/src/providers/chart-context/private/series-palette.ts +35 -0
  89. package/src/providers/chart-context/test/catalog-pointers.test.ts +61 -0
  90. package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
  91. package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -18
  92. package/src/providers/chart-context/themes.ts +7 -47
  93. package/src/providers/chart-context/types.ts +3 -0
  94. package/src/styles/chart-scope.scss +63 -58
  95. package/src/styles/test/chart-scope.test.ts +25 -40
  96. package/src/test-utils/series-fixtures.ts +31 -0
  97. package/src/types.ts +103 -73
  98. package/src/utils/bucket-info.ts +87 -0
  99. package/src/utils/color-utils.ts +0 -27
  100. package/src/utils/date-formatting.ts +174 -0
  101. package/src/utils/get-styles.ts +7 -1
  102. package/src/utils/index.ts +3 -0
  103. package/src/utils/test/bucket-info.test.ts +185 -0
  104. package/src/utils/test/color-utils.test.ts +1 -48
  105. package/src/utils/test/date-formatting.test.ts +89 -0
  106. package/src/utils/test/get-styles.test.ts +10 -7
  107. package/src/utils/test/merge-themes.test.ts +18 -46
  108. package/src/visx/types.ts +29 -2
  109. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
  110. package/src/providers/chart-context/private/theme-override-vars.ts +0 -87
  111. package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -50
  112. package/src/providers/chart-context/test/sparse-theme.test.ts +0 -35
  113. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
  114. package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -118
  115. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -47
  116. package/src/styles/test/wpds-fallbacks.test.ts +0 -131
@@ -22,14 +22,12 @@ describe( 'useGlobalChartsTheme', () => {
22
22
 
23
23
  describe( 'with GlobalChartsProvider', () => {
24
24
  it( 'should return merged theme when provider has custom theme', () => {
25
- const customTheme = { colors: [ '#FF0000', '#00FF00', '#0000FF' ] };
26
- const wrapper = createWrapper( customTheme );
25
+ const wrapper = createWrapper( { tickLength: 9 } );
27
26
 
28
27
  const { result } = renderHook( () => useGlobalChartsTheme(), { wrapper } );
29
28
 
30
- expect( result.current.colors ).toEqual( [ '#FF0000', '#00FF00', '#0000FF' ] );
31
- // Other properties should still come from default theme
32
- expect( result.current.backgroundColor ).toBe( defaultTheme.backgroundColor );
29
+ expect( result.current.tickLength ).toBe( 9 );
30
+ // Everything the consumer did not set still comes from the default theme.
33
31
  expect( result.current.gridStyles ).toEqual( defaultTheme.gridStyles );
34
32
  } );
35
33
 
@@ -39,13 +37,12 @@ describe( 'useGlobalChartsTheme', () => {
39
37
  const { result } = renderHook( () => useGlobalChartsTheme(), { wrapper } );
40
38
 
41
39
  expect( result.current ).toEqual( defaultTheme );
42
- expect( result.current.colors ).toBe( defaultTheme.colors );
43
40
  } );
44
41
  } );
45
42
 
46
43
  describe( 'theme stability', () => {
47
44
  it( 'should return the same theme object when provider theme does not change', () => {
48
- const customTheme = { colors: [ '#FF0000' ] };
45
+ const customTheme = { tickLength: 9 };
49
46
  const wrapper = createWrapper( customTheme );
50
47
 
51
48
  const { result, rerender } = renderHook( () => useGlobalChartsTheme(), { wrapper } );
@@ -61,20 +58,12 @@ describe( 'useGlobalChartsTheme', () => {
61
58
 
62
59
  describe( 'edge cases', () => {
63
60
  it( 'should handle null/undefined theme values gracefully', () => {
64
- const customTheme = { colors: undefined };
65
- const wrapper = createWrapper( customTheme );
61
+ const customTheme = { tickLength: undefined };
62
+ const wrapper = createWrapper( customTheme as unknown as Partial< ChartTheme > );
66
63
 
67
64
  const { result } = renderHook( () => useGlobalChartsTheme(), { wrapper } );
68
65
 
69
- // Should still return a complete theme with undefined colors
70
- expect( result.current.backgroundColor ).toBe( defaultTheme.backgroundColor );
71
- expect( result.current.colors ).toBeUndefined();
72
- } );
73
-
74
- it( 'should not throw when GlobalChartsContext is not available', () => {
75
- expect( () => {
76
- renderHook( () => useGlobalChartsTheme() );
77
- } ).not.toThrow();
66
+ expect( result.current.gridStyles ).toEqual( defaultTheme.gridStyles );
78
67
  } );
79
68
  } );
80
69
  } );
@@ -1,33 +1,24 @@
1
1
  import type { CompleteChartTheme } from '../../types';
2
2
 
3
3
  /**
4
- * Default theme configuration
4
+ * Default theme configuration: the shape and spacing a consumer can override.
5
+ *
6
+ * For a color, set the matching `--a8c-charts-color-*` role in CSS instead; the `var()`
7
+ * chains that deliver those live in `private/catalog-pointers.ts`.
5
8
  */
6
9
  const defaultTheme: CompleteChartTheme = {
7
- backgroundColor: 'var(--a8c-charts-color-background, #fff)',
8
- labelBackgroundColor: 'transparent', // label background color (transparent by default)
9
- // White label text sits on top of arbitrary series colors, so it has no WPDS content-foreground equivalent. Every other colour here is a bare pointer at the catalog emitted by `chart-scope.scss`; the terminal literal is the last resort for the SSR and jsdom paths, where getComputedStyle resolves nothing.
10
- labelTextColor: 'var(--a8c-charts-color-label-on-fill, #FFFFFF)',
11
- colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ],
12
10
  gridStyles: {
13
- stroke: 'var(--a8c-charts-color-grid, #dbdbdb)',
14
11
  strokeWidth: 1,
15
12
  },
16
13
  tickLength: 4,
17
- gridColor: '',
18
- gridColorDark: '',
19
14
  xTickLineStyles: {
20
- stroke: 'var(--a8c-charts-color-tick, #dbdbdb)',
21
15
  strokeWidth: 1,
22
16
  },
23
17
  xAxisLineStyles: {
24
- stroke: 'var(--a8c-charts-color-axis, #dbdbdb)',
25
18
  strokeWidth: 1,
26
19
  },
27
20
  legend: {
28
- labelStyles: {
29
- color: 'var(--a8c-charts-color-label, #1e1e1e)',
30
- },
21
+ labelStyles: {},
31
22
  containerStyles: {},
32
23
  shapeStyles: [],
33
24
  },
@@ -39,42 +30,12 @@ const defaultTheme: CompleteChartTheme = {
39
30
  // elements for axis labels and ticks. Setting `inherit` lets SVG text
40
31
  // pick up the host application's font-family via normal CSS inheritance.
41
32
  svgLabelSmall: {
42
- fill: 'var(--a8c-charts-color-label-axis, #1e1e1e)',
43
33
  fontFamily: 'inherit',
44
34
  },
45
35
  svgLabelBig: { fontFamily: 'inherit' },
46
- annotationStyles: {
47
- label: {
48
- anchorLineStroke: 'var(--a8c-charts-color-annotation, #1e1e1e)',
49
- backgroundFill: 'var(--a8c-charts-color-surface, #fff)',
50
- },
51
- connector: {
52
- stroke: 'var(--a8c-charts-color-annotation, #1e1e1e)',
53
- },
54
- circleSubject: {
55
- stroke: 'transparent',
56
- fill: 'var(--a8c-charts-color-annotation, #1e1e1e)',
57
- radius: 5,
58
- },
59
- },
60
- geoChart: {
61
- featureFillColor: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
62
- },
36
+ annotationStyles: {},
63
37
  leaderboardChart: {
64
- rowGap: 12,
65
- columnGap: 4,
66
38
  labelSpacing: 'xs',
67
- // [negative, neutral, positive]
68
- deltaColors: [
69
- 'var(--a8c-charts-color-trend-down, #cc1818)',
70
- 'var(--a8c-charts-color-trend-neutral, #707070)',
71
- 'var(--a8c-charts-color-trend-up, #008030)',
72
- ],
73
- },
74
- conversionFunnelChart: {
75
- backgroundColor: 'var(--a8c-charts-color-surface-secondary, #f4f4f4)',
76
- positiveChangeColor: 'var(--a8c-charts-color-trend-up, #008030)',
77
- negativeChangeColor: 'var(--a8c-charts-color-trend-down, #cc1818)',
78
39
  },
79
40
  lineChart: {
80
41
  lineStyles: {
@@ -96,8 +57,7 @@ const defaultTheme: CompleteChartTheme = {
96
57
  margin: { top: 2, right: 2, bottom: 2, left: 2 },
97
58
  strokeWidth: 1.5,
98
59
  },
99
- // `primaryColor` is left unset so it falls back to the palette's `colors[0]`. The compact
100
- // 11px square / 2px gap is the contribution-graph rhythm, which has no WPDS dimension.
60
+ // The compact 11px square / 2px gap is the contribution-graph rhythm, which has no WPDS dimension.
101
61
  heatmapChart: {
102
62
  compactCellGap: 2,
103
63
  compactCellSize: 11,
@@ -2,6 +2,7 @@ import { CSSProperties, ReactNode } from 'react';
2
2
  import type { BaseLegendItem } from '../../components/legend';
3
3
  import type {
4
4
  BarStyles,
5
+ ChartFormatting,
5
6
  ChartType,
6
7
  CompleteChartTheme,
7
8
  DataPointPercentage,
@@ -37,6 +38,8 @@ export interface GlobalChartsContextValue {
37
38
  unregisterChart: ( id: string ) => void;
38
39
  getChartData: ( id: string ) => ChartRegistration | undefined;
39
40
  theme: CompleteChartTheme;
41
+ /** Locale and time zone every date the charts print is rendered in. */
42
+ formatting: ChartFormatting;
40
43
  getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
41
44
  // Series visibility management shared by charts, legends, and programmatic controls.
42
45
  toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
@@ -1,64 +1,66 @@
1
1
  // The single mapping point between the --a8c-charts-* catalog and WordPress
2
- // design-system tokens.
2
+ // design-system tokens. Declares custom properties only — this stylesheet must
3
+ // never change layout.
3
4
 
4
- // Declared on the GlobalChartsProvider wrapper — not at :root — because
5
+ // Declared on the GlobalChartsProvider wrapper, not at :root, because
5
6
  // @wordpress/theme's ThemeProvider emits its generated --wpds-* ramp as inline
6
- // styles on its own wrapper div, not on the document root. Substituting at the
7
- // provider means a ThemeProvider above it is picked up; substituting at :root
8
- // would miss it entirely.
7
+ // styles on its own wrapper div. Substituting at :root would miss it entirely.
9
8
 
10
- // :where() keeps the rule at zero specificity so any consumer rule
11
- // targeting the same element wins. Chart roots deliberately do NOT
12
- // re-declare these: an element's own declaration beats an inherited one,
13
- // so re-declaring lower down would shadow a consumer override set inside
14
- // the provider tree.
15
-
16
- // The five roles a `theme` prop field can override read a `*-theme` variable
17
- // first, which GlobalChartsProvider writes inline from that field. Publishing
18
- // the consumer's value one layer out, rather than as the role itself, is what
19
- // keeps the catalog default reachable: a value that is invalid at
20
- // computed-value time (a fallback-less `var()` naming a token nothing set,
21
- // which is how premium-analytics passes its grid stroke) invalidates only the
22
- // `*-theme` variable, and the role's own fallback still resolves the mapped
23
- // --wpds-* token. Written as the role directly, that same value made the role
24
- // guaranteed-invalid and took every bare `var(--a8c-charts-color-grid)` read
25
- // site with it — gridlines painted `unset` instead of the spec grey.
26
- // `src/styles/test/chart-scope.test.ts` pins which roles carry the layer.
27
-
28
- // Declares custom properties only. This stylesheet must never change layout.
9
+ // :where() keeps the rule at zero specificity so any consumer rule targeting
10
+ // the same element wins. Chart roots deliberately do NOT re-declare these: an
11
+ // element's own declaration beats an inherited one, so re-declaring lower down
12
+ // would shadow a consumer override set inside the provider tree.
29
13
  :where(.a8c-charts-scope) {
30
- --a8c-charts-color-grid: var(--a8c-charts-color-grid-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
31
- --a8c-charts-color-axis: var(--a8c-charts-color-axis-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
32
- --a8c-charts-color-tick: var(--a8c-charts-color-tick-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
33
- --a8c-charts-color-label: var(--wpds-color-foreground-content-neutral, #1e1e1e);
34
- --a8c-charts-color-label-secondary: var(--wpds-color-foreground-content-neutral-weak, #707070);
35
- --a8c-charts-color-label-inverse: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
36
- // White label text sits on arbitrary series colours, so it has no WPDS
37
- // content-foreground equivalent.
38
- --a8c-charts-color-label-on-fill: #fff;
39
- // The SVG axis and tick labels. `svgLabelSmall.fill` publishes THIS role, so
40
- // deriving from --a8c-charts-color-label is safe and wanted: the theme field
41
- // moves only these labels, while setting the broad role in CSS still moves
42
- // every label at once. (--a8c-charts-color-surface cannot derive the same
43
- // way — see its comment; there the theme field owns the broad role.)
44
- --a8c-charts-color-label-axis: var(--a8c-charts-color-label-axis-theme, var(--a8c-charts-color-label));
45
- --a8c-charts-color-annotation: var(--wpds-color-foreground-content-neutral, #1e1e1e);
46
- --a8c-charts-color-trend-up: var(--wpds-color-foreground-content-success-weak, #008030);
47
- --a8c-charts-color-trend-down: var(--wpds-color-foreground-content-error-weak, #cc1818);
48
- --a8c-charts-color-trend-neutral: var(--wpds-color-foreground-content-neutral-weak, #707070);
49
- --a8c-charts-color-background: var(--a8c-charts-color-background-theme, var(--wpds-color-background-surface-neutral-strong, #fff));
50
- // Surfaces that float over the chart — tooltips, the annotation label, the
51
- // zoom-reset control. Same WPDS token as --a8c-charts-color-background, and
52
- // deliberately NOT derived from it: `backgroundColor` publishes the
53
- // background role, so deriving would let that theme field repaint these too.
54
- // Sibling of the background role, not a child of it.
55
- --a8c-charts-color-surface: var(--wpds-color-background-surface-neutral-strong, #fff);
56
- --a8c-charts-color-surface-secondary: var(--wpds-color-background-surface-neutral-weak, #f4f4f4);
57
- --a8c-charts-color-track: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
14
+ // The series palette. GlobalChartsProvider resolves these five slots at its
15
+ // wrapper and seeds ColorCache with whatever resolves; a slot that resolves
16
+ // to nothing is skipped, so setting only slots 1 and 3 gives a two-color
17
+ // palette, and charts generate accessible colors beyond the seeds. Only
18
+ // slot 1 carries a default.
19
+
20
+ // Slot 1 names --wp-admin-theme-color first, because that is the only leg
21
+ // that does not depend on someone else's runtime. The design system's brand
22
+ // token tracks the admin color scheme only where a WPDS root provider
23
+ // boots and emits its generated ramp inline; without one the token falls to
24
+ // the plain stylesheet rule, a static #3858e9 with no reference to the
25
+ // admin color. Leading with the admin color costs nothing:
26
+ // @wordpress/theme writes --wp-admin-theme-color from a provider's own
27
+ // accent as a legacy wp-admin override, so a provider accent still wins.
28
+ --a8c-charts-color-series-1: var(--wp-admin-theme-color, var(--wpds-color-foreground-interactive-brand));
29
+ --a8c-charts-color-grid: var(--wpds-color-stroke-surface-neutral);
30
+ --a8c-charts-color-axis-x: var(--wpds-color-stroke-surface-neutral);
31
+ --a8c-charts-color-tick-x: var(--wpds-color-stroke-surface-neutral);
32
+ // The y axis carries labels only until one of these is declared. `none`
33
+ // rather than `transparent` keeps the line out of hit-testing too.
34
+ --a8c-charts-color-axis-y: none;
35
+ --a8c-charts-color-tick-y: none;
36
+ --a8c-charts-color-label: var(--wpds-color-foreground-content-neutral);
37
+ --a8c-charts-color-label-secondary: var(--wpds-color-foreground-content-neutral-weak);
38
+ // Label text on a filled surface — a pie segment, a heatmap cell.
39
+ --a8c-charts-color-label-inverse: var(--wpds-color-foreground-interactive-neutral-strong);
40
+ // The plate behind a pie-slice label. Transparent by default, so a label
41
+ // reads straight off the slice; there is no WPDS token for "no surface".
42
+ // The literal is load-bearing: a `fill` reading a fallback-less var() on an
43
+ // undeclared property drops the declaration and paints black instead.
44
+ --a8c-charts-color-label-background: transparent;
45
+ // The SVG axis and tick labels. Narrower than --a8c-charts-color-label and
46
+ // derived from it, so setting the broad role moves every label at once while
47
+ // setting this one moves only the axis.
48
+ --a8c-charts-color-label-axis: var(--a8c-charts-color-label);
49
+ --a8c-charts-color-annotation: var(--wpds-color-foreground-content-neutral);
50
+ --a8c-charts-color-trend-up: var(--wpds-color-foreground-content-success-weak);
51
+ --a8c-charts-color-trend-down: var(--wpds-color-foreground-content-error-weak);
52
+ --a8c-charts-color-trend-neutral: var(--wpds-color-foreground-content-neutral-weak);
53
+ --a8c-charts-color-background: var(--wpds-color-background-surface-neutral-strong);
54
+ // Surfaces floating over the chart — tooltips, the annotation label, the
55
+ // zoom-reset control. Same WPDS token as --a8c-charts-color-background but
56
+ // deliberately not derived from it, so either can move without the other.
57
+ --a8c-charts-color-surface: var(--wpds-color-background-surface-neutral-strong);
58
+ --a8c-charts-color-surface-secondary: var(--wpds-color-background-surface-neutral-weak);
59
+ --a8c-charts-color-track: var(--wpds-color-background-track-neutral-weak);
58
60
  // Translucent dark surface; no WPDS token carries this.
59
61
  --a8c-charts-color-tooltip-surface: rgb(0 0 0 / 85%);
60
- --a8c-charts-color-zoom-selection: var(--wpds-color-background-interactive-brand-strong, var(--wp-admin-theme-color, #3858e9));
61
- --a8c-charts-color-zoom-selection-stroke: var(--wpds-color-stroke-interactive-brand, var(--wp-admin-theme-color, #3858e9));
62
+ --a8c-charts-color-zoom-selection: var(--wpds-color-background-interactive-brand-strong);
63
+ --a8c-charts-color-zoom-selection-stroke: var(--wpds-color-stroke-interactive-brand);
62
64
 
63
65
  // @wordpress/theme 1.0.0 removed the whole --wpds-elevation-* group while the
64
66
  // elevation model is redefined (gutenberg#80099). These carry the removed spec
@@ -74,10 +76,13 @@
74
76
  0 6px 6px 0 #00000008,
75
77
  0 8px 8px 0 #00000005;
76
78
 
77
- --a8c-charts-motion-duration-entrance: var(--wpds-motion-duration-xl, 400ms);
78
- --a8c-charts-motion-easing-entrance: var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1));
79
- --a8c-charts-border-radius-bar: var(--wpds-border-radius-md, 4px);
80
- --a8c-charts-border-radius-cell: var(--wpds-border-radius-sm, 2px);
79
+ --a8c-charts-motion-duration-entrance: var(--wpds-motion-duration-xl);
80
+ --a8c-charts-motion-easing-entrance: var(--wpds-motion-easing-expressive);
81
+ --a8c-charts-border-radius-bar: var(--wpds-border-radius-md);
82
+ --a8c-charts-border-radius-cell: var(--wpds-border-radius-sm);
81
83
  // Pill shape; no WPDS radius reaches it.
82
84
  --a8c-charts-border-radius-leaderboard-bar: 9999px;
85
+
86
+ --a8c-charts-dimension-leaderboard-row-gap: var(--wpds-dimension-gap-md);
87
+ --a8c-charts-dimension-leaderboard-column-gap: var(--wpds-dimension-gap-xs);
83
88
  }
@@ -1,13 +1,11 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import {
4
- THEME_LAYERED_ROLES,
5
- themeLayerVar,
6
- } from '../../providers/chart-context/private/theme-override-vars';
7
3
 
8
4
  const stylesheet = readFileSync( join( __dirname, '..', 'chart-scope.scss' ), 'utf8' );
9
5
  const tokensDoc = readFileSync( join( __dirname, '..', '..', '..', 'TOKENS.md' ), 'utf8' );
10
6
 
7
+ const normalize = ( value: string ): string => value.replace( /\s+/g, ' ' ).trim();
8
+
11
9
  type Entry = {
12
10
  /** The custom property this one reads, or null when the value is a literal. */
13
11
  reads: string | null;
@@ -15,23 +13,6 @@ type Entry = {
15
13
  fallback: string | null;
16
14
  };
17
15
 
18
- const normalize = ( value: string ): string => value.replace( /\s+/g, ' ' ).trim();
19
-
20
- /**
21
- * Unwraps the theme layer a `theme`-prop-overridable role carries, so the tables in `TOKENS.md` keep documenting what a role ultimately resolves to — its `--wpds-*` token and spec fallback — rather than restating the override plumbing in every row. Which roles carry the layer is pinned separately below.
22
- *
23
- * @param name - The declared property name.
24
- * @param value - Its normalized value.
25
- * @return The value with a leading `var(<name>-theme, … )` wrapper removed.
26
- */
27
- function stripThemeLayer( name: string, value: string ): string {
28
- const layered = new RegExp( `^var\\(\\s*${ themeLayerVar( name ) }\\s*,\\s*(.*)\\)$`, 's' ).exec(
29
- value
30
- );
31
-
32
- return layered ? layered[ 1 ].trim() : value;
33
- }
34
-
35
16
  /**
36
17
  * Parses each catalog declaration in the stylesheet into the property it reads and the fallback it carries. Splitting the `var()` arguments at the *first* comma keeps a fallback that is itself a function call intact — `cubic-bezier(0.25, 0, 0, 1)` would otherwise be truncated at its first argument.
37
18
  *
@@ -54,7 +35,8 @@ function parseStylesheet(): Map< string, Entry > {
54
35
  }
55
36
 
56
37
  const name = declaration.slice( 0, separator ).trim();
57
- const value = stripThemeLayer( name, normalize( declaration.slice( separator + 1 ) ) );
38
+ const value = normalize( declaration.slice( separator + 1 ) );
39
+
58
40
  const wrapped = /^var\(\s*(.*)\)$/s.exec( value );
59
41
 
60
42
  if ( ! wrapped ) {
@@ -112,10 +94,13 @@ function parseTokensDoc(): Map< string, Entry > {
112
94
  // "no mapping" before looking for a token name inside it.
113
95
  const unmapped = mapping.startsWith( '_(none' );
114
96
  const derives = /derives from (--[\w-]+)/.exec( mapping );
115
- const wpds = /(--wpds-[\w-]+)/.exec( mapping );
97
+ // `--wp-*` rather than `--wpds-*`: the series palette maps to WordPress's own
98
+ // `--wp-admin-theme-color`, since the design system's brand token is a static
99
+ // hex on every admin screen and cannot carry the admin color scheme.
100
+ const token = /(--wp[\w-]+)/.exec( mapping );
116
101
 
117
102
  entries.set( role, {
118
- reads: unmapped ? null : derives?.[ 1 ] ?? wpds?.[ 1 ] ?? null,
103
+ reads: unmapped ? null : derives?.[ 1 ] ?? token?.[ 1 ] ?? null,
119
104
  fallback: fallback === '—' ? null : normalize( fallback ),
120
105
  } );
121
106
  }
@@ -132,24 +117,24 @@ describe( 'chart scope catalog', () => {
132
117
  } );
133
118
 
134
119
  // The tables in TOKENS.md restate the stylesheet for consumers who only have the
135
- // published package. This is the check that stops the two drifting apart.
136
- it.each( [ ...declared.keys() ] )( 'documents %s with the value it is declared with', token => {
137
- expect( documented.get( token ) ).toEqual( declared.get( token ) );
138
- } );
139
-
140
- // The layer is what keeps a `theme` prop override from being able to take its role down with it: an override that is invalid at computed-value time only invalidates `<role>-theme`, and the role's own fallback still resolves the mapped token. Drop the layer from a role and that role's overrides go back to blanking every read site.
141
- it.each( THEME_LAYERED_ROLES )( 'declares %s reading its theme layer first', role => {
142
- expect( stylesheet ).toMatch(
143
- new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*,` )
144
- );
120
+ // published package. Mapping (what a role reads) must match the source. The
121
+ // Fallback column for `--wpds-*` mappings is injected at build time, so only
122
+ // literals and `--a8c-charts-*` derivations are compared as fallbacks.
123
+ it.each( [ ...declared.keys() ] )( 'documents %s with the mapping it is declared with', token => {
124
+ expect( documented.get( token )?.reads ).toEqual( declared.get( token )?.reads );
145
125
  } );
146
126
 
147
- it( 'gives a theme layer to the overridable roles and to nothing else', () => {
148
- const layered = [ ...declared.keys() ].filter( role =>
149
- new RegExp( `${ role }:\\s*var\\(\\s*${ themeLayerVar( role ) }\\s*,` ).test( stylesheet )
150
- );
151
-
152
- expect( layered.sort() ).toEqual( [ ...THEME_LAYERED_ROLES ].sort() );
127
+ it.each(
128
+ [ ...declared.keys() ].filter( token => {
129
+ const entry = declared.get( token );
130
+
131
+ return (
132
+ entry?.reads?.startsWith( '--wpds-' ) !== true &&
133
+ entry?.fallback?.includes( '--wpds-' ) !== true
134
+ );
135
+ } )
136
+ )( 'documents %s with the fallback it is declared with', token => {
137
+ expect( documented.get( token )?.fallback ).toEqual( declared.get( token )?.fallback );
153
138
  } );
154
139
 
155
140
  it( 'scopes the catalog to :where(.a8c-charts-scope) rather than :root', () => {
@@ -0,0 +1,31 @@
1
+ import type { SeriesData } from '../types';
2
+
3
+ const series = ( start: string, count: number, stepInHours: number ): SeriesData[] => [
4
+ {
5
+ label: 'views',
6
+ data: Array.from( { length: count }, ( _, index ) => ( {
7
+ date: new Date( new Date( start ).getTime() + index * stepInHours * 60 * 60 * 1000 ),
8
+ value: index,
9
+ } ) ),
10
+ },
11
+ ];
12
+
13
+ /**
14
+ * One series of hourly points starting at `start`, `count` long.
15
+ *
16
+ * @param start - ISO instant of the first point.
17
+ * @param count - Number of points.
18
+ * @return A single-series fixture.
19
+ */
20
+ export const hourlySeries = ( start: string, count: number ): SeriesData[] =>
21
+ series( start, count, 1 );
22
+
23
+ /**
24
+ * One series of daily points starting at `start`, `count` long.
25
+ *
26
+ * @param start - ISO instant of the first point.
27
+ * @param count - Number of points.
28
+ * @return A single-series fixture.
29
+ */
30
+ export const dailySeries = ( start: string, count: number ): SeriesData[] =>
31
+ series( start, count, 24 );