@automattic/charts 4.6.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 +16 -0
- package/TOKENS.md +12 -10
- package/dist/index.cjs +244 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +33 -20
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +245 -146
- 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/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/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
|
}
|
|
@@ -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);
|
|
@@ -66,7 +66,7 @@ import type { RenderTooltipParams } from '../../visx/types';
|
|
|
66
66
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
67
67
|
import type { TickFormatter } from '@visx/axis';
|
|
68
68
|
import type { GlyphProps } from '@visx/xychart';
|
|
69
|
-
import type {
|
|
69
|
+
import type { FC, Ref } from 'react';
|
|
70
70
|
|
|
71
71
|
const defaultRenderGlyph = < Datum extends object >( props: RenderLineGlyphProps< Datum > ) => {
|
|
72
72
|
return <DefaultGlyph { ...props } key={ props.key } />;
|
|
@@ -116,13 +116,11 @@ const TooltipDate: FC< { date?: Date; displayResolution: Exclude< TickResolution
|
|
|
116
116
|
* one row per visible series (label + formatted value), sorted descending by
|
|
117
117
|
* value. Reused by AreaChart, which has the same multi-series shape.
|
|
118
118
|
*
|
|
119
|
-
* @param params
|
|
120
|
-
* @param contentStyle - Explicit tooltip content color overrides.
|
|
119
|
+
* @param params - visx tooltip data and the chart's optional `bucketInfo`.
|
|
121
120
|
* @return Tooltip JSX, or `null` when no datum is hovered.
|
|
122
121
|
*/
|
|
123
122
|
export const renderDefaultTooltip = (
|
|
124
|
-
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo }
|
|
125
|
-
contentStyle?: Pick< CSSProperties, 'color' | 'background' | 'backgroundColor' >
|
|
123
|
+
params: RenderTooltipParams< DataPointDate > & { bucketInfo?: BucketInfo }
|
|
126
124
|
) => {
|
|
127
125
|
const { tooltipData, bucketInfo } = params;
|
|
128
126
|
const nearestDatum = tooltipData?.nearestDatum?.datum;
|
|
@@ -141,11 +139,7 @@ export const renderDefaultTooltip = (
|
|
|
141
139
|
} );
|
|
142
140
|
|
|
143
141
|
return (
|
|
144
|
-
<div
|
|
145
|
-
className={ styles[ 'line-chart__tooltip' ] }
|
|
146
|
-
data-testid="line-chart-tooltip-content"
|
|
147
|
-
style={ contentStyle }
|
|
148
|
-
>
|
|
142
|
+
<div data-testid="line-chart-tooltip-content">
|
|
149
143
|
<div className={ styles[ 'line-chart__tooltip-date' ] }>
|
|
150
144
|
<TooltipDate
|
|
151
145
|
date={ nearestDatum.date }
|
|
@@ -153,13 +147,7 @@ export const renderDefaultTooltip = (
|
|
|
153
147
|
/>
|
|
154
148
|
</div>
|
|
155
149
|
{ tooltipPoints.map( point => (
|
|
156
|
-
<Stack
|
|
157
|
-
key={ point.key }
|
|
158
|
-
direction="row"
|
|
159
|
-
align="center"
|
|
160
|
-
justify="space-between"
|
|
161
|
-
className={ styles[ 'line-chart__tooltip-row' ] }
|
|
162
|
-
>
|
|
150
|
+
<Stack key={ point.key } direction="row" align="center" justify="space-between">
|
|
163
151
|
<span className={ styles[ 'line-chart__tooltip-label' ] }>{ point.key }:</span>
|
|
164
152
|
<span className={ styles[ 'line-chart__tooltip-value' ] }>
|
|
165
153
|
{ formatReading( point.value ) }
|
|
@@ -577,32 +565,12 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
577
565
|
yAccessor: ( d: DataPointDate ) => d?.value,
|
|
578
566
|
};
|
|
579
567
|
|
|
580
|
-
const resolvedTooltipStyle = useMemo( () => {
|
|
581
|
-
if ( renderTooltip !== renderDefaultTooltip || ! tooltipStyle ) return tooltipStyle;
|
|
582
|
-
if ( ! tooltipStyle.color || tooltipStyle.background || tooltipStyle.backgroundColor ) {
|
|
583
|
-
return tooltipStyle;
|
|
584
|
-
}
|
|
585
|
-
return {
|
|
586
|
-
backgroundColor: 'var(--a8c-charts-color-tooltip-surface, rgb(0 0 0 / 85%))',
|
|
587
|
-
...tooltipStyle,
|
|
588
|
-
};
|
|
589
|
-
}, [ renderTooltip, tooltipStyle ] );
|
|
590
|
-
|
|
591
568
|
// Augments every renderTooltip call with the chart's bucket classification,
|
|
592
569
|
// default or custom, so a heading keyed on it can't disagree with the axis.
|
|
593
570
|
const tooltipRenderer = useMemo(
|
|
594
571
|
() => ( params: RenderTooltipParams< DataPointDate > ) =>
|
|
595
|
-
renderTooltip
|
|
596
|
-
|
|
597
|
-
{ ...params, bucketInfo },
|
|
598
|
-
{
|
|
599
|
-
color: tooltipStyle?.color,
|
|
600
|
-
background: tooltipStyle?.background,
|
|
601
|
-
backgroundColor: tooltipStyle?.backgroundColor,
|
|
602
|
-
}
|
|
603
|
-
)
|
|
604
|
-
: renderTooltip( { ...params, bucketInfo } ),
|
|
605
|
-
[ renderTooltip, bucketInfo, tooltipStyle ]
|
|
572
|
+
renderTooltip( { ...params, bucketInfo } ),
|
|
573
|
+
[ renderTooltip, bucketInfo ]
|
|
606
574
|
);
|
|
607
575
|
|
|
608
576
|
if ( error ) {
|
|
@@ -820,7 +788,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
820
788
|
detectBounds
|
|
821
789
|
snapTooltipToDatumX
|
|
822
790
|
tooltipPlacement={ tooltipPlacement }
|
|
823
|
-
style={
|
|
791
|
+
style={ tooltipStyle }
|
|
824
792
|
snapTooltipToDatumY
|
|
825
793
|
showSeriesGlyphs
|
|
826
794
|
renderTooltip={ tooltipRenderer }
|