@automattic/charts 4.11.0 → 4.12.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 +18 -0
- package/dist/index.cjs +856 -236
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +19 -13
- package/dist/index.d.cts +43 -4
- package/dist/index.d.ts +43 -4
- package/dist/index.js +855 -237
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/charts/bar-chart/bar-chart.tsx +134 -138
- 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 +29 -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 +89 -0
- package/src/charts/bar-chart/private/use-overlap-slots.ts +194 -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 +66 -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/line-chart/line-chart.tsx +22 -17
- package/src/charts/line-chart/test/line-chart.test.tsx +165 -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/xy-chart-tooltip.tsx +3 -2
- package/src/index.ts +8 -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 +10 -2
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +0 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.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,10 +63,10 @@
|
|
|
63
63
|
"typecheck": "tsgo --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@automattic/number-formatters": "^1.2.
|
|
66
|
+
"@automattic/number-formatters": "^1.2.12",
|
|
67
67
|
"@babel/runtime": "7.29.7",
|
|
68
68
|
"@date-fns/tz": "1.5.0",
|
|
69
|
-
"@react-spring/web": "10.1.
|
|
69
|
+
"@react-spring/web": "10.1.2",
|
|
70
70
|
"@visx/annotation": "^4.0.0",
|
|
71
71
|
"@visx/axis": "^4.0.0",
|
|
72
72
|
"@visx/curve": "^4.0.0",
|
|
@@ -98,8 +98,8 @@
|
|
|
98
98
|
"devDependencies": {
|
|
99
99
|
"@automattic/jetpack-webpack-config": "workspace:*",
|
|
100
100
|
"@babel/core": "^7.29.7",
|
|
101
|
-
"@storybook/addon-docs": "10.
|
|
102
|
-
"@storybook/react": "10.
|
|
101
|
+
"@storybook/addon-docs": "10.6.3",
|
|
102
|
+
"@storybook/react": "10.6.3",
|
|
103
103
|
"@testing-library/dom": "^10.0.0",
|
|
104
104
|
"@testing-library/jest-dom": "^6.0.0",
|
|
105
105
|
"@testing-library/react": "^16.0.0",
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"@tsdown/css": "0.23.1",
|
|
108
108
|
"@types/jest": "^30.0.0",
|
|
109
109
|
"@types/node": "^24.12.0",
|
|
110
|
-
"@types/react": "
|
|
111
|
-
"@types/react-dom": "
|
|
110
|
+
"@types/react": "19.3.0",
|
|
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
114
|
"@wordpress/components": "42.0.0",
|
|
@@ -120,10 +120,10 @@
|
|
|
120
120
|
"jest-extended": "7.0.0",
|
|
121
121
|
"lightningcss": "^1.33.0",
|
|
122
122
|
"magic-string": "0.30.21",
|
|
123
|
-
"react": "
|
|
124
|
-
"react-dom": "
|
|
123
|
+
"react": "19.3.0",
|
|
124
|
+
"react-dom": "19.3.0",
|
|
125
125
|
"sass-embedded": "1.97.3",
|
|
126
|
-
"storybook": "10.
|
|
126
|
+
"storybook": "10.6.3",
|
|
127
127
|
"tsdown": "0.23.1",
|
|
128
128
|
"typescript": "6.0.3"
|
|
129
129
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from '@visx/pattern';
|
|
2
|
-
import { Axis,
|
|
2
|
+
import { Axis, Grid, XYChart } from '@visx/xychart';
|
|
3
3
|
import { __, sprintf } from '@wordpress/i18n';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import { useCallback, useContext, useState, useRef, useMemo } from 'react';
|
|
6
|
-
import { Legend
|
|
5
|
+
import { Fragment, useCallback, useContext, useState, useRef, useMemo } from 'react';
|
|
6
|
+
import { Legend } from '../../components/legend';
|
|
7
7
|
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
8
8
|
import {
|
|
9
9
|
useXYChartTheme,
|
|
@@ -35,17 +35,22 @@ import {
|
|
|
35
35
|
useBarChartOptions,
|
|
36
36
|
BandHighlight,
|
|
37
37
|
BandTooltip,
|
|
38
|
-
|
|
38
|
+
BarLayer,
|
|
39
39
|
ComparisonBars,
|
|
40
40
|
DEFAULT_COMPARISON_WIDTH_FACTOR,
|
|
41
41
|
COMPARISON_INNER_GAP,
|
|
42
42
|
MAX_GROUP_PADDING,
|
|
43
43
|
COMPARISON_TICK_GAP_FACTOR,
|
|
44
44
|
BASE_BAND_PADDING_INNER,
|
|
45
|
-
countRenderedBars,
|
|
46
45
|
isBarRendered,
|
|
46
|
+
SlotTooltipMembers,
|
|
47
|
+
useBarStops,
|
|
48
|
+
useOverlapSlots,
|
|
49
|
+
getSelectedSlot,
|
|
47
50
|
} from './private';
|
|
48
|
-
import
|
|
51
|
+
import { useBarColors } from './use-bar-colors';
|
|
52
|
+
import { useBarLegendItems } from './use-bar-legend-items';
|
|
53
|
+
import type { ComparisonSeriesEntry, SelectedBar } from './private';
|
|
49
54
|
import type { BarChartProps } from './types';
|
|
50
55
|
import type { DataPointDate, SeriesData, Optional } from '../../types';
|
|
51
56
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
@@ -117,6 +122,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
117
122
|
barClassName,
|
|
118
123
|
options = {},
|
|
119
124
|
orientation = 'vertical',
|
|
125
|
+
layout = 'grouped',
|
|
120
126
|
withPatterns = false,
|
|
121
127
|
showZeroValues = false,
|
|
122
128
|
withBandHighlight = false,
|
|
@@ -141,6 +147,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
141
147
|
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
142
148
|
const legendComparisonSeriesItems = legend.comparisonSeriesItems;
|
|
143
149
|
const horizontal = orientation === 'horizontal';
|
|
150
|
+
const isOverlap = layout === 'overlap';
|
|
144
151
|
const chartId = useChartId( providedChartId );
|
|
145
152
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
146
153
|
const isSeriesVisible = useCallback(
|
|
@@ -164,12 +171,25 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
164
171
|
collapseGroups: legendCollapseGroups,
|
|
165
172
|
comparisonItem: legendComparisonItem,
|
|
166
173
|
comparisonSeriesItems: legendComparisonSeriesItems,
|
|
174
|
+
layout,
|
|
167
175
|
} ),
|
|
168
|
-
[ legendCollapseGroups, legendComparisonItem, legendComparisonSeriesItems ]
|
|
176
|
+
[ legendCollapseGroups, legendComparisonItem, legendComparisonSeriesItems, layout ]
|
|
169
177
|
);
|
|
170
|
-
const legendItems =
|
|
178
|
+
const legendItems = useBarLegendItems( dataSorted, legendOptions );
|
|
171
179
|
|
|
172
|
-
const { getElementStyles } = useGlobalChartsContext();
|
|
180
|
+
const { getElementStyles, theme: providerTheme } = useGlobalChartsContext();
|
|
181
|
+
const getBarColors = useBarColors();
|
|
182
|
+
const barColors = useMemo(
|
|
183
|
+
() => getBarColors( dataSorted, { layout } ),
|
|
184
|
+
[ layout, getBarColors, dataSorted ]
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if ( providerTheme.barChart?.barStyles?.comparison?.opacity !== undefined ) {
|
|
188
|
+
warnOnce(
|
|
189
|
+
'bar-chart-comparison-opacity',
|
|
190
|
+
'BarChart: theme.barChart.barStyles.comparison.opacity is deprecated. Comparison bars are opaque; leave it unset.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
173
193
|
|
|
174
194
|
// A hidden series is unmounted, so it is not on the band scale either — the
|
|
175
195
|
// axis has to choose its tick values from what is left. Visibility is owned by
|
|
@@ -239,45 +259,27 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
239
259
|
[ seriesWithVisibility ]
|
|
240
260
|
);
|
|
241
261
|
|
|
242
|
-
const
|
|
243
|
-
Math.max( 0, ...primaryEntries.map( e => e.series.data.length ) ) * primaryEntries.length;
|
|
262
|
+
const overlapSlots = useOverlapSlots( isOverlap, seriesWithVisibility );
|
|
244
263
|
|
|
245
264
|
const primaryKeys = useMemo(
|
|
246
265
|
() => primaryEntries.map( ( { series } ) => series.label ),
|
|
247
266
|
[ primaryEntries ]
|
|
248
267
|
);
|
|
249
268
|
|
|
250
|
-
// The keyboard
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const primarySeries = useMemo(
|
|
254
|
-
() => primaryEntries.map( ( { series } ) => series ),
|
|
255
|
-
[ primaryEntries ]
|
|
256
|
-
);
|
|
269
|
+
// The keyboard index space, the highlight and `XYChartTooltip` must share these stops, or the tooltip's datum diverges from the highlighted bar.
|
|
270
|
+
const stops = useBarStops( overlapSlots, primaryEntries );
|
|
271
|
+
const { barAt, stopOf } = stops;
|
|
257
272
|
|
|
258
|
-
// Walks the selected index the way the highlight style does — series-major
|
|
259
|
-
// within each data point — so the bar handed on is the one outlined.
|
|
260
273
|
const activateSelectedBar = useCallback(
|
|
261
|
-
(
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const dataPointIndex = Math.floor( index / primaryCount );
|
|
269
|
-
const series = primaryEntries[ index % primaryCount ]?.series;
|
|
270
|
-
const datum = series?.data[ dataPointIndex ];
|
|
271
|
-
|
|
272
|
-
if ( series && datum ) {
|
|
273
|
-
onDatumActivate?.( { datum, index: dataPointIndex, key: series.label } );
|
|
274
|
+
( stop: number ) => {
|
|
275
|
+
const bar = barAt( stop );
|
|
276
|
+
if ( bar ) {
|
|
277
|
+
onDatumActivate?.( bar );
|
|
274
278
|
}
|
|
275
279
|
},
|
|
276
|
-
[
|
|
280
|
+
[ barAt, onDatumActivate ]
|
|
277
281
|
);
|
|
278
282
|
|
|
279
|
-
const visibleSeriesKey = useMemo( () => JSON.stringify( primaryKeys ), [ primaryKeys ] );
|
|
280
|
-
|
|
281
283
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown, onChartPointerMove } =
|
|
282
284
|
useKeyboardNavigation( {
|
|
283
285
|
selectedIndex,
|
|
@@ -285,22 +287,27 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
285
287
|
isNavigating,
|
|
286
288
|
setIsNavigating,
|
|
287
289
|
chartRef,
|
|
288
|
-
totalPoints,
|
|
290
|
+
totalPoints: stops.count,
|
|
289
291
|
onActivate: activateSelectedBar,
|
|
290
|
-
visibleSeriesKey,
|
|
292
|
+
visibleSeriesKey: stops.key,
|
|
291
293
|
} );
|
|
292
294
|
|
|
293
295
|
const handlePointerMove = useCallback(
|
|
294
296
|
( { key, index }: { key: string; index: number } ) => {
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
onChartPointerMove(
|
|
297
|
+
const stop = stopOf( key, index );
|
|
298
|
+
if ( stop !== undefined ) {
|
|
299
|
+
onChartPointerMove( stop );
|
|
298
300
|
}
|
|
299
301
|
},
|
|
300
|
-
[
|
|
302
|
+
[ stopOf, onChartPointerMove ]
|
|
301
303
|
);
|
|
302
304
|
|
|
303
305
|
const comparisonEntries = useMemo( () => {
|
|
306
|
+
// An overlap layout draws its comparisons as slotted bars, not as shadows.
|
|
307
|
+
if ( isOverlap ) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
304
311
|
const primaryByGroup = new Map< string | undefined, { label: string; index: number } >(
|
|
305
312
|
primaryEntries.map( ( { series, index } ) => [
|
|
306
313
|
series.group,
|
|
@@ -321,10 +328,10 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
321
328
|
if ( ! primary || ! primaryKeys.includes( primary.label ) ) {
|
|
322
329
|
return;
|
|
323
330
|
}
|
|
324
|
-
entries.push( { series, index, primaryKey: primary.label
|
|
331
|
+
entries.push( { series, index, primaryKey: primary.label } );
|
|
325
332
|
} );
|
|
326
333
|
return entries;
|
|
327
|
-
}, [ seriesWithVisibility, primaryEntries, primaryKeys ] );
|
|
334
|
+
}, [ isOverlap, seriesWithVisibility, primaryEntries, primaryKeys ] );
|
|
328
335
|
|
|
329
336
|
// Comparison widthFactor (how much wider the shadow is than the primary) drives both the
|
|
330
337
|
// shadow width (in ComparisonBars) and the primary narrowing.
|
|
@@ -376,18 +383,48 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
376
383
|
( index: number ) => ( datum: DataPointDate ) =>
|
|
377
384
|
withPatterns
|
|
378
385
|
? `url(#${ getPatternId( chartId, index ) })`
|
|
379
|
-
: ( datum.color ??
|
|
380
|
-
[ withPatterns,
|
|
386
|
+
: ( datum.color ?? barColors[ index ] ?? '' ),
|
|
387
|
+
[ withPatterns, barColors, chartId ]
|
|
381
388
|
);
|
|
382
389
|
|
|
383
|
-
// Comparison shadow fill: when patterns are on, reuse the paired primary's pattern so the
|
|
384
|
-
// shadow reads as the same series; otherwise use the comparison series' resolved color.
|
|
385
390
|
const resolveComparisonFill = useCallback(
|
|
386
|
-
( entry: ComparisonSeriesEntry ) =>
|
|
391
|
+
( entry: ComparisonSeriesEntry, datum: DataPointDate ) =>
|
|
387
392
|
withPatterns
|
|
388
|
-
? `url(#${ getPatternId( chartId, entry.
|
|
389
|
-
:
|
|
390
|
-
[ withPatterns, chartId,
|
|
393
|
+
? `url(#${ getPatternId( chartId, entry.index ) })`
|
|
394
|
+
: ( datum.color ?? barColors[ entry.index ] ?? '' ),
|
|
395
|
+
[ withPatterns, chartId, barColors ]
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// Lists every series with a point at the hovered category, in the order of the data.
|
|
399
|
+
const renderOverlapTooltip = useCallback(
|
|
400
|
+
( { tooltipData }: RenderTooltipParams< DataPointDate > ) => {
|
|
401
|
+
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
402
|
+
if ( ! nearestDatum ) return null;
|
|
403
|
+
|
|
404
|
+
const datumByKey = tooltipData?.datumByKey ?? {};
|
|
405
|
+
const categoryLabel = chartOptions.tooltip.labelFormatter(
|
|
406
|
+
nearestDatum.label || ( nearestDatum.date ? nearestDatum.date.getTime() : 0 ),
|
|
407
|
+
0,
|
|
408
|
+
[]
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
413
|
+
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ categoryLabel }</div>
|
|
414
|
+
{ dataSorted.flatMap( ( { label } ) => {
|
|
415
|
+
const datum = datumByKey[ label ]?.datum;
|
|
416
|
+
return datum
|
|
417
|
+
? [
|
|
418
|
+
<Fragment key={ label }>
|
|
419
|
+
{ renderTooltipRow( label, formatReading( datum.value ) ) }
|
|
420
|
+
</Fragment>,
|
|
421
|
+
]
|
|
422
|
+
: [];
|
|
423
|
+
} ) }
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
[ chartOptions.tooltip, dataSorted ]
|
|
391
428
|
);
|
|
392
429
|
|
|
393
430
|
const renderDefaultTooltip = useCallback(
|
|
@@ -489,44 +526,25 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
489
526
|
[ chartId ]
|
|
490
527
|
);
|
|
491
528
|
|
|
492
|
-
const
|
|
493
|
-
|
|
529
|
+
const overlapBandSlots = useMemo(
|
|
530
|
+
() => overlapSlots && { keys: overlapSlots.slotKeys, slotOf: overlapSlots.slotOf },
|
|
531
|
+
[ overlapSlots ]
|
|
532
|
+
);
|
|
494
533
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if ( ! primaryCount ) {
|
|
500
|
-
return '';
|
|
501
|
-
}
|
|
502
|
-
const dataPointIndex = Math.floor( selectedIndex / primaryCount );
|
|
503
|
-
const seriesIndex = selectedIndex % primaryCount;
|
|
504
|
-
const seriesData = primaryEntries[ seriesIndex ]?.series;
|
|
505
|
-
const datum = seriesData?.data[ dataPointIndex ];
|
|
534
|
+
const selectedSlot = useMemo(
|
|
535
|
+
() => getSelectedSlot( overlapSlots, selectedIndex ),
|
|
536
|
+
[ overlapSlots, selectedIndex ]
|
|
537
|
+
);
|
|
506
538
|
|
|
507
|
-
|
|
508
|
-
|
|
539
|
+
const selectedBar = useMemo( (): SelectedBar | undefined => {
|
|
540
|
+
if ( overlapSlots ) {
|
|
541
|
+
return selectedSlot;
|
|
509
542
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
.reduce( ( count, { series } ) => count + countRenderedBars( series.data ), 0 ) +
|
|
516
|
-
countRenderedBars( seriesData.data.slice( 0, dataPointIndex ) );
|
|
517
|
-
|
|
518
|
-
// Use a CSS class selector instead of ID since useId() generates invalid CSS ID characters
|
|
519
|
-
const generatedStyles = `
|
|
520
|
-
.bar-chart[data-chart-id="bar-chart-${ chartId }"] .visx-bar-group .visx-bar:nth-child(${
|
|
521
|
-
domBarIndex + 1
|
|
522
|
-
}) {
|
|
523
|
-
stroke: #005fcc;
|
|
524
|
-
stroke-width: 2px;
|
|
525
|
-
}
|
|
526
|
-
`;
|
|
527
|
-
|
|
528
|
-
return generatedStyles;
|
|
529
|
-
}, [ selectedIndex, primaryEntries, chartId ] );
|
|
543
|
+
const bar = selectedIndex === undefined ? undefined : barAt( selectedIndex );
|
|
544
|
+
return bar && isBarRendered( bar.datum )
|
|
545
|
+
? { data: [ bar.datum ], slotKey: bar.key, thickness: 1 }
|
|
546
|
+
: undefined;
|
|
547
|
+
}, [ overlapSlots, selectedSlot, selectedIndex, barAt ] );
|
|
530
548
|
|
|
531
549
|
// Validate data first
|
|
532
550
|
const error = validateData( dataSorted );
|
|
@@ -557,7 +575,6 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
557
575
|
}
|
|
558
576
|
|
|
559
577
|
const gridVisibility = gridVisibilityProp ?? chartOptions.gridVisibility;
|
|
560
|
-
const highlightedBarStyle = createKeyboardHighlightStyle();
|
|
561
578
|
|
|
562
579
|
const legendPosition = legend.position ?? 'bottom';
|
|
563
580
|
const legendElement = showLegend && (
|
|
@@ -676,30 +693,16 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
676
693
|
{ withPatterns && (
|
|
677
694
|
<>
|
|
678
695
|
<defs data-testid="bar-chart-patterns">
|
|
679
|
-
{
|
|
680
|
-
renderPattern(
|
|
681
|
-
index,
|
|
682
|
-
getElementStyles( { data: seriesData, index } ).color
|
|
683
|
-
)
|
|
684
|
-
) }
|
|
696
|
+
{ barColors.map( ( color, index ) => renderPattern( index, color ) ) }
|
|
685
697
|
</defs>
|
|
686
698
|
<style>
|
|
687
|
-
{
|
|
688
|
-
createPatternBorderStyle(
|
|
689
|
-
index,
|
|
690
|
-
getElementStyles( { data: seriesData, index } ).color
|
|
691
|
-
)
|
|
699
|
+
{ barColors.map( ( color, index ) =>
|
|
700
|
+
createPatternBorderStyle( index, color )
|
|
692
701
|
) }
|
|
693
702
|
</style>
|
|
694
703
|
</>
|
|
695
704
|
) }
|
|
696
705
|
|
|
697
|
-
{ highlightedBarStyle && (
|
|
698
|
-
<style data-testid="bar-chart-keyboard-highlight">
|
|
699
|
-
{ highlightedBarStyle }
|
|
700
|
-
</style>
|
|
701
|
-
) }
|
|
702
|
-
|
|
703
706
|
{ allSeriesHidden ? (
|
|
704
707
|
<SvgEmptyState
|
|
705
708
|
x={ width / 2 }
|
|
@@ -726,40 +729,24 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
726
729
|
resolveFill={ resolveComparisonFill }
|
|
727
730
|
/>
|
|
728
731
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
/>
|
|
743
|
-
) ) }
|
|
744
|
-
</g>
|
|
745
|
-
) : (
|
|
746
|
-
<BarGroup padding={ groupPadding }>
|
|
747
|
-
{ primaryEntries.map( ( { series: seriesData, index } ) => (
|
|
748
|
-
<BarSeries
|
|
749
|
-
key={ seriesData?.label }
|
|
750
|
-
dataKey={ seriesData?.label }
|
|
751
|
-
data={ seriesData.data as DataPointDate[] }
|
|
752
|
-
yAccessor={ chartOptions.accessors.yAccessor }
|
|
753
|
-
xAccessor={ chartOptions.accessors.xAccessor }
|
|
754
|
-
colorAccessor={ getBarBackground( index ) }
|
|
755
|
-
/>
|
|
756
|
-
) ) }
|
|
757
|
-
</BarGroup>
|
|
758
|
-
) }
|
|
732
|
+
<BarLayer
|
|
733
|
+
chartId={ chartId }
|
|
734
|
+
overlapSlots={ overlapSlots }
|
|
735
|
+
selectedBar={ selectedBar }
|
|
736
|
+
primaryEntries={ primaryEntries }
|
|
737
|
+
primaryKeys={ primaryKeys }
|
|
738
|
+
getBarBackground={ getBarBackground }
|
|
739
|
+
barClassName={ barClassName }
|
|
740
|
+
groupPadding={ groupPadding }
|
|
741
|
+
horizontal={ horizontal }
|
|
742
|
+
xAccessor={ chartOptions.accessors.xAccessor }
|
|
743
|
+
yAccessor={ chartOptions.accessors.yAccessor }
|
|
744
|
+
/>
|
|
759
745
|
{ /* Do not reorder: for one key the last showTooltip wins, so this must run after the primary series. */ }
|
|
760
746
|
{ ( withTooltips || onPointerDown || onPointerUp ) && (
|
|
761
747
|
<BandTooltip
|
|
762
|
-
keys={ primaryKeys }
|
|
748
|
+
keys={ overlapSlots?.keys ?? primaryKeys }
|
|
749
|
+
slots={ overlapBandSlots }
|
|
763
750
|
groupPadding={ groupPadding }
|
|
764
751
|
withTooltips={ withTooltips }
|
|
765
752
|
onPointerDown={ onPointerDown }
|
|
@@ -796,6 +783,12 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
796
783
|
</WholeNumberTicks>
|
|
797
784
|
) }
|
|
798
785
|
|
|
786
|
+
{ withTooltips && selectedSlot && (
|
|
787
|
+
<SlotTooltipMembers
|
|
788
|
+
members={ selectedSlot.members }
|
|
789
|
+
index={ selectedSlot.index }
|
|
790
|
+
/>
|
|
791
|
+
) }
|
|
799
792
|
{ withTooltips && (
|
|
800
793
|
<XYChartTooltip
|
|
801
794
|
tooltipPlacement={ tooltipPlacement }
|
|
@@ -804,13 +797,16 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
804
797
|
detectBounds
|
|
805
798
|
snapTooltipToDatumX
|
|
806
799
|
snapTooltipToDatumY
|
|
807
|
-
renderTooltip={
|
|
800
|
+
renderTooltip={
|
|
801
|
+
renderTooltip ||
|
|
802
|
+
( isOverlap ? renderOverlapTooltip : renderDefaultTooltip )
|
|
803
|
+
}
|
|
808
804
|
selectedIndex={ selectedIndex }
|
|
809
805
|
tooltipRef={ tooltipRef }
|
|
810
806
|
keyboardFocusedClassName={
|
|
811
807
|
styles[ 'bar-chart__tooltip--keyboard-focused' ]
|
|
812
808
|
}
|
|
813
|
-
series={
|
|
809
|
+
series={ stops.series }
|
|
814
810
|
mode="individual"
|
|
815
811
|
/>
|
|
816
812
|
) }
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { default as BarChart, BarChartUnresponsive } from './bar-chart';
|
|
2
|
+
export { useBarColors } from './use-bar-colors';
|
|
3
|
+
export { useBarLegendItems } from './use-bar-legend-items';
|
|
4
|
+
export type { BarLegendOptions } from './use-bar-legend-items';
|
|
2
5
|
export type { BarChartProps, BandHighlightSelection } from './types';
|
|
@@ -40,7 +40,8 @@ export function nearestBandIndex< Datum >(
|
|
|
40
40
|
/**
|
|
41
41
|
* Correct visx's range-based band inversion, which omits outer padding.
|
|
42
42
|
* @param root0 - Registered series selection.
|
|
43
|
-
* @param root0.keys - Visible series keys.
|
|
43
|
+
* @param root0.keys - Visible series keys, back to front where bars overlap.
|
|
44
|
+
* @param root0.slots - Where series share slots: the slots of a category, in the order the band lays them out, and the slot each series sits in.
|
|
44
45
|
* @param root0.groupPadding - Padding between grouped bars.
|
|
45
46
|
* @param root0.withTooltips - Whether pointer events update the tooltip.
|
|
46
47
|
* @param root0.onPointerDown - Receives the corrected pointer-down datum.
|
|
@@ -50,6 +51,7 @@ export function nearestBandIndex< Datum >(
|
|
|
50
51
|
*/
|
|
51
52
|
export function BandTooltip( {
|
|
52
53
|
keys,
|
|
54
|
+
slots,
|
|
53
55
|
groupPadding,
|
|
54
56
|
withTooltips,
|
|
55
57
|
onPointerDown,
|
|
@@ -57,16 +59,17 @@ export function BandTooltip( {
|
|
|
57
59
|
onPointerMove,
|
|
58
60
|
}: {
|
|
59
61
|
keys: string[];
|
|
62
|
+
slots?: { keys: string[]; slotOf: ( key: string ) => string };
|
|
60
63
|
groupPadding: number;
|
|
61
64
|
onPointerMove?: ( selection: PointerSelection ) => void;
|
|
62
65
|
} & Pick< BarChartProps, 'withTooltips' | 'onPointerDown' | 'onPointerUp' > ) {
|
|
63
66
|
const { xScale, yScale, dataRegistry, horizontal } = useContext( DataContext );
|
|
64
|
-
const { showTooltip } = useContext( TooltipContext );
|
|
67
|
+
const { showTooltip, hideTooltip, updateTooltip } = useContext( TooltipContext );
|
|
65
68
|
const scale = ( horizontal ? yScale : xScale ) as BandScale | undefined;
|
|
66
69
|
const bandwidth = scale?.bandwidth?.() ?? 0;
|
|
67
70
|
const groupScale = useMemo(
|
|
68
|
-
() => createGroupScale( keys, bandwidth, groupPadding ),
|
|
69
|
-
[ keys, bandwidth, groupPadding ]
|
|
71
|
+
() => createGroupScale( slots?.keys ?? keys, bandwidth, groupPadding ),
|
|
72
|
+
[ slots, keys, bandwidth, groupPadding ]
|
|
70
73
|
);
|
|
71
74
|
const getSelections = useCallback(
|
|
72
75
|
( params: Parameters< PointerHandler >[ 0 ] ) => {
|
|
@@ -74,28 +77,26 @@ export function BandTooltip( {
|
|
|
74
77
|
if ( ! point || ! scale?.bandwidth ) {
|
|
75
78
|
return [];
|
|
76
79
|
}
|
|
80
|
+
const position = horizontal ? point.y : point.x;
|
|
77
81
|
const selections: PointerSelection[] = [];
|
|
82
|
+
const bands: number[] = [];
|
|
78
83
|
for ( const key of keys ) {
|
|
79
84
|
const entry = dataRegistry?.get( key );
|
|
80
85
|
if ( ! entry ) {
|
|
81
86
|
continue;
|
|
82
87
|
}
|
|
83
88
|
const accessor = horizontal ? entry.yAccessor : entry.xAccessor;
|
|
84
|
-
const index = nearestBandIndex(
|
|
85
|
-
entry.data,
|
|
86
|
-
accessor,
|
|
87
|
-
scale,
|
|
88
|
-
horizontal ? point.y : point.x
|
|
89
|
-
);
|
|
89
|
+
const index = nearestBandIndex( entry.data, accessor, scale, position );
|
|
90
90
|
if ( index < 0 ) {
|
|
91
91
|
continue;
|
|
92
92
|
}
|
|
93
93
|
const datum = entry.data[ index ];
|
|
94
|
-
const
|
|
94
|
+
const band = Number( scale( accessor( datum ) ) );
|
|
95
|
+
const start = band + Number( groupScale( slots ? slots.slotOf( key ) : key ) );
|
|
95
96
|
const end = start + groupScale.step();
|
|
96
|
-
const position = horizontal ? point.y : point.x;
|
|
97
97
|
const distance =
|
|
98
98
|
position >= start && position <= end ? 0 : Math.abs( position - ( start + end ) / 2 );
|
|
99
|
+
bands.push( band );
|
|
99
100
|
selections.push( {
|
|
100
101
|
event: params.event,
|
|
101
102
|
key,
|
|
@@ -106,9 +107,19 @@ export function BandTooltip( {
|
|
|
106
107
|
distanceY: horizontal ? distance : 0,
|
|
107
108
|
} );
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
+
if ( ! slots ) {
|
|
111
|
+
return selections;
|
|
112
|
+
}
|
|
113
|
+
// A series without a datum at the hovered category must not lend the tooltip a neighbor's.
|
|
114
|
+
const bandDistance = ( band: number ) => Math.abs( band + scale.bandwidth() / 2 - position );
|
|
115
|
+
const hoveredBand = bands.reduce< number | undefined >(
|
|
116
|
+
( best, band ) =>
|
|
117
|
+
best === undefined || bandDistance( band ) < bandDistance( best ) ? band : best,
|
|
118
|
+
undefined
|
|
119
|
+
);
|
|
120
|
+
return selections.filter( ( _, index ) => bands[ index ] === hoveredBand );
|
|
110
121
|
},
|
|
111
|
-
[ scale, dataRegistry, horizontal, keys, groupScale ]
|
|
122
|
+
[ scale, dataRegistry, horizontal, keys, slots, groupScale ]
|
|
112
123
|
);
|
|
113
124
|
const handlePointer = useCallback< PointerHandler >(
|
|
114
125
|
params => {
|
|
@@ -125,6 +136,10 @@ export function BandTooltip( {
|
|
|
125
136
|
onPointerMove?.( nearest );
|
|
126
137
|
}
|
|
127
138
|
if ( withTooltips && params?.event.type !== 'pointerup' ) {
|
|
139
|
+
if ( slots ) {
|
|
140
|
+
// visx merges each datum into the previous tooltip's, which would keep a bar the pointer has left.
|
|
141
|
+
updateTooltip( state => ( { ...state, tooltipData: undefined } ) );
|
|
142
|
+
}
|
|
128
143
|
selections.forEach( showTooltip );
|
|
129
144
|
}
|
|
130
145
|
const callback = params?.event.type === 'pointerdown' ? onPointerDown : onPointerUp;
|
|
@@ -135,9 +150,20 @@ export function BandTooltip( {
|
|
|
135
150
|
callback( nearest );
|
|
136
151
|
}
|
|
137
152
|
},
|
|
138
|
-
[
|
|
153
|
+
[
|
|
154
|
+
getSelections,
|
|
155
|
+
withTooltips,
|
|
156
|
+
slots,
|
|
157
|
+
showTooltip,
|
|
158
|
+
updateTooltip,
|
|
159
|
+
onPointerDown,
|
|
160
|
+
onPointerUp,
|
|
161
|
+
onPointerMove,
|
|
162
|
+
]
|
|
139
163
|
);
|
|
140
164
|
useEventEmitter( 'pointermove', withTooltips ? handlePointer : undefined );
|
|
165
|
+
// Slotted bars subscribe to no events, so nothing else closes the tooltip when the pointer leaves.
|
|
166
|
+
useEventEmitter( 'pointerout', withTooltips && slots ? hideTooltip : undefined );
|
|
141
167
|
useEventEmitter( 'pointerdown', handlePointer );
|
|
142
168
|
useEventEmitter( 'pointerup', onPointerUp ? handlePointer : undefined );
|
|
143
169
|
return null;
|