@automattic/charts 4.12.0 → 4.14.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 +35 -29
- package/TOKENS.md +3 -0
- package/dist/index.cjs +733 -2203
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +42 -13
- package/dist/index.d.cts +41 -125
- package/dist/index.d.ts +41 -125
- package/dist/index.js +756 -1881
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/charts/area-chart/area-chart.tsx +16 -6
- package/src/charts/area-chart/test/area-chart.test.tsx +42 -9
- package/src/charts/bar-chart/bar-chart.tsx +17 -12
- package/src/charts/bar-chart/private/index.ts +1 -1
- package/src/charts/bar-chart/private/slot-tooltip-members.tsx +4 -5
- package/src/charts/bar-chart/private/use-bar-stops.ts +1 -3
- package/src/charts/bar-chart/private/use-overlap-slots.ts +35 -16
- package/src/charts/bar-chart/test/bar-chart.test.tsx +115 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +24 -11
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +235 -182
- package/src/charts/conversion-funnel-chart/index.ts +2 -0
- package/src/charts/conversion-funnel-chart/private/index.ts +1 -0
- package/src/charts/conversion-funnel-chart/private/use-funnel-selection.ts +9 -47
- package/src/charts/conversion-funnel-chart/private/use-roving-step-focus.ts +83 -0
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +312 -92
- package/src/charts/conversion-funnel-chart/test/use-funnel-selection.test.ts +23 -188
- package/src/charts/conversion-funnel-chart/types.ts +22 -0
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +42 -2
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +124 -36
- package/src/charts/leaderboard-chart/private/resolve-leaderboard-entries.ts +53 -0
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +88 -0
- package/src/charts/leaderboard-chart/test/resolve-leaderboard-entries.test.ts +146 -0
- package/src/charts/leaderboard-chart/types.ts +6 -0
- package/src/charts/line-chart/line-chart.tsx +10 -8
- package/src/charts/line-chart/test/line-chart.test.tsx +75 -0
- package/src/components/legend/private/base-legend.tsx +13 -8
- package/src/components/legend/test/legend.test.tsx +125 -0
- package/src/components/legend/types.ts +9 -1
- package/src/components/tooltip/private/at-nearest-date.ts +49 -0
- package/src/components/tooltip/private/keyboard-stops.ts +67 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +10 -2
- package/src/components/tooltip/test/keyboard-stops.test.ts +36 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +135 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +8 -12
- package/src/index.ts +2 -0
- package/src/styles/chart-scope.scss +1 -0
- package/src/types.ts +5 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.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.
|
|
66
|
+
"@automattic/number-formatters": "^1.3.0",
|
|
67
67
|
"@babel/runtime": "7.29.7",
|
|
68
68
|
"@date-fns/tz": "1.5.0",
|
|
69
69
|
"@react-spring/web": "10.1.2",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
"@visx/tooltip": "^4.0.0",
|
|
84
84
|
"@visx/vendor": "^4.0.0",
|
|
85
85
|
"@visx/xychart": "^4.0.0",
|
|
86
|
-
"@wordpress/i18n": "^
|
|
87
|
-
"@wordpress/icons": "^
|
|
88
|
-
"@wordpress/theme": "
|
|
89
|
-
"@wordpress/ui": "0.
|
|
86
|
+
"@wordpress/i18n": "^7.0.0",
|
|
87
|
+
"@wordpress/icons": "^19.0.0",
|
|
88
|
+
"@wordpress/theme": "3.0.0",
|
|
89
|
+
"@wordpress/ui": "0.25.0",
|
|
90
90
|
"clsx": "2.1.1",
|
|
91
91
|
"date-fns": "^4.1.0",
|
|
92
92
|
"deepmerge": "4.3.1",
|
|
@@ -111,9 +111,9 @@
|
|
|
111
111
|
"@types/react-dom": "19.3.0",
|
|
112
112
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
113
113
|
"@visx/glyph": "4.0.0",
|
|
114
|
-
"@wordpress/components": "
|
|
115
|
-
"@wordpress/element": "
|
|
116
|
-
"@wordpress/private-apis": "
|
|
114
|
+
"@wordpress/components": "43.0.0",
|
|
115
|
+
"@wordpress/element": "10.0.0",
|
|
116
|
+
"@wordpress/private-apis": "3.0.0",
|
|
117
117
|
"babel-jest": "30.5.2",
|
|
118
118
|
"identity-obj-proxy": "^3.0.0",
|
|
119
119
|
"jest": "30.5.2",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from 'react';
|
|
14
14
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
15
15
|
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
16
|
+
import { getKeyboardStops } from '../../components/tooltip/private/keyboard-stops';
|
|
16
17
|
import {
|
|
17
18
|
useXYChartTheme,
|
|
18
19
|
useChartDataTransform,
|
|
@@ -144,14 +145,22 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
144
145
|
[ seriesWithVisibility ]
|
|
145
146
|
);
|
|
146
147
|
|
|
148
|
+
const visibleDataSeries = useMemo(
|
|
149
|
+
() => dataSorted.filter( series => isSeriesVisible( series.label ) ),
|
|
150
|
+
[ dataSorted, isSeriesVisible ]
|
|
151
|
+
);
|
|
152
|
+
|
|
147
153
|
// A normalized stack (expand/wiggle/silhouette) turns whole-number data into
|
|
148
154
|
// fractions, so the filter is skipped for every offset but 'none'.
|
|
149
155
|
const hasWholeNumberValues = useMemo(
|
|
150
|
-
() =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
() => ( ! stacked || stackOffset === 'none' ) && hasOnlyWholeNumbers( visibleDataSeries ),
|
|
157
|
+
[ visibleDataSeries, stacked, stackOffset ]
|
|
158
|
+
);
|
|
159
|
+
const visibleSeriesKey = useMemo(
|
|
160
|
+
() => JSON.stringify( visibleDataSeries.map( series => series.label ) ),
|
|
161
|
+
[ visibleDataSeries ]
|
|
154
162
|
);
|
|
163
|
+
const stops = useMemo( () => getKeyboardStops( visibleDataSeries ), [ visibleDataSeries ] );
|
|
155
164
|
|
|
156
165
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
157
166
|
selectedIndex,
|
|
@@ -159,7 +168,8 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
159
168
|
isNavigating,
|
|
160
169
|
setIsNavigating,
|
|
161
170
|
chartRef,
|
|
162
|
-
totalPoints:
|
|
171
|
+
totalPoints: stops.count,
|
|
172
|
+
visibleSeriesKey,
|
|
163
173
|
} );
|
|
164
174
|
|
|
165
175
|
// Computed from the full data set (ignoring legend visibility) so the y-axis stays
|
|
@@ -526,7 +536,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
526
536
|
keyboardFocusedClassName={
|
|
527
537
|
styles[ 'area-chart__tooltip--keyboard-focused' ]
|
|
528
538
|
}
|
|
529
|
-
series={
|
|
539
|
+
series={ visibleDataSeries }
|
|
530
540
|
/>
|
|
531
541
|
<HoverGlyphs
|
|
532
542
|
visibleSeries={ visibleSeries }
|
|
@@ -879,12 +879,6 @@ describe( 'AreaChart', () => {
|
|
|
879
879
|
} );
|
|
880
880
|
|
|
881
881
|
test( 'renders glyphs only for series with a datum at the hovered x (mismatched x-domains)', async () => {
|
|
882
|
-
// Each series' data[0] is a different date. Keyboard nav fires
|
|
883
|
-
// showTooltip for every series at its own data[selectedIndex];
|
|
884
|
-
// the LAST one wins as the nearestDatum, so the resolved hover
|
|
885
|
-
// date is Series B's. HoverGlyphs should:
|
|
886
|
-
// - Skip Series A (no datum at Series B's date) — cumulative += 0
|
|
887
|
-
// - Render a glyph for Series B
|
|
888
882
|
renderWithProvider( {
|
|
889
883
|
data: [
|
|
890
884
|
{
|
|
@@ -901,9 +895,8 @@ describe( 'AreaChart', () => {
|
|
|
901
895
|
|
|
902
896
|
const glyphs = screen.getAllByTestId( /^area-chart-hover-glyph-/ );
|
|
903
897
|
expect( glyphs ).toHaveLength( 1 );
|
|
904
|
-
|
|
905
|
-
expect( screen.
|
|
906
|
-
expect( screen.queryByTestId( 'area-chart-hover-glyph-0' ) ).not.toBeInTheDocument();
|
|
898
|
+
expect( screen.getByTestId( 'area-chart-hover-glyph-0' ) ).toBeInTheDocument();
|
|
899
|
+
expect( screen.queryByTestId( 'area-chart-hover-glyph-1' ) ).not.toBeInTheDocument();
|
|
907
900
|
} );
|
|
908
901
|
} );
|
|
909
902
|
|
|
@@ -1055,4 +1048,44 @@ describe( 'AreaChart', () => {
|
|
|
1055
1048
|
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1056
1049
|
} );
|
|
1057
1050
|
} );
|
|
1051
|
+
|
|
1052
|
+
it( 'gives no keyboard stop to a date only a hidden series has', async () => {
|
|
1053
|
+
const user = userEvent.setup();
|
|
1054
|
+
|
|
1055
|
+
render(
|
|
1056
|
+
<GlobalChartsProvider>
|
|
1057
|
+
<AreaChartUnresponsive
|
|
1058
|
+
width={ 500 }
|
|
1059
|
+
height={ 300 }
|
|
1060
|
+
stacked={ false }
|
|
1061
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1062
|
+
chartId="test-hidden-only-date-area"
|
|
1063
|
+
data={ [
|
|
1064
|
+
{
|
|
1065
|
+
label: 'Series A',
|
|
1066
|
+
data: [
|
|
1067
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
|
|
1068
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
label: 'Series B',
|
|
1073
|
+
data: [
|
|
1074
|
+
{ date: new Date( '2024-01-01' ), value: 5, label: 'Jan 1' },
|
|
1075
|
+
{ date: new Date( '2024-01-03' ), value: 15, label: 'Jan 3' },
|
|
1076
|
+
],
|
|
1077
|
+
},
|
|
1078
|
+
] }
|
|
1079
|
+
/>
|
|
1080
|
+
</GlobalChartsProvider>
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
screen.getByRole( 'application', { name: /area chart/i } ).focus();
|
|
1084
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}' );
|
|
1085
|
+
|
|
1086
|
+
await expect( screen.findByRole( 'tooltip' ) ).resolves.toHaveAttribute(
|
|
1087
|
+
'data-testid',
|
|
1088
|
+
'chart-tooltip-1'
|
|
1089
|
+
);
|
|
1090
|
+
} );
|
|
1058
1091
|
} );
|
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
useBarStops,
|
|
48
48
|
useOverlapSlots,
|
|
49
49
|
getSelectedSlot,
|
|
50
|
+
isSameCategory,
|
|
51
|
+
membersAt,
|
|
50
52
|
} from './private';
|
|
51
53
|
import { useBarColors } from './use-bar-colors';
|
|
52
54
|
import { useBarLegendItems } from './use-bar-legend-items';
|
|
@@ -442,12 +444,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
442
444
|
// Find the comparison value paired with the hovered primary series (same group)
|
|
443
445
|
// at the same category, so the tooltip can show both periods at once.
|
|
444
446
|
const comparisonEntry = comparisonEntries.find( entry => entry.primaryKey === primaryKey );
|
|
445
|
-
const comparisonDatum = comparisonEntry?.series.data.find( point =>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
? p.label === nearestDatum.label
|
|
449
|
-
: !! nearestDatum.date && !! p.date && p.date.getTime() === nearestDatum.date.getTime();
|
|
450
|
-
} ) as DataPointDate | undefined;
|
|
447
|
+
const comparisonDatum = comparisonEntry?.series.data.find( point =>
|
|
448
|
+
isSameCategory( point as DataPointDate, nearestDatum )
|
|
449
|
+
) as DataPointDate | undefined;
|
|
451
450
|
|
|
452
451
|
// With a paired comparison value, show the category as the header and one row
|
|
453
452
|
// per period (primary + comparison).
|
|
@@ -536,6 +535,16 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
536
535
|
[ overlapSlots, selectedIndex ]
|
|
537
536
|
);
|
|
538
537
|
|
|
538
|
+
const selectedGroup = useMemo( () => {
|
|
539
|
+
const stop = overlapSlots || selectedIndex === undefined ? undefined : barAt( selectedIndex );
|
|
540
|
+
if ( ! stop ) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return membersAt( primaryEntries, stop.datum, stop.key );
|
|
544
|
+
}, [ overlapSlots, selectedIndex, barAt, primaryEntries ] );
|
|
545
|
+
|
|
546
|
+
const selectedMembers = selectedSlot?.members ?? selectedGroup;
|
|
547
|
+
|
|
539
548
|
const selectedBar = useMemo( (): SelectedBar | undefined => {
|
|
540
549
|
if ( overlapSlots ) {
|
|
541
550
|
return selectedSlot;
|
|
@@ -783,11 +792,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
783
792
|
</WholeNumberTicks>
|
|
784
793
|
) }
|
|
785
794
|
|
|
786
|
-
{ withTooltips &&
|
|
787
|
-
<SlotTooltipMembers
|
|
788
|
-
members={ selectedSlot.members }
|
|
789
|
-
index={ selectedSlot.index }
|
|
790
|
-
/>
|
|
795
|
+
{ withTooltips && selectedMembers && (
|
|
796
|
+
<SlotTooltipMembers members={ selectedMembers } />
|
|
791
797
|
) }
|
|
792
798
|
{ withTooltips && (
|
|
793
799
|
<XYChartTooltip
|
|
@@ -806,7 +812,6 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
806
812
|
keyboardFocusedClassName={
|
|
807
813
|
styles[ 'bar-chart__tooltip--keyboard-focused' ]
|
|
808
814
|
}
|
|
809
|
-
series={ stops.series }
|
|
810
815
|
mode="individual"
|
|
811
816
|
/>
|
|
812
817
|
) }
|
|
@@ -5,7 +5,7 @@ export { default as SlottedBarSeries } from './slotted-bar-series';
|
|
|
5
5
|
export { BarLayer } from './bar-layer';
|
|
6
6
|
export { SelectedBarRing, type SelectedBar } from './selected-bar-ring';
|
|
7
7
|
export { SlotTooltipMembers, type SlotMember } from './slot-tooltip-members';
|
|
8
|
-
export { useOverlapSlots, getSelectedSlot, isSameCategory } from './use-overlap-slots';
|
|
8
|
+
export { useOverlapSlots, getSelectedSlot, isSameCategory, membersAt } from './use-overlap-slots';
|
|
9
9
|
export { useBarStops } from './use-bar-stops';
|
|
10
10
|
export { ComparisonBars } from './comparison-bars';
|
|
11
11
|
export type { ComparisonSeriesEntry } from './comparison-bars';
|
|
@@ -3,7 +3,7 @@ import { useContext, useEffect } from 'react';
|
|
|
3
3
|
import type { DataPointDate } from '../../../types';
|
|
4
4
|
|
|
5
5
|
/** A bar at the keyboard-selected category. */
|
|
6
|
-
export type SlotMember = { key: string; datum: DataPointDate };
|
|
6
|
+
export type SlotMember = { key: string; datum: DataPointDate; index: number };
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Fills the tooltip with every bar of the keyboard-selected category, and only those.
|
|
@@ -12,18 +12,17 @@ export type SlotMember = { key: string; datum: DataPointDate };
|
|
|
12
12
|
*
|
|
13
13
|
* @param props - Component props.
|
|
14
14
|
* @param props.members - The category's bars, the keyboard stop last.
|
|
15
|
-
* @param props.index - The selected data point.
|
|
16
15
|
* @return No visual content.
|
|
17
16
|
*/
|
|
18
|
-
export function SlotTooltipMembers( { members
|
|
17
|
+
export function SlotTooltipMembers( { members }: { members: SlotMember[] } ) {
|
|
19
18
|
const { showTooltip, updateTooltip } = useContext( TooltipContext ) ?? {};
|
|
20
19
|
|
|
21
20
|
useEffect( () => {
|
|
22
21
|
updateTooltip?.( state => ( { ...state, tooltipData: undefined } ) );
|
|
23
|
-
members.forEach(
|
|
22
|
+
members.forEach( member => showTooltip?.( member ) );
|
|
24
23
|
// `showTooltip` and `updateTooltip` are stable; the context object is not.
|
|
25
24
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
|
-
}, [ members
|
|
25
|
+
}, [ members ] );
|
|
27
26
|
|
|
28
27
|
return null;
|
|
29
28
|
}
|
|
@@ -10,8 +10,6 @@ export type StopBar = { datum: DataPointDate; index: number; key: string };
|
|
|
10
10
|
|
|
11
11
|
export type BarStops = {
|
|
12
12
|
count: number;
|
|
13
|
-
/** The series `XYChartTooltip` reads a stop's datum from. Overlap has none: `SlotTooltipMembers` fills its tooltip. */
|
|
14
|
-
series: SeriesData[];
|
|
15
13
|
/** Changes when the stops do, so keyboard navigation starts over. */
|
|
16
14
|
key: string;
|
|
17
15
|
barAt: ( stop: number ) => StopBar | undefined;
|
|
@@ -85,5 +83,5 @@ export function useBarStops(
|
|
|
85
83
|
[ overlapSlots, series ]
|
|
86
84
|
);
|
|
87
85
|
|
|
88
|
-
return { count,
|
|
86
|
+
return { count, key, barAt, stopOf };
|
|
89
87
|
}
|
|
@@ -27,7 +27,7 @@ export type StopPosition = {
|
|
|
27
27
|
dataPointIndex: number;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
export type SelectedSlot = SelectedBar & {
|
|
30
|
+
export type SelectedSlot = SelectedBar & { members: SlotMember[] };
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Whether two data points sit at the same category: the same label, or the same date.
|
|
@@ -37,7 +37,8 @@ export type SelectedSlot = SelectedBar & { index: number; members: SlotMember[]
|
|
|
37
37
|
* @return Whether `point` is at `category`.
|
|
38
38
|
*/
|
|
39
39
|
export function isSameCategory( point: DataPointDate, category: DataPointDate ): boolean {
|
|
40
|
-
|
|
40
|
+
// An empty label falls back to the date, as the band axis's accessor does.
|
|
41
|
+
return category.label
|
|
41
42
|
? point.label === category.label
|
|
42
43
|
: !! category.date && !! point.date && point.date.getTime() === category.date.getTime();
|
|
43
44
|
}
|
|
@@ -101,7 +102,7 @@ export function useOverlapSlots(
|
|
|
101
102
|
* @return The category key.
|
|
102
103
|
*/
|
|
103
104
|
function categoryKey( point: DataPointDate ): string {
|
|
104
|
-
return point.label
|
|
105
|
+
return point.label ? `label:${ point.label }` : `date:${ point.date?.getTime() }`;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
/**
|
|
@@ -152,6 +153,34 @@ function getStopPositions(
|
|
|
152
153
|
);
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Every bar at a category, each with its own data point index, the keyboard stop last.
|
|
158
|
+
*
|
|
159
|
+
* @param entries - The visible series.
|
|
160
|
+
* @param category - The data point naming the category.
|
|
161
|
+
* @param stopKey - The series the keyboard stop is on.
|
|
162
|
+
* @return The category's bars; the last one shown becomes the tooltip's nearest datum.
|
|
163
|
+
*/
|
|
164
|
+
export function membersAt(
|
|
165
|
+
entries: SeriesEntry[],
|
|
166
|
+
category: DataPointDate,
|
|
167
|
+
stopKey: string
|
|
168
|
+
): SlotMember[] {
|
|
169
|
+
const members = entries.flatMap( ( { series } ) => {
|
|
170
|
+
const index = ( series.data as DataPointDate[] ).findIndex( point =>
|
|
171
|
+
isSameCategory( point, category )
|
|
172
|
+
);
|
|
173
|
+
return index === -1
|
|
174
|
+
? []
|
|
175
|
+
: [ { key: series.label, datum: series.data[ index ] as DataPointDate, index } ];
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
return [
|
|
179
|
+
...members.filter( ( { key } ) => key !== stopKey ),
|
|
180
|
+
...members.filter( ( { key } ) => key === stopKey ),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
/**
|
|
156
185
|
* The slot a keyboard index selects, and every bar at its category.
|
|
157
186
|
*
|
|
@@ -169,13 +198,8 @@ export function getSelectedSlot(
|
|
|
169
198
|
return undefined;
|
|
170
199
|
}
|
|
171
200
|
|
|
172
|
-
const { slotKey, key: stopKey, datum: category
|
|
173
|
-
const members = slots.entries
|
|
174
|
-
const datum = ( series.data as DataPointDate[] ).find( point =>
|
|
175
|
-
isSameCategory( point, category )
|
|
176
|
-
);
|
|
177
|
-
return datum ? [ { key: series.label, datum } ] : [];
|
|
178
|
-
} );
|
|
201
|
+
const { slotKey, key: stopKey, datum: category } = position;
|
|
202
|
+
const members = membersAt( slots.entries, category, stopKey );
|
|
179
203
|
const slotBars = members.filter(
|
|
180
204
|
( { key, datum } ) => isBarRendered( datum ) && slots.slotOf( key ) === slotKey
|
|
181
205
|
);
|
|
@@ -184,11 +208,6 @@ export function getSelectedSlot(
|
|
|
184
208
|
data: slotBars.map( ( { datum } ) => datum ),
|
|
185
209
|
slotKey,
|
|
186
210
|
thickness: Math.max( ...slotBars.map( ( { key } ) => slots.thicknessOf( key ) ) ),
|
|
187
|
-
|
|
188
|
-
// The stop goes last: the last bar shown becomes the tooltip's nearest datum.
|
|
189
|
-
members: [
|
|
190
|
-
...members.filter( ( { key } ) => key !== stopKey ),
|
|
191
|
-
...members.filter( ( { key } ) => key === stopKey ),
|
|
192
|
-
],
|
|
211
|
+
members,
|
|
193
212
|
};
|
|
194
213
|
}
|
|
@@ -17,6 +17,7 @@ import { useBarChartOptions } from '../private';
|
|
|
17
17
|
import { useBarColors } from '../use-bar-colors';
|
|
18
18
|
import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
|
|
19
19
|
import type { DataPointDate, SeriesData } from '../../../types';
|
|
20
|
+
import type { RenderTooltipParams } from '../../../visx/types';
|
|
20
21
|
|
|
21
22
|
// Mock useElementSize to return non-zero dimensions in jsdom so charts render
|
|
22
23
|
const mockRefCallback = jest.fn();
|
|
@@ -2025,6 +2026,39 @@ describe( 'BarChart', () => {
|
|
|
2025
2026
|
expect( tooltip ).toHaveTextContent( 'Previous period: 15' );
|
|
2026
2027
|
} );
|
|
2027
2028
|
|
|
2029
|
+
test( 'pairs the comparison value by date when the bars have empty labels', async () => {
|
|
2030
|
+
const user = userEvent.setup();
|
|
2031
|
+
renderWithTheme( {
|
|
2032
|
+
withTooltips: true,
|
|
2033
|
+
data: [
|
|
2034
|
+
{
|
|
2035
|
+
label: 'This period',
|
|
2036
|
+
group: 'views',
|
|
2037
|
+
data: [
|
|
2038
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: '' },
|
|
2039
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: '' },
|
|
2040
|
+
],
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
label: 'Previous period',
|
|
2044
|
+
group: 'views',
|
|
2045
|
+
options: { type: 'comparison' as const },
|
|
2046
|
+
data: [
|
|
2047
|
+
{ date: new Date( '2024-01-01' ), value: 15, label: '' },
|
|
2048
|
+
{ date: new Date( '2024-01-02' ), value: 25, label: '' },
|
|
2049
|
+
],
|
|
2050
|
+
},
|
|
2051
|
+
],
|
|
2052
|
+
} );
|
|
2053
|
+
|
|
2054
|
+
screen.getByRole( 'application', { name: /bar chart/i } ).focus();
|
|
2055
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}' );
|
|
2056
|
+
|
|
2057
|
+
const tooltip = screen.getByTestId( 'chart-tooltip-1' );
|
|
2058
|
+
expect( tooltip ).toHaveTextContent( 'This period: 20' );
|
|
2059
|
+
expect( tooltip ).toHaveTextContent( 'Previous period: 25' );
|
|
2060
|
+
} );
|
|
2061
|
+
|
|
2028
2062
|
test( 'keyboard navigation past the first slot stays on the primary bar, not the comparison series', async () => {
|
|
2029
2063
|
const user = userEvent.setup();
|
|
2030
2064
|
renderWithTheme( {
|
|
@@ -3554,6 +3588,87 @@ describe( 'BarChart', () => {
|
|
|
3554
3588
|
expect( screen.queryByRole( 'tooltip' ) ).not.toBeInTheDocument();
|
|
3555
3589
|
} );
|
|
3556
3590
|
|
|
3591
|
+
const labelledViews = [
|
|
3592
|
+
{ label: 'Apr', value: 29 },
|
|
3593
|
+
{ label: 'May', value: 19 },
|
|
3594
|
+
];
|
|
3595
|
+
const april = new Date( '2025-04-01T00:00:00Z' );
|
|
3596
|
+
const may = new Date( '2025-05-01T00:00:00Z' );
|
|
3597
|
+
|
|
3598
|
+
it.each( [
|
|
3599
|
+
{
|
|
3600
|
+
name: 'aligned series',
|
|
3601
|
+
views: labelledViews,
|
|
3602
|
+
visitors: [
|
|
3603
|
+
{ label: 'Apr', value: 112 },
|
|
3604
|
+
{ label: 'May', value: 80 },
|
|
3605
|
+
],
|
|
3606
|
+
presses: 3,
|
|
3607
|
+
rows: [ 'Views=19@1', 'Visitors=80@1' ],
|
|
3608
|
+
nearest: 'Views',
|
|
3609
|
+
},
|
|
3610
|
+
{
|
|
3611
|
+
name: 'ragged series',
|
|
3612
|
+
views: labelledViews,
|
|
3613
|
+
visitors: [ { label: 'May', value: 80 } ],
|
|
3614
|
+
presses: 2,
|
|
3615
|
+
rows: [ 'Views=19@1', 'Visitors=80@0' ],
|
|
3616
|
+
nearest: 'Visitors',
|
|
3617
|
+
},
|
|
3618
|
+
{
|
|
3619
|
+
name: 'dated points with empty labels',
|
|
3620
|
+
views: [
|
|
3621
|
+
{ date: april, label: '', value: 29 },
|
|
3622
|
+
{ date: may, label: '', value: 19 },
|
|
3623
|
+
],
|
|
3624
|
+
visitors: [
|
|
3625
|
+
{ date: april, label: '', value: 112 },
|
|
3626
|
+
{ date: may, label: '', value: 80 },
|
|
3627
|
+
],
|
|
3628
|
+
presses: 3,
|
|
3629
|
+
rows: [ 'Views=19@1', 'Visitors=80@1' ],
|
|
3630
|
+
nearest: 'Views',
|
|
3631
|
+
},
|
|
3632
|
+
] )(
|
|
3633
|
+
'gives the keyboard tooltip only the bars at the selected category: $name',
|
|
3634
|
+
async ( { views, visitors, presses, rows, nearest } ) => {
|
|
3635
|
+
const user = userEvent.setup();
|
|
3636
|
+
const renderTooltip = ( { tooltipData }: RenderTooltipParams< DataPointDate > ) => (
|
|
3637
|
+
<div>
|
|
3638
|
+
<span data-testid="nearest">{ tooltipData?.nearestDatum?.key }</span>
|
|
3639
|
+
{ Object.entries( tooltipData?.datumByKey ?? {} ).map(
|
|
3640
|
+
( [ key, { datum, index } ] ) => (
|
|
3641
|
+
<span
|
|
3642
|
+
key={ key }
|
|
3643
|
+
data-testid="row"
|
|
3644
|
+
>{ `${ key }=${ datum.value }@${ index }` }</span>
|
|
3645
|
+
)
|
|
3646
|
+
) }
|
|
3647
|
+
</div>
|
|
3648
|
+
);
|
|
3649
|
+
|
|
3650
|
+
renderWithTheme( {
|
|
3651
|
+
withTooltips: true,
|
|
3652
|
+
renderTooltip,
|
|
3653
|
+
data: [
|
|
3654
|
+
{ label: 'Views', data: views, options: {} },
|
|
3655
|
+
{ label: 'Visitors', data: visitors, options: {} },
|
|
3656
|
+
],
|
|
3657
|
+
} );
|
|
3658
|
+
|
|
3659
|
+
screen.getByRole( 'application', { name: /bar chart/i } ).focus();
|
|
3660
|
+
for ( let i = 0; i < presses; i++ ) {
|
|
3661
|
+
await user.keyboard( '{ArrowRight}' );
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
expect( screen.getAllByTestId( 'row' ).map( row => row.textContent ) ).toEqual(
|
|
3665
|
+
expect.arrayContaining( rows )
|
|
3666
|
+
);
|
|
3667
|
+
expect( screen.getAllByTestId( 'row' ) ).toHaveLength( rows.length );
|
|
3668
|
+
expect( screen.getByTestId( 'nearest' ) ).toHaveTextContent( nearest );
|
|
3669
|
+
}
|
|
3670
|
+
);
|
|
3671
|
+
|
|
3557
3672
|
it( 'stops at the last visible slot on a standard chart', async () => {
|
|
3558
3673
|
const user = userEvent.setup();
|
|
3559
3674
|
|
|
@@ -52,13 +52,28 @@
|
|
|
52
52
|
height: 100%;
|
|
53
53
|
|
|
54
54
|
&--animated {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
|
|
56
|
+
.step-text,
|
|
57
|
+
.funnel-bar,
|
|
58
|
+
.bar-container {
|
|
59
|
+
transition:
|
|
60
|
+
opacity var(--wpds-motion-duration-lg) var(--wpds-motion-easing-subtle),
|
|
61
|
+
background-color var(--wpds-motion-duration-lg) var(--wpds-motion-easing-subtle);
|
|
62
|
+
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
// Dim the contents, not the step: opacity on an ancestor would also fade
|
|
66
|
+
// the bar's focus ring.
|
|
60
67
|
&--blurred {
|
|
61
|
-
|
|
68
|
+
|
|
69
|
+
.step-text,
|
|
70
|
+
.funnel-bar {
|
|
71
|
+
opacity: 0.3;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.bar-container {
|
|
75
|
+
background-color: color-mix(in sRGB, var(--a8c-charts-color-surface-secondary) 30%, transparent);
|
|
76
|
+
}
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
|
|
@@ -119,20 +134,18 @@
|
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
.tooltip-title
|
|
137
|
+
.tooltip-title,
|
|
138
|
+
.tooltip-label,
|
|
139
|
+
.tooltip-value {
|
|
123
140
|
color: var(--a8c-charts-color-label);
|
|
124
141
|
font-size: var(--wpds-typography-font-size-sm);
|
|
125
142
|
font-style: normal;
|
|
126
143
|
font-weight: var(--wpds-typography-font-weight-default);
|
|
127
|
-
line-height: var(--wpds-typography-line-height-
|
|
144
|
+
line-height: var(--wpds-typography-line-height-sm);
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
.tooltip-
|
|
131
|
-
color: var(--a8c-charts-color-label);
|
|
132
|
-
font-size: var(--wpds-typography-font-size-md);
|
|
133
|
-
font-style: normal;
|
|
147
|
+
.tooltip-value {
|
|
134
148
|
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
135
|
-
line-height: var(--wpds-typography-line-height-sm);
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
.empty-state {
|