@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment <rootDir>/tests/environment-auckland.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { GlobalChartsProvider } from '../../../providers';
|
|
6
|
+
import LineChart from '../line-chart';
|
|
7
|
+
|
|
8
|
+
const mockRefCallback = jest.fn();
|
|
9
|
+
jest.mock( '../../../hooks/use-element-size', () => ( {
|
|
10
|
+
useElementSize: () => [ mockRefCallback, 500, 300 ],
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
// Naive day strings, the shape the Stats payload gives for day buckets.
|
|
14
|
+
const data = [
|
|
15
|
+
{
|
|
16
|
+
label: 'Series A',
|
|
17
|
+
data: [
|
|
18
|
+
{ dateString: '2026-08-02', value: 1 },
|
|
19
|
+
{ dateString: '2026-08-03', value: 2 },
|
|
20
|
+
{ dateString: '2026-08-04', value: 3 },
|
|
21
|
+
],
|
|
22
|
+
options: {},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe( 'dateString under a host time zone', () => {
|
|
27
|
+
it( 'dates a naive string in the host zone, not the viewer browser zone', () => {
|
|
28
|
+
render(
|
|
29
|
+
<GlobalChartsProvider locale="en-US" timeZone="Asia/Tokyo">
|
|
30
|
+
<LineChart width={ 500 } height={ 300 } withGradientFill={ false } data={ data } />
|
|
31
|
+
</GlobalChartsProvider>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const ticks = screen.getAllByText( /^[A-Z][a-z]{2} \d{1,2}$/ );
|
|
35
|
+
expect( ticks.map( tick => tick.textContent ) ).toEqual( [ 'Aug 2', 'Aug 3', 'Aug 4' ] );
|
|
36
|
+
} );
|
|
37
|
+
} );
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BaseChartProps,
|
|
3
|
+
BucketInfo,
|
|
3
4
|
DataPointDate,
|
|
4
5
|
SeriesData,
|
|
5
6
|
SeriesChartLegendConfig,
|
|
@@ -7,9 +8,9 @@ import type {
|
|
|
7
8
|
AnnotationStyles,
|
|
8
9
|
DataPoint,
|
|
9
10
|
} from '../../types';
|
|
10
|
-
import type { RenderTooltipParams } from '../../visx/types';
|
|
11
|
+
import type { CrosshairStyle, RenderTooltipParams, XyChartTooltipProps } from '../../visx/types';
|
|
11
12
|
import type { GlyphProps } from '@visx/xychart';
|
|
12
|
-
import type { ReactNode, SVGProps, FC } from 'react';
|
|
13
|
+
import type { ReactNode, SVGProps, FC, CSSProperties } from 'react';
|
|
13
14
|
|
|
14
15
|
export type LineChartAnnotationProps = {
|
|
15
16
|
datum: DataPointDate;
|
|
@@ -37,7 +38,18 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVi
|
|
|
37
38
|
withGradientFill: boolean;
|
|
38
39
|
smoothing?: boolean;
|
|
39
40
|
curveType?: CurveType;
|
|
40
|
-
renderTooltip?: (
|
|
41
|
+
renderTooltip?: (
|
|
42
|
+
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo }
|
|
43
|
+
) => ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Place the panel below the x-axis tick labels with a pointer at the datum x; horizontal bounds still apply.
|
|
46
|
+
* @default 'auto'
|
|
47
|
+
*/
|
|
48
|
+
tooltipPlacement?: XyChartTooltipProps< DataPointDate >[ 'tooltipPlacement' ];
|
|
49
|
+
/**
|
|
50
|
+
* Inline container styles; see Below-Axis Tooltips in stories/index.docs.mdx for content color overrides.
|
|
51
|
+
*/
|
|
52
|
+
tooltipStyle?: CSSProperties;
|
|
41
53
|
withStartGlyphs?: boolean;
|
|
42
54
|
withEndGlyphs?: boolean;
|
|
43
55
|
renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
|
|
@@ -46,6 +58,8 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] >, SeriesVi
|
|
|
46
58
|
withTooltipCrosshairs?: {
|
|
47
59
|
showVertical?: boolean;
|
|
48
60
|
showHorizontal?: boolean;
|
|
61
|
+
verticalStyle?: CrosshairStyle;
|
|
62
|
+
horizontalStyle?: CrosshairStyle;
|
|
49
63
|
};
|
|
50
64
|
/**
|
|
51
65
|
* Enable drag-to-zoom on the X axis. The user drags horizontally to
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
.pie-chart {
|
|
2
|
-
|
|
2
|
+
// Anchors the tooltip, which renders next to the SVG in here.
|
|
3
|
+
&__plot {
|
|
4
|
+
position: relative;
|
|
5
|
+
}
|
|
3
6
|
|
|
4
7
|
// Fill parent when no explicit width/height provided
|
|
5
8
|
&--responsive {
|
|
@@ -7,4 +10,13 @@
|
|
|
7
10
|
width: 100%;
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
// Both literals are load-bearing: an undeclared role makes the
|
|
14
|
+
// `fill` invalid, and `fill` starts at black.
|
|
15
|
+
&__label-plate {
|
|
16
|
+
fill: var(--a8c-charts-color-label-background, transparent);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&__label-text {
|
|
20
|
+
fill: var(--a8c-charts-color-label-inverse, #f0f0f0);
|
|
21
|
+
}
|
|
10
22
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Group } from '@visx/group';
|
|
2
2
|
import { Pie } from '@visx/shape';
|
|
3
|
-
import { useTooltip
|
|
3
|
+
import { useTooltip } from '@visx/tooltip';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import { useCallback, useContext, useMemo } from 'react';
|
|
5
|
+
import { useCallback, useContext, useMemo, useRef } from 'react';
|
|
6
6
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
7
7
|
import { BaseTooltip } from '../../components/tooltip';
|
|
8
|
+
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
8
9
|
import {
|
|
9
10
|
useDataWithPercentages,
|
|
10
11
|
useLegendVisibilityData,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
useGlobalChartsTheme,
|
|
19
20
|
GlobalChartsContext,
|
|
20
21
|
} from '../../providers';
|
|
21
|
-
import {
|
|
22
|
+
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
22
23
|
import { attachSubComponents, resolveFontSize } from '../../utils';
|
|
23
24
|
import { getStringWidth } from '../../visx/text';
|
|
24
25
|
import { Center } from '../private/center';
|
|
@@ -196,14 +197,10 @@ const PieChartInternal = ( {
|
|
|
196
197
|
const chartId = useChartId( providedChartId );
|
|
197
198
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
|
|
198
199
|
useTooltip< DataPointPercentageCalculated >();
|
|
200
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
199
201
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
const { containerRef, TooltipInPortal, containerBounds } = useTooltipInPortal( {
|
|
203
|
-
detectBounds: true,
|
|
204
|
-
scroll: true,
|
|
205
|
-
debounce: 0,
|
|
206
|
-
} );
|
|
202
|
+
// The tooltip renders inside this element, so pointer coordinates are taken relative to it.
|
|
203
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
207
204
|
|
|
208
205
|
const onMouseLeave = useCallback( () => {
|
|
209
206
|
if ( ! withTooltips ) {
|
|
@@ -325,13 +322,6 @@ const PieChartInternal = ( {
|
|
|
325
322
|
} }
|
|
326
323
|
trailingContent={
|
|
327
324
|
<>
|
|
328
|
-
{ withTooltips && tooltipOpen && tooltipData && (
|
|
329
|
-
<TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
|
|
330
|
-
<div className={ CHART_SCOPE_CLASS } role="tooltip">
|
|
331
|
-
{ renderTooltip( { tooltipData } ) }
|
|
332
|
-
</div>
|
|
333
|
-
</TooltipInPortal>
|
|
334
|
-
) }
|
|
335
325
|
{ htmlChildren }
|
|
336
326
|
{ otherChildren }
|
|
337
327
|
</>
|
|
@@ -359,7 +349,7 @@ const PieChartInternal = ( {
|
|
|
359
349
|
: 0;
|
|
360
350
|
|
|
361
351
|
return (
|
|
362
|
-
<Center ref={ containerRef }>
|
|
352
|
+
<Center ref={ containerRef } className={ styles[ 'pie-chart__plot' ] }>
|
|
363
353
|
<svg
|
|
364
354
|
viewBox={ `0 0 ${ width } ${ height }` }
|
|
365
355
|
preserveAspectRatio="xMidYMid meet"
|
|
@@ -403,20 +393,15 @@ const PieChartInternal = ( {
|
|
|
403
393
|
return;
|
|
404
394
|
}
|
|
405
395
|
|
|
406
|
-
|
|
407
|
-
if (
|
|
396
|
+
const bounds = containerRef.current?.getBoundingClientRect();
|
|
397
|
+
if ( ! bounds ) {
|
|
408
398
|
return;
|
|
409
399
|
}
|
|
410
400
|
|
|
411
|
-
// Use clientX/Y and subtract containerBounds to cancel out any stale offset.
|
|
412
|
-
// TooltipInPortal calculates: tooltipLeft + containerBounds.left + scrollX
|
|
413
|
-
// By passing (clientX - containerBounds.left), we get:
|
|
414
|
-
// (clientX - containerBounds.left) + containerBounds.left + scrollX = clientX + scrollX
|
|
415
|
-
// This gives correct page coordinates regardless of stale bounds.
|
|
416
401
|
showTooltip( {
|
|
417
402
|
tooltipData: arc.data,
|
|
418
|
-
tooltipLeft: event.clientX -
|
|
419
|
-
tooltipTop: event.clientY -
|
|
403
|
+
tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
|
|
404
|
+
tooltipTop: event.clientY - bounds.top + tooltipOffsetY,
|
|
420
405
|
} );
|
|
421
406
|
};
|
|
422
407
|
|
|
@@ -450,23 +435,21 @@ const PieChartInternal = ( {
|
|
|
450
435
|
<path { ...pathProps } />
|
|
451
436
|
{ showLabels && hasSpaceForLabel && (
|
|
452
437
|
<g>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
/>
|
|
464
|
-
) }
|
|
438
|
+
<rect
|
|
439
|
+
className={ styles[ 'pie-chart__label-plate' ] }
|
|
440
|
+
x={ centroidX - backgroundWidth / 2 }
|
|
441
|
+
y={ centroidY - backgroundHeight / 2 }
|
|
442
|
+
width={ backgroundWidth }
|
|
443
|
+
height={ backgroundHeight }
|
|
444
|
+
rx={ 4 }
|
|
445
|
+
ry={ 4 }
|
|
446
|
+
pointerEvents="none"
|
|
447
|
+
/>
|
|
465
448
|
<text
|
|
449
|
+
className={ styles[ 'pie-chart__label-text' ] }
|
|
466
450
|
x={ centroidX }
|
|
467
451
|
y={ centroidY }
|
|
468
452
|
dy=".33em"
|
|
469
|
-
fill={ providerTheme.labelTextColor || '#333' }
|
|
470
453
|
fontSize={ fontSize }
|
|
471
454
|
textAnchor="middle"
|
|
472
455
|
pointerEvents="none"
|
|
@@ -486,6 +469,13 @@ const PieChartInternal = ( {
|
|
|
486
469
|
{ ! allSegmentsHidden && svgChildren }
|
|
487
470
|
</Group>
|
|
488
471
|
</svg>
|
|
472
|
+
{ withTooltips && tooltipOpen && tooltipData && (
|
|
473
|
+
<BoundedTooltip top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
|
|
474
|
+
<div className={ standaloneScopeClass } role="tooltip">
|
|
475
|
+
{ renderTooltip( { tooltipData } ) }
|
|
476
|
+
</div>
|
|
477
|
+
</BoundedTooltip>
|
|
478
|
+
) }
|
|
489
479
|
</Center>
|
|
490
480
|
);
|
|
491
481
|
} }
|
|
@@ -123,18 +123,9 @@ describe( 'PieChart', () => {
|
|
|
123
123
|
test( 'hides labels when showLabels is false', () => {
|
|
124
124
|
renderWithTheme( { showLabels: false } );
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
// Check if this text element is not the measurement element
|
|
130
|
-
return (
|
|
131
|
-
( content === 'A' || content === 'B' ) &&
|
|
132
|
-
element?.id !== '__react_svg_text_measurement_id'
|
|
133
|
-
);
|
|
134
|
-
} );
|
|
135
|
-
|
|
136
|
-
// Labels should not be present in the rendered output (excluding measurement text)
|
|
137
|
-
expect( labelElements ).toHaveLength( 0 );
|
|
126
|
+
// A plain query, so this also fails if the measurement node stops being
|
|
127
|
+
// ignored — see tests/setup-text-measurement.js.
|
|
128
|
+
expect( screen.queryAllByText( /^[AB]$/ ) ).toHaveLength( 0 );
|
|
138
129
|
} );
|
|
139
130
|
|
|
140
131
|
test( 'shows labels when showLabels is explicitly true', () => {
|
|
@@ -621,3 +612,59 @@ describe( 'PieChart', () => {
|
|
|
621
612
|
} );
|
|
622
613
|
} );
|
|
623
614
|
} );
|
|
615
|
+
|
|
616
|
+
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
617
|
+
// else the charts measure (wrapper, clipping lookups) reports the container.
|
|
618
|
+
const mockRects = () =>
|
|
619
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
620
|
+
this: Element
|
|
621
|
+
) {
|
|
622
|
+
const box = this.classList.contains( 'visx-tooltip' );
|
|
623
|
+
return {
|
|
624
|
+
left: box ? 0 : 100,
|
|
625
|
+
top: box ? 0 : 50,
|
|
626
|
+
width: box ? 120 : 400,
|
|
627
|
+
height: box ? 40 : 300,
|
|
628
|
+
right: box ? 120 : 500,
|
|
629
|
+
bottom: box ? 40 : 350,
|
|
630
|
+
x: box ? 0 : 100,
|
|
631
|
+
y: box ? 0 : 50,
|
|
632
|
+
toJSON: () => ( {} ),
|
|
633
|
+
} as DOMRect;
|
|
634
|
+
} );
|
|
635
|
+
|
|
636
|
+
describe( 'PieChart tooltip position', () => {
|
|
637
|
+
afterEach( () => {
|
|
638
|
+
jest.restoreAllMocks();
|
|
639
|
+
} );
|
|
640
|
+
|
|
641
|
+
test( 'places the box relative to the chart container, at the pointer plus the offsets', async () => {
|
|
642
|
+
mockRects();
|
|
643
|
+
const user = userEvent.setup();
|
|
644
|
+
render(
|
|
645
|
+
<GlobalChartsProvider>
|
|
646
|
+
<PieChart
|
|
647
|
+
data={ [
|
|
648
|
+
{ label: 'A', value: 60, valueDisplay: '60' },
|
|
649
|
+
{ label: 'B', value: 40, valueDisplay: '40' },
|
|
650
|
+
] }
|
|
651
|
+
width={ 400 }
|
|
652
|
+
height={ 300 }
|
|
653
|
+
withTooltips
|
|
654
|
+
/>
|
|
655
|
+
</GlobalChartsProvider>
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
await user.pointer( {
|
|
659
|
+
target: screen.getAllByTestId( 'pie-segment' )[ 0 ],
|
|
660
|
+
coords: { clientX: 150, clientY: 120 },
|
|
661
|
+
} );
|
|
662
|
+
|
|
663
|
+
// Pointer at (150, 120) is (50, 70) inside the container; the default
|
|
664
|
+
// -15px vertical offset makes that (50, 55), and the box adds 10px each way.
|
|
665
|
+
await expect( screen.findByRole( 'tooltip' ) ).resolves.toBeInTheDocument();
|
|
666
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( {
|
|
667
|
+
transform: 'translate(60px, 65px)',
|
|
668
|
+
} );
|
|
669
|
+
} );
|
|
670
|
+
} );
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
.pie-semi-circle-chart {
|
|
2
|
+
// Anchors the tooltip, which renders next to the SVG in here.
|
|
3
|
+
&__plot {
|
|
4
|
+
position: relative;
|
|
5
|
+
}
|
|
6
|
+
|
|
2
7
|
// Fill parent when no explicit width/height provided
|
|
3
8
|
&--responsive {
|
|
4
9
|
height: 100%;
|
|
@@ -6,11 +11,11 @@
|
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
.label {
|
|
9
|
-
font-weight: var(--wpds-typography-font-weight-emphasis
|
|
10
|
-
font-size: var(--wpds-typography-font-size-lg
|
|
14
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
15
|
+
font-size: var(--wpds-typography-font-size-lg);
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
.note {
|
|
14
|
-
font-size: var(--wpds-typography-font-size-md
|
|
19
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Group } from '@visx/group';
|
|
2
2
|
import { Pie } from '@visx/shape';
|
|
3
3
|
import { Text } from '@visx/text';
|
|
4
|
-
import { useTooltip
|
|
4
|
+
import { useTooltip } from '@visx/tooltip';
|
|
5
5
|
import clsx from 'clsx';
|
|
6
|
-
import { useCallback, useContext, useMemo } from 'react';
|
|
6
|
+
import { useCallback, useContext, useMemo, useRef } from 'react';
|
|
7
7
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
8
8
|
import { BaseTooltip } from '../../components/tooltip';
|
|
9
|
+
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
9
10
|
import {
|
|
10
11
|
useDataWithPercentages,
|
|
11
12
|
useLegendVisibilityData,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
useGlobalChartsContext,
|
|
19
20
|
GlobalChartsContext,
|
|
20
21
|
} from '../../providers';
|
|
21
|
-
import {
|
|
22
|
+
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
22
23
|
import { attachSubComponents } from '../../utils';
|
|
23
24
|
import { Center } from '../private/center';
|
|
24
25
|
import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
|
|
@@ -189,42 +190,25 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
189
190
|
const chartId = useChartId( providedChartId );
|
|
190
191
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
|
|
191
192
|
useTooltip< DataPointPercentageCalculated >();
|
|
193
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
192
194
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
const { containerRef, TooltipInPortal, containerBounds } = useTooltipInPortal( {
|
|
196
|
-
detectBounds: true,
|
|
197
|
-
scroll: true,
|
|
198
|
-
debounce: 0,
|
|
199
|
-
} );
|
|
195
|
+
// The tooltip renders inside this element, so pointer coordinates are taken relative to it.
|
|
196
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
200
197
|
|
|
201
198
|
const handleMouseMove = useCallback(
|
|
202
199
|
( event: MouseEvent< SVGElement >, arc: ArcData ) => {
|
|
203
|
-
|
|
204
|
-
if (
|
|
200
|
+
const bounds = containerRef.current?.getBoundingClientRect();
|
|
201
|
+
if ( ! bounds ) {
|
|
205
202
|
return;
|
|
206
203
|
}
|
|
207
204
|
|
|
208
|
-
// Use clientX/Y and subtract containerBounds to cancel out any stale offset.
|
|
209
|
-
// TooltipInPortal calculates: tooltipLeft + containerBounds.left + scrollX
|
|
210
|
-
// By passing (clientX - containerBounds.left), we get:
|
|
211
|
-
// (clientX - containerBounds.left) + containerBounds.left + scrollX = clientX + scrollX
|
|
212
|
-
// This gives correct page coordinates regardless of stale bounds.
|
|
213
205
|
showTooltip( {
|
|
214
206
|
tooltipData: arc.data,
|
|
215
|
-
tooltipLeft: event.clientX -
|
|
216
|
-
tooltipTop: event.clientY -
|
|
207
|
+
tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
|
|
208
|
+
tooltipTop: event.clientY - bounds.top + tooltipOffsetY,
|
|
217
209
|
} );
|
|
218
210
|
},
|
|
219
|
-
[
|
|
220
|
-
containerBounds.width,
|
|
221
|
-
containerBounds.height,
|
|
222
|
-
containerBounds.left,
|
|
223
|
-
containerBounds.top,
|
|
224
|
-
showTooltip,
|
|
225
|
-
tooltipOffsetX,
|
|
226
|
-
tooltipOffsetY,
|
|
227
|
-
]
|
|
211
|
+
[ showTooltip, tooltipOffsetX, tooltipOffsetY ]
|
|
228
212
|
);
|
|
229
213
|
|
|
230
214
|
const handleMouseLeave = useCallback( () => {
|
|
@@ -311,7 +295,9 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
311
295
|
const errorHeight = errorWidth / 2;
|
|
312
296
|
|
|
313
297
|
return (
|
|
314
|
-
<div
|
|
298
|
+
<div
|
|
299
|
+
className={ clsx( 'pie-semi-circle-chart', styles[ 'pie-semi-circle-chart' ], className ) }
|
|
300
|
+
>
|
|
315
301
|
<svg width={ errorWidth } height={ errorHeight } data-testid="pie-chart-svg">
|
|
316
302
|
<text x="50%" y="50%" textAnchor="middle" className={ styles.error }>
|
|
317
303
|
{ message }
|
|
@@ -372,13 +358,6 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
372
358
|
data-testid="pie-chart-container"
|
|
373
359
|
trailingContent={
|
|
374
360
|
<>
|
|
375
|
-
{ withTooltips && tooltipOpen && tooltipData && (
|
|
376
|
-
<TooltipInPortal top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
|
|
377
|
-
<div className={ CHART_SCOPE_CLASS } role="tooltip">
|
|
378
|
-
{ renderTooltip( { tooltipData } ) }
|
|
379
|
-
</div>
|
|
380
|
-
</TooltipInPortal>
|
|
381
|
-
) }
|
|
382
361
|
{ htmlChildren }
|
|
383
362
|
{ otherChildren }
|
|
384
363
|
</>
|
|
@@ -398,7 +377,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
398
377
|
const innerRadius = radius * ( 1 - thickness );
|
|
399
378
|
|
|
400
379
|
return (
|
|
401
|
-
<Center ref={ containerRef }>
|
|
380
|
+
<Center ref={ containerRef } className={ styles[ 'pie-semi-circle-chart__plot' ] }>
|
|
402
381
|
<svg
|
|
403
382
|
width={ width }
|
|
404
383
|
height={ height }
|
|
@@ -484,6 +463,13 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
484
463
|
) }
|
|
485
464
|
</Group>
|
|
486
465
|
</svg>
|
|
466
|
+
{ withTooltips && tooltipOpen && tooltipData && (
|
|
467
|
+
<BoundedTooltip top={ tooltipTop || 0 } left={ tooltipLeft || 0 }>
|
|
468
|
+
<div className={ standaloneScopeClass } role="tooltip">
|
|
469
|
+
{ renderTooltip( { tooltipData } ) }
|
|
470
|
+
</div>
|
|
471
|
+
</BoundedTooltip>
|
|
472
|
+
) }
|
|
487
473
|
</Center>
|
|
488
474
|
);
|
|
489
475
|
} }
|
|
@@ -362,3 +362,53 @@ describe( 'PieSemiCircleChart', () => {
|
|
|
362
362
|
} );
|
|
363
363
|
} );
|
|
364
364
|
} );
|
|
365
|
+
|
|
366
|
+
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
367
|
+
// else the charts measure (wrapper, clipping lookups) reports the container.
|
|
368
|
+
const mockRects = () =>
|
|
369
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
370
|
+
this: Element
|
|
371
|
+
) {
|
|
372
|
+
const box = this.classList.contains( 'visx-tooltip' );
|
|
373
|
+
return {
|
|
374
|
+
left: box ? 0 : 100,
|
|
375
|
+
top: box ? 0 : 50,
|
|
376
|
+
width: box ? 120 : 400,
|
|
377
|
+
height: box ? 40 : 300,
|
|
378
|
+
right: box ? 120 : 500,
|
|
379
|
+
bottom: box ? 40 : 350,
|
|
380
|
+
x: box ? 0 : 100,
|
|
381
|
+
y: box ? 0 : 50,
|
|
382
|
+
toJSON: () => ( {} ),
|
|
383
|
+
} as DOMRect;
|
|
384
|
+
} );
|
|
385
|
+
|
|
386
|
+
describe( 'PieSemiCircleChart tooltip position', () => {
|
|
387
|
+
afterEach( () => {
|
|
388
|
+
jest.restoreAllMocks();
|
|
389
|
+
} );
|
|
390
|
+
|
|
391
|
+
it( 'places the box relative to the chart container, at the pointer plus the offsets', async () => {
|
|
392
|
+
mockRects();
|
|
393
|
+
const user = userEvent.setup();
|
|
394
|
+
renderPieChart( {
|
|
395
|
+
data: [
|
|
396
|
+
{ label: 'A', value: 60, valueDisplay: '60' },
|
|
397
|
+
{ label: 'B', value: 40, valueDisplay: '40' },
|
|
398
|
+
],
|
|
399
|
+
withTooltips: true,
|
|
400
|
+
width: 400,
|
|
401
|
+
} );
|
|
402
|
+
|
|
403
|
+
await user.pointer( {
|
|
404
|
+
target: screen.getAllByTestId( 'pie-segment' )[ 0 ],
|
|
405
|
+
coords: { clientX: 150, clientY: 120 },
|
|
406
|
+
} );
|
|
407
|
+
|
|
408
|
+
// (150, 120) is (50, 70) inside the container; the default -15px vertical
|
|
409
|
+
// offset makes that (50, 55), and the box adds 10px each way.
|
|
410
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( {
|
|
411
|
+
transform: 'translate(60px, 65px)',
|
|
412
|
+
} );
|
|
413
|
+
} );
|
|
414
|
+
} );
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// The svg displaying inline includes a few px for descenders, which
|
|
9
9
|
// confuses some browser environments (e.g. headless Chrome) into
|
|
10
10
|
// constantly growing the container. Setting it to display as block avoids
|
|
11
|
-
// that.
|
|
12
|
-
|
|
11
|
+
// that. The tooltip renders in here too, and an inline icon in its rows
|
|
12
|
+
// must stay inline.
|
|
13
|
+
svg:not(:global(.visx-tooltip) svg) {
|
|
13
14
|
display: block;
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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/area-chart';
|
|
7
|
+
import BarChart from '../../bar-chart/bar-chart';
|
|
8
|
+
import LineChart from '../../line-chart/line-chart';
|
|
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
|
+
// 15:30 UTC is Aug 2 in the runtime zone and Aug 3 in Tokyo.
|
|
17
|
+
const START = Date.parse( '2026-08-02T15:30:00Z' );
|
|
18
|
+
|
|
19
|
+
const data = [
|
|
20
|
+
{
|
|
21
|
+
label: 'Series A',
|
|
22
|
+
data: Array.from( { length: 14 }, ( _, i ) => ( {
|
|
23
|
+
date: new Date( START + i * 24 * 60 * 60 * 1000 ),
|
|
24
|
+
value: 10 + i,
|
|
25
|
+
} ) ),
|
|
26
|
+
options: {},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const size = { width: 500, height: 300 };
|
|
31
|
+
|
|
32
|
+
// Every axis labels the data's own buckets, so the fallback names the first
|
|
33
|
+
// point's runtime-zone day rather than a round-numbered instant near it.
|
|
34
|
+
const RUNTIME_TICK = 'Aug 2';
|
|
35
|
+
|
|
36
|
+
const charts: [ string, ReactNode ][] = [
|
|
37
|
+
[ 'AreaChart', <AreaChart key="a" { ...size } data={ data } /> ],
|
|
38
|
+
[ 'BarChart', <BarChart key="b" { ...size } data={ data } /> ],
|
|
39
|
+
[ 'LineChart', <LineChart key="l" { ...size } data={ data } withGradientFill={ false } /> ],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
describe.each( charts )( '%s x axis', ( _name, chart ) => {
|
|
43
|
+
const renderIn = ( props: { locale?: string; timeZone?: string } ) =>
|
|
44
|
+
render( <GlobalChartsProvider { ...props }>{ chart }</GlobalChartsProvider> );
|
|
45
|
+
|
|
46
|
+
it( "labels ticks in the provider's locale", () => {
|
|
47
|
+
renderIn( { locale: 'de-DE', timeZone: 'Asia/Tokyo' } );
|
|
48
|
+
|
|
49
|
+
expect( screen.getByText( '3. Aug.' ) ).toBeInTheDocument();
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'falls back to the runtime locale and zone when the provider supplies none', () => {
|
|
53
|
+
renderIn( {} );
|
|
54
|
+
|
|
55
|
+
expect( screen.getByText( RUNTIME_TICK ) ).toBeInTheDocument();
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'relabels when the provider swaps time zone', () => {
|
|
59
|
+
const { rerender } = render(
|
|
60
|
+
<GlobalChartsProvider timeZone="Asia/Tokyo">{ chart }</GlobalChartsProvider>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect( screen.getByText( 'Aug 3' ) ).toBeInTheDocument();
|
|
64
|
+
|
|
65
|
+
rerender( <GlobalChartsProvider timeZone="Pacific/Honolulu">{ chart }</GlobalChartsProvider> );
|
|
66
|
+
|
|
67
|
+
expect( screen.getByText( 'Aug 2' ) ).toBeInTheDocument();
|
|
68
|
+
} );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
// The values WordPress hands out: `get_locale()` is `en_US`, and `timezone_string`
|
|
72
|
+
// is empty on a site set to a raw UTC offset. Unsanitized, `Intl` throws during
|
|
73
|
+
// render and React unmounts the host tree rather than one chart.
|
|
74
|
+
describe( 'a host locale and zone Intl cannot use', () => {
|
|
75
|
+
it( 'still renders, in the runtime zone, and warns', () => {
|
|
76
|
+
render(
|
|
77
|
+
<GlobalChartsProvider locale="en_US" timeZone="">
|
|
78
|
+
<BarChart { ...size } data={ data } />
|
|
79
|
+
</GlobalChartsProvider>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect( screen.getByText( 'Aug 2' ) ).toBeInTheDocument();
|
|
83
|
+
expect( console ).toHaveWarned();
|
|
84
|
+
} );
|
|
85
|
+
} );
|