@automattic/charts 3.2.0 → 4.0.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 +29 -0
- package/TOKENS.md +53 -58
- package/dist/index.cjs +1012 -542
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +72 -29
- package/dist/index.d.cts +151 -179
- package/dist/index.d.ts +151 -179
- package/dist/index.js +1013 -544
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +2 -1
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
- package/src/charts/heatmap-chart/types.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +93 -24
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +27 -3
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/types.ts +4 -1
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +13 -15
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +2 -3
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
- package/src/hooks/use-xychart-theme.ts +25 -9
- package/src/index.ts +4 -0
- package/src/providers/chart-context/global-charts-provider.tsx +37 -38
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +3 -13
- package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
- package/src/providers/chart-context/themes.ts +7 -52
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +39 -62
- package/src/styles/test/chart-scope.test.ts +19 -47
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/types.ts +79 -166
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +174 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/visx/types.ts +29 -2
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
|
@@ -473,3 +473,49 @@ describe( 'ConversionFunnelChart', () => {
|
|
|
473
473
|
} );
|
|
474
474
|
} );
|
|
475
475
|
} );
|
|
476
|
+
|
|
477
|
+
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
478
|
+
// else the charts measure (wrapper, clipping lookups) reports the container.
|
|
479
|
+
const mockRects = () =>
|
|
480
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
481
|
+
this: Element
|
|
482
|
+
) {
|
|
483
|
+
const box = this.classList.contains( 'visx-tooltip' );
|
|
484
|
+
return {
|
|
485
|
+
left: box ? 0 : 100,
|
|
486
|
+
top: box ? 0 : 50,
|
|
487
|
+
width: box ? 120 : 400,
|
|
488
|
+
height: box ? 40 : 300,
|
|
489
|
+
right: box ? 120 : 500,
|
|
490
|
+
bottom: box ? 40 : 350,
|
|
491
|
+
x: box ? 0 : 100,
|
|
492
|
+
y: box ? 0 : 50,
|
|
493
|
+
toJSON: () => ( {} ),
|
|
494
|
+
} as DOMRect;
|
|
495
|
+
} );
|
|
496
|
+
|
|
497
|
+
describe( 'ConversionFunnelChart tooltip position', () => {
|
|
498
|
+
afterEach( () => {
|
|
499
|
+
jest.restoreAllMocks();
|
|
500
|
+
} );
|
|
501
|
+
|
|
502
|
+
it( 'places the box relative to the chart root, at the click plus the offsets', async () => {
|
|
503
|
+
mockRects();
|
|
504
|
+
const user = userEvent.setup();
|
|
505
|
+
renderWithoutTheme( <ConversionFunnelChart { ...defaultProps } /> );
|
|
506
|
+
|
|
507
|
+
const bar = screen.getAllByRole( 'button' )[ 0 ];
|
|
508
|
+
await user.pointer( [
|
|
509
|
+
{ target: bar, coords: { clientX: 180, clientY: 140 } },
|
|
510
|
+
{ keys: '[MouseLeft]', target: bar, coords: { clientX: 180, clientY: 140 } },
|
|
511
|
+
] );
|
|
512
|
+
|
|
513
|
+
// (180, 140) is (80, 90) inside the root; the chart lifts the anchor 10px
|
|
514
|
+
// above the click, and the box adds 10px each way.
|
|
515
|
+
const box = screen.getByTestId( 'bounded-tooltip' );
|
|
516
|
+
expect( screen.getByTestId( 'conversion-funnel-chart' ) ).toContainElement( box );
|
|
517
|
+
expect( box ).toHaveStyle( {
|
|
518
|
+
transform: 'translate(90px, 90px)',
|
|
519
|
+
} );
|
|
520
|
+
} );
|
|
521
|
+
} );
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useChartScopeElement,
|
|
15
15
|
useGlobalChartsContext,
|
|
16
16
|
} from '../../providers';
|
|
17
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
17
18
|
import { lightenHexColor, normalizeColorToHex } from '../../utils/color-utils';
|
|
18
19
|
import { resolveCssVariable } from '../../utils/resolve-css-var';
|
|
19
20
|
import { sanitizeHtml } from '../../utils/sanitize-html';
|
|
@@ -144,13 +145,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
144
145
|
onError,
|
|
145
146
|
renderPlaceholder,
|
|
146
147
|
} ) => {
|
|
147
|
-
const {
|
|
148
|
-
getElementStyles,
|
|
149
|
-
theme: {
|
|
150
|
-
geoChart: { featureFillColor },
|
|
151
|
-
backgroundColor,
|
|
152
|
-
},
|
|
153
|
-
} = useGlobalChartsContext();
|
|
148
|
+
const { getElementStyles } = useGlobalChartsContext();
|
|
154
149
|
const scopeElement = useChartScopeElement();
|
|
155
150
|
const containerRef = useRef< HTMLDivElement >( null );
|
|
156
151
|
const reportedErrorIdsRef = useRef< Set< string > >( new Set() );
|
|
@@ -212,10 +207,10 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
212
207
|
const lightColorHex = lightenHexColor( fullColorHex, 0.8 );
|
|
213
208
|
// Use normalizeColorToHex to ensure HSL/RGB values from CSS variables are converted to hex. Google Charts takes a resolved-hex snapshot at render, so this does not live-update on a theme change without a re-render — an accepted asymmetry vs CSS-painted elements.
|
|
214
209
|
const backgroundColorHex =
|
|
215
|
-
normalizeColorToHex(
|
|
210
|
+
normalizeColorToHex( CATALOG_POINTERS.background, scopeElement, resolveCssVariable ) ||
|
|
216
211
|
DEFAULT_BACKGROUND_COLOR;
|
|
217
212
|
const defaultFillColorHex =
|
|
218
|
-
normalizeColorToHex(
|
|
213
|
+
normalizeColorToHex( CATALOG_POINTERS.surfaceSecondary, scopeElement, resolveCssVariable ) ||
|
|
219
214
|
DEFAULT_FEATURE_FILL_COLOR;
|
|
220
215
|
|
|
221
216
|
// Identify HTML tooltip column indices and sanitize their content to prevent XSS.
|
|
@@ -306,7 +301,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
306
301
|
ref={ containerRef }
|
|
307
302
|
className={ clsx( 'geo-chart', styles.container, className ) }
|
|
308
303
|
data-testid="geo-chart"
|
|
309
|
-
style={ { width, height
|
|
304
|
+
style={ { width, height } }
|
|
310
305
|
>
|
|
311
306
|
<Chart
|
|
312
307
|
chartType="GeoChart"
|
|
@@ -142,7 +142,7 @@ describe( 'GeoChart', () => {
|
|
|
142
142
|
} );
|
|
143
143
|
|
|
144
144
|
describe( 'Chart Options', () => {
|
|
145
|
-
test( 'configures color axis
|
|
145
|
+
test( 'configures a two-stop color axis from the palette', () => {
|
|
146
146
|
renderWithTheme();
|
|
147
147
|
|
|
148
148
|
const chartOptions = screen.getByTestId( 'chart-options' );
|
|
@@ -152,7 +152,7 @@ describe( 'GeoChart', () => {
|
|
|
152
152
|
expect( options.colorAxis.colors ).toHaveLength( 2 );
|
|
153
153
|
} );
|
|
154
154
|
|
|
155
|
-
test( 'sets datalessRegionColor from
|
|
155
|
+
test( 'sets datalessRegionColor from the surface-secondary role', () => {
|
|
156
156
|
renderWithTheme();
|
|
157
157
|
|
|
158
158
|
const chartOptions = screen.getByTestId( 'chart-options' );
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Focus-ring width shared across the grid and cell-selected focus outlines. A
|
|
2
2
|
// Sass var, not a custom property: a compile-time constant scoped to this
|
|
3
3
|
// module, so it needs no runtime cascade and never lands on the DOM.
|
|
4
|
-
$focus-ring-width: var(--wpds-border-width-focus
|
|
4
|
+
$focus-ring-width: var(--wpds-border-width-focus);
|
|
5
5
|
|
|
6
6
|
.heatmap-chart {
|
|
7
|
+
position: relative;
|
|
7
8
|
width: 100%;
|
|
8
9
|
height: 100%;
|
|
9
10
|
min-height: 0;
|
|
10
|
-
font-size: var(--wpds-typography-font-size-sm
|
|
11
|
+
font-size: var(--wpds-typography-font-size-sm);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// Height-capped grids are content-sized vertically, so the wrapper hugs them
|
|
@@ -18,7 +19,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
.heatmap-chart__empty {
|
|
21
|
-
padding: var(--wpds-dimension-padding-lg
|
|
22
|
+
padding: var(--wpds-dimension-padding-lg);
|
|
22
23
|
color: var(--a8c-charts-color-label-secondary);
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -28,9 +29,9 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
28
29
|
display: grid;
|
|
29
30
|
// --a8c-charts-dimension-heatmap-cell-gap is component-emitted from JS per
|
|
30
31
|
// render, not a catalog entry. Do not collapse this to a bare var(); the
|
|
31
|
-
// inner WPDS fallback is the only value this
|
|
32
|
-
// is unset.
|
|
33
|
-
gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs
|
|
32
|
+
// inner WPDS token (fallback injected at build) is the only value this
|
|
33
|
+
// property has when the JS var is unset.
|
|
34
|
+
gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs));
|
|
34
35
|
flex: 1 1 0;
|
|
35
36
|
width: 100%;
|
|
36
37
|
// Override flex's auto minimum so short widgets do not scroll.
|
|
@@ -39,9 +40,9 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
39
40
|
&:focus-visible {
|
|
40
41
|
outline:
|
|
41
42
|
$focus-ring-width solid
|
|
42
|
-
var(--wpds-color-stroke-focus
|
|
43
|
+
var(--wpds-color-stroke-focus);
|
|
43
44
|
outline-offset: 2px;
|
|
44
|
-
border-radius: var(--wpds-border-radius-sm
|
|
45
|
+
border-radius: var(--wpds-border-radius-sm);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -68,7 +69,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
68
69
|
.heatmap-chart__col-label,
|
|
69
70
|
.heatmap-chart__row-label {
|
|
70
71
|
color: var(--a8c-charts-color-label-secondary);
|
|
71
|
-
font-size: var(--wpds-typography-font-size-xs
|
|
72
|
+
font-size: var(--wpds-typography-font-size-xs);
|
|
72
73
|
white-space: nowrap;
|
|
73
74
|
overflow: visible;
|
|
74
75
|
}
|
|
@@ -82,7 +83,7 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
82
83
|
align-self: center;
|
|
83
84
|
justify-self: end;
|
|
84
85
|
text-align: right;
|
|
85
|
-
padding-inline-end: var(--wpds-dimension-padding-xs
|
|
86
|
+
padding-inline-end: var(--wpds-dimension-padding-xs);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
.heatmap-chart__cell {
|
|
@@ -115,19 +116,19 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
115
116
|
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
116
117
|
// an empty cell.
|
|
117
118
|
.heatmap-chart__cell--filled {
|
|
118
|
-
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-
|
|
119
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-background, #fff));
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
.heatmap-chart__cell--selected {
|
|
122
123
|
outline:
|
|
123
124
|
$focus-ring-width solid
|
|
124
|
-
var(--wpds-color-stroke-focus
|
|
125
|
+
var(--wpds-color-stroke-focus);
|
|
125
126
|
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
.heatmap-chart__cell-value {
|
|
129
130
|
color: var(--a8c-charts-color-label);
|
|
130
|
-
font-size: var(--wpds-typography-font-size-md
|
|
131
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
// Light text on fills dark enough to out-contrast dark text (decided in JS from
|
|
@@ -136,9 +137,13 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
136
137
|
color: var(--a8c-charts-color-label-inverse);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
.heatmap-chart__legend-label {
|
|
141
|
+
color: var(--a8c-charts-color-label);
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
.heatmap-chart__legend-swatch {
|
|
140
|
-
width: var(--wpds-dimension-size-3xs
|
|
141
|
-
height: var(--wpds-dimension-size-3xs
|
|
145
|
+
width: var(--wpds-dimension-size-3xs);
|
|
146
|
+
height: var(--wpds-dimension-size-3xs);
|
|
142
147
|
border-radius: var(--a8c-charts-border-radius-cell);
|
|
143
|
-
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-
|
|
148
|
+
background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-background, #fff));
|
|
144
149
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { formatNumber, formatNumberCompact } from '@automattic/number-formatters';
|
|
2
|
-
import { useTooltip
|
|
2
|
+
import { useTooltip } from '@visx/tooltip';
|
|
3
3
|
import { __ } from '@wordpress/i18n';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { BoundedTooltip } from '../../components/tooltip/private/bounded-tooltip';
|
|
6
7
|
import {
|
|
7
8
|
GlobalChartsProvider,
|
|
8
9
|
useChartId,
|
|
@@ -10,7 +11,8 @@ import {
|
|
|
10
11
|
useGlobalChartsContext,
|
|
11
12
|
GlobalChartsContext,
|
|
12
13
|
} from '../../providers';
|
|
13
|
-
import {
|
|
14
|
+
import { CATALOG_POINTERS } from '../../providers/chart-context/private/catalog-pointers';
|
|
15
|
+
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
14
16
|
import { attachSubComponents } from '../../utils';
|
|
15
17
|
import {
|
|
16
18
|
isValidHexColor,
|
|
@@ -69,23 +71,28 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
69
71
|
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >();
|
|
70
72
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, showTooltip, hideTooltip } =
|
|
71
73
|
useTooltip< HeatmapTooltipData >();
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
75
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
76
|
+
// The chart root positions the tooltip, so pointer and cell coordinates are
|
|
77
|
+
// measured against it — found by its id rather than by walking up, so
|
|
78
|
+
// whatever ChartLayout wraps the grid in cannot shift the origin.
|
|
79
|
+
const getTooltipOrigin = useCallback(
|
|
80
|
+
() =>
|
|
81
|
+
containerRef.current
|
|
82
|
+
?.closest( `[data-chart-id="heatmap-chart-${ chartId }"]` )
|
|
83
|
+
?.getBoundingClientRect() ?? null,
|
|
84
|
+
[ chartId ]
|
|
85
|
+
);
|
|
80
86
|
|
|
81
87
|
const { color: primaryColorHex } = getElementStyles( {
|
|
82
88
|
index: 0,
|
|
83
|
-
overrideColor: primaryColor
|
|
89
|
+
overrideColor: primaryColor,
|
|
84
90
|
} );
|
|
85
91
|
|
|
86
|
-
//
|
|
92
|
+
// The cell blend substitutes this role at the cell; this read happens at the scope
|
|
93
|
+
// element, so an override on the chart's own class makes the two disagree. CHARTS-255.
|
|
87
94
|
const chartBackgroundHex = normalizeColorToHex(
|
|
88
|
-
|
|
95
|
+
CATALOG_POINTERS.background,
|
|
89
96
|
scopeElement,
|
|
90
97
|
resolveCssVariable
|
|
91
98
|
);
|
|
@@ -212,20 +219,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
212
219
|
if ( ! withTooltips ) {
|
|
213
220
|
return;
|
|
214
221
|
}
|
|
222
|
+
const origin = getTooltipOrigin();
|
|
223
|
+
if ( ! origin ) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
215
226
|
const target = event.currentTarget;
|
|
216
227
|
const columnIndex = Number( target.dataset.column );
|
|
217
228
|
const rowIndex = Number( target.dataset.row );
|
|
218
|
-
// Read bounds from the ref (like the keyboard-tooltip effect) so this
|
|
219
|
-
// callback stays stable across renders.
|
|
220
|
-
const bounds = containerBoundsRef.current;
|
|
221
|
-
// TooltipInPortal re-adds containerBounds, so subtract it to land at the cursor.
|
|
222
229
|
showTooltip( {
|
|
223
|
-
tooltipLeft: event.clientX -
|
|
224
|
-
tooltipTop: event.clientY -
|
|
230
|
+
tooltipLeft: event.clientX - origin.left,
|
|
231
|
+
tooltipTop: event.clientY - origin.top,
|
|
225
232
|
tooltipData: buildTooltipData( columnIndex, rowIndex ),
|
|
226
233
|
} );
|
|
227
234
|
},
|
|
228
|
-
[ withTooltips, showTooltip, buildTooltipData ]
|
|
235
|
+
[ withTooltips, showTooltip, buildTooltipData, getTooltipOrigin ]
|
|
229
236
|
);
|
|
230
237
|
|
|
231
238
|
const handleCellMouseLeave = useCallback( () => {
|
|
@@ -241,6 +248,10 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
241
248
|
if ( ! withTooltips || selectedIndex === undefined ) {
|
|
242
249
|
return;
|
|
243
250
|
}
|
|
251
|
+
const origin = getTooltipOrigin();
|
|
252
|
+
if ( ! origin ) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
244
255
|
const col = Math.floor( selectedIndex / rows );
|
|
245
256
|
const row = selectedIndex % rows;
|
|
246
257
|
const cell =
|
|
@@ -248,13 +259,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
248
259
|
? document.getElementById( `${ chartId }-cell-${ col }-${ row }` )
|
|
249
260
|
: null;
|
|
250
261
|
const rect = cell?.getBoundingClientRect();
|
|
251
|
-
const bounds = containerBoundsRef.current;
|
|
252
262
|
showTooltip( {
|
|
253
|
-
tooltipLeft: rect ? rect.left + rect.width / 2 -
|
|
254
|
-
tooltipTop: rect ? rect.top + rect.height / 2 -
|
|
263
|
+
tooltipLeft: rect ? rect.left + rect.width / 2 - origin.left : 0,
|
|
264
|
+
tooltipTop: rect ? rect.top + rect.height / 2 - origin.top : 0,
|
|
255
265
|
tooltipData: buildTooltipData( col, row ),
|
|
256
266
|
} );
|
|
257
|
-
}, [
|
|
267
|
+
}, [
|
|
268
|
+
selectedIndex,
|
|
269
|
+
withTooltips,
|
|
270
|
+
rows,
|
|
271
|
+
chartId,
|
|
272
|
+
buildTooltipData,
|
|
273
|
+
showTooltip,
|
|
274
|
+
getTooltipOrigin,
|
|
275
|
+
] );
|
|
258
276
|
|
|
259
277
|
const defaultRenderTooltip = useCallback(
|
|
260
278
|
( info: HeatmapTooltipData ) => (
|
|
@@ -296,7 +314,6 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
296
314
|
: `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
|
|
297
315
|
const gridStyle: Record< string, string | number > = {
|
|
298
316
|
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
299
|
-
'--a8c-charts-color-heatmap-background': theme.backgroundColor,
|
|
300
317
|
gridTemplateColumns: `auto repeat(${ columns }, ${ columnTrack })`,
|
|
301
318
|
gridTemplateRows: `auto repeat(${ rows }, ${ rowTrack })`,
|
|
302
319
|
};
|
|
@@ -472,11 +489,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
472
489
|
} ) }
|
|
473
490
|
</div>
|
|
474
491
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
475
|
-
<
|
|
476
|
-
<div className={
|
|
492
|
+
<BoundedTooltip top={ tooltipTop } left={ tooltipLeft }>
|
|
493
|
+
<div className={ standaloneScopeClass } role="tooltip" tabIndex={ -1 }>
|
|
477
494
|
{ ( renderTooltip ?? defaultRenderTooltip )( tooltipData ) }
|
|
478
495
|
</div>
|
|
479
|
-
</
|
|
496
|
+
</BoundedTooltip>
|
|
480
497
|
) }
|
|
481
498
|
</ChartLayout>
|
|
482
499
|
</ChartInstanceContext.Provider>
|
|
@@ -53,6 +53,9 @@ const toDate = ( point: DataPointDate ): Date | null => {
|
|
|
53
53
|
return null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
// Labels stay English on the browser's calendar, unlike the time axis: this is a
|
|
57
|
+
// plain function the host calls outside the provider, so `GlobalChartsProvider`'s
|
|
58
|
+
// `locale` and `timeZone` cannot reach it. CHARTS-266 has the detail.
|
|
56
59
|
export const buildCalendarHeatmapData = (
|
|
57
60
|
series: DataPointDate[],
|
|
58
61
|
options: {
|
|
@@ -15,7 +15,7 @@ export interface HeatmapLegendProps {
|
|
|
15
15
|
|
|
16
16
|
export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel, moreLabel } ) => {
|
|
17
17
|
const context = useContext( HeatmapContext );
|
|
18
|
-
const { legend
|
|
18
|
+
const { legend } = useGlobalChartsTheme();
|
|
19
19
|
if ( ! context ) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
@@ -24,7 +24,11 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
26
|
<Stack direction="row" gap="xs" align="center">
|
|
27
|
-
<Text
|
|
27
|
+
<Text
|
|
28
|
+
variant="body-sm"
|
|
29
|
+
className={ styles[ 'heatmap-chart__legend-label' ] }
|
|
30
|
+
style={ labelStyle }
|
|
31
|
+
>
|
|
28
32
|
{ lessLabel ?? __( 'Less', 'jetpack-charts' ) }
|
|
29
33
|
</Text>
|
|
30
34
|
<Stack direction="row" gap="xs">
|
|
@@ -38,7 +42,6 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
38
42
|
style={
|
|
39
43
|
{
|
|
40
44
|
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
41
|
-
'--a8c-charts-color-heatmap-background': backgroundColor,
|
|
42
45
|
'--a8c-charts-heatmap-cell-intensity': intensity,
|
|
43
46
|
} as CSSProperties
|
|
44
47
|
}
|
|
@@ -46,7 +49,11 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
|
|
|
46
49
|
);
|
|
47
50
|
} ) }
|
|
48
51
|
</Stack>
|
|
49
|
-
<Text
|
|
52
|
+
<Text
|
|
53
|
+
variant="body-sm"
|
|
54
|
+
className={ styles[ 'heatmap-chart__legend-label' ] }
|
|
55
|
+
style={ labelStyle }
|
|
56
|
+
>
|
|
50
57
|
{ moreLabel ?? __( 'More', 'jetpack-charts' ) }
|
|
51
58
|
</Text>
|
|
52
59
|
</Stack>
|
|
@@ -352,24 +352,21 @@ describe( 'HeatmapChart', () => {
|
|
|
352
352
|
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#abcdef' );
|
|
353
353
|
} );
|
|
354
354
|
|
|
355
|
-
test( '
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
</GlobalChartsProvider>
|
|
360
|
-
);
|
|
361
|
-
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
362
|
-
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
363
|
-
} );
|
|
355
|
+
test( 'falls back to the first palette slot when no primaryColor prop is set', () => {
|
|
356
|
+
const scope = document.createElement( 'div' );
|
|
357
|
+
scope.style.setProperty( '--a8c-charts-color-series-1', '#0a0b0c' );
|
|
358
|
+
document.body.appendChild( scope );
|
|
364
359
|
|
|
365
|
-
test( 'falls back to the palette colors[0] when no prop or theme primaryColor is set', () => {
|
|
366
360
|
render(
|
|
367
|
-
<GlobalChartsProvider
|
|
361
|
+
<GlobalChartsProvider>
|
|
368
362
|
<HeatmapChart width={ 500 } height={ 300 } data={ data } />
|
|
369
|
-
</GlobalChartsProvider
|
|
363
|
+
</GlobalChartsProvider>,
|
|
364
|
+
{ container: scope }
|
|
370
365
|
);
|
|
371
366
|
const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
|
|
372
367
|
expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
|
|
368
|
+
|
|
369
|
+
document.body.removeChild( scope );
|
|
373
370
|
} );
|
|
374
371
|
|
|
375
372
|
test( 'the unresponsive export pins explicit width and height', () => {
|
|
@@ -393,3 +390,45 @@ describe( 'HeatmapChart', () => {
|
|
|
393
390
|
expect( chart ).not.toHaveStyle( { height: '300px' } );
|
|
394
391
|
} );
|
|
395
392
|
} );
|
|
393
|
+
|
|
394
|
+
// Chart container at (100, 50); the tooltip box measures 120x40. Everything
|
|
395
|
+
// else the charts measure (wrapper, clipping lookups) reports the container.
|
|
396
|
+
const mockRects = () =>
|
|
397
|
+
jest.spyOn( Element.prototype, 'getBoundingClientRect' ).mockImplementation( function (
|
|
398
|
+
this: Element
|
|
399
|
+
) {
|
|
400
|
+
const box = this.classList.contains( 'visx-tooltip' );
|
|
401
|
+
return {
|
|
402
|
+
left: box ? 0 : 100,
|
|
403
|
+
top: box ? 0 : 50,
|
|
404
|
+
width: box ? 120 : 400,
|
|
405
|
+
height: box ? 40 : 300,
|
|
406
|
+
right: box ? 120 : 500,
|
|
407
|
+
bottom: box ? 40 : 350,
|
|
408
|
+
x: box ? 0 : 100,
|
|
409
|
+
y: box ? 0 : 50,
|
|
410
|
+
toJSON: () => ( {} ),
|
|
411
|
+
} as DOMRect;
|
|
412
|
+
} );
|
|
413
|
+
|
|
414
|
+
describe( 'HeatmapChart tooltip position', () => {
|
|
415
|
+
afterEach( () => {
|
|
416
|
+
jest.restoreAllMocks();
|
|
417
|
+
} );
|
|
418
|
+
|
|
419
|
+
test( 'places the box relative to the chart root, at the pointer plus the offsets', async () => {
|
|
420
|
+
mockRects();
|
|
421
|
+
renderChart( { withTooltips: true, rowLabels: [ 'Mon', 'Tue', 'Wed' ] } );
|
|
422
|
+
|
|
423
|
+
await userEvent.setup().pointer( {
|
|
424
|
+
target: screen.getAllByTestId( 'heatmap-cell' )[ 0 ],
|
|
425
|
+
coords: { clientX: 180, clientY: 140 },
|
|
426
|
+
} );
|
|
427
|
+
|
|
428
|
+
// (180, 140) is (80, 90) inside the root; the box adds 10px each way.
|
|
429
|
+
await expect( screen.findByRole( 'tooltip' ) ).resolves.toBeInTheDocument();
|
|
430
|
+
expect( screen.getByTestId( 'bounded-tooltip' ) ).toHaveStyle( {
|
|
431
|
+
transform: 'translate(90px, 100px)',
|
|
432
|
+
} );
|
|
433
|
+
} );
|
|
434
|
+
} );
|
|
@@ -69,8 +69,8 @@ export interface HeatmapChartProps
|
|
|
69
69
|
/** Floor a cell's height (px) in non-compact mode; see `minCellWidth`. */
|
|
70
70
|
minCellHeight?: number;
|
|
71
71
|
/**
|
|
72
|
-
* Color the cell scale interpolates toward at the highest value
|
|
73
|
-
*
|
|
72
|
+
* Color the cell scale interpolates toward at the highest value. Defaults to the
|
|
73
|
+
* first series palette slot, `--a8c-charts-color-series-1`.
|
|
74
74
|
*/
|
|
75
75
|
primaryColor?: string;
|
|
76
76
|
renderTooltip?: ( data: HeatmapTooltipData ) => ReactNode;
|
|
@@ -13,8 +13,8 @@ export const SUBPIXEL_TOLERANCE = 0.5;
|
|
|
13
13
|
* Counts how many leading rows fit inside the content container.
|
|
14
14
|
*
|
|
15
15
|
* Rows are read from the DOM rather than derived from constants: row height
|
|
16
|
-
* depends on
|
|
17
|
-
* as a label, so any restated number would drift.
|
|
16
|
+
* depends on `--a8c-charts-dimension-leaderboard-row-gap`, label wrapping, and
|
|
17
|
+
* whatever a caller renders as a label, so any restated number would drift.
|
|
18
18
|
*
|
|
19
19
|
* Every row is a direct grid child marked with `data-row-index`; interactive rows
|
|
20
20
|
* use a button wrapper and non-interactive rows use a div wrapper.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __ } from '@wordpress/i18n';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { useGlobalChartsContext } from '../../../providers';
|
|
4
4
|
import type { BaseLegendItem } from '../../../components/legend';
|
|
5
5
|
import type { LeaderboardEntry } from '../../../types';
|
|
6
6
|
|
|
@@ -35,7 +35,6 @@ export function useLeaderboardLegendItems( {
|
|
|
35
35
|
comparison?: string;
|
|
36
36
|
};
|
|
37
37
|
} ): BaseLegendItem[] {
|
|
38
|
-
const { leaderboardChart: leaderboardChartSettings } = useGlobalChartsTheme();
|
|
39
38
|
const { getElementStyles } = useGlobalChartsContext();
|
|
40
39
|
|
|
41
40
|
return useMemo( () => {
|
|
@@ -48,7 +47,7 @@ export function useLeaderboardLegendItems( {
|
|
|
48
47
|
// Add current period legend item
|
|
49
48
|
const { color: resolvedPrimaryColor } = getElementStyles( {
|
|
50
49
|
index: 0,
|
|
51
|
-
overrideColor: primaryColor
|
|
50
|
+
overrideColor: primaryColor,
|
|
52
51
|
} );
|
|
53
52
|
|
|
54
53
|
items.push( {
|
|
@@ -60,7 +59,7 @@ export function useLeaderboardLegendItems( {
|
|
|
60
59
|
if ( withComparison && ! withOverlayLabel ) {
|
|
61
60
|
const { color: resolvedSecondaryColor } = getElementStyles( {
|
|
62
61
|
index: 1,
|
|
63
|
-
overrideColor: secondaryColor
|
|
62
|
+
overrideColor: secondaryColor,
|
|
64
63
|
} );
|
|
65
64
|
|
|
66
65
|
items.push( {
|
|
@@ -76,7 +75,6 @@ export function useLeaderboardLegendItems( {
|
|
|
76
75
|
secondaryColor,
|
|
77
76
|
withComparison,
|
|
78
77
|
legendLabels,
|
|
79
|
-
leaderboardChartSettings,
|
|
80
78
|
getElementStyles,
|
|
81
79
|
withOverlayLabel,
|
|
82
80
|
] );
|