@automattic/charts 4.11.1 → 4.13.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 +24 -0
- package/dist/index.cjs +1412 -787
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +59 -25
- package/dist/index.d.cts +64 -120
- package/dist/index.d.ts +64 -120
- package/dist/index.js +1400 -765
- 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 +143 -142
- package/src/charts/bar-chart/index.ts +3 -0
- package/src/charts/bar-chart/private/band-tooltip.tsx +41 -15
- package/src/charts/bar-chart/private/bar-layer.tsx +132 -0
- package/src/charts/bar-chart/private/comparison-bars-geometry.ts +22 -34
- package/src/charts/bar-chart/private/comparison-bars.tsx +9 -18
- package/src/charts/bar-chart/private/comparison-constants.ts +4 -10
- package/src/charts/bar-chart/private/grouped-bars.ts +68 -0
- package/src/charts/bar-chart/private/index.ts +7 -1
- package/src/charts/bar-chart/private/overlap-bars.ts +183 -0
- package/src/charts/bar-chart/private/selected-bar-ring.tsx +84 -0
- package/src/charts/bar-chart/private/slot-tooltip-members.tsx +28 -0
- package/src/charts/bar-chart/private/slotted-bar-series.tsx +84 -0
- package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +4 -11
- package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +2 -3
- package/src/charts/bar-chart/private/test/grouped-bars.test.ts +87 -0
- package/src/charts/bar-chart/private/test/overlap-bars.test.ts +159 -0
- package/src/charts/bar-chart/private/use-bar-stops.ts +87 -0
- package/src/charts/bar-chart/private/use-overlap-slots.ts +213 -0
- package/src/charts/bar-chart/private/value-domain.ts +0 -10
- package/src/charts/bar-chart/test/bar-chart-overlap.test.tsx +347 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +181 -19
- package/src/charts/bar-chart/test/use-bar-legend-items.test.tsx +81 -0
- package/src/charts/bar-chart/types.ts +3 -0
- package/src/charts/bar-chart/use-bar-colors.ts +46 -0
- package/src/charts/bar-chart/use-bar-legend-items.ts +87 -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 +41 -1
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +101 -30
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +47 -0
- package/src/charts/line-chart/line-chart.tsx +25 -18
- package/src/charts/line-chart/test/line-chart.test.tsx +240 -1
- package/src/charts/pie-chart/pie-chart.tsx +7 -2
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +7 -2
- package/src/charts/private/pie-keyboard-navigation/index.ts +0 -1
- package/src/charts/private/{pie-keyboard-navigation/pie-keyboard-navigation.module.scss → selected-ring/selected-ring.module.scss} +11 -6
- package/src/charts/private/selected-ring/selected-ring.tsx +73 -0
- package/src/charts/private/{pie-keyboard-navigation/test/selected-segment-ring.test.ts → selected-ring/test/selected-ring.test.ts} +1 -1
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +20 -0
- package/src/components/legend/hooks/use-chart-legend-items.ts +27 -3
- 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 +11 -14
- package/src/index.ts +10 -2
- package/src/providers/chart-context/global-charts-provider.tsx +2 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +0 -13
- package/src/providers/chart-context/themes.ts +0 -1
- package/src/providers/chart-context/types.ts +2 -0
- package/src/test-utils/selected-bar-ring.ts +14 -0
- package/src/types.ts +14 -5
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +0 -47
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { DataPointDate, SeriesData } from '../../../types';
|
|
2
|
+
|
|
3
|
+
/** A series' point at a keyboard stop. */
|
|
4
|
+
export type StopMember = { series: SeriesData; datum: DataPointDate; index: number };
|
|
5
|
+
|
|
6
|
+
export type KeyboardStops = {
|
|
7
|
+
count: number;
|
|
8
|
+
membersAt: ( stop: number ) => StopMember[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const timeOf = ( point: unknown ): number | undefined =>
|
|
12
|
+
( point as DataPointDate | undefined )?.date?.getTime();
|
|
13
|
+
|
|
14
|
+
const getXPositions = ( series: SeriesData[] ): number[] => {
|
|
15
|
+
const positions = new Set< number >();
|
|
16
|
+
|
|
17
|
+
for ( const { data } of series ) {
|
|
18
|
+
for ( const point of data ) {
|
|
19
|
+
const time = timeOf( point );
|
|
20
|
+
if ( time !== undefined ) {
|
|
21
|
+
positions.add( time );
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [ ...positions ].sort( ( a, b ) => a - b );
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keyboard stops over the series: the sorted union of their dates, or array indexes when no point has a date.
|
|
31
|
+
*
|
|
32
|
+
* Once any point has a date, a series of undated points has no stop.
|
|
33
|
+
*
|
|
34
|
+
* @param series - The series to step through.
|
|
35
|
+
* @return The stop count, and each series' point at a stop.
|
|
36
|
+
*/
|
|
37
|
+
export const getKeyboardStops = ( series: SeriesData[] ): KeyboardStops => {
|
|
38
|
+
const positions = getXPositions( series );
|
|
39
|
+
|
|
40
|
+
if ( positions.length === 0 ) {
|
|
41
|
+
return {
|
|
42
|
+
count: Math.max( 0, ...series.map( ( { data } ) => data.length ) ),
|
|
43
|
+
membersAt: stop =>
|
|
44
|
+
series.flatMap( s =>
|
|
45
|
+
stop < s.data.length
|
|
46
|
+
? [ { series: s, datum: s.data[ stop ] as DataPointDate, index: stop } ]
|
|
47
|
+
: []
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
count: positions.length,
|
|
54
|
+
membersAt: stop => {
|
|
55
|
+
const time = positions[ stop ];
|
|
56
|
+
if ( time === undefined ) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return series.flatMap( s => {
|
|
60
|
+
const index = s.data.findIndex( point => timeOf( point ) === time );
|
|
61
|
+
return index === -1
|
|
62
|
+
? []
|
|
63
|
+
: [ { series: s, datum: s.data[ index ] as DataPointDate, index } ];
|
|
64
|
+
} );
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
};
|
|
@@ -2,13 +2,15 @@ import { DataContext, TooltipContext } from '@visx/xychart';
|
|
|
2
2
|
import { useCallback, useContext, useState } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { TooltipBox } from '../tooltip-box';
|
|
5
|
+
import { atNearestDate } from './at-nearest-date';
|
|
5
6
|
import { BoundedTooltip, TOOLTIP_Z_INDEX } from './bounded-tooltip';
|
|
6
7
|
import type {
|
|
7
8
|
CrosshairStyle,
|
|
8
9
|
RenderTooltipGlyphProps,
|
|
9
10
|
XyChartTooltipProps,
|
|
10
11
|
} from '../../../visx/types';
|
|
11
|
-
import type {
|
|
12
|
+
import type { AxisScale } from '@visx/axis';
|
|
13
|
+
import type { DataContextType, TooltipContextType } from '@visx/xychart';
|
|
12
14
|
import type { CSSProperties, ReactNode } from 'react';
|
|
13
15
|
|
|
14
16
|
const CROSSHAIR_STROKE_WIDTH = 1.5;
|
|
@@ -314,6 +316,9 @@ export const XyChartTooltipOverlay = < Datum extends object >(
|
|
|
314
316
|
props: XyChartTooltipProps< Datum >
|
|
315
317
|
) => {
|
|
316
318
|
const tooltipContext = useContext( TooltipContext ) as TooltipContextType< Datum > | null;
|
|
319
|
+
const dataRegistry = (
|
|
320
|
+
useContext( DataContext ) as DataContextType< AxisScale, AxisScale, Datum >
|
|
321
|
+
)?.dataRegistry;
|
|
317
322
|
const [ container, setContainer ] = useState< HTMLElement | null >( null );
|
|
318
323
|
|
|
319
324
|
// Children of XYChart render inside its SVG, and the box needs an HTML parent.
|
|
@@ -327,7 +332,10 @@ export const XyChartTooltipOverlay = < Datum extends object >(
|
|
|
327
332
|
{ tooltipContext?.tooltipOpen && (
|
|
328
333
|
<XyChartTooltipContent
|
|
329
334
|
{ ...props }
|
|
330
|
-
tooltipContext={
|
|
335
|
+
tooltipContext={ {
|
|
336
|
+
...tooltipContext,
|
|
337
|
+
tooltipData: atNearestDate( tooltipContext.tooltipData, dataRegistry ),
|
|
338
|
+
} }
|
|
331
339
|
container={ container }
|
|
332
340
|
/>
|
|
333
341
|
) }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getKeyboardStops } from '../private/keyboard-stops';
|
|
2
|
+
|
|
3
|
+
describe( 'getKeyboardStops', () => {
|
|
4
|
+
it( 'gives a stop past the last date no members, even for undated points', () => {
|
|
5
|
+
const stops = getKeyboardStops( [
|
|
6
|
+
{ label: 'Dated', data: [ { date: new Date( '2025-04-01T00:00:00Z' ), value: 1 } ] },
|
|
7
|
+
{ label: 'Undated', data: [ { label: 'Apr', value: 2 } ] },
|
|
8
|
+
] );
|
|
9
|
+
|
|
10
|
+
expect( stops.count ).toBe( 1 );
|
|
11
|
+
expect( stops.membersAt( 1 ) ).toEqual( [] );
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
it( 'steps through array indexes when no point has a date', () => {
|
|
15
|
+
const first = {
|
|
16
|
+
label: 'First',
|
|
17
|
+
data: [
|
|
18
|
+
{ label: 'Jan', value: 1 },
|
|
19
|
+
{ label: 'Feb', value: 2 },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
const second = { label: 'Second', data: [ { label: 'Jan', value: 3 } ] };
|
|
23
|
+
const stops = getKeyboardStops( [ first, second ] );
|
|
24
|
+
|
|
25
|
+
expect( stops.count ).toBe( 2 );
|
|
26
|
+
expect( stops.membersAt( 0 ).map( ( { series, index } ) => [ series.label, index ] ) ).toEqual(
|
|
27
|
+
[
|
|
28
|
+
[ 'First', 0 ],
|
|
29
|
+
[ 'Second', 0 ],
|
|
30
|
+
]
|
|
31
|
+
);
|
|
32
|
+
expect( stops.membersAt( 1 ).map( ( { series, index } ) => [ series.label, index ] ) ).toEqual(
|
|
33
|
+
[ [ 'First', 1 ] ]
|
|
34
|
+
);
|
|
35
|
+
} );
|
|
36
|
+
} );
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { act, render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import {
|
|
3
|
+
import { BarSeries, LineSeries, TooltipContext, XYChart } from '@visx/xychart';
|
|
4
|
+
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
4
5
|
import { LineChartUnresponsive } from '../../../charts/line-chart/line-chart';
|
|
5
6
|
import { GlobalChartsProvider } from '../../../providers';
|
|
6
7
|
import { ChartScopeContext } from '../../../providers/chart-scope';
|
|
7
8
|
import { AccessibleTooltip, XYChartTooltip, useKeyboardNavigation } from '../xy-chart-tooltip';
|
|
9
|
+
import type { DataPointDate } from '../../../types';
|
|
10
|
+
import type { RenderTooltipParams } from '../../../visx/types';
|
|
8
11
|
import type { ReactNode } from 'react';
|
|
9
12
|
|
|
10
13
|
// A real chart is the harness rather than the subject: the crosshairs render only once visx has a data context and an open tooltip. The unresponsive export is what lets the test own the scope element — `withResponsive` otherwise provides its own wrapper as the scope.
|
|
@@ -48,6 +51,46 @@ const openTooltip = async () => {
|
|
|
48
51
|
return screen.getByTestId( 'xy-chart-tooltip-crosshair-vertical' );
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
const individualItems = [ 'First', 'Second' ].map( label => ( {
|
|
55
|
+
label,
|
|
56
|
+
data: [ { label: 'Jan', value: 10 } ],
|
|
57
|
+
options: {},
|
|
58
|
+
} ) );
|
|
59
|
+
const xAccessor = ( d: DataPointDate ) => d.label;
|
|
60
|
+
const yAccessor = ( d: DataPointDate ) => d.value;
|
|
61
|
+
const xScale = { type: 'band' } as const;
|
|
62
|
+
const yScale = { type: 'linear' } as const;
|
|
63
|
+
const renderKeys = ( { tooltipData }: RenderTooltipParams< DataPointDate > ) => (
|
|
64
|
+
<div data-testid="keys">{ Object.keys( tooltipData?.datumByKey ?? {} ).join( ',' ) }</div>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const IndividualHarness = () => {
|
|
68
|
+
const [ selectedIndex, setSelectedIndex ] = useState( 0 );
|
|
69
|
+
const selectSecond = useCallback( () => setSelectedIndex( 1 ), [] );
|
|
70
|
+
return (
|
|
71
|
+
<GlobalChartsProvider>
|
|
72
|
+
<button onClick={ selectSecond }>next</button>
|
|
73
|
+
<XYChart width={ 300 } height={ 200 } xScale={ xScale } yScale={ yScale }>
|
|
74
|
+
{ individualItems.map( item => (
|
|
75
|
+
<BarSeries
|
|
76
|
+
key={ item.label }
|
|
77
|
+
dataKey={ item.label }
|
|
78
|
+
data={ item.data }
|
|
79
|
+
xAccessor={ xAccessor }
|
|
80
|
+
yAccessor={ yAccessor }
|
|
81
|
+
/>
|
|
82
|
+
) ) }
|
|
83
|
+
<XYChartTooltip
|
|
84
|
+
mode="individual"
|
|
85
|
+
series={ individualItems }
|
|
86
|
+
selectedIndex={ selectedIndex }
|
|
87
|
+
renderTooltip={ renderKeys }
|
|
88
|
+
/>
|
|
89
|
+
</XYChart>
|
|
90
|
+
</GlobalChartsProvider>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
51
94
|
describe( 'XYChartTooltip', () => {
|
|
52
95
|
it( 'keeps AccessibleTooltip as an alias', () => {
|
|
53
96
|
expect( AccessibleTooltip ).toBe( XYChartTooltip );
|
|
@@ -129,8 +172,99 @@ describe( 'XYChartTooltip', () => {
|
|
|
129
172
|
|
|
130
173
|
await expect( openTooltip() ).resolves.toHaveAttribute( 'stroke', '#dbdbdb' );
|
|
131
174
|
} );
|
|
175
|
+
|
|
176
|
+
it( 'lists only the selected item in individual mode as the selection moves', async () => {
|
|
177
|
+
const user = userEvent.setup();
|
|
178
|
+
|
|
179
|
+
render( <IndividualHarness /> );
|
|
180
|
+
await expect( screen.findByTestId( 'keys' ) ).resolves.toHaveTextContent( /^First$/ );
|
|
181
|
+
|
|
182
|
+
await user.click( screen.getByRole( 'button', { name: 'next' } ) );
|
|
183
|
+
|
|
184
|
+
await expect( screen.findByTestId( 'keys' ) ).resolves.toHaveTextContent( /^Second$/ );
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
// Between April and May, visx hands Views its April point and Previous period, which has no April, its May point.
|
|
188
|
+
it.each( [
|
|
189
|
+
[ 'leaves out a series with no point at the nearest date', 0, 90, 'Views=10', false ],
|
|
190
|
+
[
|
|
191
|
+
'reads every series at the nearest date, not at its own nearest point',
|
|
192
|
+
90,
|
|
193
|
+
0,
|
|
194
|
+
'Views=40,Previous period=30',
|
|
195
|
+
true,
|
|
196
|
+
],
|
|
197
|
+
] )( '%s', async ( _name, viewsDistance, previousDistance, rows, previousGlyph ) => {
|
|
198
|
+
render(
|
|
199
|
+
<PointerHarness viewsDistance={ viewsDistance } previousDistance={ previousDistance } />
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await expect( screen.findByTestId( 'rows' ) ).resolves.toHaveTextContent( rows );
|
|
203
|
+
expect( screen.getByTestId( 'xy-chart-tooltip-glyph-Views' ) ).toBeInTheDocument();
|
|
204
|
+
expect( !! screen.queryByTestId( 'xy-chart-tooltip-glyph-Previous period' ) ).toBe(
|
|
205
|
+
previousGlyph
|
|
206
|
+
);
|
|
207
|
+
} );
|
|
132
208
|
} );
|
|
133
209
|
|
|
210
|
+
const april = { date: new Date( '2025-04-01' ), value: 10 };
|
|
211
|
+
const may = { date: new Date( '2025-05-01' ), value: 40 };
|
|
212
|
+
const previousMay = { date: new Date( '2025-05-01' ), value: 30 };
|
|
213
|
+
const dateAccessor = ( d: DataPointDate ) => d.date;
|
|
214
|
+
const timeScale = { type: 'time' } as const;
|
|
215
|
+
const renderRows = ( { tooltipData }: RenderTooltipParams< DataPointDate > ) => (
|
|
216
|
+
<div data-testid="rows">
|
|
217
|
+
{ Object.entries( tooltipData?.datumByKey ?? {} )
|
|
218
|
+
.map( ( [ key, { datum } ] ) => `${ key }=${ datum.value }` )
|
|
219
|
+
.join( ',' ) }
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
type PointerDistances = { viewsDistance: number; previousDistance: number };
|
|
224
|
+
|
|
225
|
+
const ReportPointer = ( { viewsDistance, previousDistance }: PointerDistances ) => {
|
|
226
|
+
const tooltipContext = useContext( TooltipContext );
|
|
227
|
+
useEffect( () => {
|
|
228
|
+
tooltipContext?.showTooltip( {
|
|
229
|
+
key: 'Views',
|
|
230
|
+
datum: april,
|
|
231
|
+
index: 0,
|
|
232
|
+
distanceX: viewsDistance,
|
|
233
|
+
distanceY: 0,
|
|
234
|
+
} );
|
|
235
|
+
tooltipContext?.showTooltip( {
|
|
236
|
+
key: 'Previous period',
|
|
237
|
+
datum: previousMay,
|
|
238
|
+
index: 0,
|
|
239
|
+
distanceX: previousDistance,
|
|
240
|
+
distanceY: 0,
|
|
241
|
+
} );
|
|
242
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
243
|
+
}, [] );
|
|
244
|
+
return null;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const PointerHarness = ( distances: PointerDistances ) => (
|
|
248
|
+
<GlobalChartsProvider>
|
|
249
|
+
<XYChart width={ 300 } height={ 200 } xScale={ timeScale } yScale={ yScale }>
|
|
250
|
+
<LineSeries
|
|
251
|
+
dataKey="Views"
|
|
252
|
+
data={ [ april, may ] }
|
|
253
|
+
xAccessor={ dateAccessor }
|
|
254
|
+
yAccessor={ yAccessor }
|
|
255
|
+
/>
|
|
256
|
+
<LineSeries
|
|
257
|
+
dataKey="Previous period"
|
|
258
|
+
data={ [ previousMay ] }
|
|
259
|
+
xAccessor={ dateAccessor }
|
|
260
|
+
yAccessor={ yAccessor }
|
|
261
|
+
/>
|
|
262
|
+
<ReportPointer { ...distances } />
|
|
263
|
+
<XYChartTooltip renderTooltip={ renderRows } showSeriesGlyphs />
|
|
264
|
+
</XYChart>
|
|
265
|
+
</GlobalChartsProvider>
|
|
266
|
+
);
|
|
267
|
+
|
|
134
268
|
// Mirrors the charts' markup: the focusable chart container wraps the element `chartRef` points at.
|
|
135
269
|
const NavigationHarness = ( {
|
|
136
270
|
totalPoints,
|
|
@@ -6,6 +6,7 @@ import { useGlobalChartsContext } from '../../providers/chart-context/hooks/use-
|
|
|
6
6
|
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
7
7
|
import { useChartScopeElement, useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
8
8
|
import { resolveCssVariable } from '../../utils';
|
|
9
|
+
import { getKeyboardStops } from './private/keyboard-stops';
|
|
9
10
|
import { XyChartTooltipOverlay } from './private/xy-chart-tooltip-overlay';
|
|
10
11
|
import type { SeriesData, DataPointDate } from '../../types';
|
|
11
12
|
import type { RenderTooltipParams, XyChartTooltipProps } from '../../visx/types';
|
|
@@ -34,7 +35,7 @@ interface XYChartTooltipProps extends Omit<
|
|
|
34
35
|
*/
|
|
35
36
|
tooltipData?: FlattenedTooltipData[];
|
|
36
37
|
/**
|
|
37
|
-
* For line charts: series data to show
|
|
38
|
+
* For line charts: series data to show every series at the selected date
|
|
38
39
|
* When provided, shows all series instead of individual tooltips
|
|
39
40
|
*/
|
|
40
41
|
series?: SeriesData[];
|
|
@@ -99,6 +100,8 @@ export const XYChartTooltip: React.FC< XYChartTooltipProps > = ( {
|
|
|
99
100
|
return flattened;
|
|
100
101
|
}, [ series, mode ] );
|
|
101
102
|
|
|
103
|
+
const stops = useMemo( () => getKeyboardStops( series ), [ series ] );
|
|
104
|
+
|
|
102
105
|
// Tracks whether this effect opened a tooltip, so it only closes its own.
|
|
103
106
|
const hasKeyboardSelection = useRef( false );
|
|
104
107
|
|
|
@@ -152,23 +155,17 @@ export const XYChartTooltip: React.FC< XYChartTooltipProps > = ( {
|
|
|
152
155
|
hasKeyboardSelection.current = true;
|
|
153
156
|
|
|
154
157
|
if ( mode === 'group' ) {
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
tooltipContext?.showTooltip( {
|
|
161
|
-
datum,
|
|
162
|
-
key: s.label,
|
|
163
|
-
index,
|
|
164
|
-
} );
|
|
165
|
-
}
|
|
158
|
+
// visx merges each shown datum into the previous tooltip data, so start empty or a series dropped from `series` keeps its row.
|
|
159
|
+
closeTooltipNow();
|
|
160
|
+
stops.membersAt( selectedIndex ).forEach( ( { series: member, datum, index } ) => {
|
|
161
|
+
tooltipContext?.showTooltip( { datum, key: member.label, index } );
|
|
166
162
|
} );
|
|
167
163
|
} else if ( mode === 'individual' ) {
|
|
168
164
|
// Show individual tooltips for each datapoint from each series.
|
|
169
165
|
if ( selectedIndex < tooltipData.length ) {
|
|
170
166
|
const tooltipItem = tooltipData[ selectedIndex ];
|
|
171
167
|
|
|
168
|
+
tooltipContext?.updateTooltip( state => ( { ...state, tooltipData: undefined } ) );
|
|
172
169
|
tooltipContext?.showTooltip( {
|
|
173
170
|
datum: tooltipItem.datum,
|
|
174
171
|
key: tooltipItem.seriesLabel,
|
|
@@ -179,7 +176,7 @@ export const XYChartTooltip: React.FC< XYChartTooltipProps > = ( {
|
|
|
179
176
|
|
|
180
177
|
// Don't include tooltipContext in the dependency array to avoid loop.
|
|
181
178
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
-
}, [ selectedIndex, tooltipData,
|
|
179
|
+
}, [ selectedIndex, tooltipData, stops ] );
|
|
183
180
|
|
|
184
181
|
// Create a focusable renderTooltip that includes accessibility features
|
|
185
182
|
const focusableRenderTooltip = useMemo( () => {
|
|
@@ -292,7 +289,7 @@ export const useKeyboardNavigation = ( {
|
|
|
292
289
|
const previousTotalPoints = useRef( totalPoints );
|
|
293
290
|
const previousVisibleSeriesKey = useRef( visibleSeriesKey );
|
|
294
291
|
|
|
295
|
-
// Hiding or showing a series
|
|
292
|
+
// Hiding or showing a series re-points every index. The count alone misses a
|
|
296
293
|
// swap that keeps it the same, such as hiding one series while revealing another of equal length.
|
|
297
294
|
// Clear rather than clamp once focus has left the chart: a new tooltip would pull focus back.
|
|
298
295
|
useEffect( () => {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Charts
|
|
2
2
|
export { AreaChart, AreaChartUnresponsive } from './charts/area-chart';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
BarChart,
|
|
5
|
+
BarChartUnresponsive,
|
|
6
|
+
useBarColors,
|
|
7
|
+
useBarLegendItems,
|
|
8
|
+
} from './charts/bar-chart';
|
|
4
9
|
export { BarListChart, BarListChartUnresponsive } from './charts/bar-list-chart';
|
|
5
10
|
export { ConversionFunnelChart } from './charts/conversion-funnel-chart';
|
|
6
11
|
export { GeoChart, GeoChartUnresponsive } from './charts/geo-chart';
|
|
@@ -76,6 +81,7 @@ export type {
|
|
|
76
81
|
LegendShape,
|
|
77
82
|
LegendShapeLabel,
|
|
78
83
|
LegendShapeRenderProps,
|
|
84
|
+
BarChartLayout,
|
|
79
85
|
} from './types';
|
|
80
86
|
export type * from './visx/types';
|
|
81
87
|
export type { PieChartProps, PieChartRenderTooltipParams } from './charts/pie-chart';
|
|
@@ -109,7 +115,7 @@ export type { LegendProps, BaseLegendProps, ChartLegendOptions } from './compone
|
|
|
109
115
|
|
|
110
116
|
// Previously available via '@automattic/charts/bar-chart', '@automattic/charts/line-chart', etc.
|
|
111
117
|
export type { AreaChartProps } from './charts/area-chart';
|
|
112
|
-
export type { BarChartProps, BandHighlightSelection } from './charts/bar-chart';
|
|
118
|
+
export type { BarChartProps, BandHighlightSelection, BarLegendOptions } from './charts/bar-chart';
|
|
113
119
|
export type {
|
|
114
120
|
BarListChartProps,
|
|
115
121
|
RenderLabelProps,
|
|
@@ -118,6 +124,8 @@ export type {
|
|
|
118
124
|
export type {
|
|
119
125
|
ConversionFunnelChartProps,
|
|
120
126
|
FunnelStep,
|
|
127
|
+
FunnelTooltipContent,
|
|
128
|
+
FunnelTooltipRow,
|
|
121
129
|
StepLabelRenderProps,
|
|
122
130
|
StepRateRenderProps,
|
|
123
131
|
MainMetricRenderProps,
|
|
@@ -371,6 +371,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
371
371
|
theme: providerTheme,
|
|
372
372
|
formatting,
|
|
373
373
|
getElementStyles,
|
|
374
|
+
backgroundColor: colorCache.background,
|
|
374
375
|
toggleSeriesVisibility,
|
|
375
376
|
setSeriesVisibility,
|
|
376
377
|
setChartHiddenSeries,
|
|
@@ -388,6 +389,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
|
|
|
388
389
|
providerTheme,
|
|
389
390
|
formatting,
|
|
390
391
|
getElementStyles,
|
|
392
|
+
colorCache.background,
|
|
391
393
|
toggleSeriesVisibility,
|
|
392
394
|
setSeriesVisibility,
|
|
393
395
|
setChartHiddenSeries,
|
|
@@ -5,7 +5,6 @@ import { useChartId } from '../hooks/use-chart-id';
|
|
|
5
5
|
import { useChartRegistration } from '../hooks/use-chart-registration';
|
|
6
6
|
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
|
|
7
7
|
import { createPaletteGenerator } from '../private/palette-generator';
|
|
8
|
-
import { defaultTheme } from '../themes';
|
|
9
8
|
import type { BaseLegendItem } from '../../../components/legend';
|
|
10
9
|
import type { SeriesData } from '../../../types';
|
|
11
10
|
import type { GlobalChartsContextValue } from '../types';
|
|
@@ -1168,12 +1167,9 @@ describe( 'ChartContext', () => {
|
|
|
1168
1167
|
legendShape: 'rect',
|
|
1169
1168
|
} );
|
|
1170
1169
|
|
|
1171
|
-
// Should get theme legend shape styles (not line styles), with the comparison bar
|
|
1172
|
-
// opacity layered on so the swatch matches the translucent comparison bar.
|
|
1173
1170
|
expect( styles.shapeStyles ).toEqual( {
|
|
1174
1171
|
fill: '#LEGEND1',
|
|
1175
1172
|
stroke: '#BORDER1',
|
|
1176
|
-
opacity: 0.5,
|
|
1177
1173
|
} );
|
|
1178
1174
|
} );
|
|
1179
1175
|
} );
|
|
@@ -2144,13 +2140,4 @@ describe( 'ChartContext', () => {
|
|
|
2144
2140
|
} );
|
|
2145
2141
|
} );
|
|
2146
2142
|
} );
|
|
2147
|
-
|
|
2148
|
-
describe( 'defaultTheme', () => {
|
|
2149
|
-
it( 'exposes default barChart comparison styles', () => {
|
|
2150
|
-
expect( defaultTheme.barChart.barStyles.comparison ).toEqual( {
|
|
2151
|
-
widthFactor: 1.5,
|
|
2152
|
-
opacity: 0.5,
|
|
2153
|
-
} );
|
|
2154
|
-
} );
|
|
2155
|
-
} );
|
|
2156
2143
|
} );
|
|
@@ -41,6 +41,8 @@ export interface GlobalChartsContextValue {
|
|
|
41
41
|
/** Locale and time zone every date the charts print is rendered in. */
|
|
42
42
|
formatting: ChartFormatting;
|
|
43
43
|
getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
|
|
44
|
+
/** The chart background as an opaque hex, for colors that must keep contrast against it. Optional so a hand-built context value still type-checks. */
|
|
45
|
+
backgroundColor?: string;
|
|
44
46
|
// Series visibility management shared by charts, legends, and programmatic controls.
|
|
45
47
|
toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
|
|
46
48
|
setSeriesVisibility: ( chartId: string, seriesLabel: string, visible: boolean ) => void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
const NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The box the bar chart's selected ring outlines, read from its path.
|
|
7
|
+
*
|
|
8
|
+
* @return The ring's x, y, width and height.
|
|
9
|
+
*/
|
|
10
|
+
export const selectedBarRingBounds = () => {
|
|
11
|
+
const d = screen.getByTestId( 'bar-chart-selected-ring' ).getAttribute( 'd' ) ?? '';
|
|
12
|
+
const [ x, y, width, height ] = ( d.match( NUMBER ) ?? [] ).map( Number );
|
|
13
|
+
return { x, y, width, height };
|
|
14
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -214,13 +214,14 @@ type LeaderboardEntryFields = {
|
|
|
214
214
|
previousValue?: number;
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
|
-
* Width of current bar, as % of the
|
|
217
|
+
* Width of current bar, as % of the largest absolute value across rows. A negative share
|
|
218
|
+
* extends the bar from a shared zero point toward the inline start, for every row.
|
|
218
219
|
*/
|
|
219
220
|
currentShare: number;
|
|
220
221
|
|
|
221
222
|
/**
|
|
222
|
-
* Width of previous bar, as % of the
|
|
223
|
-
* no matching comparison-period value.
|
|
223
|
+
* Width of previous bar, as % of the largest absolute value across rows. Omit when this
|
|
224
|
+
* row has no matching comparison-period value.
|
|
224
225
|
*/
|
|
225
226
|
previousShare?: number;
|
|
226
227
|
|
|
@@ -306,14 +307,22 @@ export type SeriesData = {
|
|
|
306
307
|
|
|
307
308
|
/**
|
|
308
309
|
* Visual styling for a bar series of a given semantic type (e.g. 'comparison').
|
|
309
|
-
* `widthFactor` is the bar width relative to the primary bar slot (1.5 = 150%)
|
|
310
|
-
* `opacity` sets the shadow translucency.
|
|
310
|
+
* `widthFactor` is the bar width relative to the primary bar slot (1.5 = 150%).
|
|
311
311
|
*/
|
|
312
312
|
export type BarStyles = {
|
|
313
313
|
widthFactor?: number;
|
|
314
|
+
/**
|
|
315
|
+
* Draws the comparison bar translucent, in its primary's color, instead of the opaque tones the chart picks.
|
|
316
|
+
* @deprecated Comparison bars are opaque. Leave this unset; set the comparison series' `options.stroke` for a color of its own.
|
|
317
|
+
*/
|
|
314
318
|
opacity?: number;
|
|
315
319
|
};
|
|
316
320
|
|
|
321
|
+
/**
|
|
322
|
+
* How a bar chart places its series in a category. `grouped` puts each primary series in a slot of its own; `overlap` puts every primary in one slot and every comparison in a second, and overlaps the `group`s within each slot.
|
|
323
|
+
*/
|
|
324
|
+
export type BarChartLayout = 'grouped' | 'overlap';
|
|
325
|
+
|
|
317
326
|
export type MultipleDataPointsDate = {
|
|
318
327
|
label: string;
|
|
319
328
|
data: DataPointDate[];
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import styles from './pie-keyboard-navigation.module.scss';
|
|
2
|
-
|
|
3
|
-
interface SelectedSegmentRingProps {
|
|
4
|
-
/** The chart's id, which keeps the clip path unique on a page with several charts. */
|
|
5
|
-
chartId: string;
|
|
6
|
-
/** The selected segment's path, from the same arc generator that draws the segment. */
|
|
7
|
-
d: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Encodes `value` for an SVG id, losslessly so distinct chart ids never share a clip path.
|
|
12
|
-
*
|
|
13
|
-
* @param value - Any string.
|
|
14
|
-
* @return `value` with each character outside `[A-Za-z0-9-]` written as `_<hex code point>_`.
|
|
15
|
-
*/
|
|
16
|
-
export const toSvgId = ( value: string ) =>
|
|
17
|
-
Array.from( value, character =>
|
|
18
|
-
/[A-Za-z0-9-]/.test( character )
|
|
19
|
-
? character
|
|
20
|
-
: `_${ character.codePointAt( 0 )?.toString( 16 ) }_`
|
|
21
|
-
).join( '' );
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Marks the selected segment with a two-tone ring drawn inside its edge, so the SVG edge cannot cut it off.
|
|
25
|
-
*
|
|
26
|
-
* @param {SelectedSegmentRingProps} props - Component props
|
|
27
|
-
* @return {JSX.Element} The ring
|
|
28
|
-
*/
|
|
29
|
-
export const SelectedSegmentRing = ( { chartId, d }: SelectedSegmentRingProps ) => {
|
|
30
|
-
const clipId = `pie-selected-ring-clip-${ toSvgId( chartId ) }`;
|
|
31
|
-
const clipPath = `url(#${ clipId })`;
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<g pointerEvents="none" aria-hidden="true">
|
|
35
|
-
<clipPath id={ clipId }>
|
|
36
|
-
<path d={ d } />
|
|
37
|
-
</clipPath>
|
|
38
|
-
<path d={ d } clipPath={ clipPath } className={ styles[ 'selected-ring__contrast' ] } />
|
|
39
|
-
<path
|
|
40
|
-
d={ d }
|
|
41
|
-
clipPath={ clipPath }
|
|
42
|
-
className={ styles[ 'selected-ring__focus' ] }
|
|
43
|
-
data-testid="pie-selected-ring"
|
|
44
|
-
/>
|
|
45
|
-
</g>
|
|
46
|
-
);
|
|
47
|
-
};
|