@automattic/charts 3.2.0 → 4.1.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 +47 -0
- package/SECURITY.md +1 -0
- package/TOKENS.md +53 -58
- package/dist/index.cjs +1483 -664
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +93 -29
- package/dist/index.d.cts +233 -247
- package/dist/index.d.ts +233 -247
- package/dist/index.js +1484 -667
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- 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 +2 -1
- 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-list-chart/bar-list-chart.tsx +5 -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 +52 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
- package/src/charts/heatmap-chart/index.ts +2 -1
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
- package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/private/index.ts +1 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
- package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
- package/src/charts/heatmap-chart/types.ts +8 -2
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
- 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/leaderboard-chart/types.ts +2 -2
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +128 -26
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
- 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/test/zoned-date-string.test.tsx +37 -0
- package/src/charts/line-chart/types.ts +17 -3
- 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 +59 -12
- 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/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +21 -17
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
- package/src/hooks/test/use-chart-margin.test.tsx +173 -3
- package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
- package/src/hooks/use-chart-data-transform.ts +10 -3
- package/src/hooks/use-chart-margin.tsx +54 -9
- package/src/hooks/use-xychart-theme.ts +25 -9
- package/src/index.ts +8 -0
- package/src/providers/chart-context/global-charts-provider.tsx +37 -38
- 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 +3 -13
- package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
- 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 -25
- package/src/providers/chart-context/themes.ts +7 -52
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +39 -62
- package/src/styles/test/chart-scope.test.ts +19 -47
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/test-utils/zoned-parsing-suite.ts +90 -0
- package/src/types.ts +79 -166
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +157 -0
- package/src/utils/date-parsing.ts +103 -52
- package/src/utils/get-edge-tick-widths.ts +33 -0
- package/src/utils/index.ts +4 -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/date-parsing.auckland.test.ts +6 -0
- package/src/utils/test/date-parsing.santiago.test.ts +6 -0
- package/src/utils/test/date-parsing.test.ts +16 -1
- package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/utils/warn-once.ts +23 -0
- package/src/visx/types.ts +56 -5
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
- 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 -183
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
- 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.1.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": {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@automattic/number-formatters": "^1.2.8",
|
|
67
67
|
"@babel/runtime": "7.29.7",
|
|
68
|
+
"@date-fns/tz": "1.4.1",
|
|
68
69
|
"@react-spring/web": "10.1.1",
|
|
69
70
|
"@visx/annotation": "^4.0.0",
|
|
70
71
|
"@visx/axis": "^4.0.0",
|
|
@@ -84,8 +85,8 @@
|
|
|
84
85
|
"@visx/xychart": "^4.0.0",
|
|
85
86
|
"@wordpress/i18n": "^6.0.0",
|
|
86
87
|
"@wordpress/icons": "^15.0.0",
|
|
87
|
-
"@wordpress/theme": "1.
|
|
88
|
-
"@wordpress/ui": "0.
|
|
88
|
+
"@wordpress/theme": "2.1.1-next.v.202608281122.0",
|
|
89
|
+
"@wordpress/ui": "0.21.0",
|
|
89
90
|
"clsx": "2.1.1",
|
|
90
91
|
"date-fns": "^4.1.0",
|
|
91
92
|
"deepmerge": "4.3.1",
|
|
@@ -110,9 +111,9 @@
|
|
|
110
111
|
"@types/react-dom": "18.3.7",
|
|
111
112
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
112
113
|
"@visx/glyph": "4.0.0",
|
|
113
|
-
"@wordpress/components": "
|
|
114
|
-
"@wordpress/element": "8.
|
|
115
|
-
"@wordpress/private-apis": "1.
|
|
114
|
+
"@wordpress/components": "40.0.0",
|
|
115
|
+
"@wordpress/element": "8.6.0",
|
|
116
|
+
"@wordpress/private-apis": "1.54.0",
|
|
116
117
|
"babel-jest": "30.4.1",
|
|
117
118
|
"identity-obj-proxy": "^3.0.0",
|
|
118
119
|
"jest": "30.4.2",
|
|
@@ -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,
|
|
@@ -587,7 +588,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
|
|
|
587
588
|
onBlur={ onChartBlur }
|
|
588
589
|
>
|
|
589
590
|
{ chartHeight > 0 && (
|
|
590
|
-
<div ref={ chartRef }>
|
|
591
|
+
<div ref={ chartRef } className={ plotStyles[ 'xy-plot' ] }>
|
|
591
592
|
<XYChart
|
|
592
593
|
theme={ theme }
|
|
593
594
|
width={ width }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment <rootDir>/tests/environment-los-angeles.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { renderHook } from '@testing-library/react';
|
|
5
|
+
import { GlobalChartsProvider } from '../../../../providers';
|
|
6
|
+
import { useBarChartOptions } from '../use-bar-chart-options';
|
|
7
|
+
import type { SeriesData } from '../../../../types';
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
|
|
10
|
+
// The runtime locale stays en-US: a bucket labeled in German, on Tokyo's
|
|
11
|
+
// calendar day, can only come from the provider.
|
|
12
|
+
|
|
13
|
+
// 15:30 UTC is Aug 2 in Los Angeles and Aug 3 in Tokyo.
|
|
14
|
+
const START = new Date( '2026-08-02T15:30:00Z' );
|
|
15
|
+
|
|
16
|
+
const daily: SeriesData[] = [
|
|
17
|
+
{
|
|
18
|
+
label: 'Series A',
|
|
19
|
+
data: Array.from( { length: 14 }, ( _, i ) => ( {
|
|
20
|
+
date: new Date( START.getTime() + i * 24 * 60 * 60 * 1000 ),
|
|
21
|
+
value: 10 + i,
|
|
22
|
+
} ) ),
|
|
23
|
+
options: {},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const withProvider =
|
|
28
|
+
( props: { locale?: string; timeZone?: string } = {} ) =>
|
|
29
|
+
( { children }: { children: ReactNode } ) => (
|
|
30
|
+
<GlobalChartsProvider { ...props }>{ children }</GlobalChartsProvider>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const optionsFor = (
|
|
34
|
+
props?: { locale?: string; timeZone?: string },
|
|
35
|
+
options?: Parameters< typeof useBarChartOptions >[ 2 ]
|
|
36
|
+
) =>
|
|
37
|
+
renderHook( () => useBarChartOptions( daily, false, options ), {
|
|
38
|
+
wrapper: withProvider( props ),
|
|
39
|
+
} ).result.current;
|
|
40
|
+
|
|
41
|
+
describe( 'useBarChartOptions with a host formatting context', () => {
|
|
42
|
+
it( 'labels the tooltip bucket in the provider locale and time zone', () => {
|
|
43
|
+
const { tooltip } = optionsFor( { locale: 'de-DE', timeZone: 'Asia/Tokyo' } );
|
|
44
|
+
|
|
45
|
+
expect( tooltip.labelFormatter( START.getTime(), 0, [] ) ).toBe( '3. August 2026' );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'labels a weekly bucket in the provider locale and time zone', () => {
|
|
49
|
+
const { tooltip } = optionsFor(
|
|
50
|
+
{ locale: 'de-DE', timeZone: 'Asia/Tokyo' },
|
|
51
|
+
{ axis: { x: { tickResolution: 'week' } } }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect( tooltip.labelFormatter( START.getTime(), 0, [] ) ).toBe( 'Week of 3. August 2026' );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'labels the axis ticks in the provider locale and time zone', () => {
|
|
58
|
+
const { axis } = optionsFor( { locale: 'de-DE', timeZone: 'Asia/Tokyo' } );
|
|
59
|
+
|
|
60
|
+
expect( axis.x.tickFormat( START, 0, [] ) ).toBe( '3. Aug.' );
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
it( 'falls back to the runtime locale and zone when the provider supplies none', () => {
|
|
64
|
+
const { tooltip, axis } = optionsFor();
|
|
65
|
+
|
|
66
|
+
expect( tooltip.labelFormatter( START.getTime(), 0, [] ) ).toBe( 'August 2, 2026' );
|
|
67
|
+
expect( axis.x.tickFormat( START, 0, [] ) ).toBe( 'Aug 2' );
|
|
68
|
+
} );
|
|
69
|
+
} );
|