@automattic/charts 3.2.0 → 4.1.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 (135) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/SECURITY.md +1 -0
  3. package/TOKENS.md +53 -58
  4. package/dist/index.cjs +1483 -664
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.css +93 -29
  7. package/dist/index.d.cts +233 -247
  8. package/dist/index.d.ts +233 -247
  9. package/dist/index.js +1484 -667
  10. package/dist/index.js.map +1 -1
  11. package/package.json +7 -6
  12. package/src/charts/area-chart/area-chart.tsx +39 -25
  13. package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
  14. package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
  15. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
  16. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
  17. package/src/charts/area-chart/types.ts +4 -7
  18. package/src/charts/bar-chart/bar-chart.tsx +2 -1
  19. package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
  20. package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
  21. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
  22. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
  23. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
  24. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
  25. package/src/charts/geo-chart/geo-chart.module.scss +1 -0
  26. package/src/charts/geo-chart/geo-chart.tsx +5 -10
  27. package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
  28. package/src/charts/heatmap-chart/heatmap-chart.module.scss +52 -16
  29. package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
  30. package/src/charts/heatmap-chart/index.ts +2 -1
  31. package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
  32. package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
  33. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
  34. package/src/charts/heatmap-chart/private/index.ts +1 -1
  35. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
  36. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
  37. package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
  38. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
  39. package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
  40. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
  41. package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
  42. package/src/charts/heatmap-chart/types.ts +8 -2
  43. package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
  44. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  45. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
  46. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
  47. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
  48. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
  49. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
  50. package/src/charts/leaderboard-chart/types.ts +2 -2
  51. package/src/charts/line-chart/line-chart.module.scss +10 -18
  52. package/src/charts/line-chart/line-chart.tsx +128 -26
  53. package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
  54. package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
  55. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
  56. package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
  57. package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
  58. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
  59. package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
  60. package/src/charts/line-chart/types.ts +17 -3
  61. package/src/charts/pie-chart/pie-chart.module.scss +13 -1
  62. package/src/charts/pie-chart/pie-chart.tsx +30 -40
  63. package/src/charts/pie-chart/test/pie-chart.test.tsx +59 -12
  64. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
  65. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
  66. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
  67. package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
  68. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  69. package/src/charts/private/test/axis-formatting.test.tsx +85 -0
  70. package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
  71. package/src/charts/private/test/time-axis-options.test.ts +267 -0
  72. package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
  73. package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
  74. package/src/charts/private/test/time-axis.test.ts +9 -69
  75. package/src/charts/private/time-axis-options.ts +106 -0
  76. package/src/charts/private/time-axis.ts +318 -136
  77. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  78. package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
  79. package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
  80. package/src/charts/sparkline/types.ts +1 -1
  81. package/src/components/legend/private/base-legend.module.scss +9 -5
  82. package/src/components/tooltip/accessible-tooltip.tsx +21 -17
  83. package/src/components/tooltip/base-tooltip.module.scss +3 -3
  84. package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
  85. package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
  86. package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
  87. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
  88. package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
  89. package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
  90. package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
  91. package/src/hooks/test/use-chart-margin.test.tsx +173 -3
  92. package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
  93. package/src/hooks/use-chart-data-transform.ts +10 -3
  94. package/src/hooks/use-chart-margin.tsx +54 -9
  95. package/src/hooks/use-xychart-theme.ts +25 -9
  96. package/src/index.ts +8 -0
  97. package/src/providers/chart-context/global-charts-provider.tsx +37 -38
  98. package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
  99. package/src/providers/chart-context/index.ts +1 -0
  100. package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
  101. package/src/providers/chart-context/private/series-palette.ts +3 -13
  102. package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
  103. package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
  104. package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
  105. package/src/providers/chart-context/themes.ts +7 -52
  106. package/src/providers/chart-context/types.ts +3 -0
  107. package/src/styles/chart-scope.scss +39 -62
  108. package/src/styles/test/chart-scope.test.ts +19 -47
  109. package/src/test-utils/series-fixtures.ts +31 -0
  110. package/src/test-utils/zoned-parsing-suite.ts +90 -0
  111. package/src/types.ts +79 -166
  112. package/src/utils/bucket-info.ts +87 -0
  113. package/src/utils/color-utils.ts +0 -27
  114. package/src/utils/date-formatting.ts +157 -0
  115. package/src/utils/date-parsing.ts +103 -52
  116. package/src/utils/get-edge-tick-widths.ts +33 -0
  117. package/src/utils/index.ts +4 -0
  118. package/src/utils/test/bucket-info.test.ts +185 -0
  119. package/src/utils/test/color-utils.test.ts +1 -48
  120. package/src/utils/test/date-formatting.test.ts +89 -0
  121. package/src/utils/test/date-parsing.auckland.test.ts +6 -0
  122. package/src/utils/test/date-parsing.santiago.test.ts +6 -0
  123. package/src/utils/test/date-parsing.test.ts +16 -1
  124. package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
  125. package/src/utils/test/get-styles.test.ts +10 -7
  126. package/src/utils/test/merge-themes.test.ts +18 -46
  127. package/src/utils/warn-once.ts +23 -0
  128. package/src/visx/types.ts +56 -5
  129. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
  130. package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
  131. package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
  132. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
  133. package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
  134. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
  135. package/src/styles/test/wpds-fallbacks.test.ts +0 -131
