@automattic/charts 1.8.0 → 1.8.1
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 +5 -0
- package/dist/index.cjs +11 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/bar-chart/bar-chart.tsx +29 -22
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
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",
|
|
@@ -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
|
) }
|
|
@@ -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.
|