@automattic/charts 3.1.1 → 4.0.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 +41 -0
- package/TOKENS.md +80 -46
- package/dist/index.cjs +1224 -662
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -23
- package/dist/index.d.cts +230 -162
- package/dist/index.d.ts +230 -162
- package/dist/index.js +1224 -663
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +38 -11
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +67 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +79 -0
- package/src/charts/bar-list-chart/bar-list-chart.tsx +45 -2
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
- package/src/charts/heatmap-chart/types.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +109 -25
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +74 -3
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/types.ts +4 -1
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/sparkline.tsx +9 -3
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +2 -2
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +38 -10
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +67 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +17 -10
- package/src/hooks/test/use-xychart-theme.test.tsx +63 -5
- package/src/hooks/use-xychart-theme.ts +33 -23
- package/src/index.ts +4 -0
- package/src/providers/chart-context/global-charts-provider.tsx +64 -54
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +35 -0
- package/src/providers/chart-context/test/catalog-pointers.test.ts +61 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -18
- package/src/providers/chart-context/themes.ts +7 -47
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +63 -58
- package/src/styles/test/chart-scope.test.ts +25 -40
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/types.ts +103 -73
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +174 -0
- package/src/utils/get-styles.ts +7 -1
- package/src/utils/index.ts +3 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/visx/types.ts +29 -2
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -87
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -50
- package/src/providers/chart-context/test/sparse-theme.test.ts +0 -35
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -118
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -47
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/charts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.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.8",
|
|
67
67
|
"@babel/runtime": "7.29.7",
|
|
68
68
|
"@react-spring/web": "10.1.1",
|
|
69
69
|
"@visx/annotation": "^4.0.0",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"@visx/xychart": "^4.0.0",
|
|
85
85
|
"@wordpress/i18n": "^6.0.0",
|
|
86
86
|
"@wordpress/icons": "^15.0.0",
|
|
87
|
-
"@wordpress/theme": "1.1.0",
|
|
88
|
-
"@wordpress/ui": "0.
|
|
87
|
+
"@wordpress/theme": "2.1.1-next.v.202608281122.0",
|
|
88
|
+
"@wordpress/ui": "0.21.0",
|
|
89
89
|
"clsx": "2.1.1",
|
|
90
90
|
"date-fns": "^4.1.0",
|
|
91
91
|
"deepmerge": "4.3.1",
|
|
@@ -103,16 +103,16 @@
|
|
|
103
103
|
"@testing-library/jest-dom": "^6.0.0",
|
|
104
104
|
"@testing-library/react": "^16.0.0",
|
|
105
105
|
"@testing-library/user-event": "^14.4.3",
|
|
106
|
-
"@tsdown/css": "0.22.
|
|
106
|
+
"@tsdown/css": "0.22.14",
|
|
107
107
|
"@types/jest": "^30.0.0",
|
|
108
108
|
"@types/node": "^24.12.0",
|
|
109
|
-
"@types/react": "18.3.
|
|
109
|
+
"@types/react": "18.3.31",
|
|
110
110
|
"@types/react-dom": "18.3.7",
|
|
111
111
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
112
112
|
"@visx/glyph": "4.0.0",
|
|
113
|
-
"@wordpress/components": "
|
|
114
|
-
"@wordpress/element": "8.
|
|
115
|
-
"@wordpress/private-apis": "1.
|
|
113
|
+
"@wordpress/components": "40.0.0",
|
|
114
|
+
"@wordpress/element": "8.6.0",
|
|
115
|
+
"@wordpress/private-apis": "1.54.0",
|
|
116
116
|
"babel-jest": "30.4.1",
|
|
117
117
|
"identity-obj-proxy": "^3.0.0",
|
|
118
118
|
"jest": "30.4.2",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"react-dom": "18.3.1",
|
|
123
123
|
"sass-embedded": "1.97.3",
|
|
124
124
|
"storybook": "10.4.6",
|
|
125
|
-
"tsdown": "0.22.
|
|
125
|
+
"tsdown": "0.22.14",
|
|
126
126
|
"typescript": "6.0.3"
|
|
127
127
|
},
|
|
128
128
|
"peerDependencies": {
|
|
@@ -22,21 +22,24 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
GlobalChartsProvider,
|
|
24
24
|
GlobalChartsContext,
|
|
25
|
+
useChartFormatting,
|
|
25
26
|
useChartId,
|
|
26
27
|
useChartRegistration,
|
|
27
28
|
useGlobalChartsContext,
|
|
28
|
-
useGlobalChartsTheme,
|
|
29
29
|
} from '../../providers';
|
|
30
30
|
import { useDefaultHiddenSeries } from '../../providers/chart-context/hooks/use-default-hidden-series';
|
|
31
31
|
import { attachSubComponents } from '../../utils';
|
|
32
|
+
import { getBucketInfo } from '../../utils/bucket-info';
|
|
32
33
|
import { renderDefaultTooltip } from '../line-chart';
|
|
33
34
|
import { useChartChildren } from '../private/chart-composition';
|
|
34
35
|
import { ChartInstanceContext, type ChartInstanceRef } from '../private/chart-instance-context';
|
|
35
36
|
import { ChartLayout } from '../private/chart-layout';
|
|
36
37
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
37
|
-
import { getCurveType
|
|
38
|
+
import { getCurveType } from '../private/time-axis';
|
|
39
|
+
import { buildTimeAxisOptions } from '../private/time-axis-options';
|
|
38
40
|
import { withResponsive } from '../private/with-responsive';
|
|
39
41
|
import { useXZoom, ZoomResetButton, ZoomSelectionRect, ZoomClip } from '../private/x-zoom';
|
|
42
|
+
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
40
43
|
import styles from './area-chart.module.scss';
|
|
41
44
|
import { AreaChartScalesRef, HoverGlyphs, validateData } from './private';
|
|
42
45
|
import type { AreaChartProps } from './types';
|
|
@@ -71,8 +74,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
71
74
|
onPointerMove,
|
|
72
75
|
onPointerOut,
|
|
73
76
|
zoomable = false,
|
|
74
|
-
rescaleYOnVisibilityChange,
|
|
75
|
-
rescaleYOnLegendToggle,
|
|
77
|
+
rescaleYOnVisibilityChange = true,
|
|
76
78
|
defaultHiddenSeries,
|
|
77
79
|
children,
|
|
78
80
|
gridVisibility,
|
|
@@ -84,10 +86,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
84
86
|
const legendShape = legend.shape ?? 'rect';
|
|
85
87
|
const legendPosition = legend.position ?? 'bottom';
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
const rescaleYOnVisibility = rescaleYOnVisibilityChange ?? rescaleYOnLegendToggle ?? true;
|
|
89
|
-
|
|
90
|
-
const providerTheme = useGlobalChartsTheme();
|
|
89
|
+
const formatting = useChartFormatting();
|
|
91
90
|
const theme = useXYChartTheme( data );
|
|
92
91
|
const chartId = useChartId( providedChartId );
|
|
93
92
|
const hiddenSeries = useDefaultHiddenSeries( chartId, defaultHiddenSeries );
|
|
@@ -160,7 +159,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
160
159
|
// around zero); letting visx derive the domain is correct there.
|
|
161
160
|
const fixedYDomain = useMemo< [ number, number ] | undefined >( () => {
|
|
162
161
|
if (
|
|
163
|
-
|
|
162
|
+
rescaleYOnVisibilityChange ||
|
|
164
163
|
! dataSorted.length ||
|
|
165
164
|
! dataSorted[ 0 ].data.length ||
|
|
166
165
|
( stacked && stackOffset !== 'none' )
|
|
@@ -202,21 +201,21 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
202
201
|
}
|
|
203
202
|
if ( max === -Infinity ) return undefined;
|
|
204
203
|
return [ Math.min( 0, min ), max ];
|
|
205
|
-
}, [ dataSorted, stacked, stackOffset,
|
|
204
|
+
}, [ dataSorted, stacked, stackOffset, rescaleYOnVisibilityChange ] );
|
|
206
205
|
|
|
207
206
|
const chartOptions = useMemo( () => {
|
|
208
|
-
const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
|
|
209
|
-
const formatter = tickFormat || getFormatter( dataSorted, tickResolution );
|
|
210
|
-
|
|
211
207
|
return {
|
|
212
208
|
axis: {
|
|
213
|
-
x: {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
209
|
+
x: buildTimeAxisOptions( {
|
|
210
|
+
dataSorted,
|
|
211
|
+
width,
|
|
212
|
+
axisOptions: options?.axis?.x,
|
|
213
|
+
scaleDomain: options?.xScale?.domain,
|
|
214
|
+
zoomDomain: zoom.domain,
|
|
215
|
+
formatting,
|
|
216
|
+
// No `isSeriesRendered`: a hidden area stays mounted with a zeroed
|
|
217
|
+
// yAccessor, so it still contributes to the x domain.
|
|
218
|
+
} ),
|
|
220
219
|
y: {
|
|
221
220
|
orientation: 'left' as const,
|
|
222
221
|
numTicks: 4,
|
|
@@ -239,7 +238,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
239
238
|
...options?.yScale,
|
|
240
239
|
},
|
|
241
240
|
};
|
|
242
|
-
}, [ options, dataSorted, width, stacked, fixedYDomain, zoom.domain ] );
|
|
241
|
+
}, [ options, dataSorted, width, stacked, fixedYDomain, zoom.domain, formatting ] );
|
|
243
242
|
|
|
244
243
|
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme );
|
|
245
244
|
|
|
@@ -284,10 +283,24 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
284
283
|
() => new Set( seriesWithVisibility.filter( s => s.isVisible ).map( s => s.series.label ) ),
|
|
285
284
|
[ seriesWithVisibility ]
|
|
286
285
|
);
|
|
286
|
+
|
|
287
|
+
// Classified from the visible series, not the axis's: a hidden area stays in
|
|
288
|
+
// the x domain for its animation, but the tooltip below drops its data, so a
|
|
289
|
+
// heading naming that series' bucket would name one no visible datum has.
|
|
290
|
+
const bucketInfo = useMemo(
|
|
291
|
+
() =>
|
|
292
|
+
getBucketInfo(
|
|
293
|
+
dataSorted.filter( series => visibleLabels.has( series.label ) ),
|
|
294
|
+
options?.axis?.x?.tickResolution
|
|
295
|
+
),
|
|
296
|
+
[ dataSorted, visibleLabels, options?.axis?.x?.tickResolution ]
|
|
297
|
+
);
|
|
287
298
|
const filteredRenderTooltip = useCallback(
|
|
288
299
|
( params: Parameters< typeof renderTooltip >[ 0 ] ) => {
|
|
289
300
|
const datumByKey = params?.tooltipData?.datumByKey;
|
|
290
|
-
if ( ! datumByKey )
|
|
301
|
+
if ( ! datumByKey ) {
|
|
302
|
+
return renderTooltip( { ...params, bucketInfo } );
|
|
303
|
+
}
|
|
291
304
|
const filtered = Object.fromEntries(
|
|
292
305
|
Object.entries( datumByKey ).filter( ( [ key ] ) => visibleLabels.has( key ) )
|
|
293
306
|
);
|
|
@@ -302,6 +315,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
302
315
|
: { ...Object.values( filtered )[ 0 ], distance: nearestDatum?.distance ?? 0 };
|
|
303
316
|
return renderTooltip( {
|
|
304
317
|
...params,
|
|
318
|
+
bucketInfo,
|
|
305
319
|
tooltipData: {
|
|
306
320
|
...params.tooltipData,
|
|
307
321
|
datumByKey: filtered,
|
|
@@ -309,7 +323,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
309
323
|
} as typeof params.tooltipData,
|
|
310
324
|
} );
|
|
311
325
|
},
|
|
312
|
-
[ renderTooltip, visibleLabels ]
|
|
326
|
+
[ renderTooltip, visibleLabels, bucketInfo ]
|
|
313
327
|
);
|
|
314
328
|
|
|
315
329
|
// Defaults that depend on stacked vs overlapping mode.
|
|
@@ -405,7 +419,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
405
419
|
onBlur={ onChartBlur }
|
|
406
420
|
>
|
|
407
421
|
{ chartHeight > 0 && (
|
|
408
|
-
<div ref={ chartRef }
|
|
422
|
+
<div ref={ chartRef } className={ plotStyles[ 'xy-plot' ] }>
|
|
409
423
|
{ zoomable && zoom.domain && <ZoomResetButton onClick={ zoom.reset } /> }
|
|
410
424
|
<XYChart
|
|
411
425
|
theme={ theme }
|
|
@@ -483,7 +497,7 @@ const AreaChartInternal = forwardRef< ChartInstanceRef, AreaChartProps >(
|
|
|
483
497
|
stackOffset={ stackOffset }
|
|
484
498
|
getElementStyles={ getElementStyles }
|
|
485
499
|
// useXYChartTheme resolved this role inside its memo, against the chart's scope element; reading it back avoids a getComputedStyle on every render.
|
|
486
|
-
strokeColor={ theme.backgroundColor
|
|
500
|
+
strokeColor={ theme.backgroundColor }
|
|
487
501
|
/>
|
|
488
502
|
</>
|
|
489
503
|
) }
|
|
@@ -317,7 +317,7 @@ describe( 'AreaChart', () => {
|
|
|
317
317
|
expect( afterToggleDomain![ 1 ] ).toBeLessThan( initialDomain![ 1 ] );
|
|
318
318
|
} );
|
|
319
319
|
|
|
320
|
-
test( 'y-axis stays pinned for unstacked area when
|
|
320
|
+
test( 'y-axis stays pinned for unstacked area when rescaleYOnVisibilityChange is false', async () => {
|
|
321
321
|
// Exercises the non-stacked branch of fixedYDomain, which scans the
|
|
322
322
|
// raw min/max across all series rather than summing stack columns.
|
|
323
323
|
const user = userEvent.setup();
|
|
@@ -330,7 +330,7 @@ describe( 'AreaChart', () => {
|
|
|
330
330
|
chartId="test-interactive-domain-pin-unstacked"
|
|
331
331
|
legend={ { interactive: true } }
|
|
332
332
|
stacked={ false }
|
|
333
|
-
|
|
333
|
+
rescaleYOnVisibilityChange={ false }
|
|
334
334
|
ref={ ref }
|
|
335
335
|
/>
|
|
336
336
|
</GlobalChartsProvider>
|
|
@@ -351,35 +351,6 @@ describe( 'AreaChart', () => {
|
|
|
351
351
|
expect( afterToggleDomain ).toEqual( initialDomain );
|
|
352
352
|
} );
|
|
353
353
|
|
|
354
|
-
test( 'y-axis stays pinned when rescaleYOnLegendToggle is false', async () => {
|
|
355
|
-
const user = userEvent.setup();
|
|
356
|
-
const ref = createRef< ChartInstanceRef >();
|
|
357
|
-
render(
|
|
358
|
-
<GlobalChartsProvider>
|
|
359
|
-
<AreaChartUnresponsive
|
|
360
|
-
{ ...defaultProps }
|
|
361
|
-
showLegend
|
|
362
|
-
chartId="test-interactive-domain-pin"
|
|
363
|
-
legend={ { interactive: true } }
|
|
364
|
-
rescaleYOnLegendToggle={ false }
|
|
365
|
-
ref={ ref }
|
|
366
|
-
/>
|
|
367
|
-
</GlobalChartsProvider>
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
const initialDomain = (
|
|
371
|
-
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
372
|
-
)?.domain();
|
|
373
|
-
expect( initialDomain ).toBeDefined();
|
|
374
|
-
|
|
375
|
-
await user.click( screen.getByText( 'Series A' ) );
|
|
376
|
-
|
|
377
|
-
const afterToggleDomain = (
|
|
378
|
-
ref.current?.getScales()?.yScale as { domain: () => number[] } | undefined
|
|
379
|
-
)?.domain();
|
|
380
|
-
expect( afterToggleDomain ).toEqual( initialDomain );
|
|
381
|
-
} );
|
|
382
|
-
|
|
383
354
|
test( 'y-axis stays pinned when rescaleYOnVisibilityChange is false', async () => {
|
|
384
355
|
const user = userEvent.setup();
|
|
385
356
|
const ref = createRef< ChartInstanceRef >();
|
|
@@ -388,7 +359,7 @@ describe( 'AreaChart', () => {
|
|
|
388
359
|
<AreaChartUnresponsive
|
|
389
360
|
{ ...defaultProps }
|
|
390
361
|
showLegend
|
|
391
|
-
chartId="test-interactive-domain-pin
|
|
362
|
+
chartId="test-interactive-domain-pin"
|
|
392
363
|
legend={ { interactive: true } }
|
|
393
364
|
rescaleYOnVisibilityChange={ false }
|
|
394
365
|
ref={ ref }
|
|
@@ -607,7 +578,7 @@ describe( 'AreaChart', () => {
|
|
|
607
578
|
chartId="test-interactive-negative"
|
|
608
579
|
showLegend
|
|
609
580
|
legend={ { interactive: true } }
|
|
610
|
-
|
|
581
|
+
rescaleYOnVisibilityChange={ false }
|
|
611
582
|
data={ [
|
|
612
583
|
{
|
|
613
584
|
label: 'Pos',
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment <rootDir>/tests/environment-los-angeles.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
6
|
+
import AreaChart from '../area-chart';
|
|
7
|
+
|
|
8
|
+
const mockRefCallback = jest.fn();
|
|
9
|
+
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
10
|
+
useElementSize: () => [ mockRefCallback, 500, 300 ],
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
const monthly = ( label: string, count: number ) => ( {
|
|
14
|
+
label,
|
|
15
|
+
data: Array.from( { length: count }, ( _, index ) => ( {
|
|
16
|
+
date: new Date( Date.UTC( 2026, index, 1, 12 ) ),
|
|
17
|
+
value: index,
|
|
18
|
+
} ) ),
|
|
19
|
+
options: {},
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
const xAxisTickOffsets = ( container: HTMLElement ) => {
|
|
23
|
+
/* eslint-disable testing-library/no-node-access -- A tick's position is an SVG attribute no query reaches. */
|
|
24
|
+
const xAxis = container.querySelectorAll( '.visx-axis' )[ 0 ];
|
|
25
|
+
return Array.from( xAxis.querySelectorAll( '.visx-axis-tick text' ) ).map( tick =>
|
|
26
|
+
Number( tick.getAttribute( 'x' ) )
|
|
27
|
+
);
|
|
28
|
+
/* eslint-enable testing-library/no-node-access */
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderChart = ( defaultHiddenSeries?: string[] ) =>
|
|
32
|
+
render(
|
|
33
|
+
<GlobalChartsProvider locale="en-US" timeZone="Asia/Tokyo">
|
|
34
|
+
<AreaChart
|
|
35
|
+
width={ 500 }
|
|
36
|
+
height={ 300 }
|
|
37
|
+
data={ [ monthly( 'Long', 12 ), monthly( 'Short', 3 ) ] }
|
|
38
|
+
defaultHiddenSeries={ defaultHiddenSeries }
|
|
39
|
+
/>
|
|
40
|
+
</GlobalChartsProvider>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
describe( 'area chart tick placement', () => {
|
|
44
|
+
it( 'keeps ticks spanning the full domain when a series is hidden', () => {
|
|
45
|
+
const shown = xAxisTickOffsets( renderChart().container );
|
|
46
|
+
const hidden = xAxisTickOffsets( renderChart( [ 'Long' ] ).container );
|
|
47
|
+
|
|
48
|
+
expect( hidden ).toEqual( shown );
|
|
49
|
+
expect( hidden.length ).toBeGreaterThan( 1 );
|
|
50
|
+
for ( const offset of hidden ) {
|
|
51
|
+
expect( offset ).toBeGreaterThanOrEqual( 0 );
|
|
52
|
+
expect( offset ).toBeLessThanOrEqual( 500 );
|
|
53
|
+
}
|
|
54
|
+
} );
|
|
55
|
+
} );
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment <rootDir>/tests/environment-los-angeles.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
6
|
+
import AreaChart from '../area-chart';
|
|
7
|
+
import type { DataPointDate } from '../../../types';
|
|
8
|
+
import type { RenderTooltipParams } from '../../../visx/types';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
|
|
11
|
+
const mockRefCallback = jest.fn();
|
|
12
|
+
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
13
|
+
useElementSize: () => [ mockRefCallback, 500, 300 ],
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
// Captures AreaChart's `filteredRenderTooltip` so its early-return branch can be
|
|
17
|
+
// called directly: visx always sets `datumByKey` when it opens a tooltip.
|
|
18
|
+
type CapturedRenderTooltip = ( params: RenderTooltipParams< DataPointDate > ) => ReactNode;
|
|
19
|
+
let capturedRenderTooltip: CapturedRenderTooltip | undefined;
|
|
20
|
+
jest.mock( '../../../components/tooltip', () => ( {
|
|
21
|
+
...jest.requireActual( '../../../components/tooltip' ),
|
|
22
|
+
AccessibleTooltip: ( props: { renderTooltip?: CapturedRenderTooltip } ) => {
|
|
23
|
+
capturedRenderTooltip = props.renderTooltip;
|
|
24
|
+
return null;
|
|
25
|
+
},
|
|
26
|
+
} ) );
|
|
27
|
+
|
|
28
|
+
// 09:30 on the Tokyo calendar day.
|
|
29
|
+
const morning = new Date( '2026-08-02T00:30:00Z' );
|
|
30
|
+
|
|
31
|
+
const datumA = { key: 'Series A', datum: { date: morning, value: 10 } as DataPointDate, index: 0 };
|
|
32
|
+
const datumB = { key: 'Series B', datum: { date: morning, value: 5 } as DataPointDate, index: 0 };
|
|
33
|
+
|
|
34
|
+
// Full de-DE/Asia/Tokyo heading for `morning`: pins the date style and the hour together.
|
|
35
|
+
const EXPECTED_HEADING = '2.8.2026, 09 Uhr';
|
|
36
|
+
|
|
37
|
+
const renderHourlyChart = ( defaultHiddenSeries?: string[] ) =>
|
|
38
|
+
render(
|
|
39
|
+
<GlobalChartsProvider locale="de-DE" timeZone="Asia/Tokyo">
|
|
40
|
+
<AreaChart
|
|
41
|
+
width={ 500 }
|
|
42
|
+
height={ 300 }
|
|
43
|
+
options={ { axis: { x: { tickResolution: 'hour' } } } }
|
|
44
|
+
defaultHiddenSeries={ defaultHiddenSeries }
|
|
45
|
+
data={ [
|
|
46
|
+
{ label: 'Series A', data: [ datumA.datum ], options: {} },
|
|
47
|
+
{ label: 'Series B', data: [ datumB.datum ], options: {} },
|
|
48
|
+
] }
|
|
49
|
+
/>
|
|
50
|
+
</GlobalChartsProvider>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const renderCaptured = ( params: RenderTooltipParams< DataPointDate > ) =>
|
|
54
|
+
render(
|
|
55
|
+
<GlobalChartsProvider locale="de-DE" timeZone="Asia/Tokyo">
|
|
56
|
+
<div data-testid="captured-tooltip">{ capturedRenderTooltip!( params ) }</div>
|
|
57
|
+
</GlobalChartsProvider>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
describe( "AreaChart's filteredRenderTooltip", () => {
|
|
61
|
+
beforeEach( () => {
|
|
62
|
+
capturedRenderTooltip = undefined;
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'names the hour on the early-return path, before datumByKey exists', () => {
|
|
66
|
+
renderHourlyChart();
|
|
67
|
+
|
|
68
|
+
const params = {
|
|
69
|
+
tooltipData: { nearestDatum: datumA, datumByKey: undefined },
|
|
70
|
+
} as unknown as RenderTooltipParams< DataPointDate >;
|
|
71
|
+
renderCaptured( params );
|
|
72
|
+
|
|
73
|
+
expect( screen.getByTestId( 'captured-tooltip' ) ).toHaveTextContent( EXPECTED_HEADING );
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'names the hour on the filtered path, with a hidden series', () => {
|
|
77
|
+
renderHourlyChart( [ 'Series B' ] );
|
|
78
|
+
|
|
79
|
+
const params = {
|
|
80
|
+
tooltipData: { nearestDatum: datumA, datumByKey: { 'Series A': datumA, 'Series B': datumB } },
|
|
81
|
+
} as unknown as RenderTooltipParams< DataPointDate >;
|
|
82
|
+
renderCaptured( params );
|
|
83
|
+
|
|
84
|
+
expect( screen.getByTestId( 'captured-tooltip' ) ).toHaveTextContent( EXPECTED_HEADING );
|
|
85
|
+
} );
|
|
86
|
+
} );
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment <rootDir>/tests/environment-los-angeles.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
7
|
+
import AreaChart from '../area-chart';
|
|
8
|
+
|
|
9
|
+
const mockRefCallback = jest.fn();
|
|
10
|
+
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
11
|
+
useElementSize: () => [ mockRefCallback, 500, 300 ],
|
|
12
|
+
} ) );
|
|
13
|
+
|
|
14
|
+
// 09:30 on the Tokyo calendar day.
|
|
15
|
+
const morning = new Date( '2026-08-02T00:30:00Z' );
|
|
16
|
+
|
|
17
|
+
describe( 'area chart tooltip bucket info', () => {
|
|
18
|
+
it( 'names the hour on a mounted chart', async () => {
|
|
19
|
+
const user = userEvent.setup();
|
|
20
|
+
render(
|
|
21
|
+
<GlobalChartsProvider locale="de-DE" timeZone="Asia/Tokyo">
|
|
22
|
+
<AreaChart
|
|
23
|
+
width={ 500 }
|
|
24
|
+
height={ 300 }
|
|
25
|
+
options={ { axis: { x: { tickResolution: 'hour' } } } }
|
|
26
|
+
data={ [ { label: 'Series A', data: [ { date: morning, value: 10 } ], options: {} } ] }
|
|
27
|
+
/>
|
|
28
|
+
</GlobalChartsProvider>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
screen.getByRole( 'grid', { name: /area chart/i } ).focus();
|
|
32
|
+
await user.keyboard( '{ArrowRight}' );
|
|
33
|
+
|
|
34
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).toHaveTextContent( '2.8.2026, 09 Uhr' );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
// The axis keeps a hidden area in its domain for the animation, but the
|
|
38
|
+
// tooltip drops its data, so the bucket has to follow the tooltip.
|
|
39
|
+
it( 'ignores a hidden series when naming the bucket', async () => {
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
const hourly = Array.from( { length: 4 }, ( _, index ) => ( {
|
|
42
|
+
date: new Date( Date.UTC( 2026, 7, 2, index ) ),
|
|
43
|
+
value: index,
|
|
44
|
+
} ) );
|
|
45
|
+
const daily = Array.from( { length: 3 }, ( _, index ) => ( {
|
|
46
|
+
date: new Date( Date.UTC( 2026, 7, 2 + index ) ),
|
|
47
|
+
value: index,
|
|
48
|
+
} ) );
|
|
49
|
+
|
|
50
|
+
render(
|
|
51
|
+
<GlobalChartsProvider locale="de-DE" timeZone="Asia/Tokyo">
|
|
52
|
+
<AreaChart
|
|
53
|
+
width={ 500 }
|
|
54
|
+
height={ 300 }
|
|
55
|
+
defaultHiddenSeries={ [ 'Hourly' ] }
|
|
56
|
+
data={ [
|
|
57
|
+
{ label: 'Hourly', data: hourly, options: {} },
|
|
58
|
+
{ label: 'Daily', data: daily, options: {} },
|
|
59
|
+
] }
|
|
60
|
+
/>
|
|
61
|
+
</GlobalChartsProvider>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
screen.getByRole( 'grid', { name: /area chart/i } ).focus();
|
|
65
|
+
await user.keyboard( '{ArrowRight}' );
|
|
66
|
+
|
|
67
|
+
expect( screen.getByTestId( 'chart-tooltip-0' ) ).not.toHaveTextContent( 'Uhr' );
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BaseChartProps,
|
|
3
|
+
BucketInfo,
|
|
3
4
|
DataPointDate,
|
|
4
5
|
SeriesData,
|
|
5
6
|
SeriesChartLegendConfig,
|
|
@@ -39,7 +40,9 @@ export interface AreaChartProps extends BaseChartProps< SeriesData[] >, SeriesVi
|
|
|
39
40
|
/**
|
|
40
41
|
* Custom tooltip renderer.
|
|
41
42
|
*/
|
|
42
|
-
renderTooltip?: (
|
|
43
|
+
renderTooltip?: (
|
|
44
|
+
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo }
|
|
45
|
+
) => ReactNode;
|
|
43
46
|
/**
|
|
44
47
|
* Whether to show crosshair lines in the tooltip.
|
|
45
48
|
*/
|
|
@@ -71,11 +74,5 @@ export interface AreaChartProps extends BaseChartProps< SeriesData[] >, SeriesVi
|
|
|
71
74
|
* @default true
|
|
72
75
|
*/
|
|
73
76
|
rescaleYOnVisibilityChange?: boolean;
|
|
74
|
-
/**
|
|
75
|
-
* @deprecated Use `rescaleYOnVisibilityChange`. The behaviour keys off series
|
|
76
|
-
* visibility changing, not specifically a legend toggle. Still honoured when
|
|
77
|
-
* `rescaleYOnVisibilityChange` is not set.
|
|
78
|
-
*/
|
|
79
|
-
rescaleYOnLegendToggle?: boolean;
|
|
80
77
|
children?: ReactNode;
|
|
81
78
|
}
|
|
@@ -27,6 +27,7 @@ import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
|
27
27
|
import { ChartLayout } from '../private/chart-layout';
|
|
28
28
|
import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
|
|
29
29
|
import { withResponsive } from '../private/with-responsive';
|
|
30
|
+
import plotStyles from '../private/xy-plot/xy-plot.module.scss';
|
|
30
31
|
import styles from './bar-chart.module.scss';
|
|
31
32
|
import {
|
|
32
33
|
useBarChartOptions,
|
|
@@ -128,6 +129,9 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
128
129
|
animation,
|
|
129
130
|
children,
|
|
130
131
|
gap = 'md',
|
|
132
|
+
onPointerDown,
|
|
133
|
+
onPointerUp,
|
|
134
|
+
onDatumActivate,
|
|
131
135
|
} ) => {
|
|
132
136
|
const legendInteractive = legend.interactive ?? false;
|
|
133
137
|
const legendCollapseGroups = legend.collapseGroups ?? false;
|
|
@@ -196,16 +200,6 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
196
200
|
Math.max( 0, ...primarySeriesForNav.map( s => s.data?.length || 0 ) ) *
|
|
197
201
|
primarySeriesForNav.length;
|
|
198
202
|
|
|
199
|
-
// Use the keyboard navigation hook
|
|
200
|
-
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
201
|
-
selectedIndex,
|
|
202
|
-
setSelectedIndex,
|
|
203
|
-
isNavigating,
|
|
204
|
-
setIsNavigating,
|
|
205
|
-
chartRef,
|
|
206
|
-
totalPoints,
|
|
207
|
-
} );
|
|
208
|
-
|
|
209
203
|
// Add visibility information from the shared legend state.
|
|
210
204
|
const seriesWithVisibility = useMemo(
|
|
211
205
|
() =>
|
|
@@ -244,6 +238,37 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
244
238
|
[ primaryEntries ]
|
|
245
239
|
);
|
|
246
240
|
|
|
241
|
+
// Walks the selected index the way the highlight style does — series-major
|
|
242
|
+
// within each data point — so the bar handed on is the one outlined.
|
|
243
|
+
const activateSelectedBar = useCallback(
|
|
244
|
+
( index: number ) => {
|
|
245
|
+
const primaryCount = primaryEntries.length;
|
|
246
|
+
|
|
247
|
+
if ( ! primaryCount ) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const dataPointIndex = Math.floor( index / primaryCount );
|
|
252
|
+
const series = primaryEntries[ index % primaryCount ]?.series;
|
|
253
|
+
const datum = series?.data[ dataPointIndex ];
|
|
254
|
+
|
|
255
|
+
if ( series && datum ) {
|
|
256
|
+
onDatumActivate?.( { datum, index: dataPointIndex, key: series.label } );
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
[ primaryEntries, onDatumActivate ]
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
|
|
263
|
+
selectedIndex,
|
|
264
|
+
setSelectedIndex,
|
|
265
|
+
isNavigating,
|
|
266
|
+
setIsNavigating,
|
|
267
|
+
chartRef,
|
|
268
|
+
totalPoints,
|
|
269
|
+
onActivate: activateSelectedBar,
|
|
270
|
+
} );
|
|
271
|
+
|
|
247
272
|
const comparisonEntries = useMemo( () => {
|
|
248
273
|
const primaryByGroup = new Map< string | undefined, { label: string; index: number } >(
|
|
249
274
|
primaryEntries.map( ( { series, index } ) => [
|
|
@@ -563,7 +588,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
563
588
|
onBlur={ onChartBlur }
|
|
564
589
|
>
|
|
565
590
|
{ chartHeight > 0 && (
|
|
566
|
-
<div ref={ chartRef }>
|
|
591
|
+
<div ref={ chartRef } className={ plotStyles[ 'xy-plot' ] }>
|
|
567
592
|
<XYChart
|
|
568
593
|
theme={ theme }
|
|
569
594
|
width={ width }
|
|
@@ -575,6 +600,8 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
575
600
|
xScale={ xScale }
|
|
576
601
|
yScale={ yScale }
|
|
577
602
|
horizontal={ horizontal }
|
|
603
|
+
onPointerDown={ onPointerDown }
|
|
604
|
+
onPointerUp={ onPointerUp }
|
|
578
605
|
pointerEventsDataKey="nearest"
|
|
579
606
|
>
|
|
580
607
|
{ ! allSeriesHidden && (
|