@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.cjs +1412 -787
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +59 -25
  5. package/dist/index.d.cts +64 -120
  6. package/dist/index.d.ts +64 -120
  7. package/dist/index.js +1400 -765
  8. package/dist/index.js.map +1 -1
  9. package/package.json +9 -9
  10. package/src/charts/area-chart/area-chart.tsx +16 -6
  11. package/src/charts/area-chart/test/area-chart.test.tsx +42 -9
  12. package/src/charts/bar-chart/bar-chart.tsx +143 -142
  13. package/src/charts/bar-chart/index.ts +3 -0
  14. package/src/charts/bar-chart/private/band-tooltip.tsx +41 -15
  15. package/src/charts/bar-chart/private/bar-layer.tsx +132 -0
  16. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +22 -34
  17. package/src/charts/bar-chart/private/comparison-bars.tsx +9 -18
  18. package/src/charts/bar-chart/private/comparison-constants.ts +4 -10
  19. package/src/charts/bar-chart/private/grouped-bars.ts +68 -0
  20. package/src/charts/bar-chart/private/index.ts +7 -1
  21. package/src/charts/bar-chart/private/overlap-bars.ts +183 -0
  22. package/src/charts/bar-chart/private/selected-bar-ring.tsx +84 -0
  23. package/src/charts/bar-chart/private/slot-tooltip-members.tsx +28 -0
  24. package/src/charts/bar-chart/private/slotted-bar-series.tsx +84 -0
  25. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +4 -11
  26. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +2 -3
  27. package/src/charts/bar-chart/private/test/grouped-bars.test.ts +87 -0
  28. package/src/charts/bar-chart/private/test/overlap-bars.test.ts +159 -0
  29. package/src/charts/bar-chart/private/use-bar-stops.ts +87 -0
  30. package/src/charts/bar-chart/private/use-overlap-slots.ts +213 -0
  31. package/src/charts/bar-chart/private/value-domain.ts +0 -10
  32. package/src/charts/bar-chart/test/bar-chart-overlap.test.tsx +347 -0
  33. package/src/charts/bar-chart/test/bar-chart.test.tsx +181 -19
  34. package/src/charts/bar-chart/test/use-bar-legend-items.test.tsx +81 -0
  35. package/src/charts/bar-chart/types.ts +3 -0
  36. package/src/charts/bar-chart/use-bar-colors.ts +46 -0
  37. package/src/charts/bar-chart/use-bar-legend-items.ts +87 -0
  38. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +24 -11
  39. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +235 -182
  40. package/src/charts/conversion-funnel-chart/index.ts +2 -0
  41. package/src/charts/conversion-funnel-chart/private/index.ts +1 -0
  42. package/src/charts/conversion-funnel-chart/private/use-funnel-selection.ts +9 -47
  43. package/src/charts/conversion-funnel-chart/private/use-roving-step-focus.ts +83 -0
  44. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +312 -92
  45. package/src/charts/conversion-funnel-chart/test/use-funnel-selection.test.ts +23 -188
  46. package/src/charts/conversion-funnel-chart/types.ts +22 -0
  47. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +41 -1
  48. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +101 -30
  49. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +47 -0
  50. package/src/charts/line-chart/line-chart.tsx +25 -18
  51. package/src/charts/line-chart/test/line-chart.test.tsx +240 -1
  52. package/src/charts/pie-chart/pie-chart.tsx +7 -2
  53. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +7 -2
  54. package/src/charts/private/pie-keyboard-navigation/index.ts +0 -1
  55. package/src/charts/private/{pie-keyboard-navigation/pie-keyboard-navigation.module.scss → selected-ring/selected-ring.module.scss} +11 -6
  56. package/src/charts/private/selected-ring/selected-ring.tsx +73 -0
  57. package/src/charts/private/{pie-keyboard-navigation/test/selected-segment-ring.test.ts → selected-ring/test/selected-ring.test.ts} +1 -1
  58. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +20 -0
  59. package/src/components/legend/hooks/use-chart-legend-items.ts +27 -3
  60. package/src/components/tooltip/private/at-nearest-date.ts +49 -0
  61. package/src/components/tooltip/private/keyboard-stops.ts +67 -0
  62. package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +10 -2
  63. package/src/components/tooltip/test/keyboard-stops.test.ts +36 -0
  64. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +135 -1
  65. package/src/components/tooltip/xy-chart-tooltip.tsx +11 -14
  66. package/src/index.ts +10 -2
  67. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  68. package/src/providers/chart-context/test/chart-context.test.tsx +0 -13
  69. package/src/providers/chart-context/themes.ts +0 -1
  70. package/src/providers/chart-context/types.ts +2 -0
  71. package/src/test-utils/selected-bar-ring.ts +14 -0
  72. package/src/types.ts +14 -5
  73. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +0 -47
