@automattic/charts 4.3.0 → 4.5.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 (41) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/TOKENS.md +2 -2
  3. package/dist/index.cjs +584 -209
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +4 -0
  6. package/dist/index.d.cts +14 -3
  7. package/dist/index.d.ts +14 -3
  8. package/dist/index.js +585 -210
  9. package/dist/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/charts/bar-chart/bar-chart.tsx +57 -23
  12. package/src/charts/bar-chart/private/band-tooltip.tsx +18 -12
  13. package/src/charts/bar-chart/private/classified-bar-series.tsx +85 -0
  14. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +3 -6
  15. package/src/charts/bar-chart/private/index.ts +1 -0
  16. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +2 -3
  17. package/src/charts/bar-chart/test/bar-chart.test.tsx +130 -1
  18. package/src/charts/bar-chart/types.ts +5 -1
  19. package/src/charts/geo-chart/geo-chart.tsx +4 -0
  20. package/src/charts/geo-chart/test/geo-chart.test.tsx +8 -0
  21. package/src/charts/geo-chart/types.ts +5 -0
  22. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +1 -1
  23. package/src/charts/pie-chart/pie-chart.module.scss +4 -0
  24. package/src/charts/pie-chart/pie-chart.tsx +103 -7
  25. package/src/charts/pie-chart/test/pie-chart.test.tsx +149 -0
  26. package/src/charts/private/chart-layout/chart-layout.tsx +5 -1
  27. package/src/components/tooltip/accessible-tooltip.tsx +40 -4
  28. package/src/components/tooltip/test/accessible-tooltip.test.tsx +82 -5
  29. package/src/providers/chart-context/global-charts-provider.tsx +57 -41
  30. package/src/providers/chart-context/private/catalog-pointers.ts +9 -1
  31. package/src/providers/chart-context/private/palette-generator.ts +305 -0
  32. package/src/providers/chart-context/private/perceptual-color.ts +219 -0
  33. package/src/providers/chart-context/private/series-palette.ts +1 -1
  34. package/src/providers/chart-context/test/chart-context.test.tsx +88 -8
  35. package/src/providers/chart-context/test/global-charts-provider-first-render.test.tsx +27 -0
  36. package/src/providers/chart-context/test/palette-generator.test.ts +181 -0
  37. package/src/providers/chart-context/test/perceptual-color.test.ts +75 -0
  38. package/src/test-utils/zoned-parsing-suite.ts +1 -1
  39. package/src/utils/color-utils.ts +5 -3
  40. package/src/utils/test/date-formatting.test.ts +1 -1
  41. package/src/providers/chart-context/private/get-chart-color.ts +0 -223
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "4.3.0",
3
+ "version": "4.5.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": {
@@ -63,7 +63,7 @@
63
63
  "typecheck": "tsgo --noEmit"
64
64
  },
