@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
@@ -12,7 +12,7 @@
12
12
 
13
13
  .main-rate {
14
14
  overflow: hidden;
15
- color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
15
+ color: var(--a8c-charts-color-label);
16
16
  text-overflow: ellipsis;
17
17
  font-size: var(--wpds-typography-font-size-xl, 18px);
18
18
  font-style: normal;
@@ -54,7 +54,7 @@
54
54
  }
55
55
 
56
56
  .step-label {
57
- color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
57
+ color: var(--a8c-charts-color-label-secondary);
58
58
  font-size: var(--wpds-typography-font-size-sm, 12px);
59
59
  font-weight: var(--wpds-typography-font-weight-default, 400);
60
60
  line-height: var(--wpds-typography-line-height-xs, 16px);
@@ -64,7 +64,7 @@
64
64
  }
65
65
 
66
66
  .step-rate {
67
- color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
67
+ color: var(--a8c-charts-color-label);
68
68
  font-size: var(--wpds-typography-font-size-md, 13px);
69
69
  font-weight: var(--wpds-typography-font-weight-emphasis, 499);
70
70
  line-height: var(--wpds-typography-line-height-xs, 16px);
@@ -72,7 +72,7 @@
72
72
 
73
73
  .bar-container {
74
74
  flex: 1;
75
- border-radius: var(--wpds-border-radius-md, 4px);
75
+ border-radius: var(--a8c-charts-border-radius-bar);
76
76
  position: relative;
77
77
  cursor: pointer;
78
78
  }
@@ -80,19 +80,24 @@
80
80
  .funnel-bar {
81
81
  width: 100%;
82
82
  min-height: 4px;
83
- border-radius: var(--wpds-border-radius-md, 4px) var(--wpds-border-radius-md, 4px) 0 0;
83
+ border-radius: var(--a8c-charts-border-radius-bar) var(--a8c-charts-border-radius-bar) 0 0;
84
84
 
85
+ // Keep the collapsed state in `from` and never on the element: a dropped
86
+ // animation then fails visible. `both`, not `forwards`, for backwards fill.
85
87
  &--animated {
86
88
  transform-origin: bottom;
87
89
  transform-box: fill-box;
88
- transform: scaleY(0);
89
90
  animation:
90
- stretch var(--wpds-motion-duration-xl, 400ms)
91
- var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
91
+ stretch var(--a8c-charts-motion-duration-entrance)
92
+ var(--a8c-charts-motion-easing-entrance) both;
92
93
  }
93
94
 
94
95
  @keyframes stretch {
95
96
 
97
+ from {
98
+ transform: scaleY(0);
99
+ }
100
+
96
101
  to {
97
102
  transform: scaleY(1);
98
103
  }
@@ -100,7 +105,7 @@
100
105
  }
101
106
 
102
107
  .tooltip-wrapper {
103
- background: var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff));
108
+ background: var(--a8c-charts-color-surface);
104
109
 
105
110
  // Override .visx-tooltip inline styles.
106
111
  border-radius: var(--wpds-border-radius-md, 4px) !important;
@@ -110,7 +115,7 @@
110
115
  }
111
116
 
112
117
  .tooltip-title {
113
- color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
118
+ color: var(--a8c-charts-color-label);
114
119
  font-size: var(--wpds-typography-font-size-sm, 12px);
115
120
  font-style: normal;
116
121
  font-weight: var(--wpds-typography-font-weight-default, 400);
@@ -118,7 +123,7 @@
118
123
  }
119
124
 