package/CHANGELOG.md CHANGED
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.1.0] - 2026-09-14
9
+ ### Added
10
+ - Heatmap: add `locale` and `timeZone` options and a `useCalendarHeatmapData` hook, so a host can bucket and label the calendar in its own zone and language instead of the viewer's. Labels with no `locale` now follow the runtime locale rather than always rendering in English, and a calendar `dateString` must start `yyyy-MM-dd`. [#52114]
11
+ - HeatmapChart: Add summary columns, per-row roll-ups drawn outside the color scale. [#52135]
12
+ - Line chart: Add options to place tooltips below the x-axis label band and customize tooltip and crosshair styles. [#52143]
13
+
14
+ ### Changed
15
+ - AccessibleTooltip: Merge supplied styles with the default box styles instead of replacing them; use unstyled to remove the box styles. [#52143]
16
+ - LeaderboardChart: Let valueFormatter return a React node, so a value can carry a tooltip. [#52177]
17
+ - Line chart: Keep the page from scrolling when keyboard focus moves to a below-axis tooltip. [#52143]
18
+
19
+ ### Fixed
20
+ - Flip the tooltip to the side of the anchor that stays inside the chart wrapper, so a box that fits in the chart no longer reaches past it into page content that paints over it. [#52123]
21
+ - HeatmapChart: Fix an endless re-render when the keyboard tooltip opens on a chart without row labels. [#52135]
22
+ - LineChart, AreaChart, BarChart: Date a naive dateString point in the provider's time zone, so every viewer reads the same day. [#52113]
23
+ - Reserve room for the first and last labels on a time axis, size the y-axis gutter from a pinned domain, and reserve nothing for a hidden y axis. [#52112]
24
+
25
+ ## [4.0.0] - 2026-09-04
26
+ ### Changed
27
+ - Inject WPDS design-token fallbacks at build time via @wordpress/theme's LightningCSS plugin. [#51722]
28
+ - Render chart tooltips inside the chart instead of a document.body portal, so they stack correctly under sticky and fixed page elements. `detectBounds` now keeps the box inside the nearest ancestor that clips its overflow, or the viewport. The tooltip's `scroll`, `debounce` and `resizeObserverPolyfill` options are deprecated and ignored. [#51640]
29
+ - Render time-axis and tooltip dates in a host-supplied locale and time zone, set on GlobalChartsProvider. Both default to the browser's, as before. [#51812]
30
+ - Update package dependencies. [#51701]
31
+
32
+ ### Removed
33
+ - Remove every color field from the chart theme. Set the matching `--a8c-charts-color-*` custom property inside the provider tree instead — every removed field named one, and they were deprecated in the previous release. `TOKENS.md` lists the catalog. `LeaderboardChart`'s `primaryColor` and `secondaryColor` props and `HeatmapChart`'s `primaryColor` prop are unaffected, and a single annotation still takes colors through its own `styles` prop.
34
+
35
+ Remove `gridColor` and `gridColorDark`, which were undocumented visx passthroughs. `gridColor` painted the y axis line and y tick marks, which no other field could reach. Add `--a8c-charts-color-axis-y` and `--a8c-charts-color-tick-y` to replace it: both resolve to `none`, so the y axis still carries labels only by default, and a chart that wants a painted y axis declares them.
36
+
37
+ Rename `--a8c-charts-color-axis` to `--a8c-charts-color-axis-x` and `--a8c-charts-color-tick` to `--a8c-charts-color-tick-x`. Both only ever painted the x axis; now that the y axis has roles of its own, the names say which one they move.
38
+
39
+ Add `leaderboard-chart`, `conversion-funnel-chart` and `bar-list-chart` classes to those charts' roots, and add the `pie-semi-circle-chart` class to that chart's error state, which was missing it. This matches the `bar-chart`, `line-chart` and `pie-chart` classes the other charts already carry, and gives a consumer somewhere to scope a role to one chart — which matters for a role more than one chart reads, since `--a8c-charts-color-surface-secondary` paints the funnel's track and `GeoChart`'s dataless regions, and the trend pair paints the funnel's change indicator and the leaderboard's deltas.
40
+
41
+ `BarListChart` and `PieSemiCircleChart`'s error state now also pass a caller's own `className` through, which they previously dropped.
42
+
43
+ Remove `--a8c-charts-color-label-on-fill`. Set `--a8c-charts-color-label-inverse` instead: both meant label text on a filled surface, and one role covers the pie segment labels and the heatmap cell values together. Pie labels follow that role's `--wpds-color-foreground-interactive-neutral-strong` mapping now, so they are off-white rather than pure white until a consumer sets the role.
44
+
45
+ Remove `theme.leaderboardChart.rowGap` and `.columnGap`. Declare `--a8c-charts-dimension-leaderboard-row-gap` or `--a8c-charts-dimension-leaderboard-column-gap` inside the provider tree instead; they map to `--wpds-dimension-gap-md` and `--wpds-dimension-gap-xs`, so the default spacing follows the design system. `TOKENS.md` lists both roles.
46
+
47
+ Remove the two remaining deprecated APIs, so this major carries every removal at once rather than spending a second one later. `AreaChart`'s `rescaleYOnLegendToggle` prop goes — use `rescaleYOnVisibilityChange`, which is the same setting under a name that matches when it applies. The `parseRgbString` helper goes — use `normalizeColorToHex`, which handles `rgb()` alongside every other format. [#51748]
48
+
49
+ ### Fixed
50
+ - Place line and area chart date ticks on the host time zone's calendar boundaries, name the hour in tooltips on hourly data, let the locale rather than a forced 12-hour clock choose how every chart's hour labels read, and hand a custom tooltip the chart's date bucket classification. [#51813]
51
+ - Tooltip: Keep the drop shadow when `--a8c-charts-color-label-axis` resolves to something other than a 6-digit hex. visx appends an alpha suffix to that color to build the shadow, which only parses after a hex, so an `rgb()` or `hsl()` value silently dropped the shadow entirely. [#51748]
52
+
8
53
  ## [3.2.0] - 2026-09-01
9
54
  ### Added
10
55
  - Bar and line charts: Report a click through the pointer event handlers and Enter or Space through an activation callback, so a chart can open the point under the pointer or the keyboard selection. [#51544]
@@ -1018,6 +1063,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1018
1063
  - Fixed lints following ESLint rule changes for TS [#40584]
1019
1064
  - Fixing a bug in Chart storybook data. [#40640]
1020
1065
 
1066
+ [4.1.0]: https://github.com/Automattic/charts/compare/v4.0.0...v4.1.0
1067
+ [4.0.0]: https://github.com/Automattic/charts/compare/v3.2.0...v4.0.0
1021
1068
  [3.2.0]: https://github.com/Automattic/charts/compare/v3.1.1...v3.2.0
1022
1069
  [3.1.1]: https://github.com/Automattic/charts/compare/v3.1.0...v3.1.1
1023
1070
  [3.1.0]: https://github.com/Automattic/charts/compare/v3.0.0...v3.1.0
package/SECURITY.md CHANGED
@@ -16,6 +16,7 @@ Our HackerOne program covers the below plugin software, as well as a variety of
16
16
  * Jetpack Protect
17
17
  * Jetpack Search
18
18
  * Jetpack Social
19
+ * Jetpack Stats
19
20
  * Jetpack VideoPress
20
21
 
21
22
  **For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.**
package/TOKENS.md CHANGED
@@ -8,15 +8,15 @@
8
8
 
9
9
  Chart roots deliberately do *not* carry the `a8c-charts-scope` class. Custom properties inherit down the tree, and an element only shadows an inherited value by re-declaring it, so a chart root that re-declared the catalog would beat an override set between it and the provider — closing off the one place consumers are meant to set overrides. This inheritance rule drives every precedence question below.
10
10
 
11
- Portal-rendered tooltips carry the class unconditionally, because a React portal renders outside the provider's DOM tree and would otherwise inherit nothing. `TrendIndicator`, `BaseTooltip` and `BaseLegend` carry it only when no provider is above them (`useStandaloneScopeClass()`).
11
+ Tooltips render inside the chart, so they carry the class only when no provider is above them, the same as `TrendIndicator`, `BaseTooltip` and `BaseLegend` (`useStandaloneScopeClass()`).
12
12
 
13
- That unconditional class has a cost: a portal tooltip re-declares the catalog on itself, so it sees only the catalog *default*, never an instance override — both the provider's inline `theme` var and a consumer rule scoped to the provider wrapper live on an ancestor the portal is not a descendant of. A bare `.a8c-charts-scope { … }` rule does reach it, matching the tooltip's own class directly.
13
+ Inside a provider a tooltip therefore inherits the provider's inline `theme` var and any consumer rule scoped to the provider wrapper, the same as the chart it belongs to.
14
14
 
15
- Each catalog entry maps to a WPDS token with the WPDS spec value as its fallback:
15
+ Each catalog entry maps to a WPDS token. Source writes the mapping bare; the LightningCSS plugin in `tsdown.config.ts` injects the WPDS spec value as a fallback into `dist/`:
16
16
 
17
17
  ```scss
18
18
  :where(.a8c-charts-scope) {
19
- --a8c-charts-color-grid: var(--wpds-color-stroke-surface-neutral, #dbdbdb);
19
+ --a8c-charts-color-grid: var(--wpds-color-stroke-surface-neutral);
20
20
  }
21
21
  ```
22
22
 
@@ -28,62 +28,43 @@ Highest first:
28
28
 
29
29
  1. The role set on any element between the chart and the provider — the closest declaration wins. A role set on the chart's *own* element reaches only what CSS paints; see "The SVG bridge".
30
30
  2. The role set by a consumer rule targeting the provider wrapper. It beats the catalog default because `:where()` is zero-specificity.
31
- 3. The theme layer `GlobalChartsProvider` writes inline from a `theme` prop override — see below.
32
- 4. The catalog default on the provider wrapper, resolving the mapped `--wpds-*` token.
33
- 5. The WPDS spec-value fallback, when no `--wpds-*` token is set either (SSR, jsdom, or WPDS not loaded). This is not a rare corner: WordPress itself defines no `--wpds-*` typography tokens, so in wp-admin the fallback is what renders. It is written by hand — see `src/styles/test/wpds-fallbacks.test.ts`, which checks each one against the installed `@wordpress/theme`.
31
+ 3. The catalog default on the provider wrapper, resolving the mapped `--wpds-*` token.
32
+ 4. The WPDS spec-value fallback, when no `--wpds-*` token is set either (SSR, jsdom, or WPDS not loaded). This is not a rare corner: WordPress itself defines no `--wpds-*` typography tokens, so in wp-admin the fallback is what renders. It is injected at build time by `@wordpress/theme`'s LightningCSS plugin.
34
33
 
35
- A CSS declaration of a role therefore beats a `theme` prop override *anywhere* it is set, the wrapper included — the prop writes a variable the role reads, not the role itself, and a role declared in CSS never reads it.
34
+ Configure catalog colors through CSS; the `theme` prop carries no colors. For local LineChart tooltip overrides, see [Below-Axis Tooltips](./?path=/docs/js-packages-charts-library-charts-line-chart--docs#below-axis-tooltips).
36
35
 
37
- An override set **above** `GlobalChartsProvider` does not apply: the provider's own declaration on its wrapper beats a value merely inherited from an ancestor. Set overrides inside the provider tree, or target the scope class itself — `.a8c-charts-scope { --a8c-charts-color-grid: #e0e0e0; }` matches every provider wrapper on the page, including the one a bare chart mounts for itself and the one a portal tooltip carries, and outranks the zero-specificity catalog default. That rule is the replacement for a page-level `:root` override. The same rule limits `@wordpress/theme`'s `ThemeProvider` to *above* the charts provider: the catalog substitutes its `--wpds-*` tokens at the wrapper, so a `ThemeProvider` mounted between the wrapper and a chart is never consulted and CSS-painted colors keep their light-mode spec fallbacks. The JS-consumed ones do not — `getElementStyles` resolves at the chart element — so that nesting shows up as a chart whose series marks retint while its gridlines, axis and surfaces do not.
36
+ An override set **above** `GlobalChartsProvider` does not apply: the provider's own declaration on its wrapper beats a value merely inherited from an ancestor. Set overrides inside the provider tree, or target the scope class itself — `.a8c-charts-scope { --a8c-charts-color-grid: #e0e0e0; }` matches every provider wrapper on the page, including the one a bare chart mounts for itself, and outranks the zero-specificity catalog default. That rule is the replacement for a page-level `:root` override. The same rule limits `@wordpress/theme`'s `ThemeProvider` to *above* the charts provider: the catalog substitutes its `--wpds-*` tokens at the wrapper, so a `ThemeProvider` mounted between the wrapper and a chart is never consulted and CSS-painted colors keep their light-mode spec fallbacks. The JS-consumed ones do not — `getElementStyles` resolves at the chart element — so that nesting shows up as a chart whose series marks retint while its gridlines, axis and surfaces do not.
38
37
 
39
- #### The theme layer
38
+ #### Broad roles and narrow ones
40
39
 
41
- Each role a `theme` prop field can override is declared reading a `*-theme` variable first:
40
+ Some roles are deliberately narrower than the obvious name, so that moving one thing does not move four:
42
41
 
43
- ```scss
44
- :where(.a8c-charts-scope) {
45
- --a8c-charts-color-grid: var(--a8c-charts-color-grid-theme, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
46
- }
47
- ```
48
-
49
- `GlobalChartsProvider` writes `--a8c-charts-color-grid-theme` inline from `theme.gridStyles.stroke`. Publishing the consumer's value one layer out is what keeps the catalog default reachable: a value that is invalid at computed-value time — `var(--wpds-color-stroke-surface-neutral)` with no fallback, in a host that never loaded the WPDS stylesheet — invalidates only the theme layer, and the role still resolves its mapped token. Written as the role itself, that same value made the role guaranteed-invalid, and that propagates to every bare `var(--a8c-charts-color-grid)` read site: `stroke` computed to `unset`, so the gridlines disappeared rather than degrading to the spec grey.
50
-
51
- A value that reads the role it would override is not published at all. The role reads its theme layer, so such a value closes a cycle through the catalog entry, and CSS marks *every* custom property in a cycle invalid — the role's own fallback is not used, and the token resolves to nothing. `withCatalogPointers` restores the theme field to the catalog pointer whether or not the value was published, so visx never paints a literal the CSS side cannot see.
52
-
53
- `src/styles/test/chart-scope.test.ts` pins which roles carry the layer, from the same list the provider maps fields with.
54
-
55
- #### A `theme`-prop override keeps the reach of the field it was set from
56
-
57
- A role is a shared name, so publishing an override as a custom property could widen it — `theme={ { svgLabelSmall: { fill: 'purple' } } }` recoloring legend labels, heatmap cell values, funnel labels and the line-chart tooltip along with the SVG axis labels it names.
58
-
59
- It doesn't, because a mapped field publishes a role read by exactly the elements that field already controlled. Where the obvious role has wider readership, one side or the other gets a role of its own:
60
-
61
- | Theme field | Publishes | Kept off it | How |
62
- |---|---|---|---|
63
- | `svgLabelSmall.fill` | `--a8c-charts-color-label-axis` | legend labels, `.heatmap-chart__cell-value`, funnel labels, the line-chart tooltip | the field takes a narrow role deriving from `--a8c-charts-color-label` |
64
- | `backgroundColor` | `--a8c-charts-color-background` | the annotation label, `.x-zoom__reset`, tooltips | those readers take `--a8c-charts-color-surface` instead |
65
-
66
- **The two are shaped differently, and the difference is load-bearing.** `--a8c-charts-color-label-axis` *derives* from `--a8c-charts-color-label`: the theme field publishes the narrow role, so the broad one stays free as a move-every-label knob. `--a8c-charts-color-surface` is a **sibling** of `--a8c-charts-color-background`, not a child — there the theme field publishes the broad role, so deriving would hand the override straight back to the surfaces it is meant to spare. Deriving is only safe when the narrow role is the one the `theme` prop writes.
42
+ | Narrow role | Broad role | What the broad one also reaches |
43
+ |---|---|---|
44
+ | `--a8c-charts-color-label-axis` | `--a8c-charts-color-label` | legend labels, `.heatmap-chart__cell-value`, funnel labels, the line-chart tooltip |
45
+ | `--a8c-charts-color-background` | — | — |
67
46
 
68
- The consequence: `--a8c-charts-color-label` moves every label, but no single role repaints the chart background and the floating surfaces together.
47
+ `--a8c-charts-color-label-axis` *derives* from `--a8c-charts-color-label`, so setting the broad role moves every label at once and setting the narrow one moves only the SVG axis labels.
69
48
 
70
- `gridStyles.stroke`, `xAxisLineStyles.stroke`, `xTickLineStyles.stroke`, `labelBackgroundColor` and `labelTextColor` need no narrow role — nothing outside the element each names reads their role.
49
+ `--a8c-charts-color-surface` — the annotation label, `.x-zoom__reset`, tooltips — is a **sibling** of `--a8c-charts-color-background`, not a child. The chart's own background and the surfaces floating over it are set apart from each other often enough that one has to be able to move without the other. The consequence: no single role repaints both.
71
50
 
72
51
  ### The SVG bridge
73
52
 
74
- **A color that is only painted is not resolved at all.** The grid, axis line, tick marks and tick labels keep their `var(--a8c-charts-color-*, …)` chain the whole way: `useXYChartTheme` spreads them through untouched, `buildChartTheme` passes them on, and visx writes the chain onto the element it paints — an inline style for the grid, a presentation attribute elsewhere. A presentation attribute is mapped to a CSS declaration, so the chain resolves there natively, in Blink, WebKit and Gecko alike.
53
+ **A color that is only painted is not resolved at all.** The grid, axis line, tick marks and tick labels reach visx as the `var(--a8c-charts-color-*, …)` chain `private/catalog-pointers.ts` holds: `useXYChartTheme` puts the chain into the theme it builds, `buildChartTheme` passes it on, and visx writes it onto the element it paints — an inline style for the grid, a presentation attribute elsewhere. A presentation attribute is mapped to a CSS declaration, so the chain resolves there natively, in Blink, WebKit and Gecko alike.
75
54
 
76
55
  That is what makes the role read **at the painted element** rather than snapshot at the provider wrapper: an override on a chart's own class reaches it, a theme change repaints with no re-render, and SSR emits the chain for the client to resolve on paint. Resolving such a color in JS would freeze it and undo all three, which is why nothing does.
77
56
 
78
- There is no stylesheet and no class involved. In particular the axes need neither: `xAxisLineStyles` and `xTickLineStyles` are x-axis-only fields, so visx paints the x axis and leaves the y axis unstroked without anything having to distinguish them.
57
+ There is no stylesheet and no class involved. In particular the axes need neither: visx takes a separate style object per axis, so each one is handed its own roles and nothing has to distinguish them after the fact.
79
58
 
80
59
  What else crosses in JS is what something reads as a *value*: the series palette, which visx turns into its `colorScale`, and the background, which the default glyph, the area-chart band, the line-chart gradient stops, the heatmap's contrast math and `GeoChart` each consume as a concrete string.
81
60
 
82
- **The tooltip is the one painted exception, because visx paints it outside the scope.** `@visx/tooltip` appends each portal container straight to `document.body`, where the catalog is not declared, so a chain handed to one reaches only its own hardcoded fallback — never the role, never a consumer's override. Two colors take that route and are therefore resolved before visx sees them: `htmlLabel.color`, in `useXYChartTheme`, and the crosshair stroke, in `AccessibleTooltip`. `htmlLabel.color` has a second reason: visx builds the tooltip's shadow as `` `0 1px 2px ${color}55` ``, and a `var()` chain cannot take a suffix — token streams do not merge across a substitution boundary, so the whole declaration is invalid and the tooltip renders flat. Giving the portal the scope class instead would fix neither: visx hardcodes the crosshair portals' `className`.
61
+ **The tooltip used to be the one painted exception, because visx painted it outside the scope.** `@visx/tooltip` appends each portal container straight to `document.body`, where the catalog is not declared, so a chain handed to one reached only its own hardcoded fallback — never the role, never a consumer's override. Charts no longer take that route: the box renders into the chart's own wrapper and the crosshairs and glyphs are drawn into the chart SVG, both inside the scope, so a chain handed to either resolves there natively.
62
+
63
+ Two colors are still resolved before visx sees them. `htmlLabel.color`, in `useXYChartTheme`, for a reason the move does not touch: visx builds the tooltip's shadow as `` `0 1px 2px ${color}55` ``, and a `var()` chain cannot take a suffix — token streams do not merge across a substitution boundary, so the whole declaration is invalid and the tooltip renders flat. The crosshair stroke, in `AccessibleTooltip`, only still matters where a consumer supplies a `ChartScopeContext` element that is not one of the chart's own ancestors; in the ordinary tree the CSS path now reaches it.
83
64
 
84
65
  Being resolved in JS, both then carry the bridge's limitations rather than the CSS path's: they read at the scope element, so a role declared on the chart's own class moves the gridlines but leaves the crosshair at the catalog value, and neither repaints on a theme change until something re-renders.
85
66
 
86
- Being JS-consumed is not a reason for a field to survive, though — where a value is *consumed* and where it is *set* are separate questions. `withCatalogPointers` parks a consumer's `theme` value in the catalog role and restores the pointer, so `theme.backgroundColor` and `theme.colors` are only ever carriers for their roles. Both are deprecated; set `--a8c-charts-color-background` and `--a8c-charts-color-series-*` instead. Those resolve through `getComputedStyle` against the chart's own scope element — never `document.documentElement` — so both delivery paths obey the same cascade. The JS theme in `themes.ts` holds a bare catalog pointer with a terminal literal (`var(--a8c-charts-color-background, #fff)`); the literal is the last resort for SSR and jsdom, where `getComputedStyle` resolves nothing.
67
+ Where a value is *consumed* says nothing about where it is *set*. These are set the same way as every other color — `--a8c-charts-color-background`, `--a8c-charts-color-series-*` — and resolved through `getComputedStyle` against the chart's own scope element, never `document.documentElement`, so both delivery paths obey the same cascade. Each pointer carries a terminal literal (`var(--a8c-charts-color-background, #fff)`) as the last resort for SSR and jsdom, where `getComputedStyle` resolves nothing.
87
68
 
88
69
  The scope element is the wrapper a chart is rendered into, which sits **above** the element the chart's own `className` lands on, so a role declared on that inner element is invisible to the JS bridge. `.line-chart { --a8c-charts-color-background: red }` therefore does not reach the glyph strokes, while `.line-chart { --a8c-charts-color-grid: red }` does reach the gridlines — the CSS-painted roles read at the painted element, so any ancestor of it will do. Scope a rule for a JS-resolved role to a wrapper around the chart rather than to the chart itself. CHARTS-255 tracks closing the remainder.
89
70
 
@@ -96,17 +77,14 @@ The scope element is the wrapper a chart is rendered into, which sits **above**
96
77
  | Role | Maps to | Fallback |
97
78
  |---|---|---|
98
79
  | `--a8c-charts-color-series-1` | `--wp-admin-theme-color` | `var(--wpds-color-foreground-interactive-brand, var(--wp-admin-theme-color, #3858e9))` |
99
- | `--a8c-charts-color-series-2` | _(none — unset until a consumer sets it)_ | — |
100
- | `--a8c-charts-color-series-3` | _(none — unset until a consumer sets it)_ | — |
101
- | `--a8c-charts-color-series-4` | _(none — unset until a consumer sets it)_ | — |
102
- | `--a8c-charts-color-series-5` | _(none — unset until a consumer sets it)_ | — |
103
80
  | `--a8c-charts-color-grid` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
104
- | `--a8c-charts-color-axis` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
105
- | `--a8c-charts-color-tick` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
81
+ | `--a8c-charts-color-axis-x` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
82
+ | `--a8c-charts-color-tick-x` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
83
+ | `--a8c-charts-color-axis-y` | _(none — unpainted unless a consumer sets it)_ | `none` |
84
+ | `--a8c-charts-color-tick-y` | _(none — unpainted unless a consumer sets it)_ | `none` |
106
85
  | `--a8c-charts-color-label` | `--wpds-color-foreground-content-neutral` | `#1e1e1e` |
107
86
  | `--a8c-charts-color-label-secondary` | `--wpds-color-foreground-content-neutral-weak` | `#707070` |
108
87
  | `--a8c-charts-color-label-inverse` | `--wpds-color-foreground-interactive-neutral-strong` | `#f0f0f0` |
109
- | `--a8c-charts-color-label-on-fill` | _(none — white-on-series-fill, no WPDS fit)_ | `#fff` |
110
88
  | `--a8c-charts-color-label-background` | _(none — transparent by default, no WPDS fit)_ | `transparent` |
111
89
  | `--a8c-charts-color-label-axis` | _(derives from `--a8c-charts-color-label`)_ | — |
112
90
  | `--a8c-charts-color-annotation` | `--wpds-color-foreground-content-neutral` | `#1e1e1e` |
@@ -119,13 +97,32 @@ The scope element is the wrapper a chart is rendered into, which sits **above**
119
97
  | `--a8c-charts-color-track` | `--wpds-color-background-track-neutral-weak` | `#f0f0f0` |
120
98
  | `--a8c-charts-color-tooltip-surface` | _(none — translucent dark surface, no WPDS fit)_ | `rgb(0 0 0 / 85%)` |
121
99
 
122
- Axis and tick share grid's WPDS token but stay distinct roles, so the three can be themed independently.
100
+ The x axis and tick roles share grid's WPDS token but stay distinct roles, so the three can be themed independently.
101
+
102
+ ### One pair of roles per axis
103
+
104
+ Each axis names itself: `--a8c-charts-color-axis-x` / `-tick-x` paint the x axis, `--a8c-charts-color-axis-y` / `-tick-y` the y. The y pair resolves to `none`, so by default that axis carries tick labels and nothing else. Declare either to paint it:
105
+
106
+ ```css
107
+ .my-dashboard {
108
+ --a8c-charts-color-axis-y: #dbdbdb;
109
+ --a8c-charts-color-tick-y: #dbdbdb;
110
+ }
111
+ ```
112
+
113
+ **There is deliberately no broad `--a8c-charts-color-axis` covering both.** It would have to mean one of two things, and neither is true: that setting it moves both axes — which cannot hold while the y side defaults to unpainted — or that it moves only x, which is a name that misleads exactly once per consumer, and silently. The tick *labels* are a different matter and do share a role (`--a8c-charts-color-label-axis`), because both axes label identically.
114
+
115
+ Gridlines also stay on one role, `--a8c-charts-color-grid`: visx paints rows and columns from a single style object, so the grid is genuinely one thing rather than two.
116
+
117
+ The terminal `none` in each chain is explicit rather than load-bearing: `stroke` already starts at `none`, so an undeclared role paints nothing either way.
118
+
119
+ **It would be load-bearing on a `fill`.** A `var()` naming an undeclared property with no fallback is invalid at computed-value time, which drops the declaration rather than leaving it absent — and `fill` starts at *black*. That is why `--a8c-charts-color-label-background` carries a terminal `transparent`: without it, an undeclared role would put a black plate behind every pie label. Give any role read by a `fill` a terminal literal, even when the intent is "paint nothing".
123
120
 
124
121
  ### The series palette
125
122
 
126
123
  The five `--a8c-charts-color-series-*` slots are the palette. `GlobalChartsProvider` resolves them once, at its wrapper, and seeds its color cache with whatever resolves; charts generate accessible colors beyond the seeds, so five slots is a cap on *seeds*, not on series. A slot that resolves to nothing is skipped and the palette compacts — set only slots 1 and 3 and the palette is two colors, in that order.
127
124
 
128
- Only slot 1 has a default, and it names `--wp-admin-theme-color` first, so series colors follow the WordPress admin color scheme with no host configuration.
125
+ Only slot 1 is declared in the catalog, and it names `--wp-admin-theme-color` first, so series colors follow the WordPress admin color scheme with no host configuration. Slots 2 to 5 are names a consumer can declare, nothing more — the catalog leaves them undeclared so an unset slot resolves to nothing and compacts out, rather than repeating a color nobody chose.
129
126
 
130
127
  The design system's brand token is the next leg rather than the first, because it only reaches the admin color scheme when a WPDS **root provider** is on the page. Measured on a live WordPress 7.1 wp-admin dashboard, `<html data-wpds-root-provider>` carries the whole generated ramp inline, derived from `--wp-admin-theme-color`. Where no root provider boots, the token falls back to the plain stylesheet rule — a static `#3858e9` with no reference to the admin color. So on WP 7.1 either order happens to work; everywhere else (WP 7.0.x, a page without a root provider, Calypso, SSR) only this one does.
131
128
 
@@ -135,12 +132,9 @@ Precedence for a series color, highest first:
135
132
 
136
133
  1. `options.stroke` on that series, resolved at the chart element. This is the per-series override.
137
134
  2. A CSS declaration of `--a8c-charts-color-series-N`, on the usual catalog rules above.
138
- 3. `theme.colors[ N - 1 ]`, which publishes slot N's theme layer. Deprecated — see below.
139
- 4. The catalog default, which exists only for slot 1.
140
-
141
- The palette is resolved per provider, so one `ColorCache` and one group-to-color map serve every chart under it and siblings agree on what a group is colored. The consequence is that a slot set on a *chart's own* element does not apply — the palette was resolved at the provider wrapper before that element existed. Use `options.stroke` for a per-chart color.
135
+ 3. The catalog default, which exists only for slot 1.
142
136
 
143
- `theme.colors` is deprecated sugar over the slots: entry N publishes slot N's theme layer through the same mechanism as every other mapped field, so a CSS declaration still outranks it and a short array leaves the later slots unset rather than blank. Entries past the fifth are ignored, with a one-time console warning. It is removed in CHARTS-263.
137
+ The palette is resolved per provider, so one `ColorCache` and one group-to-color map serve every chart under it and siblings agree on what a group is colored. Two consequences. A slot set on a *chart's own* element does not apply — the palette was resolved at the provider wrapper before that element existed; use `options.stroke` for a per-chart color. And the palette is read once, when the provider mounts, so a slot changed at runtime needs the provider to remount before it is seen.
144
138
 
145
139
  ## Non-color roles
146
140
 
@@ -151,6 +145,8 @@ The palette is resolved per provider, so one `ColorCache` and one group-to-color
151
145
  | `--a8c-charts-border-radius-bar` | `--wpds-border-radius-md` | `4px` |
152
146
  | `--a8c-charts-border-radius-cell` | `--wpds-border-radius-sm` | `2px` |
153
147
  | `--a8c-charts-border-radius-leaderboard-bar` | _(none — pill shape, no WPDS radius fits)_ | `9999px` |
148
+ | `--a8c-charts-dimension-leaderboard-row-gap` | `--wpds-dimension-gap-md` | `12px` |
149
+ | `--a8c-charts-dimension-leaderboard-column-gap` | `--wpds-dimension-gap-xs` | `4px` |
154
150
  | `--a8c-charts-elevation-xs` | _(none — `--wpds-elevation-*` removed in theme 1.0.0)_ | `0 1px 1px 0 #00000008, 0 1px 2px 0 #00000005, 0 3px 3px 0 #00000005, 0 4px 4px 0 #00000003` |
155
151
  | `--a8c-charts-elevation-sm` | _(none — `--wpds-elevation-*` removed in theme 1.0.0)_ | `0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005` |
156
152
 
@@ -175,7 +171,7 @@ The heatmap Tier-2 variables and `--a8c-charts-dimension-leaderboard-bar-hover-i
175
171
 
176
172
  ## Removed override variables
177
173
 
178
- Every role in the catalog is a supported override point, on the precedence rules above. These names are not, and no longer resolve — set the replacement instead:
174
+ These names no longer resolve. Set the replacement instead:
179
175
 
180
176
  | Removed | Set instead |
181
177
  |---|---|
@@ -184,5 +180,4 @@ Every role in the catalog is a supported override point, on the precedence rules
184
180
  | `--charts-trend-neutral-color` | `--a8c-charts-color-trend-neutral` |
185
181
  | `--a8c--charts--leaderboard--bar--border-radius` | `--a8c-charts-border-radius-leaderboard-bar` |
186
182
  | `--a8c-charts-color-focus` | `--wpds-color-stroke-focus` |
187
-
188
- Removing the first four also removes a second name for a role, read at its component's own call site rather than through the catalog: each sat inside the role as its fallback — `var(--a8c-charts-*, var(--deprecated-name, …))` — so it applied only where the role itself was unset. No `--a8c-charts-*` role is now read anywhere but bare.
183
+ | `--a8c-charts-color-label-on-fill` | `--a8c-charts-color-label-inverse` |