65
65
  "dependencies": {
66
- "@automattic/number-formatters": "^1.2.9",
66
+ "@automattic/number-formatters": "^1.2.10",
67
67
  "@babel/runtime": "7.29.7",
68
68
  "@date-fns/tz": "1.5.0",
69
69
  "@react-spring/web": "10.1.1",
@@ -35,6 +35,7 @@ import {
35
35
  useBarChartOptions,
36
36
  BandHighlight,
37
37
  BandTooltip,
38
+ ClassifiedBarSeries,
38
39
  ComparisonBars,
39
40
  DEFAULT_COMPARISON_WIDTH_FACTOR,
40
41
  COMPARISON_INNER_GAP,
@@ -111,6 +112,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
111
112
  renderTooltip,
112
113
  tooltipPlacement,
113
114
  tooltipAnchorTop,
115
+ tooltipStyle,
116
+ barClassName,
114
117
  options = {},
115
118
  orientation = 'vertical',
116
119
  withPatterns = false,
@@ -269,16 +272,27 @@ const BarChartInternal: FC< BarChartProps > = ( {
269
272
 
270
273
  const visibleSeriesKey = useMemo( () => JSON.stringify( primaryKeys ), [ primaryKeys ] );
271
274
 
272
- const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
273
- selectedIndex,
274
- setSelectedIndex,
275
- isNavigating,
276
- setIsNavigating,
277
- chartRef,
278
- totalPoints,
279
- onActivate: activateSelectedBar,
280
- visibleSeriesKey,
281
- } );
275
+ const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown, onChartPointerMove } =
276
+ useKeyboardNavigation( {
277
+ selectedIndex,
278
+ setSelectedIndex,
279
+ isNavigating,
280
+ setIsNavigating,
281
+ chartRef,
282
+ totalPoints,
283
+ onActivate: activateSelectedBar,
284
+ visibleSeriesKey,
285
+ } );
286
+
287
+ const handlePointerMove = useCallback(
288
+ ( { key, index }: { key: string; index: number } ) => {
289
+ const seriesIndex = primaryKeys.indexOf( key );
290
+ if ( seriesIndex >= 0 ) {
291
+ onChartPointerMove( index * primaryKeys.length + seriesIndex );
292
+ }
293
+ },
294
+ [ primaryKeys, onChartPointerMove ]
295
+ );
282
296
 
283
297
  const comparisonEntries = useMemo( () => {
284
298
  const primaryByGroup = new Map< string | undefined, { label: string; index: number } >(
@@ -704,19 +718,37 @@ const BarChartInternal: FC< BarChartProps > = ( {
704
718
  resolveFill={ resolveComparisonFill }
705
719
  />
706
720
 
707
- <BarGroup padding={ groupPadding }>
708
- { primaryEntries.map( ( { series: seriesData, index } ) => (
709
- <BarSeries
710
- key={ seriesData?.label }
711
- dataKey={ seriesData?.label }
712
- data={ seriesData.data as DataPointDate[] }
713
- yAccessor={ chartOptions.accessors.yAccessor }
714
- xAccessor={ chartOptions.accessors.xAccessor }
715
- colorAccessor={ getBarBackground( index ) }
716
- />
717
- ) ) }
718
- </BarGroup>
719
- { /* Do not reorder: for one key the last showTooltip wins, so this must run after BarGroup. */ }
721
+ { barClassName ? (
722
+ <g className="visx-bar-group">
723
+ { primaryEntries.map( ( { series: seriesData, index } ) => (
724
+ <ClassifiedBarSeries
725
+ key={ seriesData.label }
726
+ dataKey={ seriesData.label }
727
+ data={ seriesData.data as DataPointDate[] }
728
+ xAccessor={ chartOptions.accessors.xAccessor }
729
+ yAccessor={ chartOptions.accessors.yAccessor }
730
+ colorAccessor={ getBarBackground( index ) }
731
+ barClassName={ barClassName }
732
+ primaryKeys={ primaryKeys }
733
+ groupPadding={ groupPadding }
734
+ />
735
+ ) ) }
736
+ </g>
737
+ ) : (
738
+ <BarGroup padding={ groupPadding }>
739
+ { primaryEntries.map( ( { series: seriesData, index } ) => (
740
+ <BarSeries
741
+ key={ seriesData?.label }
742
+ dataKey={ seriesData?.label }
743
+ data={ seriesData.data as DataPointDate[] }
744
+ yAccessor={ chartOptions.accessors.yAccessor }
745
+ xAccessor={ chartOptions.accessors.xAccessor }
746
+ colorAccessor={ getBarBackground( index ) }
747
+ />
748
+ ) ) }
749
+ </BarGroup>
750
+ ) }
751
+ { /* Do not reorder: for one key the last showTooltip wins, so this must run after the primary series. */ }
720
752
  { ( withTooltips || onPointerDown || onPointerUp ) && (
721
753
  <BandTooltip
722
754
  keys={ primaryKeys }
@@ -724,6 +756,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
724
756
  withTooltips={ withTooltips }
725
757
  onPointerDown={ onPointerDown }
726
758
  onPointerUp={ onPointerUp }
759
+ onPointerMove={ handlePointerMove }
727
760
  />
728
761
  ) }
729
762
 
@@ -753,6 +786,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
753
786
  <AccessibleTooltip
754
787
  tooltipPlacement={ tooltipPlacement }
755
788
  tooltipAnchorTop={ tooltipAnchorTop }
789
+ style={ tooltipStyle }
756
790
  detectBounds
757
791
  snapTooltipToDatumX
758
792
  snapTooltipToDatumY
@@ -45,6 +45,7 @@ export function nearestBandIndex< Datum >(
45
45
  * @param root0.withTooltips - Whether pointer events update the tooltip.
46
46
  * @param root0.onPointerDown - Receives the corrected pointer-down datum.
47
47
  * @param root0.onPointerUp - Receives the corrected pointer-up datum.
48
+ * @param root0.onPointerMove - Receives the corrected datum under a moving pointer.
48
49
  * @return No visual content.
49
50
  */
50
51
  export function BandTooltip( {
@@ -53,10 +54,12 @@ export function BandTooltip( {
53
54
  withTooltips,
54
55
  onPointerDown,
55
56
  onPointerUp,
56
- }: { keys: string[]; groupPadding: number } & Pick<
57
- BarChartProps,
58
- 'withTooltips' | 'onPointerDown' | 'onPointerUp'
59
- > ) {
57
+ onPointerMove,
58
+ }: {
59
+ keys: string[];
60
+ groupPadding: number;
61
+ onPointerMove?: ( selection: PointerSelection ) => void;
62
+ } & Pick< BarChartProps, 'withTooltips' | 'onPointerDown' | 'onPointerUp' > ) {
60
63
  const { xScale, yScale, dataRegistry, horizontal } = useContext( DataContext );
61
64
  const { showTooltip } = useContext( TooltipContext );
62
65
  const scale = ( horizontal ? yScale : xScale ) as BandScale | undefined;
@@ -110,13 +113,6 @@ export function BandTooltip( {
110
113
  const handlePointer = useCallback< PointerHandler >(
111
114
  params => {
112
115
  const selections = getSelections( params );
113
- if ( withTooltips && params?.event.type !== 'pointerup' ) {
114
- selections.forEach( showTooltip );
115
- }
116
- const callback = params?.event.type === 'pointerdown' ? onPointerDown : onPointerUp;
117
- if ( params?.event.type === 'pointermove' || ! callback ) {
118
- return;
119
- }
120
116
  const nearest = selections.reduce(
121
117
  ( best, selection ) => {
122
118
  const distance = ( value: typeof selection ) =>
@@ -125,11 +121,21 @@ export function BandTooltip( {
125
121
  },
126
122
  undefined as ( typeof selections )[ number ] | undefined
127
123
  );
124
+ if ( params?.event.type === 'pointermove' && nearest ) {
125
+ onPointerMove?.( nearest );
126
+ }
127
+ if ( withTooltips && params?.event.type !== 'pointerup' ) {
128
+ selections.forEach( showTooltip );
129
+ }
130
+ const callback = params?.event.type === 'pointerdown' ? onPointerDown : onPointerUp;
131
+ if ( params?.event.type === 'pointermove' || ! callback ) {
132
+ return;
133
+ }
128
134
  if ( nearest ) {
129
135
  callback( nearest );
130
136
  }
131
137
  },
132
- [ getSelections, withTooltips, showTooltip, onPointerDown, onPointerUp ]
138
+ [ getSelections, withTooltips, showTooltip, onPointerDown, onPointerUp, onPointerMove ]
133
139
  );
134
140
  useEventEmitter( 'pointermove', withTooltips ? handlePointer : undefined );
135
141
  useEventEmitter( 'pointerdown', handlePointer );
@@ -0,0 +1,85 @@
1
+ import { DataContext, GlyphSeries } from '@visx/xychart';
2
+ import clsx from 'clsx';
3
+ import { useCallback, useContext, useMemo } from 'react';
4
+ import { createGroupScale } from './band-scale';
5
+ import { getValueScaleBaseline } from './comparison-bars-geometry';
6
+ import type { BandScale } from './band-scale';
7
+ import type { DataPointDate } from '../../../types';
8
+ import type { BarChartProps } from '../types';
9
+ import type { AxisScale } from '@visx/axis';
10
+ import type { GlyphProps, SeriesProps } from '@visx/xychart';
11
+
12
+ type Props = Pick<
13
+ SeriesProps< AxisScale, AxisScale, DataPointDate >,
14
+ 'xAccessor' | 'yAccessor'
15
+ > & {
16
+ dataKey: string;
17
+ data: DataPointDate[];
18
+ colorAccessor: ( datum: DataPointDate ) => string;
19
+ barClassName: NonNullable< BarChartProps[ 'barClassName' ] >;
20
+ primaryKeys: string[];
21
+ groupPadding: number;
22
+ };
23
+
24
+ const ClassifiedBarSeries = ( { barClassName, primaryKeys, groupPadding, ...props }: Props ) => {
25
+ const { xScale, yScale, horizontal } = useContext( DataContext );
26
+ const bandScale = ( horizontal ? yScale : xScale ) as BandScale | undefined;
27
+ const valueScale = horizontal ? xScale : yScale;
28
+ const groupScale = useMemo(
29
+ () => createGroupScale( primaryKeys, bandScale?.bandwidth?.() ?? 0, groupPadding ),
30
+ [ primaryKeys, bandScale, groupPadding ]
31
+ );
32
+ const renderGlyph = useCallback(
33
+ ( {
34
+ datum,
35
+ x,
36
+ y,
37
+ color,
38
+ onBlur,
39
+ onFocus,
40
+ onPointerMove,
41
+ onPointerOut,
42
+ onPointerUp,
43
+ }: GlyphProps< DataPointDate > ) => {
44
+ if ( ! bandScale?.bandwidth || ! valueScale ) return null;
45
+ const baseline = getValueScaleBaseline(
46
+ valueScale as Parameters< typeof getValueScaleBaseline >[ 0 ]
47
+ );
48
+ const position =
49
+ Number( bandScale( ( horizontal ? props.yAccessor : props.xAccessor )( datum ) ) ) +
50
+ ( groupScale( props.dataKey ) ?? 0 );
51
+ const value = horizontal ? x : y;
52
+ return (
53
+ <rect
54
+ className={ clsx( 'visx-bar', barClassName( datum ) ) }
55
+ // visx's own Bars makes a bar focusable on the same condition.
56
+ tabIndex={ onFocus || onBlur ? 0 : undefined }
57
+ onBlur={ onBlur }
58
+ onFocus={ onFocus }
59
+ onPointerMove={ onPointerMove }
60
+ onPointerOut={ onPointerOut }
61
+ onPointerUp={ onPointerUp }
62
+ // Same arithmetic as visx BaseBarGroup, so rounding matches the unclassified bars.
63
+ x={ horizontal ? baseline + Math.min( 0, value - baseline ) : position }
64
+ y={ horizontal ? position : baseline + Math.min( 0, value - baseline ) }
65
+ width={ horizontal ? Math.abs( value - baseline ) : groupScale.bandwidth() }
66
+ height={ horizontal ? groupScale.bandwidth() : Math.abs( value - baseline ) }
67
+ fill={ color }
68
+ />
69
+ );
70
+ },
71
+ [
72
+ bandScale,
73
+ valueScale,
74
+ horizontal,
75
+ groupScale,
76
+ props.dataKey,
77
+ props.xAccessor,
78
+ props.yAccessor,
79
+ barClassName,
80
+ ]
81
+ );
82
+ return <GlyphSeries { ...props } renderGlyph={ renderGlyph } />;
83
+ };
84
+
85
+ export default ClassifiedBarSeries;
@@ -3,9 +3,8 @@ export type ComparisonRect = { x: number; y: number; width: number; height: numb
3
3
  type ValueScale = ( ( v: number ) => number ) & { range: () => unknown[] };
4
4
 
5
5
  /**
6
- * Output position of a value scale's baseline: zero if in-domain, else the
7
- * nearest range edge. Mirrors visx's getScaleBaseline so comparison shadows
8
- * sit on the same baseline as primary bars.
6
+ * Mirrors visx's getScaleBaseline so comparison shadows and primary bars
7
+ * share the same baseline, including out-of-domain zero values.
9
8
  *
10
9
  * @param {ValueScale} scale - The continuous value scale.
11
10
  * @return {number} The baseline output position in pixels.
@@ -20,9 +19,7 @@ export function getValueScaleBaseline( scale: ValueScale ): number {
20
19
  ? Math.min( Math.max( minOutput, maybeZero ), maxOutput )
21
20
  : maxOutput;
22
21
  }
23
- return Number.isFinite( maybeZero )
24
- ? Math.min( Math.max( maybeZero, minOutput ), maxOutput )
25
- : minOutput;
22
+ return Number.isFinite( maybeZero ) ? Math.max( maybeZero, minOutput ) : minOutput;
26
23
  }
27
24
 
28
25
  /**
@@ -1,5 +1,6 @@
1
1
  export { useBarChartOptions } from './use-bar-chart-options';
2
2
  export { TruncatedXTickComponent, TruncatedYTickComponent } from './truncated-tick-component';
3
+ export { default as ClassifiedBarSeries } from './classified-bar-series';
3
4
  export { ComparisonBars } from './comparison-bars';
4
5
  export type { ComparisonSeriesEntry } from './comparison-bars';
5
6
  export {
@@ -9,12 +9,11 @@ describe( 'getValueScaleBaseline', () => {
9
9
  expect( getValueScaleBaseline( scale ) ).toBe( 200 );
10
10
  } );
11
11
 
12
- it( 'clamps to the end (range max) when 0 is outside an ascending range scale', () => {
12
+ it( 'preserves an out-of-range zero baseline for an ascending scale', () => {
13
13
  // horizontal linear scale: range [0, 200], domain [-100, -10] (zero excluded)
14
14
  const scale = ( ( v: number ) => ( ( v + 100 ) / 90 ) * 200 ) as never;
15
15
  ( scale as { range: () => number[] } ).range = () => [ 0, 200 ];
16
- // scale(0) = (100/90)*200 ≈ 222 -> clamped to 200
17
- expect( getValueScaleBaseline( scale ) ).toBe( 200 );
16
+ expect( getValueScaleBaseline( scale ) ).toBeCloseTo( 222.222222 );
18
17
  } );
19
18
  } );
20
19
 
@@ -14,7 +14,7 @@ import { steadyTrafficData } from '../../../stories/sample-data';
14
14
  import BarChart, { BarChartUnresponsive } from '../bar-chart';
15
15
  import { useBarChartOptions } from '../private';
16
16
  import type { GlobalChartsContextValue } from '../../../providers/chart-context/types';
17
- import type { SeriesData } from '../../../types';
17
+ import type { DataPointDate, SeriesData } from '../../../types';
18
18
 
19
19
  // Mock useElementSize to return non-zero dimensions in jsdom so charts render
20
20
  const mockRefCallback = jest.fn();
@@ -60,6 +60,70 @@ describe( 'BarChart', () => {
60
60
  );
61
61
  };
62
62
 
63
+ test.each(
64
+ [ 'vertical', 'horizontal' ].flatMap( orientation =>
65
+ [
66
+ [ 10, 20, 30 ],
67
+ [ -20, -10, -30 ],
68
+ [ -20, 10, 30 ],
69
+ ].map( values => ( { orientation, values } ) )
70
+ )
71
+ )(
72
+ 'adds datum classes without changing $orientation bar attributes for $values',
73
+ async ( { orientation, values } ) => {
74
+ const series = {
75
+ ...defaultProps.data[ 0 ],
76
+ data: defaultProps.data[ 0 ].data.map( ( datum, index ) => ( {
77
+ ...datum,
78
+ value: values[ index ],
79
+ } ) ),
80
+ };
81
+ const props = {
82
+ orientation,
83
+ data: [ series, { ...series, label: 'Series B' } ],
84
+ };
85
+ const view = renderWithTheme( props );
86
+ await waitFor( () => expect( getBarRects() ).toHaveLength( 6 ) );
87
+ const attributes = () =>
88
+ Array.from( getBarRects(), bar =>
89
+ [ 'x', 'y', 'width', 'height', 'fill', 'tabindex' ].map( attribute =>
90
+ bar.getAttribute( attribute )
91
+ )
92
+ );
93
+ const original = attributes();
94
+ view.unmount();
95
+ renderWithTheme( {
96
+ ...props,
97
+ barClassName: ( datum: DataPointDate ) =>
98
+ datum.value === values[ 0 ] ? 'first-value' : undefined,
99
+ } );
100
+ await waitFor( () => expect( getBarRects() ).toHaveLength( 6 ) );
101
+ expect( attributes() ).toEqual( original );
102
+ expect( getBarRects()[ 0 ] ).toHaveClass( 'visx-bar', 'first-value' );
103
+ expect( getBarRects()[ 1 ] ).not.toHaveClass( 'first-value' );
104
+ expect( getBarRects()[ 3 ] ).toHaveClass( 'first-value' );
105
+ }
106
+ );
107
+
108
+ test( 'applies tooltip style overrides while keeping keyboard tooltip content', async () => {
109
+ const user = userEvent.setup();
110
+ renderWithTheme( {
111
+ withTooltips: true,
112
+ barClassName: () => 'styled-bar',
113
+ tooltipStyle: { padding: 0, backgroundColor: 'transparent', boxShadow: 'none' },
114
+ } );
115
+ await user.tab();
116
+ await user.keyboard( '{ArrowRight}' );
117
+ const tooltip = await screen.findByRole( 'tooltip' );
118
+ expect( tooltip ).toHaveTextContent( 'Jan 1' );
119
+ expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( {
120
+ padding: '0px',
121
+ // jsdom normalizes transparent in computed styles.
122
+ 'background-color': 'rgba(0, 0, 0, 0)',
123
+ 'box-shadow': 'none',
124
+ } );
125
+ } );
126
+
63
127
  describe( 'pointer selection and focus return', () => {
64
128
  const screenTransform = Object.getOwnPropertyDescriptor( SVGElement.prototype, 'getScreenCTM' );
65
129
  beforeAll( () => {
@@ -677,6 +741,71 @@ describe( 'BarChart', () => {
677
741
  } );
678
742
  } );
679
743
 
744
+ describe( 'Grid tick counts', () => {
745
+ const getPositions = ( selector: string, coordinate: string ) => {
746
+ // eslint-disable-next-line testing-library/no-node-access -- See the visx node constraint above.
747
+ const lines = screen.getByRole( 'grid' ).querySelectorAll( selector );
748
+ return Array.from( lines, line => Number( line.getAttribute( coordinate ) ) );
749
+ };
750
+
751
+ test.each( [ 'x', 'y' ] )( 'aligns an explicit tick count with the %s axis', axis => {
752
+ renderWithTheme( {
753
+ data: [
754
+ {
755
+ label: 'Views',
756
+ data: Array.from( { length: 8 }, ( _, index ) => ( {
757
+ label: `Day ${ index + 1 }`,
758
+ value: ( index + 1 ) * 10,
759
+ } ) ),
760
+ },
761
+ ],
762
+ gridVisibility: 'xy',
763
+ options: {
764
+ axis: { [ axis ]: { numTicks: 2, tickFormat: String, axisClassName: 'test-value-axis' } },
765
+ },
766
+ } );
767
+ const grid = getPositions(
768
+ axis === 'x' ? '.visx-columns line' : '.visx-rows line',
769
+ `${ axis }1`
770
+ );
771
+ const ticks = getPositions( '.test-value-axis .visx-axis-tick line', `${ axis }1` );
772
+ expect( ticks.length ).toBeGreaterThan( 0 );
773
+ expect( grid ).toEqual( ticks );
774
+ } );
775
+
776
+ test.each( [ false, true ] )(
777
+ 'preserves the default tick count (horizontal=%s)',
778
+ horizontal => {
779
+ const axis = horizontal ? 'x' : 'y';
780
+ const positionsFor = ( axisOptions: object ) => {
781
+ const { unmount } = renderWithTheme( {
782
+ orientation: horizontal ? 'horizontal' : 'vertical',
783
+ gridVisibility: 'xy',
784
+ options: {
785
+ axis: { [ axis ]: { axisClassName: 'test-value-axis', ...axisOptions } },
786
+ },
787
+ } );
788
+ const positions = {
789
+ grid: getPositions(
790
+ horizontal ? '.visx-columns line' : '.visx-rows line',
791
+ `${ axis }1`
792
+ ),
793
+ ticks: getPositions( '.test-value-axis .visx-axis-tick line', `${ axis }1` ),
794
+ };
795
+ unmount();
796
+ return positions;
797
+ };
798
+
799
+ const omitted = positionsFor( {} );
800
+ const explicit = positionsFor( { numTicks: 4 } );
801
+
802
+ expect( omitted.ticks.length ).toBeGreaterThan( 0 );
803
+ expect( omitted.grid ).toEqual( omitted.ticks );
804
+ expect( omitted.grid ).toEqual( explicit.grid );
805
+ }
806
+ );
807
+ } );
808
+
680
809
  describe( 'Grid Visibility', () => {
681
810
  test( 'renders with different grid visibility options', () => {
682
811
  const { rerender } = renderWithTheme( { gridVisibility: 'x' } );
@@ -6,7 +6,7 @@ import type {
6
6
  SeriesVisibilityProps,
7
7
  } from '../../types';
8
8
  import type { RenderTooltipParams } from '../../visx/types';
9
- import type { ReactNode } from 'react';
9
+ import type { CSSProperties, ReactNode } from 'react';
10
10
 
11
11
  export type BandHighlightSelection = {
12
12
  datum: DataPointDate;
@@ -28,6 +28,10 @@ export interface BarChartProps extends BaseChartProps< SeriesData[] >, SeriesVis
28
28
  tooltipPlacement?: 'auto' | 'beside';
29
29
  /** Tooltip top anchor in SVG coordinates; negative offsets are supported. */
30
30
  tooltipAnchorTop?: number;
31
+ /** Inline styles merged over the tooltip box defaults. */
32
+ tooltipStyle?: CSSProperties;
33
+ /** CSS class for each primary bar, in addition to visx-bar. */
34
+ barClassName?: ( datum: DataPointDate ) => string | undefined;
31
35
  orientation?: 'horizontal' | 'vertical';
32
36
  withPatterns?: boolean;
33
37
  showZeroValues?: boolean;
@@ -130,6 +130,7 @@ function normalizeGeoChartError( eventArgs: unknown ): GeoChartError {
130
130
  * @param props.height - Height of the chart in pixels
131
131
  * @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
132
132
  * @param props.resolution - Resolution level ('countries', 'provinces', or 'metros')
133
+ * @param props.domain - Country code whose viewpoint sets the disputed borders
133
134
  * @param props.onError - Optional callback for Google Charts errors
134
135
  * @param props.className - Additional CSS class name for the chart container
135
136
  * @param props.renderPlaceholder - Optional render function for the loading placeholder
@@ -142,6 +143,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
142
143
  height,
143
144
  region = 'world',
144
145
  resolution = 'countries',
146
+ domain,
145
147
  onError,
146
148
  renderPlaceholder,
147
149
  } ) => {
@@ -262,6 +264,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
262
264
  () => ( {
263
265
  ...( region !== 'world' && { region } ),
264
266
  ...( resolution !== 'countries' && { resolution } ),
267
+ ...( domain && { domain } ),
265
268
  colorAxis: { colors: [ lightColorHex, fullColorHex ] },
266
269
  backgroundColor: backgroundColorHex,
267
270
  datalessRegionColor: defaultFillColorHex,
@@ -273,6 +276,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
273
276
  [
274
277
  region,
275
278
  resolution,
279
+ domain,
276
280
  lightColorHex,
277
281
  fullColorHex,
278
282
  backgroundColorHex,
@@ -216,6 +216,14 @@ describe( 'GeoChart', () => {
216
216
  expect( options.region ).toBeUndefined();
217
217
  } );
218
218
 
219
+ test( 'passes the domain so disputed borders follow that country', () => {
220
+ renderWithTheme( { domain: 'IN' } );
221
+
222
+ const options = JSON.parse( screen.getByTestId( 'chart-options' ).textContent || '{}' );
223
+
224
+ expect( options.domain ).toBe( 'IN' );
225
+ } );
226
+
219
227
  test( 'does not include resolution in options when set to countries (default)', () => {
220
228
  renderWithTheme();
221
229
 
@@ -48,6 +48,11 @@ export interface GeoChartProps extends Pick<
48
48
  * @default 'countries'
49
49
  */
50
50
  resolution?: GeoResolution;
51
+ /**
52
+ * ISO 3166-1 alpha-2 code of the country whose viewpoint sets the disputed borders
53
+ * (e.g., 'IN' draws Kashmir as India shows it). Google's default borders apply when omitted.
54
+ */
55
+ domain?: string;
51
56
  /**
52
57
  * Callback fired when Google Charts emits a chart error.
53
58
  */
@@ -494,7 +494,7 @@ describe( 'HeatmapChart keyboard tooltip', () => {
494
494
  } );
495
495
 
496
496
  describe( 'HeatmapChart tooltip position', () => {
497
- // Restore only this spy: `restoreAllMocks` would also unspy jest-console, and
497
+ // Restore only this spy: `restoreAllMocks` would also unspy the console guard, and
498
498
  // every later `toHaveWarned` in the file would find no spy.
499
499
  let rects: jest.SpyInstance;
500
500
  afterEach( () => {
@@ -18,5 +18,9 @@
18
18
 
19
19
  &__label-text {
20
20
  fill: var(--a8c-charts-color-label-inverse, #f0f0f0);
21
+
22
+ &--on-light {
23
+ fill: var(--a8c-charts-color-label, #1e1e1e);
24
+ }
21
25
  }
22
26
  }