@automattic/charts 4.5.0 → 4.7.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 +24 -2
- package/TOKENS.md +12 -10
- package/dist/index.cjs +248 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -20
- package/dist/index.d.cts +22 -11
- package/dist/index.d.ts +22 -11
- package/dist/index.js +249 -147
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +0 -10
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +4 -2
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +11 -1
- package/src/charts/conversion-funnel-chart/types.ts +3 -0
- package/src/charts/geo-chart/geo-chart.tsx +4 -0
- package/src/charts/geo-chart/index.ts +7 -1
- package/src/charts/geo-chart/test/geo-chart.test.tsx +8 -0
- package/src/charts/geo-chart/types.ts +12 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +10 -3
- package/src/charts/heatmap-chart/heatmap-chart.tsx +54 -37
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +129 -25
- package/src/charts/heatmap-chart/types.ts +2 -6
- package/src/charts/line-chart/crosshair-style-types.fixture.ts +23 -0
- package/src/charts/line-chart/line-chart.module.scss +0 -5
- package/src/charts/line-chart/line-chart.tsx +8 -40
- package/src/charts/line-chart/test/line-chart.test.tsx +19 -47
- package/src/charts/pie-chart/pie-chart.module.scss +9 -0
- package/src/charts/pie-chart/pie-chart.tsx +22 -76
- package/src/charts/pie-chart/test/pie-chart.test.tsx +21 -8
- package/src/charts/private/chart-composition/chart-html.tsx +1 -1
- package/src/charts/private/chart-composition/chart-svg.tsx +1 -1
- package/src/charts/private/label-text-color.ts +116 -0
- package/src/charts/private/radial-wipe-animation/radial-wipe-animation.tsx +2 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.tsx +1 -1
- package/src/charts/private/test/label-text-color.test.ts +191 -0
- package/src/charts/private/with-responsive/test/with-responsive.test.tsx +1 -0
- package/src/charts/private/with-responsive/with-responsive.tsx +2 -5
- package/src/components/tooltip/accessible-tooltip.tsx +1 -1
- package/src/components/tooltip/base-tooltip.module.scss +12 -8
- package/src/components/tooltip/base-tooltip.tsx +11 -9
- package/src/components/tooltip/private/bounded-tooltip.tsx +27 -8
- package/src/components/tooltip/private/tooltip-theme.tsx +37 -0
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +82 -1
- package/src/components/tooltip/test/tool-tip.test.tsx +5 -0
- package/src/components/tooltip/test/tooltip-theme.test.tsx +61 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +41 -28
- package/src/components/tooltip/xy-chart-tooltip.tsx +29 -29
- package/src/components/trend-indicator/trend-indicator.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/test/use-xychart-theme.test.tsx +3 -3
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-xychart-theme.ts +4 -12
- package/src/index.ts +7 -1
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +2 -4
- package/src/providers/chart-context/private/perceptual-color.ts +45 -4
- package/src/providers/chart-context/test/perceptual-color.test.ts +31 -0
- package/src/styles/chart-scope-class.ts +5 -0
- package/src/styles/chart-scope.scss +9 -6
- package/src/styles/test/chart-scope.test.ts +3 -3
- package/src/utils/color-utils.ts +2 -1
- package/src/visx/types.ts +1 -1
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +0 -31
package/package.json
CHANGED
|
@@ -114,16 +114,6 @@
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
.tooltip-wrapper {
|
|
118
|
-
background: var(--a8c-charts-color-surface);
|
|
119
|
-
|
|
120
|
-
// Override .visx-tooltip inline styles.
|
|
121
|
-
border-radius: var(--wpds-border-radius-md) !important;
|
|
122
|
-
padding: var(--wpds-dimension-padding-md) !important;
|
|
123
|
-
box-shadow: var(--a8c-charts-elevation-sm, 0 1px 2px 0 #0000000d, 0 2px 3px 0 #0000000a, 0 6px 6px 0 #00000008, 0 8px 8px 0 #00000005) !important;
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
117
|
.tooltip-title {
|
|
128
118
|
color: var(--a8c-charts-color-label);
|
|
129
119
|
font-size: var(--wpds-typography-font-size-sm);
|
|
@@ -36,6 +36,7 @@ import type { FunnelStep, ConversionFunnelChartProps } from './types';
|
|
|
36
36
|
* @param props.renderStepRate - Custom render function for step rates
|
|
37
37
|
* @param props.renderMainMetric - Custom render function for the entire main metric section
|
|
38
38
|
* @param props.renderTooltip - Custom render function for tooltip content
|
|
39
|
+
* @param props.tooltipStyle - Inline styles merged over the tooltip box defaults
|
|
39
40
|
* @return JSX element representing the conversion funnel chart
|
|
40
41
|
*/
|
|
41
42
|
const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
@@ -52,6 +53,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
52
53
|
renderStepRate,
|
|
53
54
|
renderMainMetric,
|
|
54
55
|
renderTooltip,
|
|
56
|
+
tooltipStyle,
|
|
55
57
|
} ) => {
|
|
56
58
|
const chartId = useChartId( providedChartId );
|
|
57
59
|
const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
|
|
@@ -412,7 +414,6 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
412
414
|
index: steps.findIndex( s => s.id === ( tooltipData as FunnelStep ).id ),
|
|
413
415
|
top: tooltipTop,
|
|
414
416
|
left: tooltipLeft,
|
|
415
|
-
className: styles[ 'tooltip-wrapper' ],
|
|
416
417
|
} )
|
|
417
418
|
: renderDefaultTooltip( tooltipData as FunnelStep );
|
|
418
419
|
|
|
@@ -423,7 +424,8 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
|
|
|
423
424
|
<BoundedTooltip
|
|
424
425
|
top={ tooltipTop }
|
|
425
426
|
left={ tooltipLeft }
|
|
426
|
-
className={
|
|
427
|
+
className={ standaloneScopeClass }
|
|
428
|
+
style={ tooltipStyle }
|
|
427
429
|
>
|
|
428
430
|
{ tooltipContent }
|
|
429
431
|
</BoundedTooltip>
|
|
@@ -427,10 +427,20 @@ describe( 'ConversionFunnelChart', () => {
|
|
|
427
427
|
index: 1,
|
|
428
428
|
top: expect.any( Number ),
|
|
429
429
|
left: expect.any( Number ),
|
|
430
|
-
className: 'tooltip-wrapper',
|
|
431
430
|
} );
|
|
432
431
|
} );
|
|
433
432
|
|
|
433
|
+
it( 'merges tooltipStyle onto the tooltip box', async () => {
|
|
434
|
+
const user = userEvent.setup();
|
|
435
|
+
renderWithoutTheme(
|
|
436
|
+
<ConversionFunnelChart { ...defaultProps } tooltipStyle={ { background: 'purple' } } />
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await user.click( screen.getByRole( 'button', { name: /cart/i } ) );
|
|
440
|
+
|
|
441
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( { background: 'purple' } );
|
|
442
|
+
} );
|
|
443
|
+
|
|
434
444
|
it( 'disables tooltip when renderTooltip returns null', async () => {
|
|
435
445
|
const user = userEvent.setup();
|
|
436
446
|
const customRenderTooltip = jest.fn( () => null );
|
|
@@ -50,6 +50,7 @@ export interface TooltipRenderProps {
|
|
|
50
50
|
index: number;
|
|
51
51
|
top: number;
|
|
52
52
|
left: number;
|
|
53
|
+
/** @deprecated The tooltip box draws the package surface; this is no longer set. */
|
|
53
54
|
className?: string;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -80,4 +81,6 @@ export interface ConversionFunnelChartProps extends Pick<
|
|
|
80
81
|
renderMainMetric?: ( props: MainMetricRenderProps ) => React.ReactNode;
|
|
81
82
|
/** Custom render function for tooltip content */
|
|
82
83
|
renderTooltip?: ( props: TooltipRenderProps ) => React.ReactNode;
|
|
84
|
+
/** Inline styles merged over the tooltip box defaults. */
|
|
85
|
+
tooltipStyle?: React.CSSProperties;
|
|
83
86
|
}
|
|
@@ -130,6 +130,7 @@ function normalizeGeoChartError( eventArgs: unknown ): GeoChartError {
|
|
|
130
130
|
* @param props.height - Height of the chart in pixels
|
|
131
131
|
* @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
|
|
132
132
|
* @param props.resolution - Resolution level ('countries', 'provinces', or 'metros')
|
|
133
|
+
* @param props.displayMode - Plot rows as shaded areas ('regions') or markers ('markers')
|
|
133
134
|
* @param props.domain - Country code whose viewpoint sets the disputed borders
|
|
134
135
|
* @param props.onError - Optional callback for Google Charts errors
|
|
135
136
|
* @param props.className - Additional CSS class name for the chart container
|
|
@@ -143,6 +144,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
143
144
|
height,
|
|
144
145
|
region = 'world',
|
|
145
146
|
resolution = 'countries',
|
|
147
|
+
displayMode = 'regions',
|
|
146
148
|
domain,
|
|
147
149
|
onError,
|
|
148
150
|
renderPlaceholder,
|
|
@@ -264,6 +266,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
264
266
|
() => ( {
|
|
265
267
|
...( region !== 'world' && { region } ),
|
|
266
268
|
...( resolution !== 'countries' && { resolution } ),
|
|
269
|
+
...( displayMode !== 'regions' && { displayMode } ),
|
|
267
270
|
...( domain && { domain } ),
|
|
268
271
|
colorAxis: { colors: [ lightColorHex, fullColorHex ] },
|
|
269
272
|
backgroundColor: backgroundColorHex,
|
|
@@ -276,6 +279,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
276
279
|
[
|
|
277
280
|
region,
|
|
278
281
|
resolution,
|
|
282
|
+
displayMode,
|
|
279
283
|
domain,
|
|
280
284
|
lightColorHex,
|
|
281
285
|
fullColorHex,
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
export { default as GeoChart, GeoChartUnresponsive } from './geo-chart';
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
GeoChartProps,
|
|
4
|
+
GeoRegion,
|
|
5
|
+
GeoResolution,
|
|
6
|
+
GeoDisplayMode,
|
|
7
|
+
GeoChartError,
|
|
8
|
+
} from './types';
|
|
@@ -224,6 +224,14 @@ describe( 'GeoChart', () => {
|
|
|
224
224
|
expect( options.domain ).toBe( 'IN' );
|
|
225
225
|
} );
|
|
226
226
|
|
|
227
|
+
test( 'passes the markers display mode so rows draw as points, not shaded areas', () => {
|
|
228
|
+
renderWithTheme( { displayMode: 'markers' } );
|
|
229
|
+
|
|
230
|
+
const options = JSON.parse( screen.getByTestId( 'chart-options' ).textContent || '{}' );
|
|
231
|
+
|
|
232
|
+
expect( options.displayMode ).toBe( 'markers' );
|
|
233
|
+
} );
|
|
234
|
+
|
|
227
235
|
test( 'does not include resolution in options when set to countries (default)', () => {
|
|
228
236
|
renderWithTheme();
|
|
229
237
|
|
|
@@ -15,6 +15,13 @@ export type GeoRegion = 'world' | ( string & {} );
|
|
|
15
15
|
*/
|
|
16
16
|
export type GeoResolution = 'countries' | 'provinces' | 'metros';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* How the map plots its rows.
|
|
20
|
+
* - 'regions': Shades each area the rows name
|
|
21
|
+
* - 'markers': Draws a sized, colored circle per row; a row can name a place or give `[ latitude, longitude ]` as its first two columns
|
|
22
|
+
*/
|
|
23
|
+
export type GeoDisplayMode = 'regions' | 'markers';
|
|
24
|
+
|
|
18
25
|
export interface GeoChartError {
|
|
19
26
|
id?: string;
|
|
20
27
|
message?: string;
|
|
@@ -48,6 +55,11 @@ export interface GeoChartProps extends Pick<
|
|
|
48
55
|
* @default 'countries'
|
|
49
56
|
*/
|
|
50
57
|
resolution?: GeoResolution;
|
|
58
|
+
/**
|
|
59
|
+
* How the map plots its rows: shaded areas, or one marker per row.
|
|
60
|
+
* @default 'regions'
|
|
61
|
+
*/
|
|
62
|
+
displayMode?: GeoDisplayMode;
|
|
51
63
|
/**
|
|
52
64
|
* ISO 3166-1 alpha-2 code of the country whose viewpoint sets the disputed borders
|
|
53
65
|
* (e.g., 'IN' draws Kashmir as India shows it). Google's default borders apply when omitted.
|
|
@@ -183,12 +183,19 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
183
183
|
font-size: var(--wpds-typography-font-size-md);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
.heatmap-chart__cell--strong .heatmap-chart__cell-value {
|
|
186
|
+
// Chosen per cell in JS from the blended fill; see pickLabelTextColorForFill.
|
|
187
|
+
.heatmap-chart__cell-value--inverse {
|
|
189
188
|
color: var(--a8c-charts-color-label-inverse);
|
|
190
189
|
}
|
|
191
190
|
|
|
191
|
+
.heatmap-chart__cell-value--black {
|
|
192
|
+
color: #000;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.heatmap-chart__cell-value--white {
|
|
196
|
+
color: #fff;
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
.heatmap-chart__legend-label {
|
|
193
200
|
color: var(--a8c-charts-color-label);
|
|
194
201
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { formatNumber, formatNumberCompact } from '@automattic/number-formatters';
|
|
2
|
-
import {
|
|
2
|
+
import { useTooltip } from '@visx/tooltip';
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
+
import isEqual from 'fast-deep-equal';
|
|
5
6
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
-
import tooltipStyles from '../../components/tooltip/base-tooltip.module.scss';
|
|
7
7
|
import { BoundedTooltip, TOOLTIP_Z_INDEX } from '../../components/tooltip/private/bounded-tooltip';
|
|
8
|
+
import { useIsomorphicLayoutEffect } from '../../hooks';
|
|
8
9
|
import {
|
|
9
10
|
GlobalChartsProvider,
|
|
10
11
|
useChartId,
|
|
@@ -13,19 +14,16 @@ import {
|
|
|
13
14
|
GlobalChartsContext,
|
|
14
15
|
} from '../../providers';
|
|
15
16
|
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
17
|
+
import { blendRgb, hexToRgb } from '../../providers/chart-context/private/perceptual-color';
|
|
16
18
|
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
17
19
|
import { attachSubComponents } from '../../utils';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
mixHexColors,
|
|
21
|
-
normalizeColorToHex,
|
|
22
|
-
prefersLightText,
|
|
23
|
-
} from '../../utils/color-utils';
|
|
24
|
-
import { resolveCssVariable } from '../../utils/resolve-css-var';
|
|
20
|
+
import { isValidHexColor, normalizeColorToHex } from '../../utils/color-utils';
|
|
21
|
+
import { createCssVariableResolver, resolveCssVariable } from '../../utils/resolve-css-var';
|
|
25
22
|
import { Center } from '../private/center';
|
|
26
23
|
import { useChartChildren } from '../private/chart-composition';
|
|
27
24
|
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
28
25
|
import { ChartLayout } from '../private/chart-layout';
|
|
26
|
+
import { pickLabelTextColorForFill, resolveLabelRoles } from '../private/label-text-color';
|
|
29
27
|
import { withResponsive } from '../private/with-responsive';
|
|
30
28
|
import styles from './heatmap-chart.module.scss';
|
|
31
29
|
import {
|
|
@@ -46,6 +44,7 @@ import {
|
|
|
46
44
|
import type { HeatmapContextValue } from './private';
|
|
47
45
|
import type { CellBlock, CellPosition } from './private/keyboard-navigation';
|
|
48
46
|
import type { HeatmapChartProps, HeatmapTooltipData } from './types';
|
|
47
|
+
import type { LabelRoles, LabelTextColor } from '../private/label-text-color';
|
|
49
48
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
50
49
|
import type { CSSProperties, FC } from 'react';
|
|
51
50
|
|
|
@@ -53,15 +52,19 @@ import type { CSSProperties, FC } from 'react';
|
|
|
53
52
|
// the rendered fill is the primary mixed over the chart background at 0.15 + 0.85 * intensity.
|
|
54
53
|
const CELL_MIX_FLOOR = 0.15;
|
|
55
54
|
|
|
55
|
+
// `label` is the stylesheet's default, so it needs no modifier.
|
|
56
|
+
const CELL_VALUE_MODIFIER: Record< LabelTextColor, string | undefined > = {
|
|
57
|
+
label: undefined,
|
|
58
|
+
'label-inverse': styles[ 'heatmap-chart__cell-value--inverse' ],
|
|
59
|
+
black: styles[ 'heatmap-chart__cell-value--black' ],
|
|
60
|
+
white: styles[ 'heatmap-chart__cell-value--white' ],
|
|
61
|
+
};
|
|
62
|
+
|
|
56
63
|
// One instance, not a `[]` default in the signature: `buildTooltipData` keys on
|
|
57
64
|
// it, and a fresh array per render re-ran the keyboard tooltip effect endlessly.
|
|
58
65
|
const NO_ROW_LABELS: string[] = [];
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
const TOOLTIP_BOX_STYLES: Record< 'light' | 'dark', CSSProperties > = {
|
|
62
|
-
light: { ...visxTooltipStyles, zIndex: TOOLTIP_Z_INDEX },
|
|
63
|
-
dark: { zIndex: TOOLTIP_Z_INDEX },
|
|
64
|
-
};
|
|
67
|
+
const TOOLTIP_BOX_STYLE: CSSProperties = { zIndex: TOOLTIP_Z_INDEX };
|
|
65
68
|
|
|
66
69
|
// The cell's own label wins; otherwise the group, column and row labels name it.
|
|
67
70
|
const cellName = ( info: HeatmapTooltipData ) =>
|
|
@@ -88,14 +91,13 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
88
91
|
gap = 'md',
|
|
89
92
|
withTooltips = false,
|
|
90
93
|
renderTooltip,
|
|
91
|
-
tooltipVariant = 'light',
|
|
92
94
|
tooltipStyle,
|
|
93
95
|
children,
|
|
94
96
|
} ) => {
|
|
95
97
|
const chartId = useChartId( providedChartId );
|
|
96
98
|
const tooltipBoxStyle = tooltipStyle
|
|
97
|
-
? { ...
|
|
98
|
-
:
|
|
99
|
+
? { ...TOOLTIP_BOX_STYLE, ...tooltipStyle }
|
|
100
|
+
: TOOLTIP_BOX_STYLE;
|
|
99
101
|
const { getElementStyles, theme } = useGlobalChartsContext();
|
|
100
102
|
const scopeElement = useChartScopeElement();
|
|
101
103
|
const { heatmapChart: heatmapChartSettings } = theme;
|
|
@@ -106,6 +108,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
106
108
|
useTooltip< HeatmapTooltipData >();
|
|
107
109
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
108
110
|
const containerRef = useRef< HTMLDivElement >( null );
|
|
111
|
+
const [ labelRoles, setLabelRoles ] = useState< LabelRoles | null >( null );
|
|
109
112
|
// The chart root positions the tooltip, so pointer and cell coordinates are
|
|
110
113
|
// measured against it — found by its id rather than by walking up, so
|
|
111
114
|
// whatever ChartLayout wraps the grid in cannot shift the origin.
|
|
@@ -131,18 +134,34 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
131
134
|
);
|
|
132
135
|
|
|
133
136
|
// Choose text color from the blended fill, not the raw value.
|
|
134
|
-
// If either color cannot resolve to hex, keep
|
|
137
|
+
// If either color cannot resolve to hex, keep the stylesheet's default role.
|
|
135
138
|
const primaryHex = normalizeColorToHex( primaryColorHex );
|
|
136
|
-
const
|
|
137
|
-
isValidHexColor( primaryHex ) &&
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
const cellMix =
|
|
140
|
+
isValidHexColor( primaryHex ) && isValidHexColor( chartBackgroundHex )
|
|
141
|
+
? { primary: hexToRgb( primaryHex ), background: hexToRgb( chartBackgroundHex ) }
|
|
142
|
+
: null;
|
|
143
|
+
const cellTextColor = ( intensity: number ): LabelTextColor =>
|
|
144
|
+
cellMix
|
|
145
|
+
? pickLabelTextColorForFill(
|
|
146
|
+
blendRgb(
|
|
147
|
+
cellMix.primary,
|
|
148
|
+
cellMix.background,
|
|
149
|
+
CELL_MIX_FLOOR + ( 1 - CELL_MIX_FLOOR ) * intensity
|
|
150
|
+
),
|
|
151
|
+
labelRoles,
|
|
152
|
+
'label'
|
|
153
|
+
)
|
|
154
|
+
: 'label';
|
|
155
|
+
|
|
156
|
+
// Read inside the chart's own class, where the cell text inherits them. Keyed on `data` too,
|
|
157
|
+
// because the grid is not mounted while there is nothing to draw.
|
|
158
|
+
useIsomorphicLayoutEffect( () => {
|
|
159
|
+
if ( ! containerRef.current ) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const next = resolveLabelRoles( createCssVariableResolver( containerRef.current ) );
|
|
163
|
+
setLabelRoles( previous => ( isEqual( previous, next ) ? previous : next ) );
|
|
164
|
+
}, [ data, className, primaryColorHex, chartBackgroundHex ] );
|
|
146
165
|
|
|
147
166
|
const extent = useMemo( () => getValueExtent( data ), [ data ] );
|
|
148
167
|
const heatmapContext = useMemo< HeatmapContextValue >(
|
|
@@ -547,8 +566,6 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
547
566
|
data-row={ rowIndex }
|
|
548
567
|
className={ clsx( styles[ 'heatmap-chart__cell' ], {
|
|
549
568
|
[ styles[ 'heatmap-chart__cell--filled' ] ]: filled,
|
|
550
|
-
[ styles[ 'heatmap-chart__cell--strong' ] ]:
|
|
551
|
-
filled && cellHasLightText( normalized ),
|
|
552
569
|
[ styles[ 'heatmap-chart__cell--summary' ] ]: column.summary,
|
|
553
570
|
...summaryGaps( columnIndex ),
|
|
554
571
|
[ styles[ 'heatmap-chart__cell--selected' ] ]:
|
|
@@ -566,7 +583,12 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
566
583
|
onMouseLeave={ handleCellMouseLeave }
|
|
567
584
|
>
|
|
568
585
|
{ ( drawValues || column.summary ) && present && (
|
|
569
|
-
<span
|
|
586
|
+
<span
|
|
587
|
+
className={ clsx(
|
|
588
|
+
styles[ 'heatmap-chart__cell-value' ],
|
|
589
|
+
filled && CELL_VALUE_MODIFIER[ cellTextColor( normalized ) ]
|
|
590
|
+
) }
|
|
591
|
+
>
|
|
570
592
|
{ /* Compact display; tooltip and aria-label keep full precision. */ }
|
|
571
593
|
{ formatNumberCompact( value ) }
|
|
572
594
|
</span>
|
|
@@ -596,12 +618,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
596
618
|
) }
|
|
597
619
|
</div>
|
|
598
620
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
599
|
-
<BoundedTooltip
|
|
600
|
-
top={ tooltipTop }
|
|
601
|
-
left={ tooltipLeft }
|
|
602
|
-
className={ tooltipVariant === 'dark' ? tooltipStyles.surface : undefined }
|
|
603
|
-
style={ tooltipBoxStyle }
|
|
604
|
-
>
|
|
621
|
+
<BoundedTooltip top={ tooltipTop } left={ tooltipLeft } style={ tooltipBoxStyle }>
|
|
605
622
|
<div className={ standaloneScopeClass } role="tooltip" tabIndex={ -1 }>
|
|
606
623
|
{ ( renderTooltip ?? defaultRenderTooltip )( tooltipData ) }
|
|
607
624
|
</div>
|
|
@@ -85,34 +85,22 @@ describe( 'HeatmapChart', () => {
|
|
|
85
85
|
await expect( screen.findByRole( 'tooltip' ) ).resolves.toBeInTheDocument();
|
|
86
86
|
} );
|
|
87
87
|
|
|
88
|
-
test(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
withTooltips: true,
|
|
101
|
-
tooltipVariant: 'dark',
|
|
102
|
-
rowLabels: [ 'Mon', 'Tue', 'Wed' ],
|
|
103
|
-
} );
|
|
104
|
-
await userEvent.setup().hover( screen.getAllByTestId( 'heatmap-cell' )[ 0 ] );
|
|
105
|
-
await expect( screen.findByRole( 'tooltip' ) ).resolves.toBeInTheDocument();
|
|
106
|
-
const box = screen.getByTestId( 'bounded-tooltip' );
|
|
107
|
-
expect( box ).toHaveClass( 'surface' );
|
|
108
|
-
expect( box ).not.toHaveStyle( { backgroundColor: 'rgb(255, 255, 255)' } );
|
|
109
|
-
expect( box ).not.toHaveStyle( { color: 'rgb(102, 102, 102)' } );
|
|
110
|
-
} );
|
|
88
|
+
test.each( [ undefined, 'light', 'dark' ] as const )(
|
|
89
|
+
'draws the tooltip on the package surface for tooltipVariant %s',
|
|
90
|
+
async tooltipVariant => {
|
|
91
|
+
renderChart( { withTooltips: true, tooltipVariant, rowLabels: [ 'Mon', 'Tue', 'Wed' ] } );
|
|
92
|
+
await userEvent.setup().hover( screen.getAllByTestId( 'heatmap-cell' )[ 0 ] );
|
|
93
|
+
await expect( screen.findByRole( 'tooltip' ) ).resolves.toBeInTheDocument();
|
|
94
|
+
const box = screen.getByTestId( 'bounded-tooltip' );
|
|
95
|
+
expect( box ).toHaveClass( 'surface' );
|
|
96
|
+
expect( box ).not.toHaveAttribute( 'style', expect.stringContaining( 'background' ) );
|
|
97
|
+
expect( box ).toHaveStyle( { zIndex: 3 } );
|
|
98
|
+
}
|
|
99
|
+
);
|
|
111
100
|
|
|
112
|
-
test( 'merges tooltipStyle over the
|
|
101
|
+
test( 'merges tooltipStyle over the box styles', async () => {
|
|
113
102
|
renderChart( {
|
|
114
103
|
withTooltips: true,
|
|
115
|
-
tooltipVariant: 'dark',
|
|
116
104
|
tooltipStyle: { padding: '2px 4px' },
|
|
117
105
|
rowLabels: [ 'Mon', 'Tue', 'Wed' ],
|
|
118
106
|
} );
|
|
@@ -480,6 +468,122 @@ const mockRects = () =>
|
|
|
480
468
|
} as DOMRect;
|
|
481
469
|
} );
|
|
482
470
|
|
|
471
|
+
describe( 'HeatmapChart value text contrast', () => {
|
|
472
|
+
// Extent 0..100, so each value is its own intensity in hundredths.
|
|
473
|
+
const scale: HeatmapColumn[] = [
|
|
474
|
+
{ label: 'W1', data: [ { value: 0 }, { value: 85 }, { value: 100 } ] },
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
let injectedStyle: HTMLStyleElement | null = null;
|
|
478
|
+
|
|
479
|
+
afterEach( () => {
|
|
480
|
+
injectedStyle?.remove();
|
|
481
|
+
injectedStyle = null;
|
|
482
|
+
} );
|
|
483
|
+
|
|
484
|
+
const renderScale = ( className?: string ) =>
|
|
485
|
+
render(
|
|
486
|
+
<GlobalChartsProvider>
|
|
487
|
+
<HeatmapChart
|
|
488
|
+
width={ 500 }
|
|
489
|
+
height={ 300 }
|
|
490
|
+
data={ scale }
|
|
491
|
+
primaryColor="#3858e9"
|
|
492
|
+
className={ className }
|
|
493
|
+
/>
|
|
494
|
+
</GlobalChartsProvider>
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
test( 'picks the label role that reaches AA on light and dark cells', () => {
|
|
498
|
+
renderScale();
|
|
499
|
+
|
|
500
|
+
expect( screen.getByText( '0' ) ).toHaveClass( 'heatmap-chart__cell-value', { exact: true } );
|
|
501
|
+
expect( screen.getByText( '100' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
502
|
+
} );
|
|
503
|
+
|
|
504
|
+
test( 'falls back to black on a mid-tone cell where neither role reaches AA', () => {
|
|
505
|
+
renderScale();
|
|
506
|
+
|
|
507
|
+
expect( screen.getByText( '85' ) ).toHaveClass( 'heatmap-chart__cell-value--black' );
|
|
508
|
+
} );
|
|
509
|
+
|
|
510
|
+
test( 'reads the label roles once data arrives after an empty first render', () => {
|
|
511
|
+
const { rerender } = render(
|
|
512
|
+
<GlobalChartsProvider>
|
|
513
|
+
<HeatmapChart width={ 500 } height={ 300 } data={ [] } primaryColor="#3858e9" />
|
|
514
|
+
</GlobalChartsProvider>
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
rerender(
|
|
518
|
+
<GlobalChartsProvider>
|
|
519
|
+
<HeatmapChart width={ 500 } height={ 300 } data={ scale } primaryColor="#3858e9" />
|
|
520
|
+
</GlobalChartsProvider>
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
expect( screen.getByText( '100' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
524
|
+
} );
|
|
525
|
+
|
|
526
|
+
test( 'falls back to black or white when a label role uses syntax it cannot read', () => {
|
|
527
|
+
injectedStyle = document.createElement( 'style' );
|
|
528
|
+
injectedStyle.textContent =
|
|
529
|
+
'.modern-heatmap { --a8c-charts-color-label-inverse: rgb(255 255 255); }';
|
|
530
|
+
document.head.appendChild( injectedStyle );
|
|
531
|
+
|
|
532
|
+
renderScale( 'modern-heatmap' );
|
|
533
|
+
|
|
534
|
+
expect( screen.getByText( '100' ) ).toHaveClass( 'heatmap-chart__cell-value--white' );
|
|
535
|
+
} );
|
|
536
|
+
|
|
537
|
+
test( 'measures a translucent label role as it paints over each cell', () => {
|
|
538
|
+
injectedStyle = document.createElement( 'style' );
|
|
539
|
+
injectedStyle.textContent =
|
|
540
|
+
'.translucent-heatmap { --a8c-charts-color-label: rgba(30, 30, 30, 0.3); }';
|
|
541
|
+
document.head.appendChild( injectedStyle );
|
|
542
|
+
|
|
543
|
+
renderScale( 'translucent-heatmap' );
|
|
544
|
+
|
|
545
|
+
expect( screen.getByText( '0' ) ).toHaveClass( 'heatmap-chart__cell-value--black' );
|
|
546
|
+
expect( screen.getByText( '100' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
547
|
+
} );
|
|
548
|
+
|
|
549
|
+
test( 'leaves a summary value, which sits on no fill, on the default role', () => {
|
|
550
|
+
injectedStyle = document.createElement( 'style' );
|
|
551
|
+
injectedStyle.textContent =
|
|
552
|
+
'.swapped-heatmap { --a8c-charts-color-label: #f0f0f0; --a8c-charts-color-label-inverse: #1e1e1e; }';
|
|
553
|
+
document.head.appendChild( injectedStyle );
|
|
554
|
+
|
|
555
|
+
render(
|
|
556
|
+
<GlobalChartsProvider>
|
|
557
|
+
<HeatmapChart
|
|
558
|
+
width={ 500 }
|
|
559
|
+
height={ 300 }
|
|
560
|
+
className="swapped-heatmap"
|
|
561
|
+
primaryColor="#3858e9"
|
|
562
|
+
data={ [ ...scale, { label: 'Total', summary: true, data: [ { value: 7 } ] } ] }
|
|
563
|
+
/>
|
|
564
|
+
</GlobalChartsProvider>
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
expect( screen.getByText( '7' ) ).toHaveClass( 'heatmap-chart__cell-value', { exact: true } );
|
|
568
|
+
expect( screen.getByText( '0' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
569
|
+
} );
|
|
570
|
+
|
|
571
|
+
test( "keeps one color when both label roles are set to it on the chart's own class", () => {
|
|
572
|
+
injectedStyle = document.createElement( 'style' );
|
|
573
|
+
injectedStyle.textContent =
|
|
574
|
+
'.pinned-heatmap { --a8c-charts-color-label: #767676; --a8c-charts-color-label-inverse: #767676; }';
|
|
575
|
+
document.head.appendChild( injectedStyle );
|
|
576
|
+
|
|
577
|
+
renderScale( 'pinned-heatmap' );
|
|
578
|
+
|
|
579
|
+
[ '0', '85', '100' ].forEach( value =>
|
|
580
|
+
expect( screen.getByText( value ) ).toHaveClass( 'heatmap-chart__cell-value', {
|
|
581
|
+
exact: true,
|
|
582
|
+
} )
|
|
583
|
+
);
|
|
584
|
+
} );
|
|
585
|
+
} );
|
|
586
|
+
|
|
483
587
|
describe( 'HeatmapChart keyboard tooltip', () => {
|
|
484
588
|
test( 'opens on the selected cell without row labels', async () => {
|
|
485
589
|
renderChart( { withTooltips: true } );
|
|
@@ -107,13 +107,9 @@ export interface HeatmapChartProps extends Omit<
|
|
|
107
107
|
*/
|
|
108
108
|
primaryColor?: string;
|
|
109
109
|
renderTooltip?: ( data: HeatmapTooltipData ) => ReactNode;
|
|
110
|
-
/**
|
|
111
|
-
* The tooltip box: `light` is the plain white box, `dark` the package's
|
|
112
|
-
* tooltip surface, themed through `--a8c-charts-color-tooltip-surface`.
|
|
113
|
-
* Default `light`.
|
|
114
|
-
*/
|
|
110
|
+
/** @deprecated Every tooltip draws on the package tooltip surface; this prop has no effect. */
|
|
115
111
|
tooltipVariant?: 'light' | 'dark';
|
|
116
|
-
/** Inline styles merged onto the tooltip box, over the
|
|
112
|
+
/** Inline styles merged onto the tooltip box, over the surface's own. */
|
|
117
113
|
tooltipStyle?: CSSProperties;
|
|
118
114
|
children?: ReactNode;
|
|
119
115
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* istanbul ignore file */ // Compile-only: tsgo checks these assignments; nothing imports or runs them.
|
|
2
|
+
import type { LineChartProps } from './types';
|
|
3
|
+
|
|
4
|
+
type Crosshairs = NonNullable< LineChartProps[ 'withTooltipCrosshairs' ] >;
|
|
5
|
+
|
|
6
|
+
export const paintCrosshairs: Crosshairs = {
|
|
7
|
+
verticalStyle: { stroke: 'purple', strokeWidth: 2 },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const coordinateCrosshairs: Crosshairs = {
|
|
11
|
+
// @ts-expect-error Crosshair coordinates are computed from the active datum.
|
|
12
|
+
verticalStyle: { x1: 12 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const transformedCrosshairs: Crosshairs = {
|
|
16
|
+
// @ts-expect-error Crosshair transforms are not paint attributes.
|
|
17
|
+
verticalStyle: { transform: 'translate(100 0)' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const transformedStyles: Crosshairs = {
|
|
21
|
+
// @ts-expect-error Inline crosshair styles accept only paint properties.
|
|
22
|
+
horizontalStyle: { style: { transform: 'translateX(100px)' } },
|
|
23
|
+
};
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
overflow: visible;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
&__tooltip,
|
|
21
20
|
&__annotation-label-popover {
|
|
22
21
|
background: var(--a8c-charts-color-surface);
|
|
23
22
|
color: var(--a8c-charts-color-label);
|
|
@@ -29,10 +28,6 @@
|
|
|
29
28
|
padding-bottom: var(--wpds-dimension-padding-md);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
&__tooltip-row {
|
|
33
|
-
padding: var(--wpds-dimension-padding-xs) 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
&__tooltip-label {
|
|
37
32
|
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
38
33
|
padding-right: var(--wpds-dimension-padding-lg);
|