120
125
  .tooltip-content {
121
- color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
126
+ color: var(--a8c-charts-color-label);
122
127
  font-size: var(--wpds-typography-font-size-md, 13px);
123
128
  font-style: normal;
124
129
  font-weight: var(--wpds-typography-font-weight-emphasis, 499);
@@ -127,6 +132,6 @@
127
132
 
128
133
  .empty-state {
129
134
  text-align: center;
130
- color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
135
+ color: var(--a8c-charts-color-label-secondary);
131
136
  font-size: var(--wpds-typography-font-size-lg, 16px);
132
137
  }
@@ -1,16 +1,18 @@
1
1
  import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
2
2
  import { Stack } from '@wordpress/ui';
3
3
  import clsx from 'clsx';
4
- import { type FC, useRef, useMemo, useEffect, useCallback, useContext } from 'react';
4
+ import { type FC, useRef, useMemo, useEffect, useState, useCallback, useContext } from 'react';
5
5
  import { usePrefersReducedMotion } from '../../hooks';
6
6
  import {
7
7
  GlobalChartsProvider,
8
8
  GlobalChartsContext,
9
+ ChartScopeContext,
9
10
  useChartId,
10
11
  useChartRegistration,
11
12
  useGlobalChartsTheme,
12
13
  useGlobalChartsContext,
13
14
  } from '../../providers';
15
+ import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
14
16
  import { formatPercentage, hexToRgba } from '../../utils';
15
17
  import styles from './conversion-funnel-chart.module.scss';
16
18
  import { useFunnelSelection } from './private';
@@ -55,6 +57,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
55
57
  const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
56
58
  const chartRef = useRef< HTMLDivElement >( null );
57
59
  const selectedBarRef = useRef< HTMLDivElement | null >( null );
60
+ const [ scopeNode, setScopeNode ] = useState< HTMLElement | null >( null );
58
61
 
59
62
  // Use @visx/tooltip hooks for tooltip positioning
60
63
  const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } =
@@ -74,6 +77,20 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
74
77
  scroll: true,
75
78
  } );
76
79
 
80
+ // `useTooltipInPortal` returns a fresh `containerRef` closure on every render, so depending on it directly would give this callback a new identity every render — React then detaches and reattaches the ref on every commit, flushing the scope context to null and back. Reading it through a ref keeps the callback's identity genuinely stable.
81
+ const portalContainerRefRef = useRef( portalContainerRef );
82
+ useEffect( () => {
83
+ portalContainerRefRef.current = portalContainerRef;
84
+ }, [ portalContainerRef ] );
85
+
86
+ // Stable identity so React doesn't detach/reattach (and re-render the scope context) on every commit. Keep all three assignments, in the same order, and keep it firing on unmount (node === null).
87
+ const setChartRef = useCallback( ( node: HTMLDivElement | null ) => {
88
+ // Set containerRef for @visx coordinate system
89
+ portalContainerRefRef.current( node );
90
+ chartRef.current = node;
91
+ setScopeNode( node );
92
+ }, [] );
93
+
77
94
  // Wrapper to clear selectedBarRef after clearing selection
