@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.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": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
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
|
},
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from 'react';
|
|
14
14
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
15
15
|
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
16
|
+
import { getKeyboardStops } from '../../components/tooltip/private/keyboard-stops';
|
|
16
17
|
import {
|
|
17
18
|
useXYChartTheme,
|
|
18
19
|
useChartDataTransform,
|
|
@@ -144,14 +145,22 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
144
145
|
[ seriesWithVisibility ]
|
|
145
146
|
);
|
|
146
147
|
|
|
148
|
+
const visibleDataSeries = useMemo(
|
|
149
|
+
() => dataSorted.filter( series => isSeriesVisible( series.label ) ),
|
|
150
|
+
[ dataSorted, isSeriesVisible ]
|
|
151
|
+
);
|
|
152
|
+
|
|
147
153
|
// A normalized stack (expand/wiggle/silhouette) turns whole-number data into
|
|
148
154
|
// fractions, so the filter is skipped for every offset but 'none'.
|
|
149
155
|
const hasWholeNumberValues = useMemo(
|
|
150
|
-
() =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
() => ( ! stacked || stackOffset === 'none' ) && hasOnlyWholeNumbers( visibleDataSeries ),
|
|
157
|
+
[ visibleDataSeries, stacked, stackOffset ]
|
|
158
|
+
);
|
|
159
|
+
const visibleSeriesKey = useMemo(
|
|
160
|
+
() => JSON.stringify( visibleDataSeries.map( series => series.label ) ),
|
|
161
|
+
[ visibleDataSeries ]
|
|
154
162
|
);
|
|
163
|
+
const stops = useMemo( () => getKeyboardStops( visibleDataSeries ), [ visibleDataSeries ] );
|
|
155
164
|
|
|
156
165
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
157
166
|
selectedIndex,
|
|
@@ -159,7 +168,8 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
159
168
|
isNavigating,
|
|
160
169
|
setIsNavigating,
|
|
161
170
|
chartRef,
|
|
162
|
-
totalPoints:
|
|
171
|
+
totalPoints: stops.count,
|
|
172
|
+
visibleSeriesKey,
|
|
163
173
|
} );
|
|
164
174
|
|
|
165
175
|
// Computed from the full data set (ignoring legend visibility) so the y-axis stays
|
|
@@ -526,7 +536,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
526
536
|
keyboardFocusedClassName={
|
|
527
537
|
styles[ 'area-chart__tooltip--keyboard-focused' ]
|
|
528
538
|
}
|
|
529
|
-
series={
|
|
539
|
+
series={ visibleDataSeries }
|
|
530
540
|
/>
|
|
531
541
|
<HoverGlyphs
|
|
532
542
|
visibleSeries={ visibleSeries }
|
|
@@ -879,12 +879,6 @@ describe( 'AreaChart', () => {
|
|
|
879
879
|
} );
|
|
880
880
|
|
|
881
881
|
test( 'renders glyphs only for series with a datum at the hovered x (mismatched x-domains)', async () => {
|
|
882
|
-
// Each series' data[0] is a different date. Keyboard nav fires
|
|
883
|
-
// showTooltip for every series at its own data[selectedIndex];
|
|
884
|
-
// the LAST one wins as the nearestDatum, so the resolved hover
|
|
885
|
-
// date is Series B's. HoverGlyphs should:
|
|
886
|
-
// - Skip Series A (no datum at Series B's date) — cumulative += 0
|
|
887
|
-
// - Render a glyph for Series B
|
|
888
882
|
renderWithProvider( {
|
|
889
883
|
data: [
|
|
890
884
|
{
|
|
@@ -901,9 +895,8 @@ describe( 'AreaChart', () => {
|
|
|
901
895
|
|
|
902
896
|
const glyphs = screen.getAllByTestId( /^area-chart-hover-glyph-/ );
|
|
903
897
|
expect( glyphs ).toHaveLength( 1 );
|
|
904
|
-
|
|
905
|
-
expect( screen.
|
|
906
|
-
expect( screen.queryByTestId( 'area-chart-hover-glyph-0' ) ).not.toBeInTheDocument();
|
|
898
|
+
expect( screen.getByTestId( 'area-chart-hover-glyph-0' ) ).toBeInTheDocument();
|
|
899
|
+
expect( screen.queryByTestId( 'area-chart-hover-glyph-1' ) ).not.toBeInTheDocument();
|
|
907
900
|
} );
|
|
908
901
|
} );
|
|
909
902
|
|
|
@@ -1055,4 +1048,44 @@ describe( 'AreaChart', () => {
|
|
|
1055
1048
|
expect( screen.getAllByRole( 'button' )[ 1 ] ).toHaveAttribute( 'aria-pressed', 'true' );
|
|
1056
1049
|
} );
|
|
1057
1050
|
} );
|
|
1051
|
+
|
|
1052
|
+
it( 'gives no keyboard stop to a date only a hidden series has', async () => {
|
|
1053
|
+
const user = userEvent.setup();
|
|
1054
|
+
|
|
1055
|
+
render(
|
|
1056
|
+
<GlobalChartsProvider>
|
|
1057
|
+
<AreaChartUnresponsive
|
|
1058
|
+
width={ 500 }
|
|
1059
|
+
height={ 300 }
|
|
1060
|
+
stacked={ false }
|
|
1061
|
+
defaultHiddenSeries={ [ 'Series B' ] }
|
|
1062
|
+
chartId="test-hidden-only-date-area"
|
|
1063
|
+
data={ [
|
|
1064
|
+
{
|
|
1065
|
+
label: 'Series A',
|
|
1066
|
+
data: [
|
|
1067
|
+
{ date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' },
|
|
1068
|
+
{ date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' },
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
label: 'Series B',
|
|
1073
|
+
data: [
|
|
1074
|
+
{ date: new Date( '2024-01-01' ), value: 5, label: 'Jan 1' },
|
|
1075
|
+
{ date: new Date( '2024-01-03' ), value: 15, label: 'Jan 3' },
|
|
1076
|
+
],
|
|
1077
|
+
},
|
|
1078
|
+
] }
|
|
1079
|
+
/>
|
|
1080
|
+
</GlobalChartsProvider>
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
screen.getByRole( 'application', { name: /area chart/i } ).focus();
|
|
1084
|
+
await user.keyboard( '{ArrowRight}{ArrowRight}{ArrowRight}' );
|
|
1085
|
+
|
|
1086
|
+
await expect( screen.findByRole( 'tooltip' ) ).resolves.toHaveAttribute(
|
|
1087
|
+
'data-testid',
|
|
1088
|
+
'chart-tooltip-1'
|
|
1089
|
+
);
|
|
1090
|
+
} );
|
|
1058
1091
|
} );
|
|
@@ -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,24 @@ 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,
|
|
50
|
+
isSameCategory,
|
|
51
|
+
membersAt,
|
|
47
52
|
} from './private';
|
|
48
|
-
import
|
|
53
|
+
import { useBarColors } from './use-bar-colors';
|
|
54
|
+
import { useBarLegendItems } from './use-bar-legend-items';
|
|
55
|
+
import type { ComparisonSeriesEntry, SelectedBar } from './private';
|
|
49
56
|
import type { BarChartProps } from './types';
|
|
50
57
|
import type { DataPointDate, SeriesData, Optional } from '../../types';
|
|
51
58
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
@@ -117,6 +124,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
117
124
|
barClassName,
|
|
118
125
|
options = {},
|
|
119
126
|
orientation = 'vertical',
|
|
127
|
+
layout = 'grouped',
|
|
120
128
|
withPatterns = false,
|
|
121
129
|
showZeroValues = false,
|
|
122
130
|
withBandHighlight = false,
|
|
@@ -141,6 +149,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
141
149
|
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
142
150
|
const legendComparisonSeriesItems = legend.comparisonSeriesItems;
|
|
143
151
|
const horizontal = orientation === 'horizontal';
|
|
152
|
+
const isOverlap = layout === 'overlap';
|
|
144
153
|
const chartId = useChartId( providedChartId );
|
|
145
154
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
146
155
|
const isSeriesVisible = useCallback(
|
|
@@ -164,12 +173,25 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
164
173
|
collapseGroups: legendCollapseGroups,
|
|
165
174
|
comparisonItem: legendComparisonItem,
|
|
166
175
|
comparisonSeriesItems: legendComparisonSeriesItems,
|
|
176
|
+
layout,
|
|
167
177
|
} ),
|
|
168
|
-
[ legendCollapseGroups, legendComparisonItem, legendComparisonSeriesItems ]
|
|
178
|
+
[ legendCollapseGroups, legendComparisonItem, legendComparisonSeriesItems, layout ]
|
|
169
179
|
);
|
|
170
|
-
const legendItems =
|
|
180
|
+
const legendItems = useBarLegendItems( dataSorted, legendOptions );
|
|
171
181
|
|
|
172
|
-
const { getElementStyles } = useGlobalChartsContext();
|
|
182
|
+
const { getElementStyles, theme: providerTheme } = useGlobalChartsContext();
|
|
183
|
+
const getBarColors = useBarColors();
|
|
184
|
+
const barColors = useMemo(
|
|
185
|
+
() => getBarColors( dataSorted, { layout } ),
|
|
186
|
+
[ layout, getBarColors, dataSorted ]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if ( providerTheme.barChart?.barStyles?.comparison?.opacity !== undefined ) {
|
|
190
|
+
warnOnce(
|
|
191
|
+
'bar-chart-comparison-opacity',
|
|
192
|
+
'BarChart: theme.barChart.barStyles.comparison.opacity is deprecated. Comparison bars are opaque; leave it unset.'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
173
195
|
|
|
174
196
|
// A hidden series is unmounted, so it is not on the band scale either — the
|
|
175
197
|
// axis has to choose its tick values from what is left. Visibility is owned by
|
|
@@ -239,45 +261,27 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
239
261
|
[ seriesWithVisibility ]
|
|
240
262
|
);
|
|
241
263
|
|
|
242
|
-
const
|
|
243
|
-
Math.max( 0, ...primaryEntries.map( e => e.series.data.length ) ) * primaryEntries.length;
|
|
264
|
+
const overlapSlots = useOverlapSlots( isOverlap, seriesWithVisibility );
|
|
244
265
|
|
|
245
266
|
const primaryKeys = useMemo(
|
|
246
267
|
() => primaryEntries.map( ( { series } ) => series.label ),
|
|
247
268
|
[ primaryEntries ]
|
|
248
269
|
);
|
|
249
270
|
|
|
250
|
-
// The keyboard
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const primarySeries = useMemo(
|
|
254
|
-
() => primaryEntries.map( ( { series } ) => series ),
|
|
255
|
-
[ primaryEntries ]
|
|
256
|
-
);
|
|
271
|
+
// The keyboard index space, the highlight and `XYChartTooltip` must share these stops, or the tooltip's datum diverges from the highlighted bar.
|
|
272
|
+
const stops = useBarStops( overlapSlots, primaryEntries );
|
|
273
|
+
const { barAt, stopOf } = stops;
|
|
257
274
|
|
|
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
275
|
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 } );
|
|
276
|
+
( stop: number ) => {
|
|
277
|
+
const bar = barAt( stop );
|
|
278
|
+
if ( bar ) {
|
|
279
|
+
onDatumActivate?.( bar );
|
|
274
280
|
}
|
|
275
281
|
},
|
|
276
|
-
[
|
|
282
|
+
[ barAt, onDatumActivate ]
|
|
277
283
|
);
|
|
278
284
|
|
|
279
|
-
const visibleSeriesKey = useMemo( () => JSON.stringify( primaryKeys ), [ primaryKeys ] );
|
|
280
|
-
|
|
281
285
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown, onChartPointerMove } =
|
|
282
286
|
useKeyboardNavigation( {
|
|
283
287
|
selectedIndex,
|
|
@@ -285,22 +289,27 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
285
289
|
isNavigating,
|
|
286
290
|
setIsNavigating,
|
|
287
291
|
chartRef,
|
|
288
|
-
totalPoints,
|
|
292
|
+
totalPoints: stops.count,
|
|
289
293
|
onActivate: activateSelectedBar,
|
|
290
|
-
visibleSeriesKey,
|
|
294
|
+
visibleSeriesKey: stops.key,
|
|
291
295
|
} );
|
|
292
296
|
|
|
293
297
|
const handlePointerMove = useCallback(
|
|
294
298
|
( { key, index }: { key: string; index: number } ) => {
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
onChartPointerMove(
|
|
299
|
+
const stop = stopOf( key, index );
|
|
300
|
+
if ( stop !== undefined ) {
|
|
301
|
+
onChartPointerMove( stop );
|
|
298
302
|
}
|
|
299
303
|
},
|
|
300
|
-
[
|
|
304
|
+
[ stopOf, onChartPointerMove ]
|
|
301
305
|
);
|
|
302
306
|
|
|
303
307
|
const comparisonEntries = useMemo( () => {
|
|
308
|
+
// An overlap layout draws its comparisons as slotted bars, not as shadows.
|
|
309
|
+
if ( isOverlap ) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
304
313
|
const primaryByGroup = new Map< string | undefined, { label: string; index: number } >(
|
|
305
314
|
primaryEntries.map( ( { series, index } ) => [
|
|
306
315
|
series.group,
|
|
@@ -321,10 +330,10 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
321
330
|
if ( ! primary || ! primaryKeys.includes( primary.label ) ) {
|
|
322
331
|
return;
|
|
323
332
|
}
|
|
324
|
-
entries.push( { series, index, primaryKey: primary.label
|
|
333
|
+
entries.push( { series, index, primaryKey: primary.label } );
|
|
325
334
|
} );
|
|
326
335
|
return entries;
|
|
327
|
-
}, [ seriesWithVisibility, primaryEntries, primaryKeys ] );
|
|
336
|
+
}, [ isOverlap, seriesWithVisibility, primaryEntries, primaryKeys ] );
|
|
328
337
|
|
|
329
338
|
// Comparison widthFactor (how much wider the shadow is than the primary) drives both the
|
|
330
339
|
// shadow width (in ComparisonBars) and the primary narrowing.
|
|
@@ -376,18 +385,48 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
376
385
|
( index: number ) => ( datum: DataPointDate ) =>
|
|
377
386
|
withPatterns
|
|
378
387
|
? `url(#${ getPatternId( chartId, index ) })`
|
|
379
|
-
: ( datum.color ??
|
|
380
|
-
[ withPatterns,
|
|
388
|
+
: ( datum.color ?? barColors[ index ] ?? '' ),
|
|
389
|
+
[ withPatterns, barColors, chartId ]
|
|
381
390
|
);
|
|
382
391
|
|
|
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
392
|
const resolveComparisonFill = useCallback(
|
|
386
|
-
( entry: ComparisonSeriesEntry ) =>
|
|
393
|
+
( entry: ComparisonSeriesEntry, datum: DataPointDate ) =>
|
|
387
394
|
withPatterns
|
|
388
|
-
? `url(#${ getPatternId( chartId, entry.
|
|
389
|
-
:
|
|
390
|
-
[ withPatterns, chartId,
|
|
395
|
+
? `url(#${ getPatternId( chartId, entry.index ) })`
|
|
396
|
+
: ( datum.color ?? barColors[ entry.index ] ?? '' ),
|
|
397
|
+
[ withPatterns, chartId, barColors ]
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Lists every series with a point at the hovered category, in the order of the data.
|
|
401
|
+
const renderOverlapTooltip = useCallback(
|
|
402
|
+
( { tooltipData }: RenderTooltipParams< DataPointDate > ) => {
|
|
403
|
+
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
404
|
+
if ( ! nearestDatum ) return null;
|
|
405
|
+
|
|
406
|
+
const datumByKey = tooltipData?.datumByKey ?? {};
|
|
407
|
+
const categoryLabel = chartOptions.tooltip.labelFormatter(
|
|
408
|
+
nearestDatum.label || ( nearestDatum.date ? nearestDatum.date.getTime() : 0 ),
|
|
409
|
+
0,
|
|
410
|
+
[]
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
415
|
+
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ categoryLabel }</div>
|
|
416
|
+
{ dataSorted.flatMap( ( { label } ) => {
|
|
417
|
+
const datum = datumByKey[ label ]?.datum;
|
|
418
|
+
return datum
|
|
419
|
+
? [
|
|
420
|
+
<Fragment key={ label }>
|
|
421
|
+
{ renderTooltipRow( label, formatReading( datum.value ) ) }
|
|
422
|
+
</Fragment>,
|
|
423
|
+
]
|
|
424
|
+
: [];
|
|
425
|
+
} ) }
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
},
|
|
429
|
+
[ chartOptions.tooltip, dataSorted ]
|
|
391
430
|
);
|
|
392
431
|
|
|
393
432
|
const renderDefaultTooltip = useCallback(
|
|
@@ -405,12 +444,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
405
444
|
// Find the comparison value paired with the hovered primary series (same group)
|
|
406
445
|
// at the same category, so the tooltip can show both periods at once.
|
|
407
446
|
const comparisonEntry = comparisonEntries.find( entry => entry.primaryKey === primaryKey );
|
|
408
|
-
const comparisonDatum = comparisonEntry?.series.data.find( point =>
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
? p.label === nearestDatum.label
|
|
412
|
-
: !! nearestDatum.date && !! p.date && p.date.getTime() === nearestDatum.date.getTime();
|
|
413
|
-
} ) as DataPointDate | undefined;
|
|
447
|
+
const comparisonDatum = comparisonEntry?.series.data.find( point =>
|
|
448
|
+
isSameCategory( point as DataPointDate, nearestDatum )
|
|
449
|
+
) as DataPointDate | undefined;
|
|
414
450
|
|
|
415
451
|
// With a paired comparison value, show the category as the header and one row
|
|
416
452
|
// per period (primary + comparison).
|
|
@@ -489,44 +525,35 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
489
525
|
[ chartId ]
|
|
490
526
|
);
|
|
491
527
|
|
|
492
|
-
const
|
|
493
|
-
|
|
528
|
+
const overlapBandSlots = useMemo(
|
|
529
|
+
() => overlapSlots && { keys: overlapSlots.slotKeys, slotOf: overlapSlots.slotOf },
|
|
530
|
+
[ overlapSlots ]
|
|
531
|
+
);
|
|
494
532
|
|
|
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 ];
|
|
533
|
+
const selectedSlot = useMemo(
|
|
534
|
+
() => getSelectedSlot( overlapSlots, selectedIndex ),
|
|
535
|
+
[ overlapSlots, selectedIndex ]
|
|
536
|
+
);
|
|
506
537
|
|
|
507
|
-
|
|
508
|
-
|
|
538
|
+
const selectedGroup = useMemo( () => {
|
|
539
|
+
const stop = overlapSlots || selectedIndex === undefined ? undefined : barAt( selectedIndex );
|
|
540
|
+
if ( ! stop ) {
|
|
541
|
+
return undefined;
|
|
509
542
|
}
|
|
543
|
+
return membersAt( primaryEntries, stop.datum, stop.key );
|
|
544
|
+
}, [ overlapSlots, selectedIndex, barAt, primaryEntries ] );
|
|
510
545
|
|
|
511
|
-
|
|
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
|
-
`;
|
|
546
|
+
const selectedMembers = selectedSlot?.members ?? selectedGroup;
|
|
527
547
|
|
|
528
|
-
|
|
529
|
-
|
|
548
|
+
const selectedBar = useMemo( (): SelectedBar | undefined => {
|
|
549
|
+
if ( overlapSlots ) {
|
|
550
|
+
return selectedSlot;
|
|
551
|
+
}
|
|
552
|
+
const bar = selectedIndex === undefined ? undefined : barAt( selectedIndex );
|
|
553
|
+
return bar && isBarRendered( bar.datum )
|
|
554
|
+
? { data: [ bar.datum ], slotKey: bar.key, thickness: 1 }
|
|
555
|
+
: undefined;
|
|
556
|
+
}, [ overlapSlots, selectedSlot, selectedIndex, barAt ] );
|
|
530
557
|
|
|
531
558
|
// Validate data first
|
|
532
559
|
const error = validateData( dataSorted );
|
|
@@ -557,7 +584,6 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
557
584
|
}
|
|
558
585
|
|
|
559
586
|
const gridVisibility = gridVisibilityProp ?? chartOptions.gridVisibility;
|
|
560
|
-
const highlightedBarStyle = createKeyboardHighlightStyle();
|
|
561
587
|
|
|
562
588
|
const legendPosition = legend.position ?? 'bottom';
|
|
563
589
|
const legendElement = showLegend && (
|
|
@@ -676,30 +702,16 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
676
702
|
{ withPatterns && (
|
|
677
703
|
<>
|
|
678
704
|
<defs data-testid="bar-chart-patterns">
|
|
679
|
-
{
|
|
680
|
-
renderPattern(
|
|
681
|
-
index,
|
|
682
|
-
getElementStyles( { data: seriesData, index } ).color
|
|
683
|
-
)
|
|
684
|
-
) }
|
|
705
|
+
{ barColors.map( ( color, index ) => renderPattern( index, color ) ) }
|
|
685
706
|
</defs>
|
|
686
707
|
<style>
|
|
687
|
-
{
|
|
688
|
-
createPatternBorderStyle(
|
|
689
|
-
index,
|
|
690
|
-
getElementStyles( { data: seriesData, index } ).color
|
|
691
|
-
)
|
|
708
|
+
{ barColors.map( ( color, index ) =>
|
|
709
|
+
createPatternBorderStyle( index, color )
|
|
692
710
|
) }
|
|
693
711
|
</style>
|
|
694
712
|
</>
|
|
695
713
|
) }
|
|
696
714
|
|
|
697
|
-
{ highlightedBarStyle && (
|
|
698
|
-
<style data-testid="bar-chart-keyboard-highlight">
|
|
699
|
-
{ highlightedBarStyle }
|
|
700
|
-
</style>
|
|
701
|
-
) }
|
|
702
|
-
|
|
703
715
|
{ allSeriesHidden ? (
|
|
704
716
|
<SvgEmptyState
|
|
705
717
|
x={ width / 2 }
|
|
@@ -726,40 +738,24 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
726
738
|
resolveFill={ resolveComparisonFill }
|
|
727
739
|
/>
|
|
728
740
|
|
|
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
|
-
) }
|
|
741
|
+
<BarLayer
|
|
742
|
+
chartId={ chartId }
|
|
743
|
+
overlapSlots={ overlapSlots }
|
|
744
|
+
selectedBar={ selectedBar }
|
|
745
|
+
primaryEntries={ primaryEntries }
|
|
746
|
+
primaryKeys={ primaryKeys }
|
|
747
|
+
getBarBackground={ getBarBackground }
|
|
748
|
+
barClassName={ barClassName }
|
|
749
|
+
groupPadding={ groupPadding }
|
|
750
|
+
horizontal={ horizontal }
|
|
751
|
+
xAccessor={ chartOptions.accessors.xAccessor }
|
|
752
|
+
yAccessor={ chartOptions.accessors.yAccessor }
|
|
753
|
+
/>
|
|
759
754
|
{ /* Do not reorder: for one key the last showTooltip wins, so this must run after the primary series. */ }
|
|
760
755
|
{ ( withTooltips || onPointerDown || onPointerUp ) && (
|
|
761
756
|
<BandTooltip
|
|
762
|
-
keys={ primaryKeys }
|
|
757
|
+
keys={ overlapSlots?.keys ?? primaryKeys }
|
|
758
|
+
slots={ overlapBandSlots }
|
|
763
759
|
groupPadding={ groupPadding }
|
|
764
760
|
withTooltips={ withTooltips }
|
|
765
761
|
onPointerDown={ onPointerDown }
|
|
@@ -796,6 +792,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
796
792
|
</WholeNumberTicks>
|
|
797
793
|
) }
|
|
798
794
|
|
|
795
|
+
{ withTooltips && selectedMembers && (
|
|
796
|
+
<SlotTooltipMembers members={ selectedMembers } />
|
|
797
|
+
) }
|
|
799
798
|
{ withTooltips && (
|
|
800
799
|
<XYChartTooltip
|
|
801
800
|
tooltipPlacement={ tooltipPlacement }
|
|
@@ -804,13 +803,15 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
804
803
|
detectBounds
|
|
805
804
|
snapTooltipToDatumX
|
|
806
805
|
snapTooltipToDatumY
|
|
807
|
-
renderTooltip={
|
|
806
|
+
renderTooltip={
|
|
807
|
+
renderTooltip ||
|
|
808
|
+
( isOverlap ? renderOverlapTooltip : renderDefaultTooltip )
|
|
809
|
+
}
|
|
808
810
|
selectedIndex={ selectedIndex }
|
|
809
811
|
tooltipRef={ tooltipRef }
|
|
810
812
|
keyboardFocusedClassName={
|
|
811
813
|
styles[ 'bar-chart__tooltip--keyboard-focused' ]
|
|
812
814
|
}
|
|
813
|
-
series={ primarySeries }
|
|
814
815
|
mode="individual"
|
|
815
816
|
/>
|
|
816
817
|
) }
|
|
@@ -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';
|