@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.cjs +856 -236
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +19 -13
  5. package/dist/index.d.cts +43 -4
  6. package/dist/index.d.ts +43 -4
  7. package/dist/index.js +855 -237
  8. package/dist/index.js.map +1 -1
  9. package/package.json +10 -10
  10. package/src/charts/bar-chart/bar-chart.tsx +134 -138
  11. package/src/charts/bar-chart/index.ts +3 -0
  12. package/src/charts/bar-chart/private/band-tooltip.tsx +41 -15
  13. package/src/charts/bar-chart/private/bar-layer.tsx +132 -0
  14. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +22 -34
  15. package/src/charts/bar-chart/private/comparison-bars.tsx +9 -18
  16. package/src/charts/bar-chart/private/comparison-constants.ts +4 -10
  17. package/src/charts/bar-chart/private/grouped-bars.ts +68 -0
  18. package/src/charts/bar-chart/private/index.ts +7 -1
  19. package/src/charts/bar-chart/private/overlap-bars.ts +183 -0
  20. package/src/charts/bar-chart/private/selected-bar-ring.tsx +84 -0
  21. package/src/charts/bar-chart/private/slot-tooltip-members.tsx +29 -0
  22. package/src/charts/bar-chart/private/slotted-bar-series.tsx +84 -0
  23. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +4 -11
  24. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +2 -3
  25. package/src/charts/bar-chart/private/test/grouped-bars.test.ts +87 -0
  26. package/src/charts/bar-chart/private/test/overlap-bars.test.ts +159 -0
  27. package/src/charts/bar-chart/private/use-bar-stops.ts +89 -0
  28. package/src/charts/bar-chart/private/use-overlap-slots.ts +194 -0
  29. package/src/charts/bar-chart/private/value-domain.ts +0 -10
  30. package/src/charts/bar-chart/test/bar-chart-overlap.test.tsx +347 -0
  31. package/src/charts/bar-chart/test/bar-chart.test.tsx +66 -19
  32. package/src/charts/bar-chart/test/use-bar-legend-items.test.tsx +81 -0
  33. package/src/charts/bar-chart/types.ts +3 -0
  34. package/src/charts/bar-chart/use-bar-colors.ts +46 -0
  35. package/src/charts/bar-chart/use-bar-legend-items.ts +87 -0
  36. package/src/charts/line-chart/line-chart.tsx +22 -17
  37. package/src/charts/line-chart/test/line-chart.test.tsx +165 -1
  38. package/src/charts/pie-chart/pie-chart.tsx +7 -2
  39. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +7 -2
  40. package/src/charts/private/pie-keyboard-navigation/index.ts +0 -1
  41. package/src/charts/private/{pie-keyboard-navigation/pie-keyboard-navigation.module.scss → selected-ring/selected-ring.module.scss} +11 -6
  42. package/src/charts/private/selected-ring/selected-ring.tsx +73 -0
  43. package/src/charts/private/{pie-keyboard-navigation/test/selected-segment-ring.test.ts → selected-ring/test/selected-ring.test.ts} +1 -1
  44. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +20 -0
  45. package/src/components/legend/hooks/use-chart-legend-items.ts +27 -3
  46. package/src/components/tooltip/xy-chart-tooltip.tsx +3 -2
  47. package/src/index.ts +8 -2
  48. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  49. package/src/providers/chart-context/test/chart-context.test.tsx +0 -13
  50. package/src/providers/chart-context/themes.ts +0 -1
  51. package/src/providers/chart-context/types.ts +2 -0
  52. package/src/test-utils/selected-bar-ring.ts +14 -0
  53. package/src/types.ts +10 -2
  54. 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.11.0",
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.11",
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.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.4.6",
102
- "@storybook/react": "10.4.6",
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": "18.3.31",
111
- "@types/react-dom": "18.3.7",
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": "18.3.1",
124
- "react-dom": "18.3.1",
123
+ "react": "19.3.0",
124
+ "react-dom": "19.3.0",
125
125
  "sass-embedded": "1.97.3",
