@automattic/charts 1.8.0 → 1.9.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 +21 -0
- package/dist/index.cjs +541 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +122 -34
- package/dist/index.d.cts +97 -7
- package/dist/index.d.ts +97 -7
- package/dist/index.js +539 -87
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/charts/area-chart/area-chart.module.scss +3 -1
- package/src/charts/area-chart/area-chart.tsx +5 -2
- package/src/charts/bar-chart/bar-chart.module.scss +6 -2
- package/src/charts/bar-chart/bar-chart.tsx +29 -22
- package/src/charts/bar-chart/private/comparison-bars.tsx +7 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +4 -1
- package/src/charts/bar-chart/test/bar-chart.test.tsx +141 -3
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +15 -12
- package/src/charts/geo-chart/geo-chart.tsx +6 -1
- package/src/charts/geo-chart/test/geo-chart.test.tsx +11 -1
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +103 -0
- package/src/charts/heatmap-chart/heatmap-chart.tsx +422 -0
- package/src/charts/heatmap-chart/index.ts +4 -0
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +81 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +53 -0
- package/src/charts/heatmap-chart/private/index.ts +5 -0
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +45 -0
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +88 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +301 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +34 -0
- package/src/charts/heatmap-chart/types.ts +42 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +18 -6
- package/src/charts/line-chart/line-chart.module.scss +6 -4
- package/src/charts/line-chart/line-chart.tsx +6 -2
- package/src/charts/line-chart/private/line-chart-annotation.tsx +16 -3
- package/src/charts/private/grid-control/grid-control.module.scss +1 -4
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +14 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +12 -0
- package/src/charts/private/x-zoom.module.scss +6 -3
- package/src/components/legend/private/base-legend.module.scss +3 -1
- package/src/components/tooltip/base-tooltip.module.scss +4 -1
- package/src/components/trend-indicator/trend-indicator.module.scss +5 -3
- package/src/hooks/use-xychart-theme.ts +24 -0
- package/src/index.ts +12 -0
- package/src/providers/chart-context/themes.ts +29 -16
- package/src/types.ts +19 -2
- package/src/utils/color-utils.ts +36 -0
- package/src/utils/test/color-utils.test.ts +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Display charts within Automattic products.",
|
|
5
5
|
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"typecheck": "tsgo --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@automattic/number-formatters": "^1.2.
|
|
66
|
+
"@automattic/number-formatters": "^1.2.5",
|
|
67
67
|
"@babel/runtime": "7.29.7",
|
|
68
68
|
"@react-spring/web": "10.1.1",
|
|
69
69
|
"@visx/annotation": "^4.0.0",
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@automattic/jetpack-webpack-config": "workspace:*",
|
|
99
99
|
"@babel/core": "^7.29.7",
|
|
100
|
-
"@storybook/addon-docs": "10.
|
|
101
|
-
"@storybook/react": "10.
|
|
100
|
+
"@storybook/addon-docs": "10.4.6",
|
|
101
|
+
"@storybook/react": "10.4.6",
|
|
102
102
|
"@testing-library/dom": "^10.0.0",
|
|
103
103
|
"@testing-library/jest-dom": "^6.0.0",
|
|
104
104
|
"@testing-library/react": "^16.0.0",
|
|
@@ -109,9 +109,9 @@
|
|
|
109
109
|
"@types/react-dom": "18.3.7",
|
|
110
110
|
"@typescript/native-preview": "7.0.0-dev.20260225.1",
|
|
111
111
|
"@visx/glyph": "4.0.0",
|
|
112
|
-
"@wordpress/components": "
|
|
113
|
-
"@wordpress/element": "8.0
|
|
114
|
-
"@wordpress/private-apis": "1.
|
|
112
|
+
"@wordpress/components": "36.1.0",
|
|
113
|
+
"@wordpress/element": "8.2.0",
|
|
114
|
+
"@wordpress/private-apis": "1.50.0",
|
|
115
115
|
"babel-jest": "30.4.1",
|
|
116
116
|
"identity-obj-proxy": "^3.0.0",
|
|
117
117
|
"jest": "30.4.2",
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"react": "18.3.1",
|
|
121
121
|
"react-dom": "18.3.1",
|
|
122
122
|
"sass-embedded": "1.97.3",
|
|
123
|
-
"storybook": "10.
|
|
123
|
+
"storybook": "10.4.6",
|
|
124
124
|
"tsdown": "0.22.2",
|
|
125
125
|
"typescript": "5.9.3"
|
|
126
126
|
},
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
useGlobalChartsContext,
|
|
28
28
|
useGlobalChartsTheme,
|
|
29
29
|
} from '../../providers';
|
|
30
|
-
import { attachSubComponents } from '../../utils';
|
|
30
|
+
import { attachSubComponents, resolveCssVariable } from '../../utils';
|
|
31
31
|
import { renderDefaultTooltip } from '../line-chart';
|
|
32
32
|
import { useChartChildren } from '../private/chart-composition';
|
|
33
33
|
import { ChartLayout } from '../private/chart-layout';
|
|
@@ -461,7 +461,10 @@ const AreaChartInternal = forwardRef< SingleChartRef, AreaChartProps >(
|
|
|
461
461
|
stacked={ stacked }
|
|
462
462
|
stackOffset={ stackOffset }
|
|
463
463
|
getElementStyles={ getElementStyles }
|
|
464
|
-
strokeColor={
|
|
464
|
+
strokeColor={
|
|
465
|
+
resolveCssVariable( providerTheme.backgroundColor ) ??
|
|
466
|
+
providerTheme.backgroundColor
|
|
467
|
+
}
|
|
465
468
|
/>
|
|
466
469
|
</>
|
|
467
470
|
) }
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
transform-origin: bottom;
|
|
9
9
|
transform-box: fill-box;
|
|
10
10
|
transform: scaleY(0);
|
|
11
|
-
animation:
|
|
11
|
+
animation:
|
|
12
|
+
rise var(--wpds-motion-duration-xl, 400ms)
|
|
13
|
+
var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
@keyframes rise {
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
transform-origin: left;
|
|
23
25
|
transform-box: fill-box;
|
|
24
26
|
transform: scaleX(0);
|
|
25
|
-
animation:
|
|
27
|
+
animation:
|
|
28
|
+
stretch var(--wpds-motion-duration-xl, 400ms)
|
|
29
|
+
var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
@keyframes stretch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatNumber } from '@automattic/number-formatters';
|
|
2
2
|
import { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from '@visx/pattern';
|
|
3
3
|
import { Axis, BarSeries, BarGroup, Grid, XYChart } from '@visx/xychart';
|
|
4
|
-
import { __ } from '@wordpress/i18n';
|
|
4
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
5
5
|
import clsx from 'clsx';
|
|
6
6
|
import { useCallback, useContext, useState, useRef, useMemo } from 'react';
|
|
7
7
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
@@ -83,6 +83,19 @@ const validateData = ( data: SeriesData[] ) => {
|
|
|
83
83
|
|
|
84
84
|
const getPatternId = ( chartId: string, index: number ) => `bar-pattern-${ chartId }-${ index }`;
|
|
85
85
|
|
|
86
|
+
// A "label: value" tooltip row. The join is a translated format string so the
|
|
87
|
+
// separator (a colon + space here) can be adapted per locale.
|
|
88
|
+
const renderTooltipRow = ( label: string | undefined, value: string ) => (
|
|
89
|
+
<div className={ styles[ 'bar-chart__tooltip-row' ] }>
|
|
90
|
+
{ sprintf(
|
|
91
|
+
/* translators: 1: data series, period, or category label. 2: its formatted value. */
|
|
92
|
+
__( '%1$s: %2$s', 'jetpack-charts' ),
|
|
93
|
+
label,
|
|
94
|
+
value
|
|
95
|
+
) }
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
|
|
86
99
|
const BarChartInternal: FC< BarChartProps > = ( {
|
|
87
100
|
data,
|
|
88
101
|
chartId: providedChartId,
|
|
@@ -191,6 +204,14 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
191
204
|
[ primaryEntries ]
|
|
192
205
|
);
|
|
193
206
|
|
|
207
|
+
// The keyboard-navigation index space and the highlight CSS both stride over primary
|
|
208
|
+
// bars only; the accessible tooltip must use the same list, or its datum diverges from
|
|
209
|
+
// the highlighted bar once a comparison series shifts the indices.
|
|
210
|
+
const primarySeries = useMemo(
|
|
211
|
+
() => primaryEntries.map( ( { series } ) => series ),
|
|
212
|
+
[ primaryEntries ]
|
|
213
|
+
);
|
|
214
|
+
|
|
194
215
|
const comparisonEntries = useMemo( () => {
|
|
195
216
|
const primaryByGroup = new Map< string | undefined, { label: string; index: number } >(
|
|
196
217
|
primaryEntries.map( ( { series, index } ) => [
|
|
@@ -309,20 +330,11 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
309
330
|
return (
|
|
310
331
|
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
311
332
|
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ categoryLabel }</div>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
</div>
|
|
318
|
-
<div className={ styles[ 'bar-chart__tooltip-row' ] }>
|
|
319
|
-
<span className={ styles[ 'bar-chart__tooltip-label' ] }>
|
|
320
|
-
{ comparisonEntry.series.label }:
|
|
321
|
-
</span>
|
|
322
|
-
<span className={ styles[ 'bar-chart__tooltip-value' ] }>
|
|
323
|
-
{ formatNumber( comparisonDatum.value as number ) }
|
|
324
|
-
</span>
|
|
325
|
-
</div>
|
|
333
|
+
{ renderTooltipRow( primaryKey, formatNumber( nearestDatum.value as number ) ) }
|
|
334
|
+
{ renderTooltipRow(
|
|
335
|
+
comparisonEntry.series.label,
|
|
336
|
+
formatNumber( comparisonDatum.value as number )
|
|
337
|
+
) }
|
|
326
338
|
</div>
|
|
327
339
|
);
|
|
328
340
|
}
|
|
@@ -330,12 +342,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
330
342
|
return (
|
|
331
343
|
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
332
344
|
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ primaryKey }</div>
|
|
333
|
-
|
|
334
|
-
<span className={ styles[ 'bar-chart__tooltip-label' ] }>{ categoryLabel }:</span>
|
|
335
|
-
<span className={ styles[ 'bar-chart__tooltip-value' ] }>
|
|
336
|
-
{ formatNumber( nearestDatum.value as number ) }
|
|
337
|
-
</span>
|
|
338
|
-
</div>
|
|
345
|
+
{ renderTooltipRow( categoryLabel, formatNumber( nearestDatum.value as number ) ) }
|
|
339
346
|
</div>
|
|
340
347
|
);
|
|
341
348
|
},
|
|
@@ -622,7 +629,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
622
629
|
keyboardFocusedClassName={
|
|
623
630
|
styles[ 'bar-chart__tooltip--keyboard-focused' ]
|
|
624
631
|
}
|
|
625
|
-
series={
|
|
632
|
+
series={ primarySeries }
|
|
626
633
|
mode="individual"
|
|
627
634
|
/>
|
|
628
635
|
) }
|
|
@@ -10,6 +10,12 @@ import type { ElementStyles, GetElementStylesParams } from '../../../providers';
|
|
|
10
10
|
import type { DataPointDate, SeriesData } from '../../../types';
|
|
11
11
|
import type { FC, ReactNode } from 'react';
|
|
12
12
|
|
|
13
|
+
/*
|
|
14
|
+
* `process.env.NODE_ENV` is replaced by the bundler at build time. Declare a
|
|
15
|
+
* minimal `process` locally so this file type-checks as source under `jetpack:src`.
|
|
16
|
+
*/
|
|
17
|
+
declare const process: { env: Record< string, string | undefined > };
|
|
18
|
+
|
|
13
19
|
export type ComparisonSeriesEntry = {
|
|
14
20
|
series: SeriesData;
|
|
15
21
|
index: number;
|
|
@@ -146,6 +152,7 @@ export const ComparisonBars: FC< {
|
|
|
146
152
|
<g
|
|
147
153
|
className="bar-chart__comparison-bars"
|
|
148
154
|
pointerEvents="none"
|
|
155
|
+
aria-hidden="true"
|
|
149
156
|
data-testid="bar-chart-comparison-bars"
|
|
150
157
|
>
|
|
151
158
|
{ rects }
|
|
@@ -123,8 +123,11 @@ export function useBarChartOptions(
|
|
|
123
123
|
} );
|
|
124
124
|
} );
|
|
125
125
|
if ( allValues.length > 0 ) {
|
|
126
|
+
// Keep zero in the domain so bar length stays proportional to value — a
|
|
127
|
+
// non-zero baseline would exaggerate differences between periods. Math.max
|
|
128
|
+
// keeps zero on the far side too, so charts with negative values still span 0.
|
|
126
129
|
valueScaleDomainOverride = {
|
|
127
|
-
domain: [ Math.min( ...allValues ), Math.max( ...allValues ) ],
|
|
130
|
+
domain: [ Math.min( 0, ...allValues ), Math.max( 0, ...allValues ) ],
|
|
128
131
|
};
|
|
129
132
|
}
|
|
130
133
|
}
|
|
@@ -322,6 +322,8 @@ describe( 'BarChart', () => {
|
|
|
322
322
|
await user.keyboard( '{ArrowRight}' );
|
|
323
323
|
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveFocus();
|
|
324
324
|
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'Series A' );
|
|
325
|
+
// The category/value row joins with a space after the colon.
|
|
326
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( 'Jan 1: 10' );
|
|
325
327
|
expect( screen.queryByTestId( 'chart-tooltip-1' ) ).not.toBeInTheDocument();
|
|
326
328
|
|
|
327
329
|
// Second tab should focus on the second tooltip.
|
|
@@ -409,11 +411,50 @@ describe( 'BarChart', () => {
|
|
|
409
411
|
|
|
410
412
|
await user.keyboard( '{ArrowRight}' );
|
|
411
413
|
const tooltip = screen.getByTestId( 'chart-tooltip-0' );
|
|
412
|
-
// Both
|
|
414
|
+
// Both periods are shown, each as "label: value" with a space after the colon.
|
|
415
|
+
expect( tooltip ).toHaveTextContent( 'This period: 10' );
|
|
416
|
+
expect( tooltip ).toHaveTextContent( 'Previous period: 15' );
|
|
417
|
+
} );
|
|
418
|
+
|
|
419
|
+
test( 'keyboard navigation past the first slot stays on the primary bar, not the comparison series', async () => {
|
|
420
|
+
const user = userEvent.setup();
|
|
421
|
+
renderWithTheme( {
|
|
422
|
+
withTooltips: true,
|
|
423
|
+
data: [
|
|
424
|
+
{
|
|
425
|
+
label: 'This period',
|
|
426
|
+
group: 'views',
|
|
427
|
+
data: [
|
|
428
|
+
{ label: 'Mon', value: 10 },
|
|
429
|
+
{ label: 'Tue', value: 20 },
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
label: 'Previous period',
|
|
434
|
+
group: 'views',
|
|
435
|
+
options: { type: 'comparison' as const },
|
|
436
|
+
data: [
|
|
437
|
+
{ label: 'Mon', value: 15 },
|
|
438
|
+
{ label: 'Tue', value: 25 },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
const chart = screen.getByRole( 'grid', { name: /bar chart/i } );
|
|
445
|
+
chart.focus();
|
|
446
|
+
|
|
447
|
+
// Slot 0 is the first primary bar (Mon); slot 1 must be the SECOND primary
|
|
448
|
+
// bar (Tue) — not the comparison series' first bar. The keyboard index space
|
|
449
|
+
// counts only primary bars, so the tooltip must too.
|
|
450
|
+
await user.keyboard( '{ArrowRight}' );
|
|
451
|
+
await user.keyboard( '{ArrowRight}' );
|
|
452
|
+
const tooltip = screen.getByTestId( 'chart-tooltip-1' );
|
|
453
|
+
expect( tooltip ).toHaveTextContent( 'Tue' );
|
|
413
454
|
expect( tooltip ).toHaveTextContent( 'This period' );
|
|
414
|
-
expect( tooltip ).toHaveTextContent( '
|
|
455
|
+
expect( tooltip ).toHaveTextContent( '20' );
|
|
415
456
|
expect( tooltip ).toHaveTextContent( 'Previous period' );
|
|
416
|
-
expect( tooltip ).toHaveTextContent( '
|
|
457
|
+
expect( tooltip ).toHaveTextContent( '25' );
|
|
417
458
|
} );
|
|
418
459
|
} );
|
|
419
460
|
|
|
@@ -860,6 +901,57 @@ describe( 'BarChart', () => {
|
|
|
860
901
|
expect( bars.length ).toBeGreaterThanOrEqual( 2 );
|
|
861
902
|
} );
|
|
862
903
|
|
|
904
|
+
it( 'hides the comparison shadow group from assistive technology', () => {
|
|
905
|
+
const data = [
|
|
906
|
+
{
|
|
907
|
+
label: 'This year',
|
|
908
|
+
group: 'views',
|
|
909
|
+
data: [ { label: 'Jan', value: 100 } ],
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
label: 'Last year',
|
|
913
|
+
group: 'views',
|
|
914
|
+
options: { type: 'comparison' as const },
|
|
915
|
+
data: [ { label: 'Jan', value: 80 } ],
|
|
916
|
+
},
|
|
917
|
+
];
|
|
918
|
+
render( <BarChart data={ data } width={ 400 } height={ 300 } /> );
|
|
919
|
+
|
|
920
|
+
// The shadow is decorative: no keyboard/hover target and its value is surfaced
|
|
921
|
+
// through the tooltip, so it must not be announced as a separate element.
|
|
922
|
+
expect( screen.getByTestId( 'bar-chart-comparison-bars' ) ).toHaveAttribute(
|
|
923
|
+
'aria-hidden',
|
|
924
|
+
'true'
|
|
925
|
+
);
|
|
926
|
+
} );
|
|
927
|
+
|
|
928
|
+
it( 'renders comparison shadows in horizontal orientation', () => {
|
|
929
|
+
const data = [
|
|
930
|
+
{
|
|
931
|
+
label: 'This year',
|
|
932
|
+
group: 'views',
|
|
933
|
+
data: [
|
|
934
|
+
{ label: 'Jan', value: 100 },
|
|
935
|
+
{ label: 'Feb', value: 120 },
|
|
936
|
+
],
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
label: 'Last year',
|
|
940
|
+
group: 'views',
|
|
941
|
+
options: { type: 'comparison' as const },
|
|
942
|
+
data: [
|
|
943
|
+
{ label: 'Jan', value: 80 },
|
|
944
|
+
{ label: 'Feb', value: 140 },
|
|
945
|
+
],
|
|
946
|
+
},
|
|
947
|
+
];
|
|
948
|
+
render( <BarChart data={ data } orientation="horizontal" width={ 400 } height={ 300 } /> );
|
|
949
|
+
|
|
950
|
+
const shadows = screen.getAllByTestId( /^bar-chart-comparison-\d+-\d+$/ );
|
|
951
|
+
expect( shadows ).toHaveLength( 2 );
|
|
952
|
+
expect( shadows[ 0 ] ).toHaveAttribute( 'opacity', '0.5' );
|
|
953
|
+
} );
|
|
954
|
+
|
|
863
955
|
it( 'expands value-axis domain to include comparison values exceeding the primary max', () => {
|
|
864
956
|
// Comparison value 150 exceeds primary max 100.
|
|
865
957
|
// Without domain expansion the value-axis scale config has no explicit domain,
|
|
@@ -886,6 +978,52 @@ describe( 'BarChart', () => {
|
|
|
886
978
|
expect( ( yScale.domain as number[] )[ 1 ] ).toBeGreaterThanOrEqual( 150 );
|
|
887
979
|
} );
|
|
888
980
|
|
|
981
|
+
it( 'keeps the value-axis baseline at zero so comparison bars encode magnitude truthfully', () => {
|
|
982
|
+
// All values are well above zero; the domain must still start at 0 so a bar's
|
|
983
|
+
// length stays proportional to its value (a non-zero baseline exaggerates differences).
|
|
984
|
+
const data = [
|
|
985
|
+
{
|
|
986
|
+
label: 'This period',
|
|
987
|
+
group: 'views',
|
|
988
|
+
data: [ { label: 'Mon', value: 420 } ],
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
label: 'Previous period',
|
|
992
|
+
group: 'views',
|
|
993
|
+
options: { type: 'comparison' as const },
|
|
994
|
+
data: [ { label: 'Mon', value: 510 } ],
|
|
995
|
+
},
|
|
996
|
+
];
|
|
997
|
+
|
|
998
|
+
const { result } = renderHook( () => useBarChartOptions( data, false, {} ) );
|
|
999
|
+
const yScale = result.current.yScale as { domain?: number[] };
|
|
1000
|
+
expect( yScale.domain ).toBeDefined();
|
|
1001
|
+
expect( ( yScale.domain as number[] )[ 0 ] ).toBe( 0 );
|
|
1002
|
+
} );
|
|
1003
|
+
|
|
1004
|
+
it( 'zero-bases the value-axis domain in horizontal comparison charts', () => {
|
|
1005
|
+
const data = [
|
|
1006
|
+
{
|
|
1007
|
+
label: 'This period',
|
|
1008
|
+
group: 'views',
|
|
1009
|
+
data: [ { label: 'Mon', value: 420 } ],
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
label: 'Previous period',
|
|
1013
|
+
group: 'views',
|
|
1014
|
+
options: { type: 'comparison' as const },
|
|
1015
|
+
data: [ { label: 'Mon', value: 510 } ],
|
|
1016
|
+
},
|
|
1017
|
+
];
|
|
1018
|
+
|
|
1019
|
+
// In horizontal charts the value axis is x.
|
|
1020
|
+
const { result } = renderHook( () => useBarChartOptions( data, true, {} ) );
|
|
1021
|
+
const xScale = result.current.xScale as { domain?: number[] };
|
|
1022
|
+
expect( xScale.domain ).toBeDefined();
|
|
1023
|
+
expect( ( xScale.domain as number[] )[ 0 ] ).toBe( 0 );
|
|
1024
|
+
expect( ( xScale.domain as number[] )[ 1 ] ).toBeGreaterThanOrEqual( 510 );
|
|
1025
|
+
} );
|
|
1026
|
+
|
|
889
1027
|
it( 'counts only primary series for keyboard navigation when a comparison series is present', async () => {
|
|
890
1028
|
const user = userEvent.setup();
|
|
891
1029
|
// 2 primary + 1 comparison. totalPoints should be 2*2=4, not 3*2=6.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
.main-rate {
|
|
14
14
|
overflow: hidden;
|
|
15
|
-
color: #1e1e1e;
|
|
15
|
+
color: var(--wpds-color-fg-content-neutral, #1e1e1e);
|
|
16
16
|
text-overflow: ellipsis;
|
|
17
17
|
font-size: var(--wpds-typography-font-size-xl, 18px);
|
|
18
18
|
font-style: normal;
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
.funnel-container {
|
|
35
35
|
flex: 1;
|
|
36
|
-
min-height:
|
|
36
|
+
min-height: 0;
|
|
37
37
|
width: 100%;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
height: 100%;
|
|
44
44
|
|
|
45
45
|
&--animated {
|
|
46
|
-
transition:
|
|
46
|
+
transition:
|
|
47
|
+
opacity var(--wpds-motion-duration-lg, 300ms)
|
|
48
|
+
var(--wpds-motion-easing-subtle, cubic-bezier(0.15, 0, 0.15, 1));
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
&--blurred {
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
.step-label {
|
|
55
|
-
color: #
|
|
57
|
+
color: var(--wpds-color-fg-content-neutral-weak, #707070);
|
|
56
58
|
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
57
59
|
font-weight: var(--wpds-typography-font-weight-regular, 400);
|
|
58
60
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
.step-rate {
|
|
65
|
-
color: #1e1e1e;
|
|
67
|
+
color: var(--wpds-color-fg-content-neutral, #1e1e1e);
|
|
66
68
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
67
69
|
font-weight: var(--wpds-typography-font-weight-medium, 499);
|
|
68
70
|
line-height: var(--wpds-typography-line-height-xs, 16px);
|
|
@@ -84,7 +86,9 @@
|
|
|
84
86
|
transform-origin: bottom;
|
|
85
87
|
transform-box: fill-box;
|
|
86
88
|
transform: scaleY(0);
|
|
87
|
-
animation:
|
|
89
|
+
animation:
|
|
90
|
+
stretch var(--wpds-motion-duration-xl, 400ms)
|
|
91
|
+
var(--wpds-motion-easing-expressive, cubic-bezier(0.25, 0, 0, 1)) forwards;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
@keyframes stretch {
|
|
@@ -96,18 +100,17 @@
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
.tooltip-wrapper {
|
|
99
|
-
|
|
100
|
-
background: var(--black-white-white, #fff);
|
|
103
|
+
background: var(--wpds-color-bg-surface-neutral-strong, #fff);
|
|
101
104
|
|
|
102
105
|
// Override .visx-tooltip inline styles.
|
|
103
106
|
border-radius: var(--wpds-border-radius-md, 4px) !important;
|
|
104
107
|
padding: var(--wpds-dimension-padding-md, 12px) !important;
|
|
105
|
-
box-shadow: 0 1px
|
|
108
|
+
box-shadow: var(--wpds-elevation-sm, 0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005) !important;
|
|
106
109
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
.tooltip-title {
|
|
110
|
-
color: #1e1e1e;
|
|
113
|
+
color: var(--wpds-color-fg-content-neutral, #1e1e1e);
|
|
111
114
|
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
112
115
|
font-style: normal;
|
|
113
116
|
font-weight: var(--wpds-typography-font-weight-regular, 400);
|
|
@@ -115,7 +118,7 @@
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
.tooltip-content {
|
|
118
|
-
color: #1e1e1e;
|
|
121
|
+
color: var(--wpds-color-fg-content-neutral, #1e1e1e);
|
|
119
122
|
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
120
123
|
font-style: normal;
|
|
121
124
|
font-weight: var(--wpds-typography-font-weight-medium, 499);
|
|
@@ -124,6 +127,6 @@
|
|
|
124
127
|
|
|
125
128
|
.empty-state {
|
|
126
129
|
text-align: center;
|
|
127
|
-
color: #
|
|
130
|
+
color: var(--wpds-color-fg-content-neutral-weak, #707070);
|
|
128
131
|
font-size: var(--wpds-typography-font-size-lg, 16px);
|
|
129
132
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
5
|
import clsx from 'clsx';
|
|
6
6
|
import { FC, useContext, useMemo } from 'react';
|
|
7
|
-
import { Chart } from 'react-google-charts';
|
|
7
|
+
import { Chart, type GoogleChartPackages } from 'react-google-charts';
|
|
8
8
|
/**
|
|
9
9
|
* Internal dependencies
|
|
10
10
|
*/
|
|
@@ -19,6 +19,10 @@ import { GeoChartProps } from './types';
|
|
|
19
19
|
|
|
20
20
|
const DEFAULT_FEATURE_FILL_COLOR = '#ffffff';
|
|
21
21
|
const DEFAULT_BACKGROUND_COLOR = '#ffffff';
|
|
22
|
+
// `chartPackages` replaces (not extends) react-google-charts' default `[ 'corechart', 'controls' ]`,
|
|
23
|
+
// so we restate the defaults and add `geochart`. Without it the loader backfills the geochart package
|
|
24
|
+
// late, which can clash with another Google Charts version already loaded on the page.
|
|
25
|
+
const GEO_CHART_PACKAGES: GoogleChartPackages[] = [ 'corechart', 'controls', 'geochart' ];
|
|
22
26
|
|
|
23
27
|
type GoogleChartOptions = Record< string, unknown >;
|
|
24
28
|
|
|
@@ -154,6 +158,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
154
158
|
>
|
|
155
159
|
<Chart
|
|
156
160
|
chartType="GeoChart"
|
|
161
|
+
chartPackages={ GEO_CHART_PACKAGES }
|
|
157
162
|
width={ width }
|
|
158
163
|
height={ height }
|
|
159
164
|
data={ sanitizedData.data }
|
|
@@ -4,9 +4,10 @@ import GeoChart, { GeoChartUnresponsive } from '../geo-chart';
|
|
|
4
4
|
|
|
5
5
|
// Mock react-google-charts
|
|
6
6
|
jest.mock( 'react-google-charts', () => ( {
|
|
7
|
-
Chart: jest.fn( ( { data, options, width, height } ) => {
|
|
7
|
+
Chart: jest.fn( ( { chartPackages, data, options, width, height } ) => {
|
|
8
8
|
return (
|
|
9
9
|
<div data-testid="google-chart-mock" data-width={ width } data-height={ height }>
|
|
10
|
+
<div data-testid="chart-packages">{ JSON.stringify( chartPackages ) }</div>
|
|
10
11
|
<div data-testid="chart-data">{ JSON.stringify( data ) }</div>
|
|
11
12
|
<div data-testid="chart-options">{ JSON.stringify( options ) }</div>
|
|
12
13
|
</div>
|
|
@@ -64,6 +65,15 @@ describe( 'GeoChart', () => {
|
|
|
64
65
|
expect( chart ).toHaveAttribute( 'data-width', '1200' );
|
|
65
66
|
expect( chart ).toHaveAttribute( 'data-height', '600' );
|
|
66
67
|
} );
|
|
68
|
+
|
|
69
|
+
test( 'loads the GeoChart package explicitly', () => {
|
|
70
|
+
renderWithTheme();
|
|
71
|
+
|
|
72
|
+
const chartPackages = screen.getByTestId( 'chart-packages' );
|
|
73
|
+
const packages = JSON.parse( chartPackages.textContent || '[]' );
|
|
74
|
+
|
|
75
|
+
expect( packages ).toEqual( [ 'corechart', 'controls', 'geochart' ] );
|
|
76
|
+
} );
|
|
67
77
|
} );
|
|
68
78
|
|
|
69
79
|
describe( 'Data Handling', () => {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
.heatmap-chart {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
min-height: 0;
|
|
5
|
+
font-size: var(--wpds-typography-font-size-sm, 12px);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.heatmap-chart__empty {
|
|
9
|
+
padding: var(--wpds-dimension-padding-lg, 16px);
|
|
10
|
+
color: var(--wpds-color-fg-content-neutral-weak, #707070);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Track template is set inline because CSS `repeat()` won't take a `var()`
|
|
14
|
+
// count. ARIA rows are `display: contents` so their cells join this grid.
|
|
15
|
+
.heatmap-chart__grid {
|
|
16
|
+
display: grid;
|
|
17
|
+
gap: var(--heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
|
|
18
|
+
flex: 1 1 0;
|
|
19
|
+
width: 100%;
|
|
20
|
+
// Floor so the grid stays usable when the parent has no explicit height.
|
|
21
|
+
min-height: 200px;
|
|
22
|
+
|
|
23
|
+
&:focus-visible {
|
|
24
|
+
outline:
|
|
25
|
+
var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px)) solid
|
|
26
|
+
var(--wpds-color-stroke-focus-brand, var(--wp-admin-theme-color, #3858e9));
|
|
27
|
+
outline-offset: 2px;
|
|
28
|
+
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.heatmap-chart__grid--compact {
|
|
33
|
+
flex: 0 0 auto;
|
|
34
|
+
width: max-content;
|
|
35
|
+
height: max-content;
|
|
36
|
+
min-height: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.heatmap-chart__row {
|
|
40
|
+
display: contents;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.heatmap-chart__col-label,
|
|
44
|
+
.heatmap-chart__row-label {
|
|
45
|
+
color: var(--wpds-color-fg-content-neutral-weak, #707070);
|
|
46
|
+
font-size: var(--wpds-typography-font-size-xs, 11px);
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
overflow: visible;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.heatmap-chart__col-label {
|
|
52
|
+
align-self: end;
|
|
53
|
+
text-align: left;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.heatmap-chart__row-label {
|
|
57
|
+
align-self: center;
|
|
58
|
+
justify-self: end;
|
|
59
|
+
text-align: right;
|
|
60
|
+
padding-inline-end: var(--wpds-dimension-padding-xs, 4px);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.heatmap-chart__cell {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
min-width: 0;
|
|
68
|
+
min-height: 0;
|
|
69
|
+
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
70
|
+
// Empty (no-data) default; cells with a value override it below.
|
|
71
|
+
background: var(--wpds-color-bg-track-neutral-weak, #f0f0f0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
75
|
+
// an empty cell.
|
|
76
|
+
.heatmap-chart__cell--filled {
|
|
77
|
+
background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), transparent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.heatmap-chart__cell--selected {
|
|
81
|
+
outline:
|
|
82
|
+
var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px)) solid
|
|
83
|
+
var(--wpds-color-stroke-focus-brand, var(--wp-admin-theme-color, #3858e9));
|
|
84
|
+
outline-offset: calc(-1 * var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.heatmap-chart__cell-value {
|
|
88
|
+
color: var(--wpds-color-fg-content-neutral, #1e1e1e);
|
|
89
|
+
font-size: var(--wpds-typography-font-size-md, 13px);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Light text on fills dark enough to out-contrast dark text (decided in JS from
|
|
93
|
+
// the blended fill luminance).
|
|
94
|
+
.heatmap-chart__cell--strong .heatmap-chart__cell-value {
|
|
95
|
+
color: var(--wpds-color-fg-interactive-neutral-strong, #f0f0f0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.heatmap-chart__legend-swatch {
|
|
99
|
+
width: var(--wpds-dimension-size-3xs, 12px);
|
|
100
|
+
height: var(--wpds-dimension-size-3xs, 12px);
|
|
101
|
+
border-radius: var(--wpds-border-radius-sm, 2px);
|
|
102
|
+
background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), transparent);
|
|
103
|
+
}
|