@automattic/charts 4.2.0 → 4.4.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 +28 -0
- package/dist/index.cjs +533 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +535 -129
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +42 -10
- package/src/charts/area-chart/private/validate-data.ts +2 -3
- package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
- package/src/charts/bar-chart/bar-chart.tsx +135 -96
- package/src/charts/bar-chart/index.ts +1 -1
- package/src/charts/bar-chart/private/band-highlight.tsx +3 -1
- package/src/charts/bar-chart/private/band-tooltip.tsx +1 -1
- package/src/charts/bar-chart/private/classified-bar-series.tsx +85 -0
- package/src/charts/bar-chart/private/comparison-bars-geometry.ts +3 -6
- package/src/charts/bar-chart/private/index.ts +2 -0
- package/src/charts/bar-chart/private/test/band-highlight.test.tsx +28 -2
- package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +2 -3
- package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +12 -39
- package/src/charts/bar-chart/private/value-domain.ts +90 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +614 -10
- package/src/charts/bar-chart/types.ts +43 -0
- package/src/charts/line-chart/line-chart.tsx +148 -35
- package/src/charts/line-chart/private/index.ts +1 -0
- package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
- package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
- package/src/charts/line-chart/types.ts +2 -1
- package/src/charts/private/readings.ts +34 -0
- package/src/charts/private/test/readings.test.ts +29 -0
- package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
- package/src/charts/private/whole-number-ticks.tsx +79 -0
- package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
- package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
- package/src/components/legend/private/base-legend.tsx +36 -21
- package/src/components/legend/test/legend.test.tsx +84 -0
- package/src/components/legend/types.ts +2 -0
- package/src/components/legend/utils/index.ts +0 -1
- package/src/components/tooltip/accessible-tooltip.tsx +1 -1
- package/src/hooks/test/use-chart-margin.test.tsx +35 -0
- package/src/hooks/use-chart-margin.tsx +7 -2
- package/src/providers/chart-context/global-charts-provider.tsx +1 -1
- package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
- package/src/types.ts +8 -0
- package/src/components/legend/utils/label-transform-factory.ts +0 -24
package/package.json
CHANGED
|
@@ -37,6 +37,7 @@ import { ChartLayout } from '../private/chart-layout';
|
|
|
37
37
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
38
38
|
import { getCurveType } from '../private/time-axis';
|
|
39
39
|
import { buildTimeAxisOptions } from '../private/time-axis-options';
|
|
40
|
+
import { hasOnlyWholeNumbers, WholeNumberTicks } from '../private/whole-number-ticks';
|
|
40
41
|
import { withResponsive } from '../private/with-responsive';
|
|
41
42
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
42
43
|
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
@@ -142,6 +143,15 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
142
143
|
[ seriesWithVisibility ]
|
|
143
144
|
);
|
|
144
145
|
|
|
146
|
+
// A normalized stack (expand/wiggle/silhouette) turns whole-number data into
|
|
147
|
+
// fractions, so the filter is skipped for every offset but 'none'.
|
|
148
|
+
const hasWholeNumberValues = useMemo(
|
|
149
|
+
() =>
|
|
150
|
+
( ! stacked || stackOffset === 'none' ) &&
|
|
151
|
+
hasOnlyWholeNumbers( dataSorted.filter( series => isSeriesVisible( series.label ) ) ),
|
|
152
|
+
[ dataSorted, isSeriesVisible, stacked, stackOffset ]
|
|
153
|
+
);
|
|
154
|
+
|
|
145
155
|
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
146
156
|
selectedIndex,
|
|
147
157
|
setSelectedIndex,
|
|
@@ -250,8 +260,9 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
250
260
|
withGlyph: false,
|
|
251
261
|
glyphSize: 0,
|
|
252
262
|
collapseGroups: legend.collapseGroups ?? false,
|
|
263
|
+
comparisonItem: legend.comparisonItem ?? false,
|
|
253
264
|
} ),
|
|
254
|
-
[ legend.collapseGroups ]
|
|
265
|
+
[ legend.collapseGroups, legend.comparisonItem ]
|
|
255
266
|
);
|
|
256
267
|
const legendItems = useChartLegendItems( dataSorted, legendOptions, legendShape );
|
|
257
268
|
|
|
@@ -438,15 +449,36 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
438
449
|
{ /* With every series hidden the value scale collapses, so the grid and axes
|
|
439
450
|
are dropped while the empty state stands in — otherwise they render
|
|
440
451
|
squished at the top. */ }
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
452
|
+
<WholeNumberTicks
|
|
453
|
+
axis="y"
|
|
454
|
+
numTicks={ chartOptions.axis.y.numTicks }
|
|
455
|
+
enabled={
|
|
456
|
+
hasWholeNumberValues &&
|
|
457
|
+
! chartOptions.axis.y.tickValues &&
|
|
458
|
+
! options?.yScale?.domain
|
|
459
|
+
}
|
|
460
|
+
>
|
|
461
|
+
{ tickValues => (
|
|
462
|
+
<>
|
|
463
|
+
{ ! allSeriesHidden && gridVisibility !== 'none' && (
|
|
464
|
+
<Grid
|
|
465
|
+
columns={ false }
|
|
466
|
+
numTicks={ chartOptions.axis.y.numTicks }
|
|
467
|
+
{ ...{ tickValues: tickValues ?? chartOptions.axis.y.tickValues } }
|
|
468
|
+
/>
|
|
469
|
+
) }
|
|
470
|
+
{ ! allSeriesHidden && chartOptions.axis.x.display && (
|
|
471
|
+
<Axis { ...chartOptions.axis.x } />
|
|
472
|
+
) }
|
|
473
|
+
{ ! allSeriesHidden && chartOptions.axis.y.display && (
|
|
474
|
+
<Axis
|
|
475
|
+
{ ...chartOptions.axis.y }
|
|
476
|
+
{ ...( tickValues ? { tickValues } : {} ) }
|
|
477
|
+
/>
|
|
478
|
+
) }
|
|
479
|
+
</>
|
|
480
|
+
) }
|
|
481
|
+
</WholeNumberTicks>
|
|
450
482
|
|
|
451
483
|
{ allSeriesHidden ? (
|
|
452
484
|
<SvgEmptyState
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { __ } from '@wordpress/i18n';
|
|
2
|
+
import { isInvalidReading } from '../../private/readings';
|
|
2
3
|
import type { DataPoint, DataPointDate, SeriesData } from '../../../types';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -19,9 +20,7 @@ export const validateData = ( data: SeriesData[] ) => {
|
|
|
19
20
|
const hasInvalidData = data.some( series =>
|
|
20
21
|
series.data.some(
|
|
21
22
|
( point: DataPointDate | DataPoint ) =>
|
|
22
|
-
|
|
23
|
-
point.value === null ||
|
|
24
|
-
point.value === undefined ||
|
|
23
|
+
isInvalidReading( point.value, { allowMissing: false } ) ||
|
|
25
24
|
( 'date' in point && point.date && isNaN( point.date.getTime() ) )
|
|
26
25
|
)
|
|
27
26
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen, act } from '@testing-library/react';
|
|
1
|
+
import { render, screen, within, act } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { createRef } from 'react';
|
|
4
4
|
import { GlobalChartsProvider } from '../../../providers';
|
|
@@ -188,6 +188,77 @@ describe( 'AreaChart', () => {
|
|
|
188
188
|
} );
|
|
189
189
|
} );
|
|
190
190
|
|
|
191
|
+
describe( 'Y-Axis Ticks', () => {
|
|
192
|
+
test( 'labels a whole-number range smaller than the tick count once per whole number', () => {
|
|
193
|
+
renderWithProvider( {
|
|
194
|
+
stacked: false,
|
|
195
|
+
data: [
|
|
196
|
+
{
|
|
197
|
+
label: 'Series A',
|
|
198
|
+
data: [ 0, 1, 1, 0 ].map( ( value, i ) => ( {
|
|
199
|
+
date: new Date( 2024, i + 2, 1 ),
|
|
200
|
+
value,
|
|
201
|
+
} ) ),
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
const chart = screen.getByRole( 'grid', { name: /area chart/i } );
|
|
207
|
+
const ticks = within( chart )
|
|
208
|
+
.getAllByText( /^-?[\d.,]+$/ )
|
|
209
|
+
.map( el => el.textContent );
|
|
210
|
+
expect( ticks.sort() ).toEqual( [ '0', '1' ] );
|
|
211
|
+
} );
|
|
212
|
+
|
|
213
|
+
test( 'keeps every tick on a y domain the caller pinned', () => {
|
|
214
|
+
renderWithProvider( {
|
|
215
|
+
stacked: false,
|
|
216
|
+
data: [
|
|
217
|
+
{
|
|
218
|
+
label: 'Series A',
|
|
219
|
+
data: [ 0, 0, 0 ].map( ( value, i ) => ( {
|
|
220
|
+
date: new Date( 2024, i + 2, 1 ),
|
|
221
|
+
value,
|
|
222
|
+
} ) ),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
options: {
|
|
226
|
+
yScale: { domain: [ 0, 1 ] },
|
|
227
|
+
axis: { y: { tickFormat: ( value: number ) => `${ Math.round( value * 100 ) }%` } },
|
|
228
|
+
},
|
|
229
|
+
} );
|
|
230
|
+
|
|
231
|
+
const chart = screen.getByRole( 'grid', { name: /area chart/i } );
|
|
232
|
+
expect( within( chart ).getAllByText( /^\d+%$/ ) ).toHaveLength( 6 );
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
test( 'keeps fractional ticks for a normalized stack even when the data is whole numbers', () => {
|
|
236
|
+
renderWithProvider( {
|
|
237
|
+
stacked: true,
|
|
238
|
+
stackOffset: 'expand',
|
|
239
|
+
data: [
|
|
240
|
+
{
|
|
241
|
+
label: 'Series A',
|
|
242
|
+
data: [ 0, 1, 1, 0 ].map( ( value, i ) => ( {
|
|
243
|
+
date: new Date( 2024, i + 2, 1 ),
|
|
244
|
+
value,
|
|
245
|
+
} ) ),
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: 'Series B',
|
|
249
|
+
data: [ 1, 0, 0, 1 ].map( ( value, i ) => ( {
|
|
250
|
+
date: new Date( 2024, i + 2, 1 ),
|
|
251
|
+
value,
|
|
252
|
+
} ) ),
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
const chart = screen.getByRole( 'grid', { name: /area chart/i } );
|
|
258
|
+
expect( within( chart ).getAllByText( /^-?[\d.,]+$/ ).length ).toBeGreaterThan( 2 );
|
|
259
|
+
} );
|
|
260
|
+
} );
|
|
261
|
+
|
|
191
262
|
describe( 'Stacking', () => {
|
|
192
263
|
test( 'is stacked by default', () => {
|
|
193
264
|
renderWithProvider();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { formatNumber } from '@automattic/number-formatters';
|
|
2
1
|
import { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from '@visx/pattern';
|
|
3
2
|
import { Axis, BarSeries, BarGroup, Grid, XYChart } from '@visx/xychart';
|
|
4
3
|
import { __, sprintf } from '@wordpress/i18n';
|
|
@@ -22,10 +21,13 @@ import {
|
|
|
22
21
|
} from '../../providers';
|
|
23
22
|
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
24
23
|
import { attachSubComponents } from '../../utils';
|
|
24
|
+
import { warnOnce } from '../../utils/warn-once';
|
|
25
25
|
import { useChartChildren } from '../private/chart-composition';
|
|
26
26
|
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
27
27
|
import { ChartLayout } from '../private/chart-layout';
|
|
28
|
+
import { formatReading, isInvalidReading } from '../private/readings';
|
|
28
29
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
30
|
+
import { hasOnlyWholeNumbers, WholeNumberTicks } from '../private/whole-number-ticks';
|
|
29
31
|
import { withResponsive } from '../private/with-responsive';
|
|
30
32
|
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
31
33
|
import styles from './bar-chart.module.scss';
|
|
@@ -33,53 +35,24 @@ import {
|
|
|
33
35
|
useBarChartOptions,
|
|
34
36
|
BandHighlight,
|
|
35
37
|
BandTooltip,
|
|
38
|
+
ClassifiedBarSeries,
|
|
36
39
|
ComparisonBars,
|
|
37
40
|
DEFAULT_COMPARISON_WIDTH_FACTOR,
|
|
38
41
|
COMPARISON_INNER_GAP,
|
|
39
42
|
MAX_GROUP_PADDING,
|
|
40
43
|
COMPARISON_TICK_GAP_FACTOR,
|
|
41
44
|
BASE_BAND_PADDING_INNER,
|
|
45
|
+
countRenderedBars,
|
|
46
|
+
isBarRendered,
|
|
42
47
|
} from './private';
|
|
43
48
|
import type { ComparisonSeriesEntry } from './private';
|
|
44
|
-
import type {
|
|
45
|
-
|
|
46
|
-
DataPointDate,
|
|
47
|
-
SeriesData,
|
|
48
|
-
SeriesChartLegendConfig,
|
|
49
|
-
SeriesVisibilityProps,
|
|
50
|
-
Optional,
|
|
51
|
-
} from '../../types';
|
|
49
|
+
import type { BarChartProps } from './types';
|
|
50
|
+
import type { DataPointDate, SeriesData, Optional } from '../../types';
|
|
52
51
|
import type { RenderTooltipParams } from '../../visx/types';
|
|
53
52
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
54
|
-
import type { FC,
|
|
55
|
-
|
|
56
|
-
export type BandHighlightSelection = {
|
|
57
|
-
datum: DataPointDate;
|
|
58
|
-
x: number;
|
|
59
|
-
y: number;
|
|
60
|
-
width: number;
|
|
61
|
-
height: number;
|
|
62
|
-
};
|
|
53
|
+
import type { FC, ComponentType } from 'react';
|
|
63
54
|
|
|
64
|
-
export
|
|
65
|
-
/**
|
|
66
|
-
* Legend configuration. Supports `collapseGroups` on top of the shared options.
|
|
67
|
-
*/
|
|
68
|
-
legend?: SeriesChartLegendConfig;
|
|
69
|
-
renderTooltip?: ( params: RenderTooltipParams< DataPointDate > ) => ReactNode;
|
|
70
|
-
/** Place the tooltip beside its category without vertical flipping. */
|
|
71
|
-
tooltipPlacement?: 'auto' | 'beside';
|
|
72
|
-
/** Tooltip top anchor in SVG coordinates; negative offsets are supported. */
|
|
73
|
-
tooltipAnchorTop?: number;
|
|
74
|
-
orientation?: 'horizontal' | 'vertical';
|
|
75
|
-
withPatterns?: boolean;
|
|
76
|
-
showZeroValues?: boolean;
|
|
77
|
-
/** Highlight the active band across the plot when tooltips are enabled. */
|
|
78
|
-
withBandHighlight?: boolean;
|
|
79
|
-
/** Receive active band bounds in SVG coordinates, or null when dismissed. */
|
|
80
|
-
onBandHighlightChange?: ( selection: BandHighlightSelection | null ) => void;
|
|
81
|
-
children?: ReactNode;
|
|
82
|
-
}
|
|
55
|
+
export type { BarChartProps, BandHighlightSelection } from './types';
|
|
83
56
|
|
|
84
57
|
// Base props type with optional responsive properties
|
|
85
58
|
type BarChartBaseProps = Optional< BarChartProps, 'width' | 'height' | 'size' >;
|
|
@@ -95,20 +68,18 @@ type BarChartResponsiveComponent = FC< BarChartBaseProps & ResponsiveConfig > &
|
|
|
95
68
|
|
|
96
69
|
// Validation function similar to LineChart
|
|
97
70
|
const validateData = ( data: SeriesData[] ) => {
|
|
98
|
-
if ( ! data?.length ) return 'No data available';
|
|
71
|
+
if ( ! data?.length ) return __( 'No data available', 'jetpack-charts' );
|
|
99
72
|
|
|
100
73
|
const hasInvalidData = data.some( series =>
|
|
101
74
|
series.data.some(
|
|
102
75
|
point =>
|
|
103
|
-
|
|
104
|
-
point.value === null ||
|
|
105
|
-
point.value === undefined ||
|
|
76
|
+
isInvalidReading( point.value, { allowMissing: true } ) ||
|
|
106
77
|
( ! point.label &&
|
|
107
78
|
( ! ( 'date' in point && point.date ) || isNaN( point.date.getTime() ) ) )
|
|
108
79
|
)
|
|
109
80
|
);
|
|
110
81
|
|
|
111
|
-
if ( hasInvalidData ) return 'Invalid data';
|
|
82
|
+
if ( hasInvalidData ) return __( 'Invalid data', 'jetpack-charts' );
|
|
112
83
|
return null;
|
|
113
84
|
};
|
|
114
85
|
|
|
@@ -141,6 +112,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
141
112
|
renderTooltip,
|
|
142
113
|
tooltipPlacement,
|
|
143
114
|
tooltipAnchorTop,
|
|
115
|
+
tooltipStyle,
|
|
116
|
+
barClassName,
|
|
144
117
|
options = {},
|
|
145
118
|
orientation = 'vertical',
|
|
146
119
|
withPatterns = false,
|
|
@@ -155,8 +128,16 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
155
128
|
onPointerUp,
|
|
156
129
|
onDatumActivate,
|
|
157
130
|
} ) => {
|
|
131
|
+
if ( ! withTooltips && ( withBandHighlight || onBandHighlightChange ) ) {
|
|
132
|
+
warnOnce(
|
|
133
|
+
'bar-chart-band-highlight-without-tooltips',
|
|
134
|
+
'BarChart: withBandHighlight and onBandHighlightChange require withTooltips.'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
158
138
|
const legendInteractive = legend.interactive ?? false;
|
|
159
139
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
140
|
+
const legendComparisonItem = legend.comparisonItem ?? false;
|
|
160
141
|
const horizontal = orientation === 'horizontal';
|
|
161
142
|
const chartId = useChartId( providedChartId );
|
|
162
143
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
@@ -177,8 +158,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
177
158
|
|
|
178
159
|
// Create legend items using the reusable hook
|
|
179
160
|
const legendOptions = useMemo(
|
|
180
|
-
() => ( { collapseGroups: legendCollapseGroups } ),
|
|
181
|
-
[ legendCollapseGroups ]
|
|
161
|
+
() => ( { collapseGroups: legendCollapseGroups, comparisonItem: legendComparisonItem } ),
|
|
162
|
+
[ legendCollapseGroups, legendComparisonItem ]
|
|
182
163
|
);
|
|
183
164
|
const legendItems = useChartLegendItems( dataSorted, legendOptions );
|
|
184
165
|
|
|
@@ -192,12 +173,24 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
192
173
|
[ isSeriesVisible ]
|
|
193
174
|
);
|
|
194
175
|
|
|
176
|
+
const hasWholeNumberValues = useMemo(
|
|
177
|
+
() => hasOnlyWholeNumbers( dataSorted.filter( isSeriesRendered ) ),
|
|
178
|
+
[ dataSorted, isSeriesRendered ]
|
|
179
|
+
);
|
|
180
|
+
|
|
195
181
|
const chartOptions = useBarChartOptions(
|
|
196
182
|
dataWithVisibleZeros,
|
|
197
183
|
horizontal,
|
|
198
184
|
options,
|
|
199
185
|
isSeriesRendered
|
|
200
186
|
);
|
|
187
|
+
const valueAxis = horizontal ? chartOptions.axis.x : chartOptions.axis.y;
|
|
188
|
+
const callerValueDomain = horizontal ? options.xScale?.domain : options.yScale?.domain;
|
|
189
|
+
const wholeNumberTicksProps = {
|
|
190
|
+
axis: horizontal ? ( 'x' as const ) : ( 'y' as const ),
|
|
191
|
+
numTicks: valueAxis.numTicks,
|
|
192
|
+
enabled: hasWholeNumberValues && ! valueAxis.tickValues && ! callerValueDomain,
|
|
193
|
+
};
|
|
201
194
|
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme, horizontal );
|
|
202
195
|
const chartRef = useRef< HTMLDivElement >( null );
|
|
203
196
|
|
|
@@ -404,14 +397,14 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
404
397
|
|
|
405
398
|
// With a paired comparison value, show the category as the header and one row
|
|
406
399
|
// per period (primary + comparison).
|
|
407
|
-
if ( comparisonEntry && comparisonDatum
|
|
400
|
+
if ( comparisonEntry && comparisonDatum ) {
|
|
408
401
|
return (
|
|
409
402
|
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
410
403
|
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ categoryLabel }</div>
|
|
411
|
-
{ renderTooltipRow( primaryKey,
|
|
404
|
+
{ renderTooltipRow( primaryKey, formatReading( nearestDatum.value ) ) }
|
|
412
405
|
{ renderTooltipRow(
|
|
413
406
|
comparisonEntry.series.label,
|
|
414
|
-
|
|
407
|
+
formatReading( comparisonDatum.value )
|
|
415
408
|
) }
|
|
416
409
|
</div>
|
|
417
410
|
);
|
|
@@ -420,7 +413,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
420
413
|
return (
|
|
421
414
|
<div className={ styles[ 'bar-chart__tooltip' ] }>
|
|
422
415
|
<div className={ styles[ 'bar-chart__tooltip-header' ] }>{ primaryKey }</div>
|
|
423
|
-
{ renderTooltipRow( categoryLabel,
|
|
416
|
+
{ renderTooltipRow( categoryLabel, formatReading( nearestDatum.value ) ) }
|
|
424
417
|
</div>
|
|
425
418
|
);
|
|
426
419
|
},
|
|
@@ -482,34 +475,33 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
482
475
|
const createKeyboardHighlightStyle = useCallback( () => {
|
|
483
476
|
if ( selectedIndex === undefined ) return '';
|
|
484
477
|
|
|
485
|
-
// Use only primary entries
|
|
478
|
+
// Use only primary entries: comparison series have no .visx-bar elements, so
|
|
486
479
|
// their indices must not appear in the nth-child selector.
|
|
487
480
|
// Pattern: [series1[0], series2[0], series3[0], series1[1], series2[1], series3[1], ...]
|
|
488
481
|
const primaryCount = primaryEntries.length;
|
|
489
|
-
|
|
490
|
-
const dataPointIndex = Math.floor( selectedIndex / primaryCount );
|
|
491
|
-
const seriesIndex = selectedIndex % primaryCount;
|
|
492
|
-
|
|
493
|
-
// Only highlight if we're within valid bounds
|
|
494
|
-
if ( dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount ) {
|
|
482
|
+
if ( ! primaryCount ) {
|
|
495
483
|
return '';
|
|
496
484
|
}
|
|
497
|
-
|
|
485
|
+
const dataPointIndex = Math.floor( selectedIndex / primaryCount );
|
|
486
|
+
const seriesIndex = selectedIndex % primaryCount;
|
|
498
487
|
const seriesData = primaryEntries[ seriesIndex ]?.series;
|
|
499
|
-
|
|
488
|
+
const datum = seriesData?.data[ dataPointIndex ];
|
|
489
|
+
|
|
490
|
+
if ( ! seriesData || ! datum || ! isBarRendered( datum ) ) {
|
|
500
491
|
return '';
|
|
501
492
|
}
|
|
502
493
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
494
|
+
// The single .visx-bar-group holds series after series, and only the points visx drew a bar for.
|
|
495
|
+
const domBarIndex =
|
|
496
|
+
primaryEntries
|
|
497
|
+
.slice( 0, seriesIndex )
|
|
498
|
+
.reduce( ( count, { series } ) => count + countRenderedBars( series.data ), 0 ) +
|
|
499
|
+
countRenderedBars( seriesData.data.slice( 0, dataPointIndex ) );
|
|
508
500
|
|
|
509
501
|
// Use a CSS class selector instead of ID since useId() generates invalid CSS ID characters
|
|
510
502
|
const generatedStyles = `
|
|
511
503
|
.bar-chart[data-chart-id="bar-chart-${ chartId }"] .visx-bar-group .visx-bar:nth-child(${
|
|
512
|
-
|
|
504
|
+
domBarIndex + 1
|
|
513
505
|
}) {
|
|
514
506
|
stroke: #005fcc;
|
|
515
507
|
stroke-width: 2px;
|
|
@@ -633,21 +625,33 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
633
625
|
) }
|
|
634
626
|
|
|
635
627
|
{ ! allSeriesHidden && (
|
|
636
|
-
|
|
637
|
-
{
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
628
|
+
<WholeNumberTicks { ...wholeNumberTicksProps }>
|
|
629
|
+
{ valueTicks => (
|
|
630
|
+
<>
|
|
631
|
+
{ /* Visx forwards tickValues to its grid primitives but omits it from GridProps. */ }
|
|
632
|
+
<Grid
|
|
633
|
+
columns={ gridVisibility.includes( 'y' ) }
|
|
634
|
+
rows={ false }
|
|
635
|
+
numTicks={ chartOptions.axis.x.numTicks }
|
|
636
|
+
{ ...{
|
|
637
|
+
tickValues:
|
|
638
|
+
( horizontal ? valueTicks : undefined ) ??
|
|
639
|
+
chartOptions.axis.x.tickValues,
|
|
640
|
+
} }
|
|
641
|
+
/>
|
|
642
|
+
<Grid
|
|
643
|
+
columns={ false }
|
|
644
|
+
rows={ gridVisibility.includes( 'x' ) }
|
|
645
|
+
numTicks={ chartOptions.axis.y.numTicks }
|
|
646
|
+
{ ...{
|
|
647
|
+
tickValues:
|
|
648
|
+
( horizontal ? undefined : valueTicks ) ??
|
|
649
|
+
chartOptions.axis.y.tickValues,
|
|
650
|
+
} }
|
|
651
|
+
/>
|
|
652
|
+
</>
|
|
653
|
+
) }
|
|
654
|
+
</WholeNumberTicks>
|
|
651
655
|
) }
|
|
652
656
|
|
|
653
657
|
{ withPatterns && (
|
|
@@ -671,7 +675,11 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
671
675
|
</>
|
|
672
676
|
) }
|
|
673
677
|
|
|
674
|
-
{ highlightedBarStyle &&
|
|
678
|
+
{ highlightedBarStyle && (
|
|
679
|
+
<style data-testid="bar-chart-keyboard-highlight">
|
|
680
|
+
{ highlightedBarStyle }
|
|
681
|
+
</style>
|
|
682
|
+
) }
|
|
675
683
|
|
|
676
684
|
{ allSeriesHidden ? (
|
|
677
685
|
<SvgEmptyState
|
|
@@ -699,19 +707,37 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
699
707
|
resolveFill={ resolveComparisonFill }
|
|
700
708
|
/>
|
|
701
709
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
710
|
+
{ barClassName ? (
|
|
711
|
+
<g className="visx-bar-group">
|
|
712
|
+
{ primaryEntries.map( ( { series: seriesData, index } ) => (
|
|
713
|
+
<ClassifiedBarSeries
|
|
714
|
+
key={ seriesData.label }
|
|
715
|
+
dataKey={ seriesData.label }
|
|
716
|
+
data={ seriesData.data as DataPointDate[] }
|
|
717
|
+
xAccessor={ chartOptions.accessors.xAccessor }
|
|
718
|
+
yAccessor={ chartOptions.accessors.yAccessor }
|
|
719
|
+
colorAccessor={ getBarBackground( index ) }
|
|
720
|
+
barClassName={ barClassName }
|
|
721
|
+
primaryKeys={ primaryKeys }
|
|
722
|
+
groupPadding={ groupPadding }
|
|
723
|
+
/>
|
|
724
|
+
) ) }
|
|
725
|
+
</g>
|
|
726
|
+
) : (
|
|
727
|
+
<BarGroup padding={ groupPadding }>
|
|
728
|
+
{ primaryEntries.map( ( { series: seriesData, index } ) => (
|
|
729
|
+
<BarSeries
|
|
730
|
+
key={ seriesData?.label }
|
|
731
|
+
dataKey={ seriesData?.label }
|
|
732
|
+
data={ seriesData.data as DataPointDate[] }
|
|
733
|
+
yAccessor={ chartOptions.accessors.yAccessor }
|
|
734
|
+
xAccessor={ chartOptions.accessors.xAccessor }
|
|
735
|
+
colorAccessor={ getBarBackground( index ) }
|
|
736
|
+
/>
|
|
737
|
+
) ) }
|
|
738
|
+
</BarGroup>
|
|
739
|
+
) }
|
|
740
|
+
{ /* Do not reorder: for one key the last showTooltip wins, so this must run after the primary series. */ }
|
|
715
741
|
{ ( withTooltips || onPointerDown || onPointerUp ) && (
|
|
716
742
|
<BandTooltip
|
|
717
743
|
keys={ primaryKeys }
|
|
@@ -726,16 +752,29 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
726
752
|
visx collapses the domain and the axes render squished at the top. Drop them
|
|
727
753
|
while the empty state stands in. */ }
|
|
728
754
|
{ ! allSeriesHidden && (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
755
|
+
<WholeNumberTicks { ...wholeNumberTicksProps }>
|
|
756
|
+
{ valueTicks => (
|
|
757
|
+
<>
|
|
758
|
+
<Axis
|
|
759
|
+
{ ...chartOptions.axis.x }
|
|
760
|
+
{ ...( horizontal && valueTicks ? { tickValues: valueTicks } : {} ) }
|
|
761
|
+
/>
|
|
762
|
+
<Axis
|
|
763
|
+
{ ...chartOptions.axis.y }
|
|
764
|
+
{ ...( ! horizontal && valueTicks
|
|
765
|
+
? { tickValues: valueTicks }
|
|
766
|
+
: {} ) }
|
|
767
|
+
/>
|
|
768
|
+
</>
|
|
769
|
+
) }
|
|
770
|
+
</WholeNumberTicks>
|
|
733
771
|
) }
|
|
734
772
|
|
|
735
773
|
{ withTooltips && (
|
|
736
774
|
<AccessibleTooltip
|
|
737
775
|
tooltipPlacement={ tooltipPlacement }
|
|
738
776
|
tooltipAnchorTop={ tooltipAnchorTop }
|
|
777
|
+
style={ tooltipStyle }
|
|
739
778
|
detectBounds
|
|
740
779
|
snapTooltipToDatumX
|
|
741
780
|
snapTooltipToDatumY
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as BarChart, BarChartUnresponsive } from './bar-chart';
|
|
2
|
-
export type { BarChartProps, BandHighlightSelection } from './
|
|
2
|
+
export type { BarChartProps, BandHighlightSelection } from './types';
|
|
@@ -4,7 +4,7 @@ import { useDeepMemo } from '../../../hooks';
|
|
|
4
4
|
import { CATALOG_POINTERS } from '../../../providers/chart-context/private/catalog-pointers';
|
|
5
5
|
import type { BandScale } from './band-scale';
|
|
6
6
|
import type { DataPointDate } from '../../../types';
|
|
7
|
-
import type { BandHighlightSelection } from '../
|
|
7
|
+
import type { BandHighlightSelection } from '../types';
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
10
10
|
visible: boolean;
|
|
@@ -41,6 +41,8 @@ export function BandHighlight( { visible, horizontal, onChange }: Props ) {
|
|
|
41
41
|
}
|
|
42
42
|
return {
|
|
43
43
|
datum: nearest.datum as DataPointDate,
|
|
44
|
+
key: nearest.key,
|
|
45
|
+
index: nearest.index,
|
|
44
46
|
x: horizontal ? ( margin?.left ?? 0 ) : position,
|
|
45
47
|
y: horizontal ? position : ( margin?.top ?? 0 ),
|
|
46
48
|
width: horizontal ? innerWidth : scale.bandwidth(),
|
|
@@ -2,7 +2,7 @@ import { DataContext, TooltipContext, useEventEmitter } from '@visx/xychart';
|
|
|
2
2
|
import { useCallback, useContext, useMemo } from 'react';
|
|
3
3
|
import { createGroupScale } from './band-scale';
|
|
4
4
|
import type { BandScale } from './band-scale';
|
|
5
|
-
import type { BarChartProps } from '../
|
|
5
|
+
import type { BarChartProps } from '../types';
|
|
6
6
|
|
|
7
7
|
type PointerHandler = NonNullable< Parameters< typeof useEventEmitter >[ 1 ] >;
|
|
8
8
|
type PointerSelection = Parameters< NonNullable< BarChartProps[ 'onPointerUp' ] > >[ 0 ];
|