@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
|
@@ -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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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}
|
|
30
|
-
* @param {boolean}
|
|
31
|
-
* @param {number}
|
|
32
|
-
* @param {number}
|
|
33
|
-
* @param {number}
|
|
34
|
-
* @param {number}
|
|
35
|
-
* @param {number}
|
|
36
|
-
* @
|
|
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
|
|
36
|
+
export function computeSlotRect( params: {
|
|
40
37
|
horizontal: boolean;
|
|
41
38
|
bandPosition: number;
|
|
42
39
|
slotOffset: number;
|
|
43
40
|
slotThickness: number;
|
|
44
|
-
|
|
45
|
-
baseline: number;
|
|
41
|
+
valuePositions: number[];
|
|
46
42
|
widthFactor: number;
|
|
47
|
-
} ):
|
|
48
|
-
const {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
52
|
+
return { x: valueStart, y: start, width: valueLength, height: thickness };
|
|
65
53
|
}
|
|
66
|
-
return { x:
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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={
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
19
|
+
export { isBarRendered } from './value-domain';
|
|
14
20
|
export { BandHighlight } from './band-highlight';
|
|
15
21
|
export { BandTooltip } from './band-tooltip';
|