@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,8 +1,9 @@
|
|
|
1
1
|
import { formatNumber, formatNumberCompact } from '@automattic/number-formatters';
|
|
2
|
-
import { useTooltip
|
|
2
|
+
import { useTooltip } from '@visx/tooltip';
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
6
7
|
import {
|
|
7
8
|
GlobalChartsProvider,
|
|
8
9
|
useChartId,
|
|
@@ -10,7 +11,8 @@ import {
|
|
|
10
11
|
useGlobalChartsContext,
|
|
11
12
|
GlobalChartsContext,
|
|
12
13
|
} from '../../providers';
|
|
13
|
-
import {
|
|
14
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
15
|
+
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
14
16
|
import { attachSubComponents } from '../../utils';
|
|
15
17
|
import {
|
|
16
18
|
isValidHexColor,
|
|
@@ -69,23 +71,28 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
69
71
|
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >();
|
|
70
72
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, showTooltip, hideTooltip } =
|
|
71
73
|
useTooltip< HeatmapTooltipData >();
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
75
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
76
|
+
// The chart root positions the tooltip, so pointer and cell coordinates are
|
|
77
|
+
// measured against it — found by its id rather than by walking up, so
|
|
78
|
+
// whatever ChartLayout wraps the grid in cannot shift the origin.
|
|
79
|
+
const getTooltipOrigin = useCallback(
|
|
80
|
+
() =>
|
|
81
|
+
containerRef.current
|
|
82
|
+
?.closest( `[data-chart-id="heatmap-chart-${ chartId }"]` )
|
|
83
|
+
?.getBoundingClientRect() ?? null,
|
|
84
|
+
[ chartId ]
|
|
85
|
+
);
|
|
80
86
|
|
|
81
87
|
const { color: primaryColorHex } = getElementStyles( {
|
|
82
88
|
index: 0,
|
|
83
|
-
overrideColor: primaryColor
|
|
89
|
+
overrideColor: primaryColor,
|
|
84
90
|
} );
|
|
85
91
|
|
|
86
|
-
//
|
|
92
|
+
// The cell blend substitutes this role at the cell; this read happens at the scope
|
|
93
|
+
// element, so an override on the chart's own class makes the two disagree. CHARTS-255.
|
|
87
94
|
const chartBackgroundHex = normalizeColorToHex(
|
|
88
|
-
|
|
95
|
+
CATALOG_POINTERS.background,
|
|
89
96
|
scopeElement,
|
|
90
97
|
resolveCssVariable
|
|
91
98
|
);
|
|
@@ -212,20 +219,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
212
219
|
if ( ! withTooltips ) {
|
|
213
220
|
return;
|
|
214
221
|
}
|
|
222
|
+
const origin = getTooltipOrigin();
|
|
223
|
+
if ( ! origin ) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
215
226
|
const target = event.currentTarget;
|
|
216
227
|
const columnIndex = Number( target.dataset.column );
|
|
217
228
|
const rowIndex = Number( target.dataset.row );
|
|
218
|
-
// Read bounds from the ref (like the keyboard-tooltip effect) so this
|
|
219
|
-
// callback stays stable across renders.
|
|
220
|
-
const bounds = containerBoundsRef.current;
|
|
221
|
-
// TooltipInPortal re-adds containerBounds, so subtract it to land at the cursor.
|
|
222
229
|
showTooltip( {
|
|
223
|
-
tooltipLeft: event.clientX -
|
|
224
|
-
tooltipTop: event.clientY -
|
|
230
|
+
tooltipLeft: event.clientX - origin.left,
|
|
231
|
+
tooltipTop: event.clientY - origin.top,
|
|
225
232
|
tooltipData: buildTooltipData( columnIndex, rowIndex ),
|
|
226
233
|
} );
|
|
227
234
|
},
|
|
228
|
-
[ withTooltips, showTooltip, buildTooltipData ]
|
|
235
|
+
[ withTooltips, showTooltip, buildTooltipData, getTooltipOrigin ]
|
|
229
236
|
);
|
|
230
237
|
|
|
231
238
|
const handleCellMouseLeave = useCallback( () => {
|
|
@@ -241,6 +248,10 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
241
248
|
if ( ! withTooltips || selectedIndex === undefined ) {
|
|
242
249
|
return;
|
|
243
250
|
}
|
|
251
|
+
const origin = getTooltipOrigin();
|
|
252
|
+
if ( ! origin ) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
244
255
|
const col = Math.floor( selectedIndex / rows );
|
|
245
256
|
const row = selectedIndex % rows;
|
|
246
257
|
const cell =
|
|
@@ -248,13 +259,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
248
259
|
? document.getElementById( `${ chartId }-cell-${ col }-${ row }` )
|
|
249
260
|
: null;
|
|
250
261
|
const rect = cell?.getBoundingClientRect();
|
|
251
|
-
const bounds = containerBoundsRef.current;
|
|
252
262
|
showTooltip( {
|
|
253
|
-
tooltipLeft: rect ? rect.left + rect.width / 2 -
|
|
254
|
-
tooltipTop: rect ? rect.top + rect.height / 2 -
|
|
263
|
+
tooltipLeft: rect ? rect.left + rect.width / 2 - origin.left : 0,
|
|
264
|
+
tooltipTop: rect ? rect.top + rect.height / 2 - origin.top : 0,
|
|
255
265
|
tooltipData: buildTooltipData( col, row ),
|
|
256
266
|
} );
|
|
257
|
-
}, [
|
|
267
|
+
}, [
|
|
268
|
+
selectedIndex,
|
|
269
|
+
withTooltips,
|
|
270
|
+
rows,
|
|
271
|
+
chartId,
|
|
272
|
+
buildTooltipData,
|
|
273
|
+
showTooltip,
|
|
274
|
+
getTooltipOrigin,
|
|
275
|
+
] );
|
|
258
276
|
|
|
259
277
|
const defaultRenderTooltip = useCallback(
|
|
260
278
|
( info: HeatmapTooltipData ) => (
|
|
@@ -296,7 +314,6 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
296
314
|
: `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
|
|
297
315
|
const gridStyle: Record< string, string | number > = {
|
|
298
316
|
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
299
|
-
'--a8c-charts-color-heatmap-background': theme.backgroundColor,
|
|
300
317
|
gridTemplateColumns: `auto repeat(${ columns }, ${ columnTrack })`,
|
|
301
318
|
gridTemplateRows: `auto repeat(${ rows }, ${ rowTrack })`,
|
|
302
319
|
};
|
|
@@ -472,11 +489,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
472
489
|
} ) }
|
|
473
490
|
</div>
|
|
474
491
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
475
|
-
<
|
|
476
|
-
<div className={
|
|
492
|
+
<BoundedTooltip top={ tooltipTop } left={ tooltipLeft }>
|
|
493
|
+
<div className={ standaloneScopeClass } role="tooltip" tabIndex={ -1 }>
|
|
477
494
|
{ ( renderTooltip ?? defaultRenderTooltip )( tooltipData ) }
|
|
478
495
|
</div>
|
|
479
|
-
</
|
|
496
|
+
</BoundedTooltip>
|
|
480
497
|
) }
|
|
481
498
|
</ChartLayout>
|
|
482
499
|
</ChartInstanceContext.Provider>
|
|
@@ -53,6 +53,9 @@ const toDate = ( point: DataPointDate ): Date | null => {
|
|
|
53
53
|
return null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
// Labels stay English on the browser's calendar, unlike the time axis: this is a
|
|
57
|
+
// plain function the host calls outside the provider, so `GlobalChartsProvider`'s
|
|
58
|
+
// `locale` and `timeZone` cannot reach it. CHARTS-266 has the detail.
|
|
56
59
|
export const buildCalendarHeatmapData = (
|
|
57
60
|
series: DataPointDate[],
|
|
58
61
|
options: {
|
|
@@ -15,7 +15,7 @@ export interface HeatmapLegendProps {
|
|
|
15
15
|
|
|
16
16
|
export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel, moreLabel } ) => {
|
|
17
17
|
const context = useContext( HeatmapContext );
|
|
18
|
-
const { legend
|
|
18
|
+
const { legend } = useGlobalChartsTheme();
|
|
19
19
|
if ( ! context ) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
@@ -24,7 +24,11 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
26
|
<Stack direction="row" gap="xs" align="center">
|
|
27
|
-
<Text
|
|
27
|
+
<Text
|
|
28
|
+
variant="body-sm"
|
|
29
|
+
className={ styles[ 'heatmap-chart__legend-label' ] }
|
|
30
|
+
style={ labelStyle }
|
|
31
|
+
>
|
|
28
32
|
{ lessLabel ?? __( 'Less', 'jetpack-charts' ) }
|
|
29
33
|
</Text>
|
|
30
34
|
<Stack direction="row" gap="xs">
|
|
@@ -38,7 +42,6 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
38
42
|
style={
|
|
39
43
|
{
|
|
40
44
|
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
41
|
-
'--a8c-charts-color-heatmap-background': backgroundColor,
|
|
42
45
|
'--a8c-charts-heatmap-cell-intensity': intensity,
|
|
43
46
|
} as CSSProperties
|
|
44
47
|
}
|
|
@@ -46,7 +49,11 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
46
49
|
);
|
|
47
50
|
} ) }
|
|
48
51
|
</Stack>
|
|
49
|
-
<Text
|
|
52
|
+
<Text
|
|
53
|
+
variant="body-sm"
|
|
54
|
+
className={ styles[ 'heatmap-chart__legend-label' ] }
|
|
55
|
+
style={ labelStyle }
|
|
56
|
+
>
|
|
50
57
|
{ moreLabel ?? __( 'More', 'jetpack-charts' ) }
|
|
51
58
|
</Text>
|
|
52
59
|
</Stack>
|
|
@@ -352,24 +352,21 @@ describe( 'HeatmapChart', () => {
|
|
|
352
352
|
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#abcdef' );
|
|
353
353
|
} );
|
|
354
354
|
|
|
355
|
-
test( '
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
</GlobalChartsProvider>
|
|
360
|
-
);
|
|
361
|
-
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
362
|
-
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
363
|
-
} );
|
|
355
|
+
test( 'falls back to the first palette slot when no primaryColor prop is set', () => {
|
|
356
|
+
const scope = document.createElement( 'div' );
|
|
357
|
+
scope.style.setProperty( '--a8c-charts-color-series-1', '#0a0b0c' );
|
|
358
|
+
document.body.appendChild( scope );
|
|
364
359
|
|
|
365
|
-
test( 'falls back to the palette colors[0] when no prop or theme primaryColor is set', () => {
|
|
366
360
|
render(
|
|
367
|
-
<GlobalChartsProvider
|
|
361
|
+
<GlobalChartsProvider>
|
|
368
362
|
<HeatmapChart width={ 500 } height={ 300 } data={ data } />
|
|
369
|
-
</GlobalChartsProvider
|
|
363
|
+
</GlobalChartsProvider>,
|
|
364
|
+
{ container: scope }
|
|
370
365
|
);
|
|
371
366
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
372
367
|
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
368
|
+
|
|
369
|
+
document.body.removeChild( scope );
|
|
373
370
|
} );
|
|
374
371
|
|
|
375
372
|
test( 'the unresponsive export pins explicit width and height', () => {
|
|
@@ -393,3 +390,45 @@ describe( 'HeatmapChart', () => {
|
|
|
393
390
|
expect( chart ).not.toHaveStyle( { height: '300px' } );
|
|
394
391
|
} );
|
|
395
392
|
} );
|
|
393
|
+
|
|
394
|
+
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
395
|
+
// else the charts measure (wrapper, clipping lookups) reports the container.
|
|
396
|
+
const mockRects = () =>
|
|
397
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
398
|
+
this: Element
|
|
399
|
+
) {
|
|
400
|
+
const box = this.classList.contains( 'visx-tooltip' );
|
|
401
|
+
return {
|
|
402
|
+
left: box ? 0 : 100,
|
|
403
|
+
top: box ? 0 : 50,
|
|
404
|
+
width: box ? 120 : 400,
|
|
405
|
+
height: box ? 40 : 300,
|
|
406
|
+
right: box ? 120 : 500,
|
|
407
|
+
bottom: box ? 40 : 350,
|
|
408
|
+
x: box ? 0 : 100,
|
|
409
|
+
y: box ? 0 : 50,
|
|
410
|
+
toJSON: () => ( {} ),
|
|
411
|
+
} as DOMRect;
|
|
412
|
+
} );
|
|
413
|
+
|
|
414
|
+
describe( 'HeatmapChart tooltip position', () => {
|
|
415
|
+
afterEach( () => {
|
|
416
|
+
jest.restoreAllMocks();
|
|
417
|
+
} );
|
|
418
|
+
|
|
419
|
+
test( 'places the box relative to the chart root, at the pointer plus the offsets', async () => {
|
|
420
|
+
mockRects();
|
|
421
|
+
renderChart( { withTooltips: true, rowLabels: [ 'Mon', 'Tue', 'Wed' ] } );
|
|
422
|
+
|
|
423
|
+
await userEvent.setup().pointer( {
|
|
424
|
+
target: screen.getAllByTestId( 'heatmap-cell' )[ 0 ],
|
|
425
|
+
coords: { clientX: 180, clientY: 140 },
|
|
426
|
+
} );
|
|
427
|
+
|
|
428
|
+
// (180, 140) is (80, 90) inside the root; the box adds 10px each way.
|
|
429
|
+
await expect( screen.findByRole( 'tooltip' ) ).resolves.toBeInTheDocument();
|
|
430
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( {
|
|
431
|
+
transform: 'translate(90px, 100px)',
|
|
432
|
+
} );
|
|
433
|
+
} );
|
|
434
|
+
} );
|
|
@@ -69,8 +69,8 @@ export interface HeatmapChartProps
|
|
|
69
69
|
/** Floor a cell's height (px) in non-compact mode; see `minCellWidth`. */
|
|
70
70
|
minCellHeight?: number;
|
|
71
71
|
/**
|
|
72
|
-
* Color the cell scale interpolates toward at the highest value
|
|
73
|
-
*
|
|
72
|
+
* Color the cell scale interpolates toward at the highest value. Defaults to the
|
|
73
|
+
* first series palette slot, `--a8c-charts-color-series-1`.
|
|
74
74
|
*/
|
|
75
75
|
primaryColor?: string;
|
|
76
76
|
renderTooltip?: ( data: HeatmapTooltipData ) => ReactNode;
|
|
@@ -13,8 +13,8 @@ export const SUBPIXEL_TOLERANCE = 0.5;
|
|
|
13
13
|
* Counts how many leading rows fit inside the content container.
|
|
14
14
|
*
|
|
15
15
|
* Rows are read from the DOM rather than derived from constants: row height
|
|
16
|
-
* depends on
|
|
17
|
-
* as a label, so any restated number would drift.
|
|
16
|
+
* depends on `--a8c-charts-dimension-leaderboard-row-gap`, label wrapping, and
|
|
17
|
+
* whatever a caller renders as a label, so any restated number would drift.
|
|
18
18
|
*
|
|
19
19
|
* Every row is a direct grid child marked with `data-row-index`; interactive rows
|
|
20
20
|
* use a button wrapper and non-interactive rows use a div wrapper.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __ } from '@wordpress/i18n';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useGlobalChartsContext } from '../../../providers';
|
|
4
4
|
import type { BaseLegendItem } from '../../../components/legend';
|
|
5
5
|
import type { LeaderboardEntry } from '../../../types';
|
|
6
6
|
|
|
@@ -35,7 +35,6 @@ export function useLeaderboardLegendItems( {
|
|
|
35
35
|
comparison?: string;
|
|
36
36
|
};
|
|
37
37
|
} ): BaseLegendItem[] {
|
|
38
|
-
const { leaderboardChart: leaderboardChartSettings } = useGlobalChartsTheme();
|
|
39
38
|
const { getElementStyles } = useGlobalChartsContext();
|
|
40
39
|
|
|
41
40
|
return useMemo( () => {
|
|
@@ -48,7 +47,7 @@ export function useLeaderboardLegendItems( {
|
|
|
48
47
|
// Add current period legend item
|
|
49
48
|
const { color: resolvedPrimaryColor } = getElementStyles( {
|
|
50
49
|
index: 0,
|
|
51
|
-
overrideColor: primaryColor
|
|
50
|
+
overrideColor: primaryColor,
|
|
52
51
|
} );
|
|
53
52
|
|
|
54
53
|
items.push( {
|
|
@@ -60,7 +59,7 @@ export function useLeaderboardLegendItems( {
|
|
|
60
59
|
if ( withComparison && ! withOverlayLabel ) {
|
|
61
60
|
const { color: resolvedSecondaryColor } = getElementStyles( {
|
|
62
61
|
index: 1,
|
|
63
|
-
overrideColor: secondaryColor
|
|
62
|
+
overrideColor: secondaryColor,
|
|
64
63
|
} );
|
|
65
64
|
|
|
66
65
|
items.push( {
|
|
@@ -76,7 +75,6 @@ export function useLeaderboardLegendItems( {
|
|
|
76
75
|
secondaryColor,
|
|
77
76
|
withComparison,
|
|
78
77
|
legendLabels,
|
|
79
|
-
leaderboardChartSettings,
|
|
80
78
|
getElementStyles,
|
|
81
79
|
withOverlayLabel,
|
|
82
80
|
] );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.leaderboardChart {
|
|
2
2
|
transition:
|
|
3
|
-
opacity var(--wpds-motion-duration-lg
|
|
4
|
-
var(--wpds-motion-easing-subtle
|
|
3
|
+
opacity var(--wpds-motion-duration-lg)
|
|
4
|
+
var(--wpds-motion-easing-subtle);
|
|
5
5
|
|
|
6
6
|
&--responsive {
|
|
7
7
|
height: 100%;
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
.label {
|
|
52
|
-
padding-left: var(--wpds-dimension-padding-sm
|
|
52
|
+
padding-left: var(--wpds-dimension-padding-sm);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
min-height: 6px;
|
|
59
59
|
// Keeps a sliver of bar visible for tiny shares, and stops the interactive
|
|
60
60
|
// row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
|
|
61
|
-
min-width: var(--wpds-dimension-size-5xs
|
|
61
|
+
min-width: var(--wpds-dimension-size-5xs);
|
|
62
62
|
|
|
63
63
|
border-radius: var(--a8c-charts-border-radius-leaderboard-bar);
|
|
64
64
|
z-index: -1; /* places it behind the label */
|
|
@@ -97,6 +97,18 @@
|
|
|
97
97
|
font-variant-numeric: tabular-nums;
|
|
98
98
|
min-width: 3ch;
|
|
99
99
|
text-align: end;
|
|
100
|
+
|
|
101
|
+
&--down {
|
|
102
|
+
color: var(--a8c-charts-color-trend-down);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&--neutral {
|
|
106
|
+
color: var(--a8c-charts-color-trend-neutral);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&--up {
|
|
110
|
+
color: var(--a8c-charts-color-trend-up);
|
|
111
|
+
}
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
.deltaPlaceholder {
|
|
@@ -108,10 +120,10 @@
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
.emptyState {
|
|
111
|
-
padding: var(--wpds-dimension-padding-3xl
|
|
123
|
+
padding: var(--wpds-dimension-padding-3xl) var(--wpds-dimension-padding-lg);
|
|
112
124
|
text-align: center;
|
|
113
125
|
color: var(--a8c-charts-color-label-secondary);
|
|
114
|
-
font-size: var(--wpds-typography-font-size-md
|
|
126
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
115
127
|
font-style: italic;
|
|
116
128
|
}
|
|
117
129
|
|
|
@@ -133,7 +145,7 @@
|
|
|
133
145
|
// interactive row's outline (and its inset offset) stay equal. A Sass var,
|
|
134
146
|
// not a custom property: a compile-time constant scoped to this module, so it
|
|
135
147
|
// needs no runtime cascade and never lands on the DOM as an unprefixed var.
|
|
136
|
-
$focus-ring-width: var(--wpds-border-width-focus
|
|
148
|
+
$focus-ring-width: var(--wpds-border-width-focus);
|
|
137
149
|
|
|
138
150
|
.row {
|
|
139
151
|
display: grid;
|
|
@@ -169,16 +181,16 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
169
181
|
// value slide is given an RTL override below. No row background.
|
|
170
182
|
.bar {
|
|
171
183
|
transition:
|
|
172
|
-
opacity var(--wpds-motion-duration-sm
|
|
173
|
-
var(--wpds-motion-easing-subtle
|
|
174
|
-
width var(--wpds-motion-duration-sm
|
|
175
|
-
var(--wpds-motion-easing-subtle
|
|
184
|
+
opacity var(--wpds-motion-duration-sm)
|
|
185
|
+
var(--wpds-motion-easing-subtle),
|
|
186
|
+
width var(--wpds-motion-duration-sm)
|
|
187
|
+
var(--wpds-motion-easing-subtle);
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
.valueContainer {
|
|
179
191
|
transition:
|
|
180
|
-
transform var(--wpds-motion-duration-sm
|
|
181
|
-
var(--wpds-motion-easing-subtle
|
|
192
|
+
transform var(--wpds-motion-duration-sm)
|
|
193
|
+
var(--wpds-motion-easing-subtle);
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
&:hover,
|
|
@@ -186,7 +198,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
186
198
|
|
|
187
199
|
// Fixed reserve for the chevron — the value slides by it in full, and the
|
|
188
200
|
// bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
|
|
189
|
-
--a8c-charts-dimension-leaderboard-bar-hover-inset: var(--wpds-dimension-gap-2xl
|
|
201
|
+
--a8c-charts-dimension-leaderboard-bar-hover-inset: var(--wpds-dimension-gap-2xl);
|
|
190
202
|
|
|
191
203
|
.bar {
|
|
192
204
|
opacity: 0.5;
|
|
@@ -208,10 +220,10 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
&:focus-visible {
|
|
211
|
-
border-radius: var(--wpds-border-radius-sm
|
|
223
|
+
border-radius: var(--wpds-border-radius-sm);
|
|
212
224
|
outline:
|
|
213
225
|
$focus-ring-width solid
|
|
214
|
-
var(--wpds-color-stroke-focus
|
|
226
|
+
var(--wpds-color-stroke-focus);
|
|
215
227
|
// Inset by its own width so the outer edge hugs the row boundary: the
|
|
216
228
|
// outline then sits in the shared row padding, clear of the bar, and stays
|
|
217
229
|
// inside the overflow:auto container that would clip an outset ring.
|
|
@@ -221,15 +233,15 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
221
233
|
|
|
222
234
|
.chevron {
|
|
223
235
|
position: absolute;
|
|
224
|
-
inset-inline-end: var(--wpds-dimension-padding-xs
|
|
236
|
+
inset-inline-end: var(--wpds-dimension-padding-xs);
|
|
225
237
|
top: 0;
|
|
226
238
|
bottom: 0;
|
|
227
239
|
margin-block: auto;
|
|
228
240
|
opacity: 0;
|
|
229
241
|
color: var(--a8c-charts-color-label-secondary);
|
|
230
242
|
transition:
|
|
231
|
-
opacity var(--wpds-motion-duration-sm
|
|
232
|
-
var(--wpds-motion-easing-subtle
|
|
243
|
+
opacity var(--wpds-motion-duration-sm)
|
|
244
|
+
var(--wpds-motion-easing-subtle);
|
|
233
245
|
pointer-events: none;
|
|
234
246
|
|
|
235
247
|
// The glyph points toward the inline end, so flip it to point left in RTL.
|
|
@@ -26,6 +26,15 @@ import styles from './leaderboard-chart.module.scss';
|
|
|
26
26
|
import type { LeaderboardChartProps } from './types';
|
|
27
27
|
import type { LeaderboardEntry } from '../../types';
|
|
28
28
|
|
|
29
|
+
// Handed to `Grid` as a chain rather than set in this component's stylesheet: `Grid` writes
|
|
30
|
+
// its own gaps through an Emotion class, which a module class of equal specificity would not
|
|
31
|
+
// reliably outrank. The chain lands on the element and resolves there.
|
|
32
|
+
const ROW_GAP = 'var(--a8c-charts-dimension-leaderboard-row-gap, 12px)';
|
|
33
|
+
const COLUMN_GAP = 'var(--a8c-charts-dimension-leaderboard-column-gap, 4px)';
|
|
34
|
+
|
|
35
|
+
/** Indexed by `Math.sign( delta ) + 1`, so [negative, neutral, positive]. */
|
|
36
|
+
const DELTA_TREND_CLASS = [ 'deltaValue--down', 'deltaValue--neutral', 'deltaValue--up' ] as const;
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* Default value formatter using formatMetricValue
|
|
31
40
|
*
|
|
@@ -198,22 +207,15 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
198
207
|
|
|
199
208
|
// Process children to extract compound components
|
|
200
209
|
const { legendChildren, nonLegendChildren } = useChartChildren( children, 'LeaderboardChart' );
|
|
201
|
-
const {
|
|
202
|
-
labelSpacing,
|
|
203
|
-
rowGap,
|
|
204
|
-
columnGap,
|
|
205
|
-
primaryColor: settingsPrimaryColor,
|
|
206
|
-
secondaryColor: settingsSecondaryColor,
|
|
207
|
-
deltaColors,
|
|
208
|
-
} = leaderboardChartSettings;
|
|
210
|
+
const { labelSpacing } = leaderboardChartSettings;
|
|
209
211
|
const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
|
|
210
212
|
const { color: resolvedPrimaryColor } = getElementStyles( {
|
|
211
213
|
index: 0,
|
|
212
|
-
overrideColor: primaryColor
|
|
214
|
+
overrideColor: primaryColor,
|
|
213
215
|
} );
|
|
214
216
|
const { color: resolvedSecondaryColor } = getElementStyles( {
|
|
215
217
|
index: 1,
|
|
216
|
-
overrideColor: secondaryColor
|
|
218
|
+
overrideColor: secondaryColor,
|
|
217
219
|
} );
|
|
218
220
|
|
|
219
221
|
// Create legend items using the custom hook
|
|
@@ -292,6 +294,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
292
294
|
legendElement={ false }
|
|
293
295
|
legendChildren={ legendChildren }
|
|
294
296
|
className={ clsx(
|
|
297
|
+
'leaderboard-chart',
|
|
295
298
|
styles.leaderboardChart,
|
|
296
299
|
{
|
|
297
300
|
[ styles[ 'leaderboardChart--responsive' ] ]: ! propWidth && ! propHeight,
|
|
@@ -336,6 +339,7 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
336
339
|
legendElement={ legendElement }
|
|
337
340
|
legendChildren={ legendChildren }
|
|
338
341
|
className={ clsx(
|
|
342
|
+
'leaderboard-chart',
|
|
339
343
|
styles.leaderboardChart,
|
|
340
344
|
{
|
|
341
345
|
[ styles[ 'leaderboardChart--responsive' ] ]: ! propWidth && ! propHeight,
|
|
@@ -372,8 +376,8 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
372
376
|
) : (
|
|
373
377
|
<Grid
|
|
374
378
|
templateColumns="minmax(0, 1fr) auto"
|
|
375
|
-
rowGap={
|
|
376
|
-
columnGap={
|
|
379
|
+
rowGap={ ROW_GAP }
|
|
380
|
+
columnGap={ COLUMN_GAP }
|
|
377
381
|
data-leaderboard-grid
|
|
378
382
|
>
|
|
379
383
|
{ data.map( ( entry, rowIndex ) => {
|
|
@@ -389,8 +393,11 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
389
393
|
const hasDelta = hasDeltaValue( entry );
|
|
390
394
|
const showComparisonValue = showComparisonColumn && hasDelta;
|
|
391
395
|
const showComparisonPlaceholder = showComparisonColumn && ! hasDelta;
|
|
392
|
-
|
|
393
|
-
const
|
|
396
|
+
// Math.sign gives -1/0/1; the placeholder has no delta to read, so it takes neutral.
|
|
397
|
+
const deltaTrendClass =
|
|
398
|
+
styles[
|
|
399
|
+
DELTA_TREND_CLASS[ showComparisonValue ? Math.sign( entry.delta ) + 1 : 1 ]
|
|
400
|
+
];
|
|
394
401
|
|
|
395
402
|
const rowCells = (
|
|
396
403
|
<>
|
|
@@ -417,15 +424,18 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
|
|
|
417
424
|
{ isPrimaryVisible && <Text>{ valueFormatter( entry.currentValue ) }</Text> }
|
|
418
425
|
|
|
419
426
|
{ showComparisonValue && (
|
|
420
|
-
<Text className={ styles.deltaValue
|
|
427
|
+
<Text className={ clsx( styles.deltaValue, deltaTrendClass ) }>
|
|
421
428
|
{ deltaFormatter( entry.delta ) }
|
|
422
429
|
</Text>
|
|
423
430
|
) }
|
|
424
431
|
|
|
425
432
|
{ showComparisonPlaceholder && (
|
|
426
433
|
<Text
|
|
427
|
-
className={ clsx(
|
|
428
|
-
|
|
434
|
+
className={ clsx(
|
|
435
|
+
styles.deltaValue,
|
|
436
|
+
styles.deltaPlaceholder,
|
|
437
|
+
deltaTrendClass
|
|
438
|
+
) }
|
|
429
439
|
>
|
|
430
440
|
<span aria-hidden="true">—</span>
|
|
431
441
|
<VisuallyHidden as="span">
|
|
@@ -665,4 +665,18 @@ describe( 'LeaderboardChart', () => {
|
|
|
665
665
|
expect( screen.getAllByRole( 'button' ) ).toHaveLength( 1 );
|
|
666
666
|
} );
|
|
667
667
|
} );
|
|
668
|
+
|
|
669
|
+
describe( 'grid gaps', () => {
|
|
670
|
+
// eslint-disable-next-line testing-library/no-node-access -- The grid is rendered by `Grid` from @wordpress/components, which takes no test id.
|
|
671
|
+
const grid = () => document.querySelector( '[data-leaderboard-grid]' );
|
|
672
|
+
|
|
673
|
+
it( 'hands the catalog role to the grid', () => {
|
|
674
|
+
render( <LeaderboardChart data={ mockData } /> );
|
|
675
|
+
|
|
676
|
+
expect( grid() ).toHaveStyle( {
|
|
677
|
+
gridRowGap: 'var(--a8c-charts-dimension-leaderboard-row-gap, 12px)',
|
|
678
|
+
gridColumnGap: 'var(--a8c-charts-dimension-leaderboard-column-gap, 4px)',
|
|
679
|
+
} );
|
|
680
|
+
} );
|
|
681
|
+
} );
|
|
668
682
|
} );
|
|
@@ -113,14 +113,8 @@ describe( 'useLeaderboardLegendItems', () => {
|
|
|
113
113
|
} );
|
|
114
114
|
|
|
115
115
|
describe( 'Color handling', () => {
|
|
116
|
-
it( '
|
|
117
|
-
const
|
|
118
|
-
leaderboardChart: {
|
|
119
|
-
primaryColor: '#FF0000',
|
|
120
|
-
secondaryColor: '#00FF00',
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
const wrapper = createWrapper( customTheme );
|
|
116
|
+
it( 'gives both legend items a color from the palette', () => {
|
|
117
|
+
const wrapper = createWrapper();
|
|
124
118
|
const { result } = renderHook(
|
|
125
119
|
() =>
|
|
126
120
|
useLeaderboardLegendItems( {
|
|
@@ -131,7 +125,6 @@ describe( 'useLeaderboardLegendItems', () => {
|
|
|
131
125
|
{ wrapper }
|
|
132
126
|
);
|
|
133
127
|
|
|
134
|
-
// Note: The actual color will be resolved by the context, but we can check structure
|
|
135
128
|
expect( result.current[ 0 ].color ).toBeTruthy();
|
|
136
129
|
expect( result.current[ 1 ].color ).toBeTruthy();
|
|
137
130
|
} );
|