@@ -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;
@@ -0,0 +1,132 @@
1
+ import { BarGroup, BarSeries } from '@visx/xychart';
2
+ import ClassifiedBarSeries from './classified-bar-series';
3
+ import { SelectedBarRing } from './selected-bar-ring';
4
+ import SlottedBarSeries from './slotted-bar-series';
5
+ import type { SelectedBar } from './selected-bar-ring';
6
+ import type { OverlapSlots } from './use-overlap-slots';
7
+ import type { DataPointDate, SeriesData } from '../../../types';
8
+ import type { BarChartProps } from '../types';
9
+ import type { AxisScale } from '@visx/axis';
10
+ import type { SeriesProps } from '@visx/xychart';
11
+ import type { FC } from 'react';
12
+
13
+ type Accessors = Pick<
14
+ SeriesProps< AxisScale, AxisScale, DataPointDate >,
15
+ 'xAccessor' | 'yAccessor'
16
+ >;
17
+
18
+ type Props = Accessors & {
19
+ chartId: string;
20
+ overlapSlots: OverlapSlots | undefined;
21
+ selectedBar: SelectedBar | undefined;
22
+ primaryEntries: { series: SeriesData; index: number }[];
23
+ primaryKeys: string[];
24
+ getBarBackground: ( index: number ) => ( datum: DataPointDate ) => string;
25
+ barClassName: BarChartProps[ 'barClassName' ];
26
+ groupPadding: number;
27
+ horizontal: boolean;
28
+ };
29
+
30
+ /**
31
+ * Draws the bars in front of any comparison shadows, slotted for an overlap layout and grouped by visx otherwise, then the selected bar's ring.
32
+ *
33
+ * @param props - Component props.
34
+ * @param props.chartId - The chart's id, which keeps the ring's clip path unique on a page with several charts.
35
+ * @param props.overlapSlots - The overlap layout, or undefined for a grouped one.
36
+ * @param props.selectedBar - The bar or slot the keyboard has selected.
37
+ * @param props.primaryEntries - The visible primary series, for a grouped layout.
38
+ * @param props.primaryKeys - Their labels, in group order.
39
+ * @param props.getBarBackground - Reads a series' bar fill.
40
+ * @param props.barClassName - Adds classes to a bar.
41
+ * @param props.groupPadding - The padding between the bars of a category.
42
+ * @param props.horizontal - Whether the bars run horizontally.
43
+ * @param props.xAccessor - Reads a datum's x value.
44
+ * @param props.yAccessor - Reads a datum's y value.
45
+ * @return The bars.
46
+ */
47
+ export const BarLayer: FC< Props > = ( {
48
+ chartId,
49
+ overlapSlots,
50
+ selectedBar,
51
+ primaryEntries,
52
+ primaryKeys,
53
+ getBarBackground,
54
+ barClassName,
55
+ groupPadding,
56
+ horizontal,
57
+ xAccessor,
58
+ yAccessor,
59
+ } ) => {
60
+ let bars;
61
+ if ( overlapSlots ) {
62
+ bars = (
63
+ <g className="visx-bar-group">
64
+ { overlapSlots.entries.map( ( { series, index } ) => (
65
+ <SlottedBarSeries
66
+ key={ series.label }
67
+ dataKey={ series.label }
68
+ data={ series.data as DataPointDate[] }
69
+ xAccessor={ xAccessor }
70
+ yAccessor={ yAccessor }
71
+ colorAccessor={ getBarBackground( index ) }
72
+ barClassName={ barClassName }
73
+ slotKeys={ overlapSlots.slotKeys }
74
+ slotKey={ overlapSlots.slotOf( series.label ) }
75
+ thickness={ overlapSlots.thicknessOf( series.label ) }
76
+ groupPadding={ groupPadding }
77
+ />
78
+ ) ) }
79
+ </g>
80
+ );
81
+ } else if ( barClassName ) {
82
+ bars = (
83
+ <g className="visx-bar-group">
84
+ { primaryEntries.map( ( { series, index } ) => (
85
+ <ClassifiedBarSeries
86
+ key={ series.label }
87
+ dataKey={ series.label }
88
+ data={ series.data as DataPointDate[] }
89
+ xAccessor={ xAccessor }
90
+ yAccessor={ yAccessor }
91
+ colorAccessor={ getBarBackground( index ) }
92
+ barClassName={ barClassName }
93
+ primaryKeys={ primaryKeys }
94
+ groupPadding={ groupPadding }
95
+ />
96
+ ) ) }
97
+ </g>
98
+ );
99
+ } else {
100
+ bars = (
101
+ <BarGroup padding={ groupPadding }>
102
+ { primaryEntries.map( ( { series, index } ) => (
103
+ <BarSeries
104
+ key={ series.label }
105
+ dataKey={ series.label }
106
+ data={ series.data as DataPointDate[] }
107
+ xAccessor={ xAccessor }
108
+ yAccessor={ yAccessor }
109
+ colorAccessor={ getBarBackground( index ) }
110
+ />
111
+ ) ) }
112
+ </BarGroup>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <>
118
+ { bars }
119
+ { selectedBar && (
120
+ <SelectedBarRing
121
+ chartId={ chartId }
122
+ selected={ selectedBar }
123
+ slotKeys={ overlapSlots?.slotKeys ?? primaryKeys }
124
+ groupPadding={ groupPadding }
125
+ horizontal={ horizontal }
126
+ xAccessor={ xAccessor }
127
+ yAccessor={ yAccessor as ( datum: DataPointDate ) => number | undefined }
128
+ />
129
+ ) }
130
+ </>
131
+ );
132
+ };
@@ -1,10 +1,9 @@
1
- export type ComparisonRect = { x: number; y: number; width: number; height: number };
1
+ export type SlotRect = { x: number; y: number; width: number; height: number };
2
2
 
3
3
  type ValueScale = ( ( v: number ) => number ) & { range: () => unknown[] };
4
4
 
5
5
  /**
6
- * Mirrors visx's getScaleBaseline so comparison shadows and primary bars
7
- * share the same baseline, including out-of-domain zero values.
6
+ * Mirrors visx's getScaleBaseline so bars drawn outside visx share the baseline of its bars, including out-of-domain zero values.
8
7
  *
9
8
  * @param {ValueScale} scale - The continuous value scale.
10
9
  * @return {number} The baseline output position in pixels.
@@ -23,45 +22,34 @@ export function getValueScaleBaseline( scale: ValueScale ): number {
23
22
  }
24
23
 
25
24
  /**
26
- * Compute the rect for a comparison "shadow" bar, centered on the paired
27
- * primary bar slot and scaled by `widthFactor`.
25
+ * Compute the rect that spans `valuePositions` along the value axis, centered on a slot and scaled by `widthFactor` across it.
28
26
  *
29
- * @param {object} params - Geometry inputs.
30
- * @param {boolean} params.horizontal - True for a horizontal bar chart, false for vertical.
31
- * @param {number} params.bandPosition - bandScale(category): start px of the category band.
32
- * @param {number} params.slotOffset - groupScale(primaryKey): offset of the primary slot within the band.
33
- * @param {number} params.slotThickness - groupScale.bandwidth(): primary bar thickness in px.
34
- * @param {number} params.valuePosition - valueScale(value): output px for the bar's data value.
35
- * @param {number} params.baseline - getValueScaleBaseline(valueScale): zero-line output px.
36
- * @param {number} params.widthFactor - Shadow thickness multiplier, e.g. 1.5 for 150% width.
37
- * @return {ComparisonRect} The {x, y, width, height} of the shadow rect.
27
+ * @param {object} params - Geometry inputs.
28
+ * @param {boolean} params.horizontal - True for a horizontal bar chart, false for vertical.
29
+ * @param {number} params.bandPosition - bandScale(category): start px of the category band.
30
+ * @param {number} params.slotOffset - groupScale(slotKey): offset of the slot within the band.
31
+ * @param {number} params.slotThickness - groupScale.bandwidth(): slot thickness in px.
32
+ * @param {number[]} params.valuePositions - Output px the rect must reach, such as the baseline and each bar's value.
33
+ * @param {number} params.widthFactor - Thickness multiplier, e.g. 0.5 for half the slot.
34
+ * @return {SlotRect} The {x, y, width, height} of the rect.
38
35
  */
39
- export function computeComparisonRect( params: {
36
+ export function computeSlotRect( params: {
40
37
  horizontal: boolean;
41
38
  bandPosition: number;
42
39
  slotOffset: number;
43
40
  slotThickness: number;
44
- valuePosition: number;
45
- baseline: number;
41
+ valuePositions: number[];
46
42
  widthFactor: number;
47
- } ): ComparisonRect {
48
- const {
49
- horizontal,
50
- bandPosition,
51
- slotOffset,
52
- slotThickness,
53
- valuePosition,
54
- baseline,
55
- widthFactor,
56
- } = params;
57
- const slotStart = bandPosition + slotOffset;
58
- const shadowThickness = slotThickness * widthFactor;
59
- const shadowStart = slotStart + slotThickness / 2 - shadowThickness / 2;
60
- const valueStart = Math.min( valuePosition, baseline );
61
- const valueLength = Math.abs( baseline - valuePosition );
43
+ } ): SlotRect {
44
+ const { horizontal, bandPosition, slotOffset, slotThickness, valuePositions, widthFactor } =
45
+ params;
46
+ const thickness = slotThickness * widthFactor;
47
+ const start = bandPosition + slotOffset + slotThickness / 2 - thickness / 2;
48
+ const valueStart = Math.min( ...valuePositions );
49
+ const valueLength = Math.max( ...valuePositions ) - valueStart;
62
50
 
63
51
  if ( horizontal ) {
64
- return { x: valueStart, y: shadowStart, width: valueLength, height: shadowThickness };
52
+ return { x: valueStart, y: start, width: valueLength, height: thickness };
65
53
  }
66
- return { x: shadowStart, y: valueStart, width: shadowThickness, height: valueLength };
54
+ return { x: start, y: valueStart, width: thickness, height: valueLength };
67
55
  }
@@ -1,11 +1,8 @@
1
1
  import { DataContext } from '@visx/xychart';
2
2
  import { useContext } from 'react';
3
3
  import { createGroupScale } from './band-scale';
4
- import { computeComparisonRect, getValueScaleBaseline } from './comparison-bars-geometry';
5
- import {
6
- DEFAULT_COMPARISON_OPACITY,
7
- DEFAULT_COMPARISON_WIDTH_FACTOR,
8
- } from './comparison-constants';
4
+ import { computeSlotRect, getValueScaleBaseline } from './comparison-bars-geometry';
5
+ import { DEFAULT_COMPARISON_WIDTH_FACTOR } from './comparison-constants';
9
6
  import type { BandScale } from './band-scale';
10
7
  import type { ElementStyles, GetElementStylesParams } from '../../../providers';
11
8
  import type { DataPointDate, SeriesData } from '../../../types';
@@ -21,8 +18,6 @@ export type ComparisonSeriesEntry = {
21
18
  series: SeriesData;
22
19
  index: number;
23
20
  primaryKey: string;
24
- /** dataSorted index of the paired primary series (used to share its pattern fill). */
25
- primaryIndex: number;
26
21
  };
27
22
 
28
23
  // Minimal shape we need from visx scales — avoids spreading `any` while
@@ -30,7 +25,7 @@ export type ComparisonSeriesEntry = {
30
25
  type AnyScale = ( ( input: unknown ) => number ) &
31
26
  Partial< Pick< BandScale, 'bandwidth' > > & { range: () => unknown[] };
32
27
 
33
- // Renders translucent "shadow" bars for comparison series behind their paired primary bars.
28
+ // Renders the bars of comparison series, wider and behind their paired primary bars.
34
29
  // IMPORTANT: each comparison datum's category key (label or date) must exactly match a key
35
30
  // used by the primary series — if it doesn't, bandScale() returns undefined and the shadow
36
31
  // is silently skipped. Ensure comparison data reuses the same label/date values as primary.
@@ -42,8 +37,8 @@ export const ComparisonBars: FC< {
42
37
  xAccessor: ( d: DataPointDate ) => string | number | Date | undefined;
43
38
  yAccessor: ( d: DataPointDate ) => number | undefined;
44
39
  getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
45
- /** Resolves the shadow fill — the paired primary's pattern when patterns are on, else a color. */
46
- resolveFill: ( entry: ComparisonSeriesEntry ) => string;
40
+ /** Resolves a comparison bar's fill: its pattern when patterns are on, else its datum's or its series' color. */
41
+ resolveFill: ( entry: ComparisonSeriesEntry, datum: DataPointDate ) => string;
47
42
  } > = ( {
48
43
  comparisonEntries,
49
44
  primaryKeys,
@@ -90,10 +85,7 @@ export const ComparisonBars: FC< {
90
85
  }
91
86
 
92
87
  const { barStyles } = getElementStyles( { data: series, index } );
93
- const opacity = barStyles?.opacity ?? DEFAULT_COMPARISON_OPACITY; // safety net; CompleteChartTheme guarantees this value
94
88
  const widthFactor = barStyles?.widthFactor ?? DEFAULT_COMPARISON_WIDTH_FACTOR;
95
- // Fill is the paired primary's pattern (when patterns are on) or its resolved color.
96
- const fill = resolveFill( entry );
97
89
  // The shadow is `widthFactor` × the (narrowed) primary slot. bar-chart.tsx narrows the
98
90
  // primary bars by widening the group padding so this ratio holds with real geometry.
99
91
 
@@ -114,13 +106,12 @@ export const ComparisonBars: FC< {
114
106
  return;
115
107
  }
116
108
 
117
- const rect = computeComparisonRect( {
109
+ const rect = computeSlotRect( {
118
110
  horizontal,
119
111
  bandPosition,
120
112
  slotOffset: slotOffset as number,
121
113
  slotThickness,
122
- valuePosition,
123
- baseline,
114
+ valuePositions: [ baseline, valuePosition ],
124
115
  widthFactor,
125
116
  } );
126
117
 
@@ -133,8 +124,8 @@ export const ComparisonBars: FC< {
133
124
  y={ rect.y }
134
125
  width={ rect.width }
135
126
  height={ rect.height }
136
- fill={ fill }
137
- opacity={ opacity }
127
+ fill={ resolveFill( entry, datum ) }
128
+ opacity={ barStyles?.opacity }
138
129
  />
139
130
  );
140
131
  } );
@@ -1,22 +1,16 @@
1
1
  // Shared comparison-mode tuning constants. These drive the geometry of the
2
- // translucent "shadow" bars and how the primary bars are narrowed to sit in front of them.
2
+ // comparison bars and how the primary bars are narrowed to sit in front of them.
3
3
 
4
4
  /**
5
- * How much wider the comparison shadow is than the primary bar. Fallback used when the
5
+ * How much wider the comparison bar is than the primary bar. Fallback used when the
6
6
  * theme value (`barChart.barStyles.comparison.widthFactor`) is absent. Also drives the
7
- * primary narrowing — the primary stays `1 / widthFactor` of the shadow.
7
+ * primary narrowing — the primary stays `1 / widthFactor` of the comparison.
8
8
  */
9
9
  export const DEFAULT_COMPARISON_WIDTH_FACTOR = 1.5;
10
10
 
11
- /**
12
- * Opacity of the comparison shadow. Fallback used when the theme value
13
- * (`barChart.barStyles.comparison.opacity`) is absent.
14
- */
15
- export const DEFAULT_COMPARISON_OPACITY = 0.5;
16
-
17
11
  /**
18
12
  * Fraction of each per-series step left as a gap between bars within a single tick.
19
- * Larger = more space between adjacent series; the shadow spans `1 - COMPARISON_INNER_GAP` of the step.
13
+ * Larger = more space between adjacent series; the comparison bar spans `1 - COMPARISON_INNER_GAP` of the step.
20
14
  */
21
15
  export const COMPARISON_INNER_GAP = 0.1;
22
16
 
@@ -0,0 +1,68 @@
1
+ import { isValidHexColor } from '../../../utils/color-utils';
2
+ import { shadeWithContrast, tintWithContrast } from './overlap-bars';
3
+ import type { SeriesData } from '../../../types';
4
+
5
+ /**
6
+ * Pairs each comparison series with the primary of its `group`, or with the only primary.
7
+ *
8
+ * @param series - The chart's series.
9
+ * @return The index of each paired comparison's primary, keyed by the comparison's index.
10
+ */
11
+ function getComparisonPrimaries( series: SeriesData[] ): Map< number, number > {
12
+ const primaries = series.flatMap( ( item, index ) =>
13
+ item.options?.type === 'comparison' ? [] : [ { group: item.group, index } ]
14
+ );
15
+ const primaryByGroup = new Map( primaries.map( ( { group, index } ) => [ group, index ] ) );
16
+ const onlyPrimary = primaries.length === 1 ? primaries[ 0 ].index : undefined;
17
+ const pairs = new Map< number, number >();
18
+
19
+ series.forEach( ( item, index ) => {
20
+ const primary =
21
+ item.options?.type === 'comparison'
22
+ ? ( primaryByGroup.get( item.group ) ?? onlyPrimary )
23
+ : undefined;
24
+
25
+ if ( primary !== undefined ) {
26
+ pairs.set( index, primary );
27
+ }
28
+ } );
29
+
30
+ return pairs;
31
+ }
32
+
33
+ /**
34
+ * The colors a grouped bar chart fills its series with. A comparison takes the lightest tint of its primary's color that keeps 3:1 against the background, and the primary in front of it the shade that keeps 3:1 against that tint. A primary with its own `stroke` keeps it, and a comparison with its own, different `stroke` keeps the pair's colors as given.
35
+ *
36
+ * @param series - The chart's series.
37
+ * @param colors - Each series' resolved color.
38
+ * @param background - The hex chart background.
39
+ * @return Each series' fill, by index.
40
+ */
41
+ export function getGroupedBarColors(
42
+ series: SeriesData[],
43
+ colors: string[],
44
+ background: string
45
+ ): string[] {
46
+ const barColors = [ ...colors ];
47
+
48
+ getComparisonPrimaries( series ).forEach( ( primary, comparison ) => {
49
+ const base = colors[ primary ];
50
+ const hasOwnColor =
51
+ !! series[ comparison ].options?.stroke &&
52
+ colors[ comparison ].toLowerCase() !== base.toLowerCase();
53
+
54
+ // A paint such as `currentColor` or `url(#gradient)` cannot be toned, so the pair keeps the colors it was given.
55
+ if ( hasOwnColor || ! isValidHexColor( base ) || ! isValidHexColor( background ) ) {
56
+ return;
57
+ }
58
+
59
+ const tint = tintWithContrast( base, background );
60
+ barColors[ comparison ] = tint;
61
+
62
+ if ( ! series[ primary ].options?.stroke ) {
63
+ barColors[ primary ] = shadeWithContrast( base, tint, background );
64
+ }
65
+ } );
66
+
67
+ return barColors;
68
+ }
@@ -1,6 +1,12 @@
1
1
  export { useBarChartOptions } from './use-bar-chart-options';
2
2
  export { TruncatedXTickComponent, TruncatedYTickComponent } from './truncated-tick-component';
3
3
  export { default as ClassifiedBarSeries } from './classified-bar-series';
4
+ export { default as SlottedBarSeries } from './slotted-bar-series';
5
+ export { BarLayer } from './bar-layer';
6
+ export { SelectedBarRing, type SelectedBar } from './selected-bar-ring';
7
+ export { SlotTooltipMembers, type SlotMember } from './slot-tooltip-members';
8
+ export { useOverlapSlots, getSelectedSlot, isSameCategory, membersAt } from './use-overlap-slots';
9
+ export { useBarStops } from './use-bar-stops';
4
10
  export { ComparisonBars } from './comparison-bars';
5
11
  export type { ComparisonSeriesEntry } from './comparison-bars';
6
12
  export {
@@ -10,6 +16,6 @@ export {
10
16
  COMPARISON_TICK_GAP_FACTOR,
11
17
  } from './comparison-constants';
12
18
  export { BASE_BAND_PADDING_INNER } from './use-bar-chart-options';
13
- export { countRenderedBars, isBarRendered } from './value-domain';
19
+ export { isBarRendered } from './value-domain';
14
20
  export { BandHighlight } from './band-highlight';
15
21
  export { BandTooltip } from './band-tooltip';