@automattic/charts 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/SECURITY.md +1 -0
- package/TOKENS.md +53 -58
- package/dist/index.cjs +1483 -664
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +93 -29
- package/dist/index.d.cts +233 -247
- package/dist/index.d.ts +233 -247
- package/dist/index.js +1484 -667
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/charts/area-chart/area-chart.tsx +39 -25
- package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
- package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
- package/src/charts/area-chart/types.ts +4 -7
- package/src/charts/bar-chart/bar-chart.tsx +2 -1
- package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
- package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
- package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
- package/src/charts/geo-chart/geo-chart.module.scss +1 -0
- package/src/charts/geo-chart/geo-chart.tsx +5 -10
- package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +52 -16
- package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
- package/src/charts/heatmap-chart/index.ts +2 -1
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
- package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
- package/src/charts/heatmap-chart/private/index.ts +1 -1
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
- package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
- package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
- package/src/charts/heatmap-chart/types.ts +8 -2
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
- package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
- package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
- package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
- package/src/charts/leaderboard-chart/types.ts +2 -2
- package/src/charts/line-chart/line-chart.module.scss +10 -18
- package/src/charts/line-chart/line-chart.tsx +128 -26
- package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
- package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
- package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
- package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
- package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
- package/src/charts/line-chart/types.ts +17 -3
- package/src/charts/pie-chart/pie-chart.module.scss +13 -1
- package/src/charts/pie-chart/pie-chart.tsx +30 -40
- package/src/charts/pie-chart/test/pie-chart.test.tsx +59 -12
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
- package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/test/axis-formatting.test.tsx +85 -0
- package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
- package/src/charts/private/test/time-axis-options.test.ts +267 -0
- package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
- package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
- package/src/charts/private/test/time-axis.test.ts +9 -69
- package/src/charts/private/time-axis-options.ts +106 -0
- package/src/charts/private/time-axis.ts +318 -136
- package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
- package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
- package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
- package/src/charts/sparkline/types.ts +1 -1
- package/src/components/legend/private/base-legend.module.scss +9 -5
- package/src/components/tooltip/accessible-tooltip.tsx +21 -17
- package/src/components/tooltip/base-tooltip.module.scss +3 -3
- package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
- package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
- package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
- package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
- package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
- package/src/hooks/test/use-chart-margin.test.tsx +173 -3
- package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
- package/src/hooks/use-chart-data-transform.ts +10 -3
- package/src/hooks/use-chart-margin.tsx +54 -9
- package/src/hooks/use-xychart-theme.ts +25 -9
- package/src/index.ts +8 -0
- package/src/providers/chart-context/global-charts-provider.tsx +37 -38
- package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
- package/src/providers/chart-context/index.ts +1 -0
- package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
- package/src/providers/chart-context/private/series-palette.ts +3 -13
- package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
- package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
- package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
- package/src/providers/chart-context/themes.ts +7 -52
- package/src/providers/chart-context/types.ts +3 -0
- package/src/styles/chart-scope.scss +39 -62
- package/src/styles/test/chart-scope.test.ts +19 -47
- package/src/test-utils/series-fixtures.ts +31 -0
- package/src/test-utils/zoned-parsing-suite.ts +90 -0
- package/src/types.ts +79 -166
- package/src/utils/bucket-info.ts +87 -0
- package/src/utils/color-utils.ts +0 -27
- package/src/utils/date-formatting.ts +157 -0
- package/src/utils/date-parsing.ts +103 -52
- package/src/utils/get-edge-tick-widths.ts +33 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/test/bucket-info.test.ts +185 -0
- package/src/utils/test/color-utils.test.ts +1 -48
- package/src/utils/test/date-formatting.test.ts +89 -0
- package/src/utils/test/date-parsing.auckland.test.ts +6 -0
- package/src/utils/test/date-parsing.santiago.test.ts +6 -0
- package/src/utils/test/date-parsing.test.ts +16 -1
- package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
- package/src/utils/test/get-styles.test.ts +10 -7
- package/src/utils/test/merge-themes.test.ts +18 -46
- package/src/utils/warn-once.ts +23 -0
- package/src/visx/types.ts +56 -5
- package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
- package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
- package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
- package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
- package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
- package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
- package/src/styles/test/wpds-fallbacks.test.ts +0 -131
|
@@ -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 {
|
|
@@ -112,22 +113,53 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
112
113
|
background: color-mix(in sRGB, var(--a8c-charts-color-track) 45%, transparent);
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// A roll-up beside the grid, on another scale from it: no fill, its figure
|
|
117
|
+
// always printed and emphasised.
|
|
118
|
+
.heatmap-chart__cell--summary {
|
|
119
|
+
background: none;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.heatmap-chart__col-label--summary {
|
|
123
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.heatmap-chart__cell--summary .heatmap-chart__cell-value {
|
|
127
|
+
font-weight: var(--wpds-typography-font-weight-emphasis);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Compact rows are `compactCellSize` tall, so the figure shrinks to fit them.
|
|
131
|
+
.heatmap-chart__grid--compact .heatmap-chart__cell--summary .heatmap-chart__cell-value {
|
|
132
|
+
font-size: var(--wpds-typography-font-size-xs);
|
|
133
|
+
line-height: 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Deliberately on top of the grid gap: one extra gap sets a summary column
|
|
137
|
+
// apart from the data beside it, on cells and label alike, and the auto track
|
|
138
|
+
// grows by the same amount.
|
|
139
|
+
.heatmap-chart__gap-start {
|
|
140
|
+
margin-inline-start: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.heatmap-chart__gap-end {
|
|
144
|
+
margin-inline-end: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs));
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
116
148
|
// an empty cell.
|
|
117
149
|
.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-
|
|
150
|
+
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
151
|
}
|
|
120
152
|
|
|
121
153
|
.heatmap-chart__cell--selected {
|
|
122
154
|
outline:
|
|
123
155
|
$focus-ring-width solid
|
|
124
|
-
var(--wpds-color-stroke-focus
|
|
156
|
+
var(--wpds-color-stroke-focus);
|
|
125
157
|
outline-offset: calc(-1 * #{$focus-ring-width});
|
|
126
158
|
}
|
|
127
159
|
|
|
128
160
|
.heatmap-chart__cell-value {
|
|
129
161
|
color: var(--a8c-charts-color-label);
|
|
130
|
-
font-size: var(--wpds-typography-font-size-md
|
|
162
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
// Light text on fills dark enough to out-contrast dark text (decided in JS from
|
|
@@ -136,9 +168,13 @@ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-fo
|
|
|
136
168
|
color: var(--a8c-charts-color-label-inverse);
|
|
137
169
|
}
|
|
138
170
|
|
|
171
|
+
.heatmap-chart__legend-label {
|
|
172
|
+
color: var(--a8c-charts-color-label);
|
|
173
|
+
}
|
|
174
|
+
|
|
139
175
|
.heatmap-chart__legend-swatch {
|
|
140
|
-
width: var(--wpds-dimension-size-3xs
|
|
141
|
-
height: var(--wpds-dimension-size-3xs
|
|
176
|
+
width: var(--wpds-dimension-size-3xs);
|
|
177
|
+
height: var(--wpds-dimension-size-3xs);
|
|
142
178
|
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-
|
|
179
|
+
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
180
|
}
|
|
@@ -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,
|
|
@@ -41,6 +43,10 @@ import type { CSSProperties, FC } from 'react';
|
|
|
41
43
|
// the rendered fill is the primary mixed over the chart background at 0.15 + 0.85 * intensity.
|
|
42
44
|
const CELL_MIX_FLOOR = 0.15;
|
|
43
45
|
|
|
46
|
+
// One instance, not a `[]` default in the signature: `buildTooltipData` keys on
|
|
47
|
+
// it, and a fresh array per render re-ran the keyboard tooltip effect endlessly.
|
|
48
|
+
const NO_ROW_LABELS: string[] = [];
|
|
49
|
+
|
|
44
50
|
const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
45
51
|
data,
|
|
46
52
|
chartId: providedChartId,
|
|
@@ -53,7 +59,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
53
59
|
maxCellHeight,
|
|
54
60
|
minCellWidth,
|
|
55
61
|
minCellHeight,
|
|
56
|
-
rowLabels =
|
|
62
|
+
rowLabels = NO_ROW_LABELS,
|
|
57
63
|
primaryColor,
|
|
58
64
|
gap = 'md',
|
|
59
65
|
withTooltips = false,
|
|
@@ -69,23 +75,28 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
69
75
|
const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >();
|
|
70
76
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, showTooltip, hideTooltip } =
|
|
71
77
|
useTooltip< HeatmapTooltipData >();
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const standaloneScopeClass = useStandaloneScopeClass();
|
|
79
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
80
|
+
// The chart root positions the tooltip, so pointer and cell coordinates are
|
|
81
|
+
// measured against it — found by its id rather than by walking up, so
|
|
82
|
+
// whatever ChartLayout wraps the grid in cannot shift the origin.
|
|
83
|
+
const getTooltipOrigin = useCallback(
|
|
84
|
+
() =>
|
|
85
|
+
containerRef.current
|
|
86
|
+
?.closest( `[data-chart-id="heatmap-chart-${ chartId }"]` )
|
|
87
|
+
?.getBoundingClientRect() ?? null,
|
|
88
|
+
[ chartId ]
|
|
89
|
+
);
|
|
80
90
|
|
|
81
91
|
const { color: primaryColorHex } = getElementStyles( {
|
|
82
92
|
index: 0,
|
|
83
|
-
overrideColor: primaryColor
|
|
93
|
+
overrideColor: primaryColor,
|
|
84
94
|
} );
|
|
85
95
|
|
|
86
|
-
//
|
|
96
|
+
// The cell blend substitutes this role at the cell; this read happens at the scope
|
|
97
|
+
// element, so an override on the chart's own class makes the two disagree. CHARTS-255.
|
|
87
98
|
const chartBackgroundHex = normalizeColorToHex(
|
|
88
|
-
|
|
99
|
+
CATALOG_POINTERS.background,
|
|
89
100
|
scopeElement,
|
|
90
101
|
resolveCssVariable
|
|
91
102
|
);
|
|
@@ -212,20 +223,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
212
223
|
if ( ! withTooltips ) {
|
|
213
224
|
return;
|
|
214
225
|
}
|
|
226
|
+
const origin = getTooltipOrigin();
|
|
227
|
+
if ( ! origin ) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
215
230
|
const target = event.currentTarget;
|
|
216
231
|
const columnIndex = Number( target.dataset.column );
|
|
217
232
|
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
233
|
showTooltip( {
|
|
223
|
-
tooltipLeft: event.clientX -
|
|
224
|
-
tooltipTop: event.clientY -
|
|
234
|
+
tooltipLeft: event.clientX - origin.left,
|
|
235
|
+
tooltipTop: event.clientY - origin.top,
|
|
225
236
|
tooltipData: buildTooltipData( columnIndex, rowIndex ),
|
|
226
237
|
} );
|
|
227
238
|
},
|
|
228
|
-
[ withTooltips, showTooltip, buildTooltipData ]
|
|
239
|
+
[ withTooltips, showTooltip, buildTooltipData, getTooltipOrigin ]
|
|
229
240
|
);
|
|
230
241
|
|
|
231
242
|
const handleCellMouseLeave = useCallback( () => {
|
|
@@ -241,6 +252,10 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
241
252
|
if ( ! withTooltips || selectedIndex === undefined ) {
|
|
242
253
|
return;
|
|
243
254
|
}
|
|
255
|
+
const origin = getTooltipOrigin();
|
|
256
|
+
if ( ! origin ) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
244
259
|
const col = Math.floor( selectedIndex / rows );
|
|
245
260
|
const row = selectedIndex % rows;
|
|
246
261
|
const cell =
|
|
@@ -248,13 +263,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
248
263
|
? document.getElementById( `${ chartId }-cell-${ col }-${ row }` )
|
|
249
264
|
: null;
|
|
250
265
|
const rect = cell?.getBoundingClientRect();
|
|
251
|
-
const bounds = containerBoundsRef.current;
|
|
252
266
|
showTooltip( {
|
|
253
|
-
tooltipLeft: rect ? rect.left + rect.width / 2 -
|
|
254
|
-
tooltipTop: rect ? rect.top + rect.height / 2 -
|
|
267
|
+
tooltipLeft: rect ? rect.left + rect.width / 2 - origin.left : 0,
|
|
268
|
+
tooltipTop: rect ? rect.top + rect.height / 2 - origin.top : 0,
|
|
255
269
|
tooltipData: buildTooltipData( col, row ),
|
|
256
270
|
} );
|
|
257
|
-
}, [
|
|
271
|
+
}, [
|
|
272
|
+
selectedIndex,
|
|
273
|
+
withTooltips,
|
|
274
|
+
rows,
|
|
275
|
+
chartId,
|
|
276
|
+
buildTooltipData,
|
|
277
|
+
showTooltip,
|
|
278
|
+
getTooltipOrigin,
|
|
279
|
+
] );
|
|
258
280
|
|
|
259
281
|
const defaultRenderTooltip = useCallback(
|
|
260
282
|
( info: HeatmapTooltipData ) => (
|
|
@@ -294,10 +316,18 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
294
316
|
const rowTrack = compact
|
|
295
317
|
? 'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
296
318
|
: `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
|
|
319
|
+
// A summary column takes a content-sized track: a roll-up is wider than a
|
|
320
|
+
// cell, and a shared track would stretch every cell to fit it. `max-content`
|
|
321
|
+
// as the max keeps the leftover width out of it once the data tracks hit
|
|
322
|
+
// `maxCellWidth`, where a plain `auto` would absorb it.
|
|
323
|
+
const columnTracks = data.some( column => column.summary )
|
|
324
|
+
? data
|
|
325
|
+
.map( column => ( column.summary ? 'minmax(auto, max-content)' : columnTrack ) )
|
|
326
|
+
.join( ' ' )
|
|
327
|
+
: `repeat(${ columns }, ${ columnTrack })`;
|
|
297
328
|
const gridStyle: Record< string, string | number > = {
|
|
298
329
|
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
299
|
-
|
|
300
|
-
gridTemplateColumns: `auto repeat(${ columns }, ${ columnTrack })`,
|
|
330
|
+
gridTemplateColumns: `auto ${ columnTracks }`,
|
|
301
331
|
gridTemplateRows: `auto repeat(${ rows }, ${ rowTrack })`,
|
|
302
332
|
};
|
|
303
333
|
if ( compact ) {
|
|
@@ -305,6 +335,21 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
305
335
|
gridStyle[ '--a8c-charts-dimension-heatmap-cell-size' ] = `${ compactCellSize }px`;
|
|
306
336
|
}
|
|
307
337
|
|
|
338
|
+
// A summary column sits one gap apart from the data on either side; two
|
|
339
|
+
// summaries side by side share no extra gap.
|
|
340
|
+
const summaryGaps = ( columnIndex: number ) => {
|
|
341
|
+
if ( ! data[ columnIndex ]?.summary ) {
|
|
342
|
+
return {};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
[ styles[ 'heatmap-chart__gap-start' ] ]:
|
|
347
|
+
columnIndex > 0 && ! data[ columnIndex - 1 ]?.summary,
|
|
348
|
+
[ styles[ 'heatmap-chart__gap-end' ] ]:
|
|
349
|
+
columnIndex < columns - 1 && ! data[ columnIndex + 1 ]?.summary,
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
|
|
308
353
|
const activeDescendant =
|
|
309
354
|
selectedIndex !== undefined
|
|
310
355
|
? `${ chartId }-cell-${ Math.floor( selectedIndex / rows ) }-${ selectedIndex % rows }`
|
|
@@ -357,7 +402,10 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
357
402
|
{ data.map( ( column, columnIndex ) => (
|
|
358
403
|
<span
|
|
359
404
|
key={ `col-${ columnIndex }` }
|
|
360
|
-
className={ styles[ 'heatmap-chart__col-label' ]
|
|
405
|
+
className={ clsx( styles[ 'heatmap-chart__col-label' ], {
|
|
406
|
+
[ styles[ 'heatmap-chart__col-label--summary' ] ]: column.summary,
|
|
407
|
+
...summaryGaps( columnIndex ),
|
|
408
|
+
} ) }
|
|
361
409
|
>
|
|
362
410
|
{ column.label }
|
|
363
411
|
</span>
|
|
@@ -416,7 +464,9 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
416
464
|
|
|
417
465
|
const value = cell?.value ?? null;
|
|
418
466
|
const present = isPresent( value );
|
|
419
|
-
|
|
467
|
+
// A summary cell is on another scale, so it takes no fill.
|
|
468
|
+
const filled = present && ! column.summary;
|
|
469
|
+
const normalized = filled ? getNormalizedValue( value, extent ) : 0;
|
|
420
470
|
const flatIndex = columnIndex * rows + rowIndex;
|
|
421
471
|
const info = buildTooltipData( columnIndex, rowIndex );
|
|
422
472
|
const accessibleName =
|
|
@@ -432,7 +482,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
432
482
|
<div
|
|
433
483
|
key={ `cell-${ columnIndex }-${ rowIndex }` }
|
|
434
484
|
id={ `${ chartId }-cell-${ columnIndex }-${ rowIndex }` }
|
|
435
|
-
data-testid=
|
|
485
|
+
data-testid={ column.summary ? 'heatmap-cell-summary' : 'heatmap-cell' }
|
|
436
486
|
role="gridcell"
|
|
437
487
|
// Focus stays on the grid (aria-activedescendant); cells are
|
|
438
488
|
// focusable but out of the tab order.
|
|
@@ -442,14 +492,16 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
442
492
|
data-column={ columnIndex }
|
|
443
493
|
data-row={ rowIndex }
|
|
444
494
|
className={ clsx( styles[ 'heatmap-chart__cell' ], {
|
|
445
|
-
[ styles[ 'heatmap-chart__cell--filled' ] ]:
|
|
495
|
+
[ styles[ 'heatmap-chart__cell--filled' ] ]: filled,
|
|
446
496
|
[ styles[ 'heatmap-chart__cell--strong' ] ]:
|
|
447
|
-
|
|
497
|
+
filled && cellHasLightText( normalized ),
|
|
498
|
+
[ styles[ 'heatmap-chart__cell--summary' ] ]: column.summary,
|
|
499
|
+
...summaryGaps( columnIndex ),
|
|
448
500
|
[ styles[ 'heatmap-chart__cell--selected' ] ]:
|
|
449
501
|
selectedIndex === flatIndex,
|
|
450
502
|
} ) }
|
|
451
503
|
style={
|
|
452
|
-
|
|
504
|
+
filled
|
|
453
505
|
? ( {
|
|
454
506
|
'--a8c-charts-heatmap-cell-intensity': normalized,
|
|
455
507
|
} as CSSProperties )
|
|
@@ -458,7 +510,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
458
510
|
onMouseMove={ handleCellMouseMove }
|
|
459
511
|
onMouseLeave={ handleCellMouseLeave }
|
|
460
512
|
>
|
|
461
|
-
{ drawValues && present && (
|
|
513
|
+
{ ( drawValues || column.summary ) && present && (
|
|
462
514
|
<span className={ styles[ 'heatmap-chart__cell-value' ] }>
|
|
463
515
|
{ /* Compact display; tooltip and aria-label keep full precision. */ }
|
|
464
516
|
{ formatNumberCompact( value ) }
|
|
@@ -472,11 +524,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
472
524
|
} ) }
|
|
473
525
|
</div>
|
|
474
526
|
{ withTooltips && tooltipOpen && tooltipData && (
|
|
475
|
-
<
|
|
476
|
-
<div className={
|
|
527
|
+
<BoundedTooltip top={ tooltipTop } left={ tooltipLeft }>
|
|
528
|
+
<div className={ standaloneScopeClass } role="tooltip" tabIndex={ -1 }>
|
|
477
529
|
{ ( renderTooltip ?? defaultRenderTooltip )( tooltipData ) }
|
|
478
530
|
</div>
|
|
479
|
-
</
|
|
531
|
+
</BoundedTooltip>
|
|
480
532
|
) }
|
|
481
533
|
</ChartLayout>
|
|
482
534
|
</ChartInstanceContext.Provider>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as HeatmapChart, HeatmapChartUnresponsive } from './heatmap-chart';
|
|
2
2
|
export { buildCalendarHeatmapData } from './private';
|
|
3
|
-
export
|
|
3
|
+
export { useCalendarHeatmapData } from './use-calendar-heatmap-data';
|
|
4
|
+
export type { CalendarHeatmapOptions, CalendarHeatmapResult } from './private';
|
|
4
5
|
export type { HeatmapChartProps, HeatmapColumn, HeatmapCell, HeatmapTooltipData } from './types';
|