78
95
  const clearSelectionAndRef = useCallback( () => {
79
96
  clearSelection();
@@ -303,6 +320,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
303
320
  align="center"
304
321
  justify="center"
305
322
  data-testid="conversion-funnel-chart"
323
+ ref={ setScopeNode }
306
324
  className={ clsx(
307
325
  styles[ 'conversion-funnel-chart' ],
308
326
  loading && styles[ 'conversion-funnel-chart--loading' ],
@@ -326,11 +344,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
326
344
  direction="column"
327
345
  gap="xl"
328
346
  data-testid="conversion-funnel-chart"
329
- ref={ node => {
330
- // Set containerRef for @visx coordinate system
331
- portalContainerRef( node );
332
- chartRef.current = node;
333
- } }
347
+ ref={ setChartRef }
334
348
  className={ clsx(
335
349
  styles[ 'conversion-funnel-chart' ],
336
350
  loading && styles[ 'conversion-funnel-chart--loading' ],
@@ -338,89 +352,96 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
338
352
  ) }
339
353
  style={ { ...style, height: resolvedHeight } }
340
354
  >
341
- { /* Main Metric */ }
342
- { renderMainMetric ? (
343
- renderMainMetric( {
344
- mainRate,
345
- changeIndicator,
346
- className: styles[ 'main-metric' ],
347
- changeColor,
348
- } )
349
- ) : (
350
- <Stack direction="row" align="baseline" gap="sm" className={ styles[ 'main-metric' ] }>
351
- { renderDefaultMainMetric() }
352
- </Stack>
353
- ) }
354
-
355
- { /* Funnel Steps */ }
356
- <Stack direction="row" align="flex-end" gap="lg" className={ styles[ 'funnel-container' ] }>
357
- { steps.map( ( step, index ) => {
358
- const barHeight = ( step.rate / maxRate ) * 100;
359
- const { isBlurred } = getStepState( step.id );
360
-
361
- return (
362
- <Stack
363
- key={ step.id }
364
- direction="column"
365
- data-testid="funnel-step"
366
- className={ clsx(
367
- styles[ 'funnel-step' ],
368
- isColorPaletteResolved && styles[ 'funnel-step--animated' ],
369
- isBlurred && styles[ 'funnel-step--blurred' ]
370
- ) }
371
- gap="xl"
372
- >
373
- { /* Step Label and Rate */ }
374
- <Stack direction="column" gap="xs">
375
- { renderStepLabel ? (
376
- renderStepLabel( {
377
- step,
378
- index,
379
- className: styles[ 'step-label' ],
380
- } )
381
- ) : (
382
- <span className={ styles[ 'step-label' ] }>{ step.label }</span>
383
- ) }
384
- { renderStepRate ? (
385
- renderStepRate( {
386
- step,
387
- index,
388
- className: styles[ 'step-rate' ],
389
- } )
390
- ) : (
391
- <span className={ styles[ 'step-rate' ] }>
392
- { formatPercentage( step.rate ) }
393
- </span>
394
- ) }
395
- </Stack>
396
-
397
- { /* Funnel Bar */ }
355
+ <ChartScopeContext.Provider value={ scopeNode }>
356
+ { /* Main Metric */ }
357
+ { renderMainMetric ? (
358
+ renderMainMetric( {
359
+ mainRate,
360
+ changeIndicator,
361
+ className: styles[ 'main-metric' ],
362
+ changeColor,
363
+ } )
364
+ ) : (
365
+ <Stack direction="row" align="baseline" gap="sm" className={ styles[ 'main-metric' ] }>
366
+ { renderDefaultMainMetric() }
367
+ </Stack>
368
+ ) }
369
+
370
+ { /* Funnel Steps */ }
371
+ <Stack
372
+ direction="row"
373
+ align="flex-end"
374
+ gap="lg"
375
+ className={ styles[ 'funnel-container' ] }
376
+ >
377
+ { steps.map( ( step, index ) => {
378
+ const barHeight = ( step.rate / maxRate ) * 100;
379
+ const { isBlurred } = getStepState( step.id );
380
+
381
+ return (
398
382
  <Stack
383
+ key={ step.id }
399
384
  direction="column"
400
- justify="flex-end"
401
- className={ styles[ 'bar-container' ] }
402
- onClick={ stepHandlers.get( step.id )?.onClick }
403
- onKeyDown={ stepHandlers.get( step.id )?.onKeyDown }
404
- role="button"
405
- tabIndex={ isBlurred ? -1 : 0 }
406
- aria-label={ step.label }
407
- style={ { backgroundColor: barBackgroundColor } }
385
+ data-testid="funnel-step"
386
+ className={ clsx(
387
+ styles[ 'funnel-step' ],
388
+ isColorPaletteResolved && styles[ 'funnel-step--animated' ],
389
+ isBlurred && styles[ 'funnel-step--blurred' ]
390
+ ) }
391
+ gap="xl"
408
392
  >
409
- <div
410
- className={ clsx( styles[ 'funnel-bar' ], {
411
- [ styles[ 'funnel-bar--animated' ] ]:
412
- animation && ! loading && ! prefersReducedMotion,
413
- } ) }
414
- style={ {
415
- height: `${ barHeight }%`,
416
- backgroundColor: barColor,
417
- } }
418
- />
393
+ { /* Step Label and Rate */ }
394
+ <Stack direction="column" gap="xs">
395
+ { renderStepLabel ? (
396
+ renderStepLabel( {
397
+ step,
398
+ index,
399
+ className: styles[ 'step-label' ],
400
+ } )
401
+ ) : (
402
+ <span className={ styles[ 'step-label' ] }>{ step.label }</span>
403
+ ) }
404
+ { renderStepRate ? (
405
+ renderStepRate( {
406
+ step,
407
+ index,
408
+ className: styles[ 'step-rate' ],
409
+ } )
410
+ ) : (
411
+ <span className={ styles[ 'step-rate' ] }>
412
+ { formatPercentage( step.rate ) }
413
+ </span>
414
+ ) }
415
+ </Stack>
416
+
417
+ { /* Funnel Bar */ }
418
+ <Stack
419
+ direction="column"
420
+ justify="flex-end"
421
+ className={ styles[ 'bar-container' ] }
422
+ onClick={ stepHandlers.get( step.id )?.onClick }
423
+ onKeyDown={ stepHandlers.get( step.id )?.onKeyDown }
424
+ role="button"
425
+ tabIndex={ isBlurred ? -1 : 0 }
426
+ aria-label={ step.label }
427
+ style={ { backgroundColor: barBackgroundColor } }
428
+ >
429
+ <div
430
+ className={ clsx( styles[ 'funnel-bar' ], {
431
+ [ styles[ 'funnel-bar--animated' ] ]:
432
+ animation && ! loading && ! prefersReducedMotion,
433
+ } ) }
434
+ style={ {
435
+ height: `${ barHeight }%`,
436
+ backgroundColor: barColor,
437
+ } }
438
+ />
439
+ </Stack>
419
440
  </Stack>
420
- </Stack>
421
- );
422
- } ) }
423
- </Stack>
441
+ );
442
+ } ) }
443
+ </Stack>
444
+ </ChartScopeContext.Provider>
424
445
  </Stack>
425
446
 
426
447
  { /* Tooltip Portal */ }
@@ -446,7 +467,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
446
467
  key={ Math.random() }
447
468
  top={ tooltipTop }
448
469
  left={ tooltipLeft }
449
- className={ styles[ 'tooltip-wrapper' ] }
470
+ className={ clsx( CHART_SCOPE_CLASS, styles[ 'tooltip-wrapper' ] ) }
450
471
  >
451
472
  { tooltipContent }
452
473
  </TooltipInPortal>
@@ -0,0 +1,36 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { useContext } from 'react';
3
+ import { ChartScopeContext } from '../../../providers/chart-scope';
4
+ import ConversionFunnelChart from '../conversion-funnel-chart';
5
+ import type { FunnelStep } from '../types';
6
+
7
+ const steps: FunnelStep[] = [
8
+ { id: 'sessions', label: 'Sessions', rate: 100, count: 10000 },
9
+ { id: 'purchase', label: 'Purchase', rate: 10.3, count: 1030 },
10
+ ];
11
+
12
+ const ScopeProbe = () => {
13
+ const scopeNode = useContext( ChartScopeContext );
14
+ return <span data-testid="scope-probe">{ scopeNode ? 'has-scope' : 'no-scope' }</span>;
15
+ };
16
+
17
+ const renderScopeProbe = () => <ScopeProbe />;
18
+
19
+ describe( 'ConversionFunnelChart chart scope', () => {
20
+ it( 'publishes a non-null scope node after transitioning from empty to populated steps', () => {
21
+ // The normal loading pattern: steps arrives empty first (empty-state branch renders), then populated (main branch renders). The scope node must be republished on that transition, not frozen at whatever the first mount saw.
22
+ const { rerender } = render(
23
+ <ConversionFunnelChart mainRate={ 0 } steps={ [] } renderMainMetric={ renderScopeProbe } />
24
+ );
25
+
26
+ rerender(
27
+ <ConversionFunnelChart
28
+ mainRate={ 10.3 }
29
+ steps={ steps }
30
+ renderMainMetric={ renderScopeProbe }
31
+ />
32
+ );
33
+
34
+ expect( screen.getByTestId( 'scope-probe' ) ).toHaveTextContent( 'has-scope' );
35
+ } );
36
+ } );
@@ -8,7 +8,12 @@ import { Chart, type GoogleChartPackages, type ReactGoogleChartEvent } from 'rea
8
8
  /**
9
9
  * Internal dependencies
10
10
  */
11
- import { GlobalChartsContext, GlobalChartsProvider, useGlobalChartsContext } from '../../providers';
11
+ import {
12
+ GlobalChartsContext,
13
+ GlobalChartsProvider,
14
+ useChartScopeElement,
15
+ useGlobalChartsContext,
16
+ } from '../../providers';
12
17
  import { lightenHexColor, normalizeColorToHex } from '../../utils/color-utils';
13
18
  import { resolveCssVariable } from '../../utils/resolve-css-var';
14
19
  import { sanitizeHtml } from '../../utils/sanitize-html';
@@ -146,6 +151,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
146
151
  backgroundColor,
147
152
  },
148
153
  } = useGlobalChartsContext();
154
+ const scopeElement = useChartScopeElement();
149
155
  const containerRef = useRef< HTMLDivElement >( null );
150
156
  const reportedErrorIdsRef = useRef< Set< string > >( new Set() );
151
157
 
@@ -204,11 +210,13 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
204
210
  // Google charts doesn't accept CSS variables, so we need to convert them to hex colors
205
211
  const fullColorHex = getElementStyles( { index: 0 } ).color;
206
212
  const lightColorHex = lightenHexColor( fullColorHex, 0.8 );
207
- // Use normalizeColorToHex to ensure HSL/RGB values from CSS variables are converted to hex
213
+ // Use normalizeColorToHex to ensure HSL/RGB values from CSS variables are converted to hex. Google Charts takes a resolved-hex snapshot at render, so this does not live-update on a theme change without a re-render — an accepted asymmetry vs CSS-painted elements.
208
214
  const backgroundColorHex =
209
- normalizeColorToHex( backgroundColor, null, resolveCssVariable ) || DEFAULT_BACKGROUND_COLOR;
215
+ normalizeColorToHex( backgroundColor, scopeElement, resolveCssVariable ) ||
216
+ DEFAULT_BACKGROUND_COLOR;
210
217
  const defaultFillColorHex =
211
- normalizeColorToHex( featureFillColor, null, resolveCssVariable ) || DEFAULT_FEATURE_FILL_COLOR;
218
+ normalizeColorToHex( featureFillColor, scopeElement, resolveCssVariable ) ||
219
+ DEFAULT_FEATURE_FILL_COLOR;
212
220
 
213
221
  // Identify HTML tooltip column indices and sanitize their content to prevent XSS.
214
222
  const sanitizedData = useMemo( () => {
@@ -19,13 +19,17 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
19
19
 
20
20
  .heatmap-chart__empty {
21
21
  padding: var(--wpds-dimension-padding-lg, 16px);
22
- color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
22
+ color: var(--a8c-charts-color-label-secondary);
23
23
  }
24
24
 
25
25
  // Track template is set inline because CSS `repeat()` won't take a `var()`
26
26
  // count. ARIA rows are `display: contents` so their cells join this grid.
27
27
  .heatmap-chart__grid {
28
28
  display: grid;
29
+ // --a8c-charts-dimension-heatmap-cell-gap is component-emitted from JS per
30
+ // render, not a catalog entry. Do not collapse this to a bare var(); the
31
+ // inner WPDS fallback is the only value this property has when the JS var
32
+ // is unset.
29
33
  gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
30
34
  flex: 1 1 0;
31
35
  width: 100%;
@@ -35,7 +39,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
35
39
  &:focus-visible {
36
40
  outline:
37
41
  $focus-ring-width solid
38
- var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
42
+ var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
39
43
  outline-offset: 2px;
40
44
  border-radius: var(--wpds-border-radius-sm, 2px);
41
45
  }
@@ -63,7 +67,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
63
67
 
64
68
  .heatmap-chart__col-label,
65
69
  .heatmap-chart__row-label {
66
- color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
70
+ color: var(--a8c-charts-color-label-secondary);
67
71
  font-size: var(--wpds-typography-font-size-xs, 11px);
68
72
  white-space: nowrap;
69
73
  overflow: visible;
@@ -87,9 +91,9 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
87
91
  justify-content: center;
88
92
  min-width: 0;
89
93
  min-height: 0;
90
- border-radius: var(--wpds-border-radius-sm, 2px);
94
+ border-radius: var(--a8c-charts-border-radius-cell);
91
95
  // Empty (no-data) default; cells with a value override it below.
92
- background: var(--a8c-charts-color-track, var(--wpds-color-background-track-neutral-weak, #f0f0f0));
96
+ background: var(--a8c-charts-color-track);
93
97
  }
94
98
 
95
99
  // An empty grid slot (calendar ragged edges): keeps its track but paints and
@@ -108,24 +112,24 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
108
112
  .heatmap-chart__cell--selected {
109
113
  outline:
110
114
  $focus-ring-width solid
111
- var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
115
+ var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
112
116
  outline-offset: calc(-1 * #{$focus-ring-width});
113
117
  }
114
118
 
115
119
  .heatmap-chart__cell-value {
116
- color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
120
+ color: var(--a8c-charts-color-label);
117
121
  font-size: var(--wpds-typography-font-size-md, 13px);
118
122
  }
119
123
 
120
124
  // Light text on fills dark enough to out-contrast dark text (decided in JS from
121
125
  // the blended fill luminance).
122
126
  .heatmap-chart__cell--strong .heatmap-chart__cell-value {
123
- color: var(--a8c-charts-color-label-inverse, var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0));
127
+ color: var(--a8c-charts-color-label-inverse);
124
128
  }
125
129
 
126
130
  .heatmap-chart__legend-swatch {
127
131
  width: var(--wpds-dimension-size-3xs, 12px);
128
132
  height: var(--wpds-dimension-size-3xs, 12px);
129
- border-radius: var(--wpds-border-radius-sm, 2px);
133
+ border-radius: var(--a8c-charts-border-radius-cell);
130
134
  background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
131
135
  }
@@ -6,9 +6,11 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'r
6
6
  import {
7
7
  GlobalChartsProvider,
8
8
  useChartId,
9
+ useChartScopeElement,
9
10
  useGlobalChartsContext,
10
11
  GlobalChartsContext,
11
12
  } from '../../providers';
13
+ import { CHART_SCOPE_CLASS } from '../../styles/chart-scope-class';
12
14
  import { attachSubComponents } from '../../utils';
13
15
  import {
14
16
  isValidHexColor,
@@ -16,6 +18,7 @@ import {
16
18
  normalizeColorToHex,
17
19
  prefersLightText,
18
20
  } from '../../utils/color-utils';
21
+ import { resolveCssVariable } from '../../utils/resolve-css-var';
19
22
  import { Center } from '../private/center';
20
23
  import { useChartChildren } from '../private/chart-composition';
21
24
  import { ChartLayout } from '../private/chart-layout';
@@ -58,7 +61,8 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
58
61
  children,
59
62
  } ) => {
60
63
  const chartId = useChartId( providedChartId );
61
- const { getElementStyles, resolveThemeColor, theme } = useGlobalChartsContext();
64
+ const { getElementStyles, theme } = useGlobalChartsContext();
65
+ const scopeElement = useChartScopeElement();
62
66
  const { heatmapChart: heatmapChartSettings } = theme;
63
67
  const { nonLegendChildren } = useChartChildren( children, 'HeatmapChart' );
64
68
 
@@ -79,9 +83,12 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
79
83
  overrideColor: primaryColor || heatmapChartSettings.primaryColor,
80
84
  } );
81
85
 
82
- // Resolve the background in the provider's theme scope so the blended-fill text
83
- // color tracks a themed (e.g. dark) background.
84
- const chartBackgroundHex = resolveThemeColor( theme.backgroundColor );
86
+ // Resolve the background against this chart's own scope element (not the provider's), matching where `--a8c-charts-color-heatmap-background` is substituted for the cell blend below — a chart-level override otherwise disagrees with a provider-level read.
87
+ const chartBackgroundHex = normalizeColorToHex(
88
+ theme.backgroundColor,
89
+ scopeElement,
90
+ resolveCssVariable
91
+ );
85
92
 
86
93
  // Choose text color from the blended fill, not the raw value.
87
94
  // If either color cannot resolve to hex, keep dark text.
@@ -443,7 +450,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
443
450
  </div>
444
451
  { withTooltips && tooltipOpen && tooltipData && (
445
452
  <TooltipInPortal top={ tooltipTop } left={ tooltipLeft }>
446
- <div role="tooltip" tabIndex={ -1 }>
453
+ <div className={ CHART_SCOPE_CLASS } role="tooltip" tabIndex={ -1 }>
447
454
  { ( renderTooltip ?? defaultRenderTooltip )( tooltipData ) }
448
455
  </div>
449
456
  </TooltipInPortal>
@@ -60,20 +60,26 @@
60
60
  // row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
61
61
  min-width: var(--wpds-dimension-size-5xs, 4px);
62
62
 
63
- border-radius: var(--a8c-charts-border-radius-leaderboard-bar, var(--a8c--charts--leaderboard--bar--border-radius, 9999px));
63
+ border-radius: var(--a8c-charts-border-radius-leaderboard-bar);
64
64
  z-index: -1; /* places it behind the label */
65
65
 
66
+ // Keep the collapsed state in `from` and never on the element: a dropped
67
+ // animation then fails visible. `both`, not `forwards`, for the
68
+ // backwards fill.
66
69
  &--animated {
67
70
  transform-origin: left;
68
71
  transform-box: fill-box;
69
- transform: scaleX(0);
70
72
  animation:
71
- stretch var(--wpds-motion-duration-xl, 400ms)
72
- var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
73
+ stretch var(--a8c-charts-motion-duration-entrance)
74
+ var(--a8c-charts-motion-easing-entrance) both;
73
75
  }
74
76
 
75
77
  @keyframes stretch {
76
78
 
79
+ from {
80
+ transform: scaleX(0);
81
+ }
82
+
77
83
  to {
78
84
  transform: scaleX(1);
79
85
  }
@@ -104,7 +110,7 @@
104
110
  .emptyState {
105
111
  padding: var(--wpds-dimension-padding-3xl, 32px) var(--wpds-dimension-padding-lg, 16px);
106
112
  text-align: center;
107
- color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
113
+ color: var(--a8c-charts-color-label-secondary);
108
114
  font-size: var(--wpds-typography-font-size-md, 13px);
109
115
  font-style: italic;
110
116
  }
@@ -205,7 +211,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
205
211
  border-radius: var(--wpds-border-radius-sm, 2px);
206
212
  outline:
207
213
  $focus-ring-width solid
208
- var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
214
+ var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
209
215
  // Inset by its own width so the outer edge hugs the row boundary: the
210
216
  // outline then sits in the shared row padding, clear of the bar, and stays
211
217
  // inside the overflow:auto container that would clip an outset ring.
@@ -220,7 +226,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
220
226
  bottom: 0;
221
227
  margin-block: auto;
222
228
  opacity: 0;
223
- color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
229
+ color: var(--a8c-charts-color-label-secondary);
224
230
  transition:
225
231
  opacity var(--wpds-motion-duration-sm, 100ms)
226
232
  var(--wpds-motion-easing-subtle, cubic-bezier(0.15, 0, 0.15, 1));
@@ -65,8 +65,6 @@ export interface LeaderboardChartProps
65
65
  */
66
66
  style?: React.CSSProperties & {
67
67
  '--a8c-charts-border-radius-leaderboard-bar'?: string;
68
- /** @deprecated Use `--a8c-charts-border-radius-leaderboard-bar`. */
69
- '--a8c--charts--leaderboard--bar--border-radius'?: string;
70
68
  };
71
69
 
72
70
  /**
@@ -1,14 +1,15 @@
1
1
  .line-chart {
2
2
  position: relative;
3
3
 
4
+ // Keep the collapsed state in `from` and never on the element: a dropped
5
+ // animation then fails visible. `both`, not `forwards`, for backwards fill.
4
6
  &--animated {
5
7
 
6
8
  path {
7
9
  transform-origin: 0 95%;
8
- transform: scaleY(0);
9
10
  animation:
10
- rise var(--wpds-motion-duration-xl, 400ms)
11
- var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
11
+ rise var(--a8c-charts-motion-duration-entrance)
12
+ var(--a8c-charts-motion-easing-entrance) both;
12
13
  }
13
14
  }
14
15
 
@@ -18,8 +19,8 @@
18
19
 
19
20
  &__tooltip,
20
21
  &__annotation-label-popover {
21
- background: var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff));
22
- color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
22
+ background: var(--a8c-charts-color-surface);
23
+ color: var(--a8c-charts-color-label);
23
24
  padding: var(--wpds-dimension-padding-sm, 8px);
24
25
  }
25
26
 
@@ -110,6 +111,10 @@
110
111
 
111
112
  @keyframes rise {
112
113
 
114
+ from {
115
+ transform: scaleY(0);
116
+ }
117
+
113
118
  to {
114
119
  transform: scaleY(1);
115
120
  }