@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.
- package/CHANGELOG.md +28 -0
- package/TOKENS.md +107 -48
- package/dist/index.cjs +584 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +97 -43
- package/dist/index.d.cts +46 -41
- package/dist/index.d.ts +46 -41
- package/dist/index.js +582 -210
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/area-chart/area-chart.module.scss +8 -3
- package/src/charts/area-chart/area-chart.tsx +3 -5
- package/src/charts/bar-chart/bar-chart.module.scss +14 -6
- package/src/charts/bar-chart/bar-chart.tsx +24 -15
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +197 -30
- package/src/charts/bar-chart/test/bar-chart.test.tsx +685 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +17 -12
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
- package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
- package/src/charts/geo-chart/geo-chart.tsx +12 -4
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +13 -9
- package/src/charts/heatmap-chart/heatmap-chart.tsx +12 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
- package/src/charts/leaderboard-chart/types.ts +0 -2
- package/src/charts/line-chart/line-chart.module.scss +10 -5
- package/src/charts/line-chart/line-chart.tsx +3 -5
- package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
- package/src/charts/pie-chart/pie-chart.tsx +4 -1
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -1
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/time-axis.test.ts +247 -2
- package/src/charts/private/time-axis.ts +252 -63
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/components/legend/private/base-legend.tsx +3 -1
- package/src/components/legend/test/legend.test.tsx +2 -1
- package/src/components/tooltip/accessible-tooltip.tsx +4 -2
- package/src/components/tooltip/base-tooltip.tsx +8 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +22 -1
- package/src/components/trend-indicator/trend-indicator.module.scss +3 -6
- package/src/components/trend-indicator/trend-indicator.tsx +3 -0
- package/src/hooks/test/use-chart-margin.test.tsx +60 -0
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +89 -0
- package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
- package/src/hooks/use-chart-margin.tsx +3 -4
- package/src/hooks/use-xychart-theme.ts +19 -11
- package/src/index.ts +9 -2
- package/src/providers/chart-context/global-charts-provider.tsx +35 -12
- package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
- package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +0 -40
- package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
- package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
- package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
- package/src/providers/chart-context/themes.ts +18 -27
- package/src/providers/chart-context/types.ts +0 -3
- package/src/providers/chart-scope/chart-scope-context.ts +8 -0
- package/src/providers/chart-scope/index.ts +3 -0
- package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
- package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
- package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
- package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
- package/src/providers/index.ts +1 -0
- package/src/styles/chart-scope-class.ts +6 -0
- package/src/styles/chart-scope.scss +83 -0
- package/src/styles/test/chart-scope.test.ts +165 -0
- package/src/types.ts +6 -40
- package/src/utils/index.ts +1 -1
- package/src/utils/resolve-css-var.ts +69 -28
- package/src/utils/test/create-css-variable-resolver.test.ts +83 -0
- package/src/charts/private/grid-control/grid-control.module.scss +0 -8
- package/src/charts/private/grid-control/grid-control.tsx +0 -26
- package/src/charts/private/grid-control/index.ts +0 -1
- 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
|
|
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
|
|
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
|
|
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(--
|
|
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(--
|
|
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(--
|
|
91
|
-
var(--
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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={
|
|
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
|
-
{
|
|
342
|
-
|
|
343
|
-
renderMainMetric(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
401
|
-
className={
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
</
|
|
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 {
|
|
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,
|
|
215
|
+
normalizeColorToHex( backgroundColor, scopeElement, resolveCssVariable ) ||
|
|
216
|
+
DEFAULT_BACKGROUND_COLOR;
|
|
210
217
|
const defaultFillColorHex =
|
|
211
|
-
normalizeColorToHex( featureFillColor,
|
|
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
|
|
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(--
|
|
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
|
|
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(--
|
|
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
|
|
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(--
|
|
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
|
|
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
|
|
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(--
|
|
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,
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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(--
|
|
72
|
-
var(--
|
|
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
|
|
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(--
|
|
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
|
|
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(--
|
|
11
|
-
var(--
|
|
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-
|
|
22
|
-
color: var(--a8c-charts-color-label
|
|
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
|
}
|