@automattic/charts 3.1.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/TOKENS.md +80 -46
- package/dist/index.cjs +1224 -662
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -23
- package/dist/index.d.cts +230 -162
- package/dist/index.d.ts +230 -162
- package/dist/index.js +1224 -663
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +38 -11
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +67 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +79 -0
- package/src/charts/bar-list-chart/bar-list-chart.tsx +45 -2
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
- package/src/charts/heatmap-chart/types.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +109 -25
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +74 -3
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/types.ts +4 -1
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/sparkline.tsx +9 -3
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +2 -2
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +38 -10
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +67 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +17 -10
- package/src/hooks/test/use-xychart-theme.test.tsx +63 -5
- package/src/hooks/use-xychart-theme.ts +33 -23
- package/src/index.ts +4 -0
- package/src/providers/chart-context/global-charts-provider.tsx +64 -54
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +35 -0
- package/src/providers/chart-context/test/catalog-pointers.test.ts +61 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -18
- package/src/providers/chart-context/themes.ts +7 -47
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +63 -58
- package/src/styles/test/chart-scope.test.ts +25 -40
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/types.ts +103 -73
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +174 -0
- package/src/utils/get-styles.ts +7 -1
- package/src/utils/index.ts +3 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/visx/types.ts +29 -2
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -87
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -50
- package/src/providers/chart-context/test/sparse-theme.test.ts +0 -35
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -118
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -47
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useTooltip
|
|
1
|
+
import { useTooltip } from '@visx/tooltip';
|
|
2
2
|
import { Stack } from '@wordpress/ui';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { type FC, useRef, useMemo, useEffect, useState, useCallback, useContext } from 'react';
|
|
5
|
+
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
5
6
|
import { usePrefersReducedMotion } from '../../hooks';
|
|
6
7
|
import {
|
|
7
8
|
GlobalChartsProvider,
|
|
@@ -9,11 +10,11 @@ import {
|
|
|
9
10
|
ChartScopeContext,
|
|
10
11
|
useChartId,
|
|
11
12
|
useChartRegistration,
|
|
12
|
-
useGlobalChartsTheme,
|
|
13
13
|
useGlobalChartsContext,
|
|
14
14
|
} from '../../providers';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
16
|
+
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
17
|
+
import { formatPercentage } from '../../utils';
|
|
17
18
|
import styles from './conversion-funnel-chart.module.scss';
|
|
18
19
|
import { useFunnelSelection } from './private';
|
|
19
20
|
import type { FunnelStep, ConversionFunnelChartProps } from './types';
|
|
@@ -53,7 +54,6 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
53
54
|
renderTooltip,
|
|
54
55
|
} ) => {
|
|
55
56
|
const chartId = useChartId( providedChartId );
|
|
56
|
-
const { conversionFunnelChart: conversionFunnelChartSettings } = useGlobalChartsTheme();
|
|
57
57
|
const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
|
|
58
58
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
59
59
|
const selectedBarRef = useRef< HTMLDivElement | null >( null );
|
|
@@ -66,27 +66,9 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
66
66
|
// Use custom hook for selection management
|
|
67
67
|
const { handleBarClick, handleBarKeyDown, clearSelection, getStepState } =
|
|
68
68
|
useFunnelSelection( hideTooltip );
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
TooltipInPortal,
|
|
72
|
-
containerBounds,
|
|
73
|
-
} = useTooltipInPortal( {
|
|
74
|
-
// use TooltipWithBounds for boundary detection
|
|
75
|
-
detectBounds: true,
|
|
76
|
-
// when tooltip containers are scrolled, this will correctly update the Tooltip position
|
|
77
|
-
scroll: true,
|
|
78
|
-
} );
|
|
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).
|
|
69
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
70
|
+
// Stable identity so React doesn't detach/reattach (and re-render the scope context) on every commit. Keep it firing on unmount (node === null).
|
|
87
71
|
const setChartRef = useCallback( ( node: HTMLDivElement | null ) => {
|
|
88
|
-
// Set containerRef for @visx coordinate system
|
|
89
|
-
portalContainerRefRef.current( node );
|
|
90
72
|
chartRef.current = node;
|
|
91
73
|
setScopeNode( node );
|
|
92
74
|
}, [] );
|
|
@@ -110,43 +92,32 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
110
92
|
[ showTooltip ]
|
|
111
93
|
);
|
|
112
94
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
( event: React.MouseEvent ) => {
|
|
120
|
-
// Don't return coords until container bounds are measured
|
|
121
|
-
if ( containerBounds.width === 0 || containerBounds.height === 0 ) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
95
|
+
// The chart root positions the tooltip, so coordinates are taken relative to it.
|
|
96
|
+
const getMouseTooltipCoords = useCallback( ( event: React.MouseEvent ) => {
|
|
97
|
+
const bounds = chartRef.current?.getBoundingClientRect();
|
|
98
|
+
if ( ! bounds ) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
124
101
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
[ containerBounds.width, containerBounds.height, containerBounds.left, containerBounds.top ]
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// Helper function to get tooltip coordinates for keyboard events
|
|
134
|
-
// Use fresh getBoundingClientRect() and subtract containerBounds to cancel out stale offset.
|
|
135
|
-
const getKeyboardTooltipCoords = useCallback(
|
|
136
|
-
( event: React.KeyboardEvent ) => {
|
|
137
|
-
// Don't return coords until container bounds are measured
|
|
138
|
-
if ( containerBounds.width === 0 || containerBounds.height === 0 ) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
102
|
+
return {
|
|
103
|
+
x: event.clientX - bounds.left,
|
|
104
|
+
y: event.clientY - bounds.top,
|
|
105
|
+
};
|
|
106
|
+
}, [] );
|
|
141
107
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
108
|
+
// Keyboard interaction anchors the tooltip at the top centre of the focused step.
|
|
109
|
+
const getKeyboardTooltipCoords = useCallback( ( event: React.KeyboardEvent ) => {
|
|
110
|
+
const bounds = chartRef.current?.getBoundingClientRect();
|
|
111
|
+
if ( ! bounds ) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
116
|
+
return {
|
|
117
|
+
x: rect.left + rect.width / 2 - bounds.left,
|
|
118
|
+
y: rect.top - bounds.top,
|
|
119
|
+
};
|
|
120
|
+
}, [] );
|
|
150
121
|
|
|
151
122
|
// Helper function to handle step interaction (both click and keyboard)
|
|
152
123
|
const handleStepInteraction = useCallback(
|
|
@@ -246,32 +217,26 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
246
217
|
// Resolve height: explicit height prop > style.height > default 100%
|
|
247
218
|
const resolvedHeight = height ?? style?.height ?? '100%';
|
|
248
219
|
|
|
249
|
-
//
|
|
250
|
-
const { primaryColor, backgroundColor, positiveChangeColor, negativeChangeColor } =
|
|
251
|
-
conversionFunnelChartSettings;
|
|
252
|
-
|
|
253
|
-
// Resolve bar color using getElementStyles with primaryColor as override
|
|
220
|
+
// The bar takes the first palette slot; move `--a8c-charts-color-series-1` to change it.
|
|
254
221
|
const { color: barColor } = getElementStyles
|
|
255
|
-
? getElementStyles( {
|
|
256
|
-
|
|
257
|
-
overrideColor: primaryColor,
|
|
258
|
-
} )
|
|
259
|
-
: { color: primaryColor || '#000000' };
|
|
222
|
+
? getElementStyles( { index: 0 } )
|
|
223
|
+
: { color: '#000000' };
|
|
260
224
|
|
|
261
|
-
// Determine change indicator color
|
|
262
225
|
const isPositiveChange = changeIndicator?.startsWith( '+' );
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Create light background version of primary color if not set
|
|
266
|
-
const barBackgroundColor =
|
|
267
|
-
backgroundColor || hexToRgba( barColor, 0.08 ) || 'rgba(0, 0, 0, 0.08)';
|
|
226
|
+
// Only for `renderMainMetric`, which hands a consumer's own markup something to paint with.
|
|
227
|
+
const changeColor = isPositiveChange ? CATALOG_POINTERS.trendUp : CATALOG_POINTERS.trendDown;
|
|
268
228
|
|
|
269
229
|
// Default main metric rendering function
|
|
270
230
|
const renderDefaultMainMetric = () => (
|
|
271
231
|
<>
|
|
272
232
|
<span className={ styles[ 'main-rate' ] }>{ formatPercentage( mainRate ) }</span>
|
|
273
233
|
{ changeIndicator && (
|
|
274
|
-
<span
|
|
234
|
+
<span
|
|
235
|
+
className={ clsx(
|
|
236
|
+
styles[ 'change-indicator' ],
|
|
237
|
+
styles[ isPositiveChange ? 'change-indicator--positive' : 'change-indicator--negative' ]
|
|
238
|
+
) }
|
|
239
|
+
>
|
|
275
240
|
{ changeIndicator }
|
|
276
241
|
</span>
|
|
277
242
|
) }
|
|
@@ -322,6 +287,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
322
287
|
data-testid="conversion-funnel-chart"
|
|
323
288
|
ref={ setScopeNode }
|
|
324
289
|
className={ clsx(
|
|
290
|
+
'conversion-funnel-chart',
|
|
325
291
|
styles[ 'conversion-funnel-chart' ],
|
|
326
292
|
loading && styles[ 'conversion-funnel-chart--loading' ],
|
|
327
293
|
className
|
|
@@ -339,112 +305,104 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
339
305
|
const maxRate = Math.max( ...steps.map( step => step.rate ) );
|
|
340
306
|
|
|
341
307
|
return (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
308
|
+
<Stack
|
|
309
|
+
direction="column"
|
|
310
|
+
gap="xl"
|
|
311
|
+
data-testid="conversion-funnel-chart"
|
|
312
|
+
ref={ setChartRef }
|
|
313
|
+
className={ clsx(
|
|
314
|
+
'conversion-funnel-chart',
|
|
315
|
+
styles[ 'conversion-funnel-chart' ],
|
|
316
|
+
loading && styles[ 'conversion-funnel-chart--loading' ],
|
|
317
|
+
className
|
|
318
|
+
) }
|
|
319
|
+
style={ { ...style, height: resolvedHeight } }
|
|
320
|
+
>
|
|
321
|
+
<ChartScopeContext.Provider value={ scopeNode }>
|
|
322
|
+
{ /* Main Metric */ }
|
|
323
|
+
{ renderMainMetric ? (
|
|
324
|
+
renderMainMetric( {
|
|
325
|
+
mainRate,
|
|
326
|
+
changeIndicator,
|
|
327
|
+
className: styles[ 'main-metric' ],
|
|
328
|
+
changeColor,
|
|
329
|
+
} )
|
|
330
|
+
) : (
|
|
331
|
+
<Stack direction="row" align="baseline" gap="sm" className={ styles[ 'main-metric' ] }>
|
|
332
|
+
{ renderDefaultMainMetric() }
|
|
333
|
+
</Stack>
|
|
352
334
|
) }
|
|
353
|
-
style={ { ...style, height: resolvedHeight } }
|
|
354
|
-
>
|
|
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
335
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
336
|
+
{ /* Funnel Steps */ }
|
|
337
|
+
<Stack direction="row" align="flex-end" gap="lg" className={ styles[ 'funnel-container' ] }>
|
|
338
|
+
{ steps.map( ( step, index ) => {
|
|
339
|
+
const barHeight = ( step.rate / maxRate ) * 100;
|
|
340
|
+
const { isBlurred } = getStepState( step.id );
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<Stack
|
|
344
|
+
key={ step.id }
|
|
345
|
+
direction="column"
|
|
346
|
+
data-testid="funnel-step"
|
|
347
|
+
className={ clsx(
|
|
348
|
+
styles[ 'funnel-step' ],
|
|
349
|
+
isColorPaletteResolved && styles[ 'funnel-step--animated' ],
|
|
350
|
+
isBlurred && styles[ 'funnel-step--blurred' ]
|
|
351
|
+
) }
|
|
352
|
+
gap="xl"
|
|
353
|
+
>
|
|
354
|
+
{ /* Step Label and Rate */ }
|
|
355
|
+
<Stack direction="column" gap="xs">
|
|
356
|
+
{ renderStepLabel ? (
|
|
357
|
+
renderStepLabel( {
|
|
358
|
+
step,
|
|
359
|
+
index,
|
|
360
|
+
className: styles[ 'step-label' ],
|
|
361
|
+
} )
|
|
362
|
+
) : (
|
|
363
|
+
<span className={ styles[ 'step-label' ] }>{ step.label }</span>
|
|
364
|
+
) }
|
|
365
|
+
{ renderStepRate ? (
|
|
366
|
+
renderStepRate( {
|
|
367
|
+
step,
|
|
368
|
+
index,
|
|
369
|
+
className: styles[ 'step-rate' ],
|
|
370
|
+
} )
|
|
371
|
+
) : (
|
|
372
|
+
<span className={ styles[ 'step-rate' ] }>
|
|
373
|
+
{ formatPercentage( step.rate ) }
|
|
374
|
+
</span>
|
|
375
|
+
) }
|
|
376
|
+
</Stack>
|
|
377
|
+
|
|
378
|
+
{ /* Funnel Bar */ }
|
|
382
379
|
<Stack
|
|
383
|
-
key={ step.id }
|
|
384
380
|
direction="column"
|
|
385
|
-
|
|
386
|
-
className={
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
381
|
+
justify="flex-end"
|
|
382
|
+
className={ styles[ 'bar-container' ] }
|
|
383
|
+
onClick={ stepHandlers.get( step.id )?.onClick }
|
|
384
|
+
onKeyDown={ stepHandlers.get( step.id )?.onKeyDown }
|
|
385
|
+
role="button"
|
|
386
|
+
tabIndex={ isBlurred ? -1 : 0 }
|
|
387
|
+
aria-label={ step.label }
|
|
392
388
|
>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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>
|
|
389
|
+
<div
|
|
390
|
+
className={ clsx( styles[ 'funnel-bar' ], {
|
|
391
|
+
[ styles[ 'funnel-bar--animated' ] ]:
|
|
392
|
+
animation && ! loading && ! prefersReducedMotion,
|
|
393
|
+
} ) }
|
|
394
|
+
style={ {
|
|
395
|
+
height: `${ barHeight }%`,
|
|
396
|
+
backgroundColor: barColor,
|
|
397
|
+
} }
|
|
398
|
+
/>
|
|
440
399
|
</Stack>
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
</
|
|
445
|
-
</
|
|
400
|
+
</Stack>
|
|
401
|
+
);
|
|
402
|
+
} ) }
|
|
403
|
+
</Stack>
|
|
404
|
+
</ChartScopeContext.Provider>
|
|
446
405
|
|
|
447
|
-
{ /* Tooltip Portal */ }
|
|
448
406
|
{ tooltipOpen &&
|
|
449
407
|
tooltipData &&
|
|
450
408
|
( () => {
|
|
@@ -462,18 +420,16 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
462
420
|
if ( ! tooltipContent ) return null;
|
|
463
421
|
|
|
464
422
|
return (
|
|
465
|
-
<
|
|
466
|
-
// set this to random so it correctly updates with parent bounds
|
|
467
|
-
key={ Math.random() }
|
|
423
|
+
<BoundedTooltip
|
|
468
424
|
top={ tooltipTop }
|
|
469
425
|
left={ tooltipLeft }
|
|
470
|
-
className={ clsx(
|
|
426
|
+
className={ clsx( standaloneScopeClass, styles[ 'tooltip-wrapper' ] ) }
|
|
471
427
|
>
|
|
472
428
|
{ tooltipContent }
|
|
473
|
-
</
|
|
429
|
+
</BoundedTooltip>
|
|
474
430
|
);
|
|
475
431
|
} )() }
|
|
476
|
-
|
|
432
|
+
</Stack>
|
|
477
433
|
);
|
|
478
434
|
};
|
|
479
435
|
|
|
@@ -473,3 +473,49 @@ describe( 'ConversionFunnelChart', () => {
|
|
|
473
473
|
} );
|
|
474
474
|
} );
|
|
475
475
|
} );
|
|
476
|
+
|
|
477
|
+
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
478
|
+
// else the charts measure (wrapper, clipping lookups) reports the container.
|
|
479
|
+
const mockRects = () =>
|
|
480
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
481
|
+
this: Element
|
|
482
|
+
) {
|
|
483
|
+
const box = this.classList.contains( 'visx-tooltip' );
|
|
484
|
+
return {
|
|
485
|
+
left: box ? 0 : 100,
|
|
486
|
+
top: box ? 0 : 50,
|
|
487
|
+
width: box ? 120 : 400,
|
|
488
|
+
height: box ? 40 : 300,
|
|
489
|
+
right: box ? 120 : 500,
|
|
490
|
+
bottom: box ? 40 : 350,
|
|
491
|
+
x: box ? 0 : 100,
|
|
492
|
+
y: box ? 0 : 50,
|
|
493
|
+
toJSON: () => ( {} ),
|
|
494
|
+
} as DOMRect;
|
|
495
|
+
} );
|
|
496
|
+
|
|
497
|
+
describe( 'ConversionFunnelChart tooltip position', () => {
|
|
498
|
+
afterEach( () => {
|
|
499
|
+
jest.restoreAllMocks();
|
|
500
|
+
} );
|
|
501
|
+
|
|
502
|
+
it( 'places the box relative to the chart root, at the click plus the offsets', async () => {
|
|
503
|
+
mockRects();
|
|
504
|
+
const user = userEvent.setup();
|
|
505
|
+
renderWithoutTheme( <ConversionFunnelChart { ...defaultProps } /> );
|
|
506
|
+
|
|
507
|
+
const bar = screen.getAllByRole( 'button' )[ 0 ];
|
|
508
|
+
await user.pointer( [
|
|
509
|
+
{ target: bar, coords: { clientX: 180, clientY: 140 } },
|
|
510
|
+
{ keys: '[MouseLeft]', target: bar, coords: { clientX: 180, clientY: 140 } },
|
|
511
|
+
] );
|
|
512
|
+
|
|
513
|
+
// (180, 140) is (80, 90) inside the root; the chart lifts the anchor 10px
|
|
514
|
+
// above the click, and the box adds 10px each way.
|
|
515
|
+
const box = screen.getByTestId( 'bounded-tooltip' );
|
|
516
|
+
expect( screen.getByTestId( 'conversion-funnel-chart' ) ).toContainElement( box );
|
|
517
|
+
expect( box ).toHaveStyle( {
|
|
518
|
+
transform: 'translate(90px, 90px)',
|
|
519
|
+
} );
|
|
520
|
+
} );
|
|
521
|
+
} );
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useChartScopeElement,
|
|
15
15
|
useGlobalChartsContext,
|
|
16
16
|
} from '../../providers';
|
|
17
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
17
18
|
import { lightenHexColor, normalizeColorToHex } from '../../utils/color-utils';
|
|
18
19
|
import { resolveCssVariable } from '../../utils/resolve-css-var';
|
|
19
20
|
import { sanitizeHtml } from '../../utils/sanitize-html';
|
|
@@ -144,13 +145,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
144
145
|
onError,
|
|
145
146
|
renderPlaceholder,
|
|
146
147
|
} ) => {
|
|
147
|
-
const {
|
|
148
|
-
getElementStyles,
|
|
149
|
-
theme: {
|
|
150
|
-
geoChart: { featureFillColor },
|
|
151
|
-
backgroundColor,
|
|
152
|
-
},
|
|
153
|
-
} = useGlobalChartsContext();
|
|
148
|
+
const { getElementStyles } = useGlobalChartsContext();
|
|
154
149
|
const scopeElement = useChartScopeElement();
|
|
155
150
|
const containerRef = useRef< HTMLDivElement >( null );
|
|
156
151
|
const reportedErrorIdsRef = useRef< Set< string > >( new Set() );
|
|
@@ -212,10 +207,10 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
212
207
|
const lightColorHex = lightenHexColor( fullColorHex, 0.8 );
|
|
213
208
|
// 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.
|
|
214
209
|
const backgroundColorHex =
|
|
215
|
-
normalizeColorToHex(
|
|
210
|
+
normalizeColorToHex( CATALOG_POINTERS.background, scopeElement, resolveCssVariable ) ||
|
|
216
211
|
DEFAULT_BACKGROUND_COLOR;
|
|
217
212
|
const defaultFillColorHex =
|
|
218
|
-
normalizeColorToHex(
|
|
213
|
+
normalizeColorToHex( CATALOG_POINTERS.surfaceSecondary, scopeElement, resolveCssVariable ) ||
|
|
219
214
|
DEFAULT_FEATURE_FILL_COLOR;
|
|
220
215
|
|
|
221
216
|
// Identify HTML tooltip column indices and sanitize their content to prevent XSS.
|
|
@@ -306,7 +301,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
306
301
|
ref={ containerRef }
|
|
307
302
|
className={ clsx( 'geo-chart', styles.container, className ) }
|
|
308
303
|
data-testid="geo-chart"
|
|
309
|
-
style={ { width, height
|
|
304
|
+
style={ { width, height } }
|
|
310
305
|
>
|
|
311
306
|
<Chart
|
|
312
307
|
chartType="GeoChart"
|
|
@@ -142,7 +142,7 @@ describe( 'GeoChart', () => {
|
|
|
142
142
|
} );
|
|
143
143
|
|
|
144
144
|
describe( 'Chart Options', () => {
|
|
145
|
-
test( 'configures color axis
|
|
145
|
+
test( 'configures a two-stop color axis from the palette', () => {
|
|
146
146
|
renderWithTheme();
|
|
147
147
|
|
|
148
148
|
const chartOptions = screen.getByTestId( 'chart-options' );
|
|
@@ -152,7 +152,7 @@ describe( 'GeoChart', () => {
|
|
|
152
152
|
expect( options.colorAxis.colors ).toHaveLength( 2 );
|
|
153
153
|
} );
|
|
154
154
|
|
|
155
|
-
test( 'sets datalessRegionColor from
|
|
155
|
+
test( 'sets datalessRegionColor from the surface-secondary role', () => {
|
|
156
156
|
renderWithTheme();
|
|
157
157
|
|
|
158
158
|
const chartOptions = screen.getByTestId( 'chart-options' );
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Focus-ring width shared across the grid and cell-selected focus outlines. A
|
|
2
2
|
// Sass var, not a custom property: a compile-time constant scoped to this
|
|
3
3
|
// module, so it needs no runtime cascade and never lands on the DOM.
|
|
4
|
-
$focus-ring-width: var(--wpds-border-width-focus
|
|
4
|
+
$focus-ring-width: var(--wpds-border-width-focus);
|
|
5
5
|
|
|
6
6
|
.heatmap-chart {
|
|
7
|
+
position: relative;
|
|
7
8
|
width: 100%;
|
|
8
9
|
height: 100%;
|
|
9
10
|
min-height: 0;
|
|
10
|
-
font-size: var(--wpds-typography-font-size-sm
|
|
11
|
+
font-size: var(--wpds-typography-font-size-sm);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// Height-capped grids are content-sized vertically, so the wrapper hugs them
|
|
@@ -18,7 +19,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
.heatmap-chart__empty {
|
|
21
|
-
padding: var(--wpds-dimension-padding-lg
|
|
22
|
+
padding: var(--wpds-dimension-padding-lg);
|
|
22
23
|
color: var(--a8c-charts-color-label-secondary);
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -28,9 +29,9 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
28
29
|
display: grid;
|
|
29
30
|
// --a8c-charts-dimension-heatmap-cell-gap is component-emitted from JS per
|
|
30
31
|
// render, not a catalog entry. Do not collapse this to a bare var(); the
|
|
31
|
-
// inner WPDS fallback is the only value this
|
|
32
|
-
// is unset.
|
|
33
|
-
gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs
|
|
32
|
+
// inner WPDS token (fallback injected at build) is the only value this
|
|
33
|
+
// property has when the JS var is unset.
|
|
34
|
+
gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs));
|
|
34
35
|
flex: 1 1 0;
|
|
35
36
|
width: 100%;
|
|
36
37
|
// Override flex's auto minimum so short widgets do not scroll.
|
|
@@ -39,9 +40,9 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
39
40
|
&:focus-visible {
|
|
40
41
|
outline:
|
|
41
42
|
$focus-ring-width solid
|
|
42
|
-
var(--wpds-color-stroke-focus
|
|
43
|
+
var(--wpds-color-stroke-focus);
|
|
43
44
|
outline-offset: 2px;
|
|
44
|
-
border-radius: var(--wpds-border-radius-sm
|
|
45
|
+
border-radius: var(--wpds-border-radius-sm);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -68,7 +69,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
68
69
|
.heatmap-chart__col-label,
|
|
69
70
|
.heatmap-chart__row-label {
|
|
70
71
|
color: var(--a8c-charts-color-label-secondary);
|
|
71
|
-
font-size: var(--wpds-typography-font-size-xs
|
|
72
|
+
font-size: var(--wpds-typography-font-size-xs);
|
|
72
73
|
white-space: nowrap;
|
|
73
74
|
overflow: visible;
|
|
74
75
|
}
|
|
@@ -82,7 +83,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
82
83
|
align-self: center;
|
|
83
84
|
justify-self: end;
|
|
84
85
|
text-align: right;
|
|
85
|
-
padding-inline-end: var(--wpds-dimension-padding-xs
|
|
86
|
+
padding-inline-end: var(--wpds-dimension-padding-xs);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
.heatmap-chart__cell {
|
|
@@ -115,19 +116,19 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
115
116
|
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
116
117
|
// an empty cell.
|
|
117
118
|
.heatmap-chart__cell--filled {
|
|
118
|
-
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-
|
|
119
|
+
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-background, #fff));
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
.heatmap-chart__cell--selected {
|
|
122
123
|
outline:
|
|
123
124
|
$focus-ring-width solid
|
|
124
|
-
var(--wpds-color-stroke-focus
|
|
125
|
+
var(--wpds-color-stroke-focus);
|
|
125
126
|
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
.heatmap-chart__cell-value {
|
|
129
130
|
color: var(--a8c-charts-color-label);
|
|
130
|
-
font-size: var(--wpds-typography-font-size-md
|
|
131
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
// Light text on fills dark enough to out-contrast dark text (decided in JS from
|
|
@@ -136,9 +137,13 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
136
137
|
color: var(--a8c-charts-color-label-inverse);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
.heatmap-chart__legend-label {
|
|
141
|
+
color: var(--a8c-charts-color-label);
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
.heatmap-chart__legend-swatch {
|
|
140
|
-
width: var(--wpds-dimension-size-3xs
|
|
141
|
-
height: var(--wpds-dimension-size-3xs
|
|
145
|
+
width: var(--wpds-dimension-size-3xs);
|
|
146
|
+
height: var(--wpds-dimension-size-3xs);
|
|
142
147
|
border-radius: var(--a8c-charts-border-radius-cell);
|
|
143
|
-
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-
|
|
148
|
+
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-background, #fff));
|
|
144
149
|
}
|