@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.
- package/CHANGELOG.md +20 -0
- package/TOKENS.md +2 -2
- package/dist/index.cjs +584 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +4 -0
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +585 -210
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/bar-chart/bar-chart.tsx +57 -23
- package/src/charts/bar-chart/private/band-tooltip.tsx +18 -12
- 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 +1 -0
- package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +2 -3
- package/src/charts/bar-chart/test/bar-chart.test.tsx +130 -1
- package/src/charts/bar-chart/types.ts +5 -1
- package/src/charts/geo-chart/geo-chart.tsx +4 -0
- package/src/charts/geo-chart/test/geo-chart.test.tsx +8 -0
- package/src/charts/geo-chart/types.ts +5 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +1 -1
- package/src/charts/pie-chart/pie-chart.module.scss +4 -0
- package/src/charts/pie-chart/pie-chart.tsx +103 -7
- package/src/charts/pie-chart/test/pie-chart.test.tsx +149 -0
- package/src/charts/private/chart-layout/chart-layout.tsx +5 -1
- package/src/components/tooltip/accessible-tooltip.tsx +40 -4
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +82 -5
- package/src/providers/chart-context/global-charts-provider.tsx +57 -41
- package/src/providers/chart-context/private/catalog-pointers.ts +9 -1
- package/src/providers/chart-context/private/palette-generator.ts +305 -0
- package/src/providers/chart-context/private/perceptual-color.ts +219 -0
- package/src/providers/chart-context/private/series-palette.ts +1 -1
- package/src/providers/chart-context/test/chart-context.test.tsx +88 -8
- package/src/providers/chart-context/test/global-charts-provider-first-render.test.tsx +27 -0
- package/src/providers/chart-context/test/palette-generator.test.ts +181 -0
- package/src/providers/chart-context/test/perceptual-color.test.ts +75 -0
- package/src/test-utils/zoned-parsing-suite.ts +1 -1
- package/src/utils/color-utils.ts +5 -3
- package/src/utils/test/date-formatting.test.ts +1 -1
- 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
|
+
"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.
|
|
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 } =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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( '
|
|
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
|
-
|
|
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
|
|
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( () => {
|