@automattic/charts 4.7.0 → 4.8.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 +6 -3
- package/dist/index.cjs +212 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +10 -9
- package/dist/index.d.cts +60 -20
- package/dist/index.d.ts +60 -20
- package/dist/index.js +212 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +2 -2
- package/src/charts/area-chart/test/area-chart.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +20 -14
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +25 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +6 -4
- package/src/charts/heatmap-chart/heatmap-chart.tsx +39 -29
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +2 -2
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +2 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +99 -7
- package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
- package/src/charts/heatmap-chart/types.ts +4 -3
- package/src/charts/line-chart/line-chart.tsx +2 -2
- package/src/charts/pie-chart/pie-chart.tsx +4 -4
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -4
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +1 -1
- package/src/charts/private/test/label-text-color.test.ts +6 -8
- package/src/components/tooltip/base-tooltip.tsx +31 -32
- package/src/components/tooltip/index.ts +3 -1
- package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
- package/src/components/tooltip/private/label-value-content.tsx +8 -0
- package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
- package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
- package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
- package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +282 -365
- package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
- package/src/components/tooltip/tooltip-box.tsx +43 -0
- package/src/components/tooltip/types.ts +7 -1
- package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
- package/src/hooks/test/use-chart-margin.test.tsx +8 -4
- package/src/hooks/use-chart-margin.tsx +21 -18
- package/src/index.ts +8 -3
- package/src/providers/chart-context/global-charts-provider.tsx +5 -14
- package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
- package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
- package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
- package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
- package/src/styles/chart-scope.scss +1 -0
- package/src/test-utils/zoned-parsing-suite.ts +0 -10
- package/src/utils/test/date-formatting.test.ts +0 -10
- package/src/components/tooltip/accessible-tooltip.tsx +0 -433
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
|
@@ -172,6 +172,26 @@ describe( 'HeatmapChart', () => {
|
|
|
172
172
|
).toHaveLength( 4 );
|
|
173
173
|
} );
|
|
174
174
|
|
|
175
|
+
test( 'paints the legend on the same scale as the cells', () => {
|
|
176
|
+
render(
|
|
177
|
+
<GlobalChartsProvider>
|
|
178
|
+
<HeatmapChart width={ 500 } height={ 300 } data={ data } primaryColor="#3858e9">
|
|
179
|
+
<HeatmapChart.Legend />
|
|
180
|
+
</HeatmapChart>
|
|
181
|
+
</GlobalChartsProvider>
|
|
182
|
+
);
|
|
183
|
+
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
184
|
+
|
|
185
|
+
for ( const swatch of screen.getAllByTestId( 'heatmap-legend-swatch' ) ) {
|
|
186
|
+
for ( const end of [ 'primary', 'low', 'high' ] ) {
|
|
187
|
+
const property = `--a8c-charts-color-heatmap-${ end }`;
|
|
188
|
+
expect( swatch.style.getPropertyValue( property ) ).toBe(
|
|
189
|
+
grid.style.getPropertyValue( property )
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} );
|
|
194
|
+
|
|
175
195
|
test( 'ArrowDown moves focus within a column, ArrowRight moves to next column', async () => {
|
|
176
196
|
renderChart( { rowLabels: [ 'Mon', 'Tue', 'Wed' ] } );
|
|
177
197
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
@@ -409,6 +429,78 @@ describe( 'HeatmapChart', () => {
|
|
|
409
429
|
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#abcdef' );
|
|
410
430
|
} );
|
|
411
431
|
|
|
432
|
+
test( 'leaves the fill to the stylesheet when the primary color cannot resolve to hex', () => {
|
|
433
|
+
render(
|
|
434
|
+
<GlobalChartsProvider>
|
|
435
|
+
<HeatmapChart width={ 500 } height={ 300 } data={ data } primaryColor="oklch(0.6 0.2 260)">
|
|
436
|
+
<HeatmapChart.Legend />
|
|
437
|
+
</HeatmapChart>
|
|
438
|
+
</GlobalChartsProvider>
|
|
439
|
+
);
|
|
440
|
+
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
441
|
+
const swatches = screen.getAllByTestId( 'heatmap-legend-swatch' );
|
|
442
|
+
|
|
443
|
+
for ( const element of [ grid, ...swatches ] ) {
|
|
444
|
+
expect( element.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe(
|
|
445
|
+
'oklch(0.6 0.2 260)'
|
|
446
|
+
);
|
|
447
|
+
expect( element.style.getPropertyValue( '--a8c-charts-color-heatmap-low' ) ).toBe( '' );
|
|
448
|
+
expect( element.style.getPropertyValue( '--a8c-charts-color-heatmap-high' ) ).toBe( '' );
|
|
449
|
+
}
|
|
450
|
+
expect( screen.getByText( '4' ) ).toHaveClass( 'heatmap-chart__cell-value', { exact: true } );
|
|
451
|
+
} );
|
|
452
|
+
|
|
453
|
+
test( 'builds the scale for white on a see-through background, as the palette does', () => {
|
|
454
|
+
const scope = document.createElement( 'div' );
|
|
455
|
+
scope.style.setProperty( '--a8c-charts-color-background', 'transparent' );
|
|
456
|
+
document.body.appendChild( scope );
|
|
457
|
+
|
|
458
|
+
render(
|
|
459
|
+
<GlobalChartsProvider>
|
|
460
|
+
<HeatmapChart width={ 500 } height={ 300 } data={ data } primaryColor="#3858e9" />
|
|
461
|
+
</GlobalChartsProvider>,
|
|
462
|
+
{ container: scope }
|
|
463
|
+
);
|
|
464
|
+
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
465
|
+
|
|
466
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-low' ) ).toBe( '#6c83ef' );
|
|
467
|
+
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-high' ) ).toBe( '#283ea5' );
|
|
468
|
+
|
|
469
|
+
document.body.removeChild( scope );
|
|
470
|
+
} );
|
|
471
|
+
|
|
472
|
+
test( 'reads the computed style once per re-render for the fill scale', () => {
|
|
473
|
+
const { rerender } = renderChart();
|
|
474
|
+
const spy = jest.spyOn( window, 'getComputedStyle' );
|
|
475
|
+
|
|
476
|
+
rerender(
|
|
477
|
+
<GlobalChartsProvider>
|
|
478
|
+
<HeatmapChart width={ 500 } height={ 300 } data={ data } />
|
|
479
|
+
</GlobalChartsProvider>
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
483
|
+
spy.mockRestore();
|
|
484
|
+
} );
|
|
485
|
+
|
|
486
|
+
test( 'paints a zero as an empty cell, still announcing its value', () => {
|
|
487
|
+
renderChart();
|
|
488
|
+
const zero = screen.getByRole( 'gridcell', { name: 'W2: 0' } );
|
|
489
|
+
|
|
490
|
+
expect( zero ).not.toHaveClass( 'heatmap-chart__cell--filled' );
|
|
491
|
+
expect( zero.style.getPropertyValue( '--a8c-charts-heatmap-cell-intensity' ) ).toBe( '' );
|
|
492
|
+
expect( screen.getByRole( 'gridcell', { name: 'W1: 1' } ) ).toHaveClass(
|
|
493
|
+
'heatmap-chart__cell--filled'
|
|
494
|
+
);
|
|
495
|
+
} );
|
|
496
|
+
|
|
497
|
+
test( 'puts the lowest non-zero value on the lowest step', () => {
|
|
498
|
+
renderChart();
|
|
499
|
+
const lowest = screen.getByRole( 'gridcell', { name: 'W1: 1' } );
|
|
500
|
+
|
|
501
|
+
expect( lowest.style.getPropertyValue( '--a8c-charts-heatmap-cell-intensity' ) ).toBe( '0' );
|
|
502
|
+
} );
|
|
503
|
+
|
|
412
504
|
test( 'falls back to the first palette slot when no primaryColor prop is set', () => {
|
|
413
505
|
const scope = document.createElement( 'div' );
|
|
414
506
|
scope.style.setProperty( '--a8c-charts-color-series-1', '#0a0b0c' );
|
|
@@ -469,9 +561,9 @@ const mockRects = () =>
|
|
|
469
561
|
} );
|
|
470
562
|
|
|
471
563
|
describe( 'HeatmapChart value text contrast', () => {
|
|
472
|
-
//
|
|
564
|
+
// The low end, a mid-tone, and the high end of the scale.
|
|
473
565
|
const scale: HeatmapColumn[] = [
|
|
474
|
-
{ label: 'W1', data: [ { value:
|
|
566
|
+
{ label: 'W1', data: [ { value: 1 }, { value: 30 }, { value: 100 } ] },
|
|
475
567
|
];
|
|
476
568
|
|
|
477
569
|
let injectedStyle: HTMLStyleElement | null = null;
|
|
@@ -497,14 +589,14 @@ describe( 'HeatmapChart value text contrast', () => {
|
|
|
497
589
|
test( 'picks the label role that reaches AA on light and dark cells', () => {
|
|
498
590
|
renderScale();
|
|
499
591
|
|
|
500
|
-
expect( screen.getByText( '
|
|
592
|
+
expect( screen.getByText( '1' ) ).toHaveClass( 'heatmap-chart__cell-value', { exact: true } );
|
|
501
593
|
expect( screen.getByText( '100' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
502
594
|
} );
|
|
503
595
|
|
|
504
596
|
test( 'falls back to black on a mid-tone cell where neither role reaches AA', () => {
|
|
505
597
|
renderScale();
|
|
506
598
|
|
|
507
|
-
expect( screen.getByText( '
|
|
599
|
+
expect( screen.getByText( '30' ) ).toHaveClass( 'heatmap-chart__cell-value--black' );
|
|
508
600
|
} );
|
|
509
601
|
|
|
510
602
|
test( 'reads the label roles once data arrives after an empty first render', () => {
|
|
@@ -542,7 +634,7 @@ describe( 'HeatmapChart value text contrast', () => {
|
|
|
542
634
|
|
|
543
635
|
renderScale( 'translucent-heatmap' );
|
|
544
636
|
|
|
545
|
-
expect( screen.getByText( '
|
|
637
|
+
expect( screen.getByText( '1' ) ).toHaveClass( 'heatmap-chart__cell-value--black' );
|
|
546
638
|
expect( screen.getByText( '100' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
547
639
|
} );
|
|
548
640
|
|
|
@@ -565,7 +657,7 @@ describe( 'HeatmapChart value text contrast', () => {
|
|
|
565
657
|
);
|
|
566
658
|
|
|
567
659
|
expect( screen.getByText( '7' ) ).toHaveClass( 'heatmap-chart__cell-value', { exact: true } );
|
|
568
|
-
expect( screen.getByText( '
|
|
660
|
+
expect( screen.getByText( '1' ) ).toHaveClass( 'heatmap-chart__cell-value--inverse' );
|
|
569
661
|
} );
|
|
570
662
|
|
|
571
663
|
test( "keeps one color when both label roles are set to it on the chart's own class", () => {
|
|
@@ -576,7 +668,7 @@ describe( 'HeatmapChart value text contrast', () => {
|
|
|
576
668
|
|
|
577
669
|
renderScale( 'pinned-heatmap' );
|
|
578
670
|
|
|
579
|
-
[ '
|
|
671
|
+
[ '1', '30', '100' ].forEach( value =>
|
|
580
672
|
expect( screen.getByText( value ) ).toHaveClass( 'heatmap-chart__cell-value', {
|
|
581
673
|
exact: true,
|
|
582
674
|
} )
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MIN_BACKGROUND_CONTRAST } from '../../../providers/chart-context/private/palette-generator';
|
|
2
|
+
import { contrastRatio } from '../../../providers/chart-context/private/perceptual-color';
|
|
3
|
+
import { mixHexColors } from '../../../utils/color-utils';
|
|
4
|
+
import { getHeatmapScale, HEATMAP_HIGH_CONTRAST } from '../private/heatmap-scale';
|
|
5
|
+
|
|
6
|
+
// The wp-admin scheme colors, then the pre-7.0 ones, which include a sunrise below 3:1 on white.
|
|
7
|
+
const PRIMARIES = [
|
|
8
|
+
'#007cba',
|
|
9
|
+
'#3858e9',
|
|
10
|
+
'#437aa8',
|
|
11
|
+
'#916745',
|
|
12
|
+
'#646c3e',
|
|
13
|
+
'#cf4339',
|
|
14
|
+
'#567958',
|
|
15
|
+
'#ad631e',
|
|
16
|
+
'#0085ba',
|
|
17
|
+
'#dd823b',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe.each( [
|
|
21
|
+
[ 'light', '#ffffff', '#f0f0f0' ],
|
|
22
|
+
[ 'dark', '#242424', '#272727' ],
|
|
23
|
+
] )( 'getHeatmapScale on a %s background', ( _mode, background, emptyCell ) => {
|
|
24
|
+
it.each( PRIMARIES )(
|
|
25
|
+
'keeps the lowest step of %s at 3:1 against the background and empty cell, no further',
|
|
26
|
+
primary => {
|
|
27
|
+
const { low } = getHeatmapScale( primary, background, emptyCell );
|
|
28
|
+
const contrasts = [ contrastRatio( low, background ), contrastRatio( low, emptyCell ) ];
|
|
29
|
+
|
|
30
|
+
expect( Math.min( ...contrasts ) ).toBeGreaterThanOrEqual( MIN_BACKGROUND_CONTRAST );
|
|
31
|
+
expect( Math.min( ...contrasts ) ).toBeLessThan( MIN_BACKGROUND_CONTRAST + 0.1 );
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
it.each( PRIMARIES )( 'runs the scale of %s out to 9:1', primary => {
|
|
36
|
+
const { high } = getHeatmapScale( primary, background );
|
|
37
|
+
|
|
38
|
+
expect( contrastRatio( high, background ) ).toBeGreaterThanOrEqual( HEATMAP_HIGH_CONTRAST );
|
|
39
|
+
} );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
describe( 'getHeatmapScale', () => {
|
|
43
|
+
it( 'keeps a primary already past 9:1 as the high end', () => {
|
|
44
|
+
expect( getHeatmapScale( '#1d2327', '#ffffff' ).high ).toBe( '#1d2327' );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
it.each( [
|
|
48
|
+
[ '#ffffff', '#808080' ],
|
|
49
|
+
[ '#ffd000', '#767676' ],
|
|
50
|
+
[ '#3858e9', '#808080' ],
|
|
51
|
+
[ '#1e1e1e', '#999999' ],
|
|
52
|
+
] )( 'keeps every step of %s at 3:1 on a mid-gray %s', ( primary, background ) => {
|
|
53
|
+
const { low, high } = getHeatmapScale( primary, background );
|
|
54
|
+
|
|
55
|
+
for ( let step = 0; step <= 20; step++ ) {
|
|
56
|
+
expect(
|
|
57
|
+
contrastRatio( mixHexColors( low, high, step / 20 ), background )
|
|
58
|
+
).toBeGreaterThanOrEqual( MIN_BACKGROUND_CONTRAST );
|
|
59
|
+
}
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it.each( [
|
|
63
|
+
[ 'no step clears it', '#ffffff', '#555555' ],
|
|
64
|
+
[ 'clearing it would pass the high end', '#ffffff', '#767676' ],
|
|
65
|
+
[ 'clearing it would pass the high end', '#1e1e1e', '#6b6b6b' ],
|
|
66
|
+
] )(
|
|
67
|
+
'measures against the background alone when %s (%s, %s)',
|
|
68
|
+
( _case, background, emptyCell ) => {
|
|
69
|
+
expect( getHeatmapScale( '#3858e9', background, emptyCell ) ).toEqual(
|
|
70
|
+
getHeatmapScale( '#3858e9', background )
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it( 'ends on black or white when the background leaves no room for 9:1', () => {
|
|
76
|
+
expect( getHeatmapScale( '#3858e9', '#808080' ).high ).toBe( '#000000' );
|
|
77
|
+
} );
|
|
78
|
+
} );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getValueExtent, getNormalizedValue } from '../private/use-heatmap-colors';
|
|
1
|
+
import { getValueExtent, getNormalizedValue, isEmptyValue } from '../private/use-heatmap-colors';
|
|
2
2
|
import type { HeatmapColumn } from '../types';
|
|
3
3
|
|
|
4
4
|
const data: HeatmapColumn[] = [
|
|
@@ -7,8 +7,12 @@ const data: HeatmapColumn[] = [
|
|
|
7
7
|
];
|
|
8
8
|
|
|
9
9
|
describe( 'getValueExtent', () => {
|
|
10
|
-
test( 'returns [min, max] ignoring null/NaN', () => {
|
|
11
|
-
expect( getValueExtent( data ) ).toEqual( [
|
|
10
|
+
test( 'returns [min, max] ignoring null/NaN and empty zeros', () => {
|
|
11
|
+
expect( getValueExtent( data ) ).toEqual( [ 5, 20 ] );
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
test( 'keeps a zero in the extent when the data has negatives', () => {
|
|
15
|
+
expect( getValueExtent( [ { data: [ { value: -4 }, { value: 0 } ] } ] ) ).toEqual( [ -4, 0 ] );
|
|
12
16
|
} );
|
|
13
17
|
|
|
14
18
|
test( 'leaves summary columns out of the extent', () => {
|
|
@@ -16,11 +20,12 @@ describe( 'getValueExtent', () => {
|
|
|
16
20
|
...data,
|
|
17
21
|
{ label: 'Total', summary: true, data: [ { value: 500 }, { value: 900 }, { value: null } ] },
|
|
18
22
|
];
|
|
19
|
-
expect( getValueExtent( withTotals ) ).toEqual( [
|
|
23
|
+
expect( getValueExtent( withTotals ) ).toEqual( [ 5, 20 ] );
|
|
20
24
|
} );
|
|
21
25
|
|
|
22
26
|
test( 'returns [0, 0] for all-empty data', () => {
|
|
23
27
|
expect( getValueExtent( [ { data: [ { value: null } ] } ] ) ).toEqual( [ 0, 0 ] );
|
|
28
|
+
expect( getValueExtent( [ { data: [ { value: 0 }, { value: 0 } ] } ] ) ).toEqual( [ 0, 0 ] );
|
|
24
29
|
} );
|
|
25
30
|
} );
|
|
26
31
|
|
|
@@ -39,8 +44,15 @@ describe( 'getNormalizedValue', () => {
|
|
|
39
44
|
test( 'returns 1 when min === max', () => {
|
|
40
45
|
expect( getNormalizedValue( 7, [ 7, 7 ] ) ).toBe( 1 );
|
|
41
46
|
} );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
describe( 'isEmptyValue', () => {
|
|
50
|
+
test( 'paints a zero as empty in data with no negatives', () => {
|
|
51
|
+
expect( isEmptyValue( 0, [ 0, 20 ] ) ).toBe( true );
|
|
52
|
+
expect( isEmptyValue( 5, [ 0, 20 ] ) ).toBe( false );
|
|
53
|
+
} );
|
|
42
54
|
|
|
43
|
-
test( '
|
|
44
|
-
expect(
|
|
55
|
+
test( 'keeps a zero on the scale when the data has negatives', () => {
|
|
56
|
+
expect( isEmptyValue( 0, [ -4, 6 ] ) ).toBe( false );
|
|
45
57
|
} );
|
|
46
58
|
} );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BaseChartProps } from '../../types';
|
|
2
2
|
import type { CSSProperties, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
/** A single heatmap cell. `value: null` marks an empty cell. */
|
|
4
|
+
/** A single heatmap cell. `value: null` marks an empty cell, as does a zero when no value is negative. */
|
|
5
5
|
export type HeatmapCell = {
|
|
6
6
|
/** Per-cell label used in the tooltip / accessible name. */
|
|
7
7
|
label?: string;
|
|
@@ -102,8 +102,9 @@ export interface HeatmapChartProps extends Omit<
|
|
|
102
102
|
/** Floor a cell's height (px) in non-compact mode; see `minCellWidth`. */
|
|
103
103
|
minCellHeight?: number;
|
|
104
104
|
/**
|
|
105
|
-
* Color the cell scale
|
|
106
|
-
* first series palette slot,
|
|
105
|
+
* Color the cell scale is built from, deepened at the highest value toward 9:1
|
|
106
|
+
* against the background. Defaults to the first series palette slot,
|
|
107
|
+
* `--a8c-charts-color-series-1`.
|
|
107
108
|
*/
|
|
108
109
|
primaryColor?: string;
|
|
109
110
|
renderTooltip?: ( data: HeatmapTooltipData ) => ReactNode;
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
useCallback,
|
|
16
16
|
} from 'react';
|
|
17
17
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
18
|
-
import {
|
|
18
|
+
import { XYChartTooltip, useKeyboardNavigation } from '../../components/tooltip';
|
|
19
19
|
import {
|
|
20
20
|
useXYChartTheme,
|
|
21
21
|
useChartDataTransform,
|
|
@@ -784,7 +784,7 @@ const LineChartInternal = forwardRef< ChartInstanceRef, LineChartProps >(
|
|
|
784
784
|
</ZoomClip>
|
|
785
785
|
|
|
786
786
|
{ withTooltips && (
|
|
787
|
-
<
|
|
787
|
+
<XYChartTooltip
|
|
788
788
|
detectBounds
|
|
789
789
|
snapTooltipToDatumX
|
|
790
790
|
tooltipPlacement={ tooltipPlacement }
|
|
@@ -6,8 +6,8 @@ import clsx from 'clsx';
|
|
|
6
6
|
import isEqual from 'fast-deep-equal';
|
|
7
7
|
import { useCallback, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
9
|
-
import { BaseTooltip } from '../../components/tooltip';
|
|
10
9
|
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
10
|
+
import { LabelValueContent } from '../../components/tooltip/private/label-value-content';
|
|
11
11
|
import {
|
|
12
12
|
useDataWithPercentages,
|
|
13
13
|
useLegendVisibilityData,
|
|
@@ -57,13 +57,13 @@ export type PieChartRenderTooltipParams = {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Default tooltip renderer for pie charts.
|
|
60
|
-
* Renders
|
|
60
|
+
* Renders the default `label: value` tooltip content for the hovered segment.
|
|
61
61
|
*
|
|
62
62
|
* @param {PieChartRenderTooltipParams} params - The tooltip parameters containing the hovered data point
|
|
63
63
|
* @return {ReactNode} The rendered tooltip content
|
|
64
64
|
*/
|
|
65
65
|
const renderDefaultPieTooltip = ( { tooltipData }: PieChartRenderTooltipParams ): ReactNode => {
|
|
66
|
-
return <
|
|
66
|
+
return <LabelValueContent data={ tooltipData } />;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
|
|
@@ -127,7 +127,7 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > {
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Custom render function for tooltip content.
|
|
130
|
-
* When provided, replaces the default
|
|
130
|
+
* When provided, replaces the default `label: value` tooltip with custom content.
|
|
131
131
|
*/
|
|
132
132
|
renderTooltip?: ( params: PieChartRenderTooltipParams ) => ReactNode;
|
|
133
133
|
}
|
|
@@ -5,8 +5,8 @@ import { useTooltip } from '@visx/tooltip';
|
|
|
5
5
|
import clsx from 'clsx';
|
|
6
6
|
import { useCallback, useContext, useMemo, useRef } from 'react';
|
|
7
7
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
8
|
-
import { BaseTooltip } from '../../components/tooltip';
|
|
9
8
|
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
9
|
+
import { LabelValueContent } from '../../components/tooltip/private/label-value-content';
|
|
10
10
|
import {
|
|
11
11
|
useDataWithPercentages,
|
|
12
12
|
useLegendVisibilityData,
|
|
@@ -53,7 +53,7 @@ export type PieSemiCircleChartRenderTooltipParams = {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Default tooltip renderer for semi-circle pie charts.
|
|
56
|
-
* Renders
|
|
56
|
+
* Renders the default `label: value` tooltip content for the hovered segment.
|
|
57
57
|
*
|
|
58
58
|
* @param {PieSemiCircleChartRenderTooltipParams} params - The tooltip parameters containing the hovered data point
|
|
59
59
|
* @return {ReactNode} The rendered tooltip content
|
|
@@ -61,7 +61,7 @@ export type PieSemiCircleChartRenderTooltipParams = {
|
|
|
61
61
|
const renderDefaultPieSemiCircleTooltip = ( {
|
|
62
62
|
tooltipData,
|
|
63
63
|
}: PieSemiCircleChartRenderTooltipParams ): ReactNode => {
|
|
64
|
-
return <
|
|
64
|
+
return <LabelValueContent data={ tooltipData } />;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
const PAD_ANGLE = 0.03; // Padding between segments
|
|
@@ -122,7 +122,7 @@ export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercen
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Custom render function for tooltip content.
|
|
125
|
-
* When provided, replaces the default
|
|
125
|
+
* When provided, replaces the default `label: value` tooltip with custom content.
|
|
126
126
|
*/
|
|
127
127
|
renderTooltip?: ( params: PieSemiCircleChartRenderTooltipParams ) => ReactNode;
|
|
128
128
|
}
|
|
@@ -96,7 +96,7 @@ describe( 'PieSemiCircleChart', () => {
|
|
|
96
96
|
|
|
97
97
|
await user.hover( firstSegment );
|
|
98
98
|
|
|
99
|
-
// Wait for tooltip to be visible - it should show
|
|
99
|
+
// Wait for tooltip to be visible - it should show the default label: value content
|
|
100
100
|
const tooltip = await screen.findByRole( 'tooltip' );
|
|
101
101
|
expect( tooltip ).toHaveTextContent( 'MacOS' );
|
|
102
102
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
luminanceContrastRatio,
|
|
6
6
|
rgbLuminance,
|
|
7
7
|
} from '../../../providers/chart-context/private/perceptual-color';
|
|
8
|
+
import { getHeatmapScale } from '../../heatmap-chart/private/heatmap-scale';
|
|
8
9
|
import {
|
|
9
10
|
pickLabelTextColor,
|
|
10
11
|
pickLabelTextColorForFill,
|
|
@@ -131,9 +132,9 @@ describe( 'pickLabelTextColorForFill', () => {
|
|
|
131
132
|
} );
|
|
132
133
|
|
|
133
134
|
describe.each( [
|
|
134
|
-
[ 'white', '#ffffff' ],
|
|
135
|
-
[ 'dark', '#
|
|
136
|
-
] )( 'across a heatmap scale on a %s background', ( _name, background ) => {
|
|
135
|
+
[ 'white', '#ffffff', '#f0f0f0' ],
|
|
136
|
+
[ 'dark', '#242424', '#272727' ],
|
|
137
|
+
] )( 'across a heatmap scale on a %s background', ( _name, background, emptyCell ) => {
|
|
137
138
|
const painted: Record< LabelTextColor, number > = {
|
|
138
139
|
label: rgbLuminance( hexToRgb( '#1e1e1e' ) ),
|
|
139
140
|
'label-inverse': rgbLuminance( hexToRgb( '#f0f0f0' ) ),
|
|
@@ -155,12 +156,9 @@ describe( 'pickLabelTextColorForFill', () => {
|
|
|
155
156
|
];
|
|
156
157
|
|
|
157
158
|
it.each( primaries )( 'reaches AA on every intensity of %s', primary => {
|
|
159
|
+
const { low, high } = getHeatmapScale( primary, background, emptyCell );
|
|
158
160
|
for ( let step = 0; step <= 200; step++ ) {
|
|
159
|
-
const fill = blendRgb(
|
|
160
|
-
hexToRgb( primary ),
|
|
161
|
-
hexToRgb( background ),
|
|
162
|
-
0.15 + 0.85 * ( step / 200 )
|
|
163
|
-
);
|
|
161
|
+
const fill = blendRgb( hexToRgb( high ), hexToRgb( low ), step / 200 );
|
|
164
162
|
const choice = pickLabelTextColorForFill( fill, DEFAULT_ROLES, 'label' );
|
|
165
163
|
// Checked on the 8-bit channels the browser paints as well as the exact mix.
|
|
166
164
|
const [ r, g, b ] = fill.map( Math.round );
|
|
@@ -1,36 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import styles from './base-tooltip.module.scss';
|
|
5
|
-
import { TooltipTheme } from './private/tooltip-theme';
|
|
1
|
+
import { LabelValueContent } from './private/label-value-content';
|
|
2
|
+
import { TooltipBox } from './tooltip-box';
|
|
3
|
+
import type { TooltipData } from './types';
|
|
6
4
|
import type { CSSProperties, ComponentType, ReactNode } from 'react';
|
|
7
5
|
|
|
8
|
-
type TooltipData = {
|
|
9
|
-
label: string;
|
|
10
|
-
value: number;
|
|
11
|
-
valueDisplay?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
6
|
type TooltipComponentProps = {
|
|
15
7
|
data: TooltipData;
|
|
16
8
|
className?: string;
|
|
17
9
|
};
|
|
18
10
|
|
|
19
11
|
type TooltipCommonProps = {
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
/** @deprecated Position the box with `style`, or use `TooltipBox`. */
|
|
13
|
+
top?: number;
|
|
14
|
+
/** @deprecated Position the box with `style`, or use `TooltipBox`. */
|
|
15
|
+
left?: number;
|
|
22
16
|
style?: CSSProperties;
|
|
23
17
|
className?: string;
|
|
24
18
|
/**
|
|
25
19
|
* Whether to render the tooltip container div. When false, only renders the content.
|
|
26
|
-
*
|
|
20
|
+
* @deprecated Render the content directly; `TooltipBox` draws the box.
|
|
27
21
|
* @default true
|
|
28
22
|
*/
|
|
29
23
|
renderContainer?: boolean;
|
|
30
24
|
};
|
|
31
25
|
|
|
32
26
|
type DefaultDataTooltip = {
|
|
27
|
+
/** @deprecated Pass the content as children, or use `TooltipBox`. */
|
|
33
28
|
data: TooltipData;
|
|
29
|
+
/** @deprecated Pass the content as children, or use `TooltipBox`. */
|
|
34
30
|
component?: ComponentType< TooltipComponentProps >;
|
|
35
31
|
children?: never;
|
|
36
32
|
};
|
|
@@ -43,17 +39,25 @@ type CustomTooltip = {
|
|
|
43
39
|
|
|
44
40
|
type BaseTooltipProps = TooltipCommonProps & ( DefaultDataTooltip | CustomTooltip );
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
/**
|
|
43
|
+
* A tooltip box with children or `label: value` content. Prefer `TooltipBox`.
|
|
44
|
+
*
|
|
45
|
+
* @param props - Tooltip props.
|
|
46
|
+
* @param props.data - Data for the default content.
|
|
47
|
+
* @param props.top - Deprecated vertical position.
|
|
48
|
+
* @param props.left - Deprecated horizontal position.
|
|
49
|
+
* @param props.component - Deprecated content component.
|
|
50
|
+
* @param props.children - Tooltip content.
|
|
51
|
+
* @param props.className - Class name passed to the content component.
|
|
52
|
+
* @param props.style - Styles for the box.
|
|
53
|
+
* @param props.renderContainer - Deprecated; render the content alone when false.
|
|
54
|
+
* @return The tooltip.
|
|
55
|
+
*/
|
|
52
56
|
export const BaseTooltip = ( {
|
|
53
57
|
data,
|
|
54
58
|
top,
|
|
55
59
|
left,
|
|
56
|
-
component: Component =
|
|
60
|
+
component: Component = LabelValueContent,
|
|
57
61
|
children,
|
|
58
62
|
className,
|
|
59
63
|
style,
|
|
@@ -65,17 +69,12 @@ export const BaseTooltip = ( {
|
|
|
65
69
|
return content;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
>
|
|
75
|
-
{ content }
|
|
76
|
-
</div>
|
|
77
|
-
</TooltipTheme>
|
|
78
|
-
);
|
|
72
|
+
const placement =
|
|
73
|
+
top === undefined && left === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: ( { position: 'absolute', top, left, transform: 'translate(-50%, -100%)' } as const );
|
|
76
|
+
|
|
77
|
+
return <TooltipBox style={ { ...placement, ...style } }>{ content }</TooltipBox>;
|
|
79
78
|
};
|
|
80
79
|
|
|
81
80
|
export type { BaseTooltipProps, TooltipData };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { BaseTooltip } from './base-tooltip';
|
|
2
|
-
export { AccessibleTooltip, useKeyboardNavigation } from './
|
|
2
|
+
export { AccessibleTooltip, XYChartTooltip, useKeyboardNavigation } from './xy-chart-tooltip';
|
|
3
|
+
export { TooltipBox } from './tooltip-box';
|
|
3
4
|
export type { BaseTooltipProps, TooltipData } from './base-tooltip';
|
|
5
|
+
export type { TooltipBoxProps } from './tooltip-box';
|
|
4
6
|
export type { TooltipProps } from './types';
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { Tooltip } from '@visx/tooltip';
|
|
2
|
-
import clsx from 'clsx';
|
|
3
1
|
import { useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import styles from '../base-tooltip.module.scss';
|
|
6
|
-
import { TooltipTheme } from './tooltip-theme';
|
|
2
|
+
import { TooltipBox } from '../tooltip-box';
|
|
7
3
|
import type { TooltipPlacement } from '../../../visx/types';
|
|
8
|
-
import type {
|
|
4
|
+
import type { TooltipBoxProps } from '../tooltip-box';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
9
6
|
|
|
10
7
|
type Bounds = { left: number; top: number; right: number; bottom: number };
|
|
11
8
|
type Size = { width: number; height: number };
|
|
12
9
|
|
|
13
|
-
export type BoundedTooltipProps = Omit<
|
|
10
|
+
export type BoundedTooltipProps = Omit< TooltipBoxProps, 'children' > & {
|
|
11
|
+
children?: ReactNode;
|
|
14
12
|
/** Anchor, in the coordinates of the positioned wrapper the box renders into. */
|
|
15
13
|
left?: number;
|
|
16
14
|
top?: number;
|
|
15
|
+
offsetLeft?: number;
|
|
16
|
+
offsetTop?: number;
|
|
17
17
|
placement?: TooltipPlacement;
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -135,7 +135,7 @@ export const getBoundedPosition = ( {
|
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
138
|
+
* A `TooltipBox`, positioned like visx's `TooltipWithBounds` but kept inside the
|
|
139
139
|
* nearest clipping ancestor — or the viewport when there is none — rather than
|
|
140
140
|
* inside its own parent. Rendered in-tree, a tooltip's parent is the chart
|
|
141
141
|
* wrapper, which is often narrower than the box; measuring against the parent
|
|
@@ -144,7 +144,7 @@ export const getBoundedPosition = ( {
|
|
|
144
144
|
* Re-measures on every render, so a box whose content changes width between
|
|
145
145
|
* two hovers is placed for its current size.
|
|
146
146
|
*
|
|
147
|
-
* @param props -
|
|
147
|
+
* @param props - `TooltipBox` props plus placement.
|
|
148
148
|
* @param props.left - Anchor x, in wrapper coordinates.
|
|
149
149
|
* @param props.top - Anchor y, in wrapper coordinates.
|
|
150
150
|
* @param props.offsetLeft - Gap between the anchor and the box, horizontally.
|
|
@@ -213,11 +213,13 @@ export const BoundedTooltip = ( {
|
|
|
213
213
|
const x = position?.x ?? left + offsetLeft;
|
|
214
214
|
const y = position?.y ?? top + ( placement === 'below-axis' ? POINTER_HEIGHT : offsetTop );
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
<
|
|
216
|
+
return (
|
|
217
|
+
<TooltipBox
|
|
218
218
|
ref={ nodeRef }
|
|
219
|
+
role="presentation"
|
|
219
220
|
data-testid="bounded-tooltip"
|
|
220
|
-
|
|
221
|
+
unstyled={ unstyled }
|
|
222
|
+
className={ className }
|
|
221
223
|
style={ {
|
|
222
224
|
position: 'absolute',
|
|
223
225
|
left: 0,
|
|
@@ -245,8 +247,6 @@ export const BoundedTooltip = ( {
|
|
|
245
247
|
/>
|
|
246
248
|
) }
|
|
247
249
|
{ children }
|
|
248
|
-
</
|
|
250
|
+
</TooltipBox>
|
|
249
251
|
);
|
|
250
|
-
|
|
251
|
-
return unstyled ? box : <TooltipTheme>{ box }</TooltipTheme>;
|
|
252
252
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { formatNumber } from '@automattic/number-formatters';
|
|
2
|
+
import type { TooltipData } from '../types';
|
|
3
|
+
|
|
4
|
+
export const LabelValueContent = ( { data }: { data: TooltipData } ) => (
|
|
5
|
+
<>
|
|
6
|
+
{ data.label }: { data.valueDisplay || formatNumber( data.value ) }
|
|
7
|
+
</>
|
|
8
|
+
);
|