@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.cjs +533 -127
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +27 -2
  5. package/dist/index.d.ts +27 -2
  6. package/dist/index.js +535 -129
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/charts/area-chart/area-chart.tsx +42 -10
  10. package/src/charts/area-chart/private/validate-data.ts +2 -3
  11. package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
  12. package/src/charts/bar-chart/bar-chart.tsx +135 -96
  13. package/src/charts/bar-chart/index.ts +1 -1
  14. package/src/charts/bar-chart/private/band-highlight.tsx +3 -1
  15. package/src/charts/bar-chart/private/band-tooltip.tsx +1 -1
  16. package/src/charts/bar-chart/private/classified-bar-series.tsx +85 -0
  17. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +3 -6
  18. package/src/charts/bar-chart/private/index.ts +2 -0
  19. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +28 -2
  20. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +2 -3
  21. package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
  22. package/src/charts/bar-chart/private/use-bar-chart-options.ts +12 -39
  23. package/src/charts/bar-chart/private/value-domain.ts +90 -0
  24. package/src/charts/bar-chart/test/bar-chart.test.tsx +614 -10
  25. package/src/charts/bar-chart/types.ts +43 -0
  26. package/src/charts/line-chart/line-chart.tsx +148 -35
  27. package/src/charts/line-chart/private/index.ts +1 -0
  28. package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
  29. package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
  30. package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
  31. package/src/charts/line-chart/types.ts +2 -1
  32. package/src/charts/private/readings.ts +34 -0
  33. package/src/charts/private/test/readings.test.ts +29 -0
  34. package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
  35. package/src/charts/private/whole-number-ticks.tsx +79 -0
  36. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
  37. package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
  38. package/src/components/legend/private/base-legend.tsx +36 -21
  39. package/src/components/legend/test/legend.test.tsx +84 -0
  40. package/src/components/legend/types.ts +2 -0
  41. package/src/components/legend/utils/index.ts +0 -1
  42. package/src/components/tooltip/accessible-tooltip.tsx +1 -1
  43. package/src/hooks/test/use-chart-margin.test.tsx +35 -0
  44. package/src/hooks/use-chart-margin.tsx +7 -2
  45. package/src/providers/chart-context/global-charts-provider.tsx +1 -1
  46. package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
  47. package/src/types.ts +8 -0
  48. package/src/components/legend/utils/label-transform-factory.ts +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "4.2.0",
3
+ "version": "4.4.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": {
@@ -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
- { ! allSeriesHidden && gridVisibility !== 'none' && (
442
- <Grid columns={ false } numTicks={ 4 } />
443
- ) }
444
- { ! allSeriesHidden && chartOptions.axis.x.display && (
445
- <Axis { ...chartOptions.axis.x } />
446
- ) }
447
- { ! allSeriesHidden && chartOptions.axis.y.display && (
448
- <Axis { ...chartOptions.axis.y } />
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
- isNaN( point.value as number ) ||
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
- BaseChartProps,
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, ReactNode, ComponentType } from 'react';
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 interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVisibilityProps {
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
- isNaN( point.value as number ) ||
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 && comparisonDatum.value != null ) {
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, formatNumber( nearestDatum.value as number ) ) }
404
+ { renderTooltipRow( primaryKey, formatReading( nearestDatum.value ) ) }
412
405
  { renderTooltipRow(
413
406
  comparisonEntry.series.label,
414
- formatNumber( comparisonDatum.value as number )
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, formatNumber( nearestDatum.value as number ) ) }
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 — comparison series have no .visx-bar elements so
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
- const maxDataPoints = Math.max( ...primaryEntries.map( e => e.series.data.length ) );
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
- if ( ! seriesData || dataPointIndex >= seriesData.data.length ) {
488
+ const datum = seriesData?.data[ dataPointIndex ];
489
+
490
+ if ( ! seriesData || ! datum || ! isBarRendered( datum ) ) {
500
491
  return '';
501
492
  }
502
493
 
503
- // Based on the DOM structure analysis:
504
- // - All bars are in a single .visx-bar-group
505
- // - Bars are ordered as: [series1[0], series1[1], series2[0], series2[1], ...]
506
- // - So we need to calculate the actual bar index in the DOM
507
- const actualBarIndex = seriesIndex * maxDataPoints + dataPointIndex;
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
- actualBarIndex + 1
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
- { /* Visx forwards tickValues to its grid primitives but omits it from GridProps. */ }
638
- <Grid
639
- columns={ gridVisibility.includes( 'y' ) }
640
- rows={ false }
641
- numTicks={ 4 }
642
- { ...{ tickValues: chartOptions.axis.x.tickValues } }
643
- />
644
- <Grid
645
- columns={ false }
646
- rows={ gridVisibility.includes( 'x' ) }
647
- numTicks={ 4 }
648
- { ...{ tickValues: chartOptions.axis.y.tickValues } }
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 && <style>{ highlightedBarStyle }</style> }
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
- <BarGroup padding={ groupPadding }>
703
- { primaryEntries.map( ( { series: seriesData, index } ) => (
704
- <BarSeries
705
- key={ seriesData?.label }
706
- dataKey={ seriesData?.label }
707
- data={ seriesData.data as DataPointDate[] }
708
- yAccessor={ chartOptions.accessors.yAccessor }
709
- xAccessor={ chartOptions.accessors.xAccessor }
710
- colorAccessor={ getBarBackground( index ) }
711
- />
712
- ) ) }
713
- </BarGroup>
714
- { /* Do not reorder: for one key the last showTooltip wins, so this must run after BarGroup. */ }
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
- <Axis { ...chartOptions.axis.x } />
731
- <Axis { ...chartOptions.axis.y } />
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 './bar-chart';
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 '../bar-chart';
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 '../bar-chart';
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 ];