126
- "storybook": "10.4.6",
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, BarSeries, BarGroup, Grid, XYChart } from '@visx/xychart';
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, useChartLegendItems } from '../../components/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
- ClassifiedBarSeries,
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 type { ComparisonSeriesEntry } from './private';
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 = useChartLegendItems( dataSorted, legendOptions );
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 totalPoints =
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-navigation index space and the highlight CSS both stride over primary
251
- // bars only; `XYChartTooltip` must use the same list, or its datum diverges from
252
- // the highlighted bar once a comparison series shifts the indices.
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
- ( index: number ) => {
262
- const primaryCount = primaryEntries.length;
263
-
264
- if ( ! primaryCount ) {
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
- [ primaryEntries, onDatumActivate ]
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 seriesIndex = primaryKeys.indexOf( key );
296
- if ( seriesIndex >= 0 ) {
297
- onChartPointerMove( index * primaryKeys.length + seriesIndex );
297
+ const stop = stopOf( key, index );
298
+ if ( stop !== undefined ) {
299
+ onChartPointerMove( stop );
298
300
  }
299
301
  },
300
- [ primaryKeys, onChartPointerMove ]
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, primaryIndex: primary.index } );
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 ?? getElementStyles( { data: dataSorted[ index ], index } ).color ),
380
- [ withPatterns, getElementStyles, dataSorted, chartId ]
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.primaryIndex ) })`
389
- : getElementStyles( { data: entry.series, index: entry.index } ).color,
390
- [ withPatterns, chartId, getElementStyles ]
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 createKeyboardHighlightStyle = useCallback( () => {
493
- if ( selectedIndex === undefined ) return '';
529
+ const overlapBandSlots = useMemo(
530
+ () => overlapSlots && { keys: overlapSlots.slotKeys, slotOf: overlapSlots.slotOf },
531
+ [ overlapSlots ]
532
+ );
494
533
 
495
- // Use only primary entries: comparison series have no .visx-bar elements, so
496
- // their indices must not appear in the nth-child selector.
497
- // Pattern: [series1[0], series2[0], series3[0], series1[1], series2[1], series3[1], ...]
498
- const primaryCount = primaryEntries.length;
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
- if ( ! seriesData || ! datum || ! isBarRendered( datum ) ) {
508
- return '';
539
+ const selectedBar = useMemo( (): SelectedBar | undefined => {
540
+ if ( overlapSlots ) {
541
+ return selectedSlot;
509
542
  }
510
-
511
- // The single .visx-bar-group holds series after series, and only the points visx drew a bar for.
512
- const domBarIndex =
513
- primaryEntries
514
- .slice( 0, seriesIndex )
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
- { dataSorted.map( ( seriesData, index ) =>
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
- { dataSorted.map( ( seriesData, index ) =>
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
- { barClassName ? (
730
- <g className="visx-bar-group">
731
- { primaryEntries.map( ( { series: seriesData, index } ) => (
732
- <ClassifiedBarSeries
733
- key={ seriesData.label }
734
- dataKey={ seriesData.label }
735
- data={ seriesData.data as DataPointDate[] }
736
- xAccessor={ chartOptions.accessors.xAccessor }
737
- yAccessor={ chartOptions.accessors.yAccessor }
738
- colorAccessor={ getBarBackground( index ) }
739
- barClassName={ barClassName }
740
- primaryKeys={ primaryKeys }
741
- groupPadding={ groupPadding }
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={ renderTooltip || renderDefaultTooltip }
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={ primarySeries }
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 start = Number( scale( accessor( datum ) ) ) + Number( groupScale( key ) );
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
- return selections;
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
- [ getSelections, withTooltips, showTooltip, onPointerDown, onPointerUp, onPointerMove ]
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;