@automattic/charts 4.7.0 → 4.9.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 +30 -0
- package/TOKENS.md +6 -3
- package/dist/index.cjs +834 -321
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +68 -17
- package/dist/index.d.cts +102 -28
- package/dist/index.d.ts +102 -28
- package/dist/index.js +838 -327
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/area-chart/area-chart.tsx +7 -4
- package/src/charts/area-chart/test/area-chart.test.tsx +24 -15
- package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
- package/src/charts/area-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/area-chart/types.ts +2 -0
- package/src/charts/bar-chart/bar-chart.module.scss +7 -0
- package/src/charts/bar-chart/bar-chart.tsx +25 -16
- package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
- package/src/charts/bar-chart/test/bar-chart.test.tsx +106 -62
- package/src/charts/bar-chart/types.ts +2 -0
- package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +3 -1
- 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 +16 -8
- package/src/charts/heatmap-chart/heatmap-chart.tsx +183 -66
- package/src/charts/heatmap-chart/private/column-groups.ts +79 -8
- package/src/charts/heatmap-chart/private/fit-compact-cells.ts +72 -0
- package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +15 -3
- package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
- package/src/charts/heatmap-chart/private/index.ts +6 -3
- package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
- package/src/charts/heatmap-chart/test/column-groups.test.ts +41 -4
- package/src/charts/heatmap-chart/test/fit-compact-cells.test.ts +59 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +231 -8
- 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 +6 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +46 -4
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -1
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +53 -0
- package/src/charts/leaderboard-chart/types.ts +6 -1
- package/src/charts/line-chart/line-chart.tsx +7 -4
- package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +19 -9
- package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +1 -1
- package/src/charts/line-chart/test/line-chart.test.tsx +45 -35
- package/src/charts/line-chart/test/tooltip-resolution.test.tsx +2 -2
- package/src/charts/line-chart/types.ts +5 -3
- package/src/charts/pie-chart/pie-chart.tsx +153 -143
- package/src/charts/pie-chart/test/pie-chart.test.tsx +171 -0
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +126 -124
- package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +55 -1
- package/src/charts/private/chart-composition/use-chart-children.ts +11 -6
- package/src/charts/private/pie-keyboard-navigation/index.ts +4 -0
- package/src/charts/private/pie-keyboard-navigation/order-arcs-for-navigation.ts +10 -0
- package/src/charts/private/pie-keyboard-navigation/pie-keyboard-navigation.module.scss +16 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-announcement.tsx +14 -0
- package/src/charts/private/pie-keyboard-navigation/pie-selection-output.tsx +82 -0
- package/src/charts/private/pie-keyboard-navigation/pie-tooltip.tsx +44 -0
- package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +47 -0
- package/src/charts/private/pie-keyboard-navigation/test/order-arcs-for-navigation.test.ts +34 -0
- package/src/charts/private/pie-keyboard-navigation/test/selected-segment-ring.test.ts +14 -0
- package/src/charts/private/pie-keyboard-navigation/use-pie-keyboard-navigation.ts +139 -0
- package/src/charts/private/test/label-text-color.test.ts +6 -8
- package/src/charts/private/x-zoom/test/x-zoom.test.tsx +3 -2
- 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 +283 -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/types.ts +18 -2
- 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
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
// module, so it needs no runtime cascade and never lands on the DOM.
|
|
4
4
|
$focus-ring-width: var(--wpds-border-width-focus);
|
|
5
5
|
|
|
6
|
+
$heatmap-empty-fill: var(--a8c-charts-color-track);
|
|
7
|
+
|
|
8
|
+
// JS sets the ends (getHeatmapScale): 3:1 against background and empty cell at
|
|
9
|
+
// the low end. Unresolved, the floor is 15% of the primary.
|
|
10
|
+
$heatmap-fill: color-mix(in sRGB, var(--a8c-charts-color-heatmap-high, var(--a8c-charts-color-heatmap-primary)) calc(var(--a8c-charts-heatmap-cell-intensity) * 100%), var(--a8c-charts-color-heatmap-low, color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) 15%, var(--a8c-charts-color-background, #fff))));
|
|
11
|
+
|
|
6
12
|
.heatmap-chart {
|
|
7
13
|
position: relative;
|
|
8
14
|
// Keeps the tooltip's z-index inside the chart, as `.xy-plot` does.
|
|
@@ -92,9 +98,9 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
92
98
|
|
|
93
99
|
// Beneath its columns, set off from the last row by more than the cell gap.
|
|
94
100
|
.heatmap-chart__group-label {
|
|
95
|
-
color: var(--a8c-charts-color-label
|
|
96
|
-
font-size: var(--wpds-typography-font-size-
|
|
97
|
-
line-height: var(--wpds-typography-line-height-
|
|
101
|
+
color: var(--a8c-charts-color-label);
|
|
102
|
+
font-size: var(--wpds-typography-font-size-md);
|
|
103
|
+
line-height: var(--wpds-typography-line-height-sm);
|
|
98
104
|
white-space: nowrap;
|
|
99
105
|
text-align: center;
|
|
100
106
|
padding-block-start: var(--wpds-dimension-padding-xs);
|
|
@@ -115,7 +121,7 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
115
121
|
min-height: 0;
|
|
116
122
|
border-radius: var(--a8c-charts-border-radius-cell);
|
|
117
123
|
// Empty (no-data) default; cells with a value override it below.
|
|
118
|
-
background:
|
|
124
|
+
background: $heatmap-empty-fill;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
// An empty grid slot (calendar ragged edges): keeps its track but paints and
|
|
@@ -165,10 +171,8 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
165
171
|
margin-inline-end: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs));
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
// Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
|
|
169
|
-
// an empty cell.
|
|
170
174
|
.heatmap-chart__cell--filled {
|
|
171
|
-
background:
|
|
175
|
+
background: $heatmap-fill;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
.heatmap-chart__cell--selected {
|
|
@@ -204,7 +208,11 @@ $focus-ring-width: var(--wpds-border-width-focus);
|
|
|
204
208
|
width: var(--wpds-dimension-size-3xs);
|
|
205
209
|
height: var(--wpds-dimension-size-3xs);
|
|
206
210
|
border-radius: var(--a8c-charts-border-radius-cell);
|
|
207
|
-
background:
|
|
211
|
+
background: $heatmap-fill;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.heatmap-chart__legend-swatch--empty {
|
|
215
|
+
background: $heatmap-empty-fill;
|
|
208
216
|
}
|
|
209
217
|
|
|
210
218
|
// One band: the steps butt against each other and only the band's ends round.
|
|
@@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import isEqual from 'fast-deep-equal';
|
|
6
6
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { flushSync } from 'react-dom';
|
|
7
8
|
import { BoundedTooltip, TOOLTIP_Z_INDEX } from '../../components/tooltip/private/bounded-tooltip';
|
|
8
9
|
import { useIsomorphicLayoutEffect } from '../../hooks';
|
|
9
10
|
import {
|
|
@@ -13,12 +14,17 @@ import {
|
|
|
13
14
|
useGlobalChartsContext,
|
|
14
15
|
GlobalChartsContext,
|
|
15
16
|
} from '../../providers';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
BACKGROUND_FALLBACK,
|
|
19
|
+
CATALOG_POINTERS,
|
|
20
|
+
} from '../../providers/chart-context/private/catalog-pointers';
|
|
17
21
|
import { blendRgb, hexToRgb } from '../../providers/chart-context/private/perceptual-color';
|
|
22
|
+
import { resolveOpaqueHex } from '../../providers/chart-context/private/resolve-opaque-hex';
|
|
18
23
|
import { useStandaloneScopeClass } from '../../providers/chart-scope';
|
|
19
24
|
import { attachSubComponents } from '../../utils';
|
|
20
25
|
import { isValidHexColor, normalizeColorToHex } from '../../utils/color-utils';
|
|
21
|
-
import { createCssVariableResolver
|
|
26
|
+
import { createCssVariableResolver } from '../../utils/resolve-css-var';
|
|
27
|
+
import { warnOnce } from '../../utils/warn-once';
|
|
22
28
|
import { Center } from '../private/center';
|
|
23
29
|
import { useChartChildren } from '../private/chart-composition';
|
|
24
30
|
import { ChartInstanceContext } from '../private/chart-instance-context';
|
|
@@ -27,12 +33,16 @@ import { pickLabelTextColorForFill, resolveLabelRoles } from '../private/label-t
|
|
|
27
33
|
import { withResponsive } from '../private/with-responsive';
|
|
28
34
|
import styles from './heatmap-chart.module.scss';
|
|
29
35
|
import {
|
|
36
|
+
fitCompactCells,
|
|
37
|
+
getHeatmapScale,
|
|
30
38
|
getValueExtent,
|
|
31
39
|
getNormalizedValue,
|
|
32
40
|
HeatmapContext,
|
|
33
41
|
HeatmapLegend,
|
|
42
|
+
isEmptyValue,
|
|
34
43
|
isPresent,
|
|
35
44
|
resolveColumnGroups,
|
|
45
|
+
wrapColumnGroups,
|
|
36
46
|
} from './private';
|
|
37
47
|
import {
|
|
38
48
|
firstCalendarCell,
|
|
@@ -41,17 +51,13 @@ import {
|
|
|
41
51
|
stepCalendarCell,
|
|
42
52
|
stepGridCell,
|
|
43
53
|
} from './private/keyboard-navigation';
|
|
44
|
-
import type { HeatmapContextValue } from './private';
|
|
54
|
+
import type { CompactCellFit, HeatmapContextValue } from './private';
|
|
45
55
|
import type { CellBlock, CellPosition } from './private/keyboard-navigation';
|
|
46
56
|
import type { HeatmapChartProps, HeatmapTooltipData } from './types';
|
|
47
57
|
import type { LabelRoles, LabelTextColor } from '../private/label-text-color';
|
|
48
58
|
import type { ResponsiveConfig } from '../private/with-responsive';
|
|
49
59
|
import type { CSSProperties, FC } from 'react';
|
|
50
60
|
|
|
51
|
-
// Mirrors the color-mix floor in heatmap-chart.module.scss (.heatmap-chart__cell--filled):
|
|
52
|
-
// the rendered fill is the primary mixed over the chart background at 0.15 + 0.85 * intensity.
|
|
53
|
-
const CELL_MIX_FLOOR = 0.15;
|
|
54
|
-
|
|
55
61
|
// `label` is the stylesheet's default, so it needs no modifier.
|
|
56
62
|
const CELL_VALUE_MODIFIER: Record< LabelTextColor, string | undefined > = {
|
|
57
63
|
label: undefined,
|
|
@@ -66,6 +72,27 @@ const NO_ROW_LABELS: string[] = [];
|
|
|
66
72
|
|
|
67
73
|
const TOOLTIP_BOX_STYLE: CSSProperties = { zIndex: TOOLTIP_Z_INDEX };
|
|
68
74
|
|
|
75
|
+
const largest = ( root: Element, selector: string, size: 'offsetWidth' | 'offsetHeight' ) =>
|
|
76
|
+
Math.max(
|
|
77
|
+
0,
|
|
78
|
+
...Array.from( root.querySelectorAll< HTMLElement >( selector ), el => el[ size ] )
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// The box the grid may fill: the chart's own box less its other in-flow children
|
|
82
|
+
// (the legend, say) and the gaps between them.
|
|
83
|
+
const gridBox = ( chart: HTMLElement, grid: HTMLElement ) => {
|
|
84
|
+
let height = chart.clientHeight;
|
|
85
|
+
const rowGap = parseFloat( getComputedStyle( chart ).rowGap ) || 0;
|
|
86
|
+
Array.from( chart.children ).forEach( child => {
|
|
87
|
+
const { display, position } = getComputedStyle( child );
|
|
88
|
+
if ( child === grid || display === 'none' || position === 'absolute' || position === 'fixed' ) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
height -= ( child as HTMLElement ).offsetHeight + rowGap;
|
|
92
|
+
} );
|
|
93
|
+
return { width: chart.clientWidth, height };
|
|
94
|
+
};
|
|
95
|
+
|
|
69
96
|
// The cell's own label wins; otherwise the group, column and row labels name it.
|
|
70
97
|
const cellName = ( info: HeatmapTooltipData ) =>
|
|
71
98
|
info.cellLabel ||
|
|
@@ -78,6 +105,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
78
105
|
height = 0,
|
|
79
106
|
className,
|
|
80
107
|
compact = false,
|
|
108
|
+
fitCells = false,
|
|
81
109
|
showValues,
|
|
82
110
|
maxCellWidth,
|
|
83
111
|
maxCellHeight,
|
|
@@ -108,16 +136,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
108
136
|
useTooltip< HeatmapTooltipData >();
|
|
109
137
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
110
138
|
const containerRef = useRef< HTMLDivElement >( null );
|
|
139
|
+
const chartRef = useRef< HTMLDivElement >( null );
|
|
111
140
|
const [ labelRoles, setLabelRoles ] = useState< LabelRoles | null >( null );
|
|
112
|
-
// The chart root positions the tooltip, so pointer and cell coordinates are
|
|
113
|
-
// measured against it — found by its id rather than by walking up, so
|
|
114
|
-
// whatever ChartLayout wraps the grid in cannot shift the origin.
|
|
115
141
|
const getTooltipOrigin = useCallback(
|
|
116
|
-
() =>
|
|
117
|
-
|
|
118
|
-
?.closest( `[data-chart-id="heatmap-chart-${ chartId }"]` )
|
|
119
|
-
?.getBoundingClientRect() ?? null,
|
|
120
|
-
[ chartId ]
|
|
142
|
+
() => chartRef.current?.getBoundingClientRect() ?? null,
|
|
143
|
+
[]
|
|
121
144
|
);
|
|
122
145
|
|
|
123
146
|
const { color: primaryColorHex } = getElementStyles( {
|
|
@@ -125,29 +148,37 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
125
148
|
overrideColor: primaryColor,
|
|
126
149
|
} );
|
|
127
150
|
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
151
|
+
// Both read at the scope element, so an override on the chart's own class reaches neither the
|
|
152
|
+
// fill scale nor the text color (CHARTS-255). See-through counts as white, as for the palette.
|
|
153
|
+
const resolveAtScope = createCssVariableResolver( scopeElement );
|
|
154
|
+
const chartBackgroundHex =
|
|
155
|
+
resolveOpaqueHex( CATALOG_POINTERS.background, resolveAtScope ) ?? BACKGROUND_FALLBACK;
|
|
156
|
+
const emptyCellHex = resolveOpaqueHex( CATALOG_POINTERS.track, resolveAtScope );
|
|
135
157
|
|
|
136
|
-
//
|
|
137
|
-
// If either color cannot resolve to hex, keep the stylesheet's default role.
|
|
158
|
+
// If the primary cannot resolve to hex, the stylesheet falls back to its own blend.
|
|
138
159
|
const primaryHex = normalizeColorToHex( primaryColorHex );
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
const scale = useMemo(
|
|
161
|
+
() =>
|
|
162
|
+
isValidHexColor( primaryHex )
|
|
163
|
+
? getHeatmapScale( primaryHex, chartBackgroundHex, emptyCellHex )
|
|
164
|
+
: null,
|
|
165
|
+
[ primaryHex, chartBackgroundHex, emptyCellHex ]
|
|
166
|
+
);
|
|
167
|
+
const fillVars = useMemo(
|
|
168
|
+
() => ( {
|
|
169
|
+
'--a8c-charts-color-heatmap-primary': primaryColorHex,
|
|
170
|
+
...( scale && {
|
|
171
|
+
'--a8c-charts-color-heatmap-low': scale.low,
|
|
172
|
+
'--a8c-charts-color-heatmap-high': scale.high,
|
|
173
|
+
} ),
|
|
174
|
+
} ),
|
|
175
|
+
[ primaryColorHex, scale ]
|
|
176
|
+
);
|
|
177
|
+
// Choose text color from the fill the cell paints, mirroring .heatmap-chart__cell--filled.
|
|
143
178
|
const cellTextColor = ( intensity: number ): LabelTextColor =>
|
|
144
|
-
|
|
179
|
+
scale
|
|
145
180
|
? pickLabelTextColorForFill(
|
|
146
|
-
blendRgb(
|
|
147
|
-
cellMix.primary,
|
|
148
|
-
cellMix.background,
|
|
149
|
-
CELL_MIX_FLOOR + ( 1 - CELL_MIX_FLOOR ) * intensity
|
|
150
|
-
),
|
|
181
|
+
blendRgb( hexToRgb( scale.high ), hexToRgb( scale.low ), intensity ),
|
|
151
182
|
labelRoles,
|
|
152
183
|
'label'
|
|
153
184
|
)
|
|
@@ -165,8 +196,8 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
165
196
|
|
|
166
197
|
const extent = useMemo( () => getValueExtent( data ), [ data ] );
|
|
167
198
|
const heatmapContext = useMemo< HeatmapContextValue >(
|
|
168
|
-
() => ( { extent,
|
|
169
|
-
[ extent,
|
|
199
|
+
() => ( { extent, fillVars } ),
|
|
200
|
+
[ extent, fillVars ]
|
|
170
201
|
);
|
|
171
202
|
|
|
172
203
|
const columns = data.length;
|
|
@@ -179,6 +210,67 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
179
210
|
|
|
180
211
|
const { compactCellGap, compactCellSize, groupGap } = heatmapChartSettings;
|
|
181
212
|
const drawValues = showValues ?? ! compact;
|
|
213
|
+
const hasColumnLabels = data.some( column => Boolean( column.label ) );
|
|
214
|
+
const hasGroups = groupLayout.groups.length > 0;
|
|
215
|
+
|
|
216
|
+
const [ fit, setFit ] = useState< CompactCellFit | null >( null );
|
|
217
|
+
// A summary track is as wide as its figure, which the fit cannot size as a cell.
|
|
218
|
+
const hasSummary = data.some( column => column.summary );
|
|
219
|
+
const fitting = compact && fitCells && ! hasSummary;
|
|
220
|
+
if ( fitCells && ! fitting ) {
|
|
221
|
+
warnOnce(
|
|
222
|
+
'heatmap:fitCells',
|
|
223
|
+
'fitCells applies only in compact mode without summary columns, so the cells keep their size.'
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
useIsomorphicLayoutEffect( () => {
|
|
227
|
+
const chart = chartRef.current;
|
|
228
|
+
const grid = containerRef.current;
|
|
229
|
+
if ( ! fitting || ! chart || ! grid ) {
|
|
230
|
+
setFit( null );
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const measure = () => {
|
|
234
|
+
const next = fitCompactCells( {
|
|
235
|
+
...gridBox( chart, grid ),
|
|
236
|
+
layout: groupLayout,
|
|
237
|
+
rows,
|
|
238
|
+
cellGap: compactCellGap,
|
|
239
|
+
groupGap,
|
|
240
|
+
rowLabelWidth: largest( grid, `.${ styles[ 'heatmap-chart__row-label' ] }`, 'offsetWidth' ),
|
|
241
|
+
columnLabelHeight: hasColumnLabels
|
|
242
|
+
? largest( grid, `.${ styles[ 'heatmap-chart__col-label' ] }`, 'offsetHeight' )
|
|
243
|
+
: null,
|
|
244
|
+
groupLabelHeight: hasGroups
|
|
245
|
+
? largest( grid, `.${ styles[ 'heatmap-chart__group-label' ] }`, 'offsetHeight' )
|
|
246
|
+
: null,
|
|
247
|
+
minCellSize: compactCellSize,
|
|
248
|
+
} );
|
|
249
|
+
setFit( previous => ( isEqual( previous, next ) ? previous : next ) );
|
|
250
|
+
};
|
|
251
|
+
measure();
|
|
252
|
+
if ( typeof ResizeObserver === 'undefined' ) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// Commit before later observers in this delivery read the grid, or they see the
|
|
256
|
+
// old cells at the new width, and before paint, so that frame is never drawn.
|
|
257
|
+
const observer = new ResizeObserver( () => flushSync( measure ) );
|
|
258
|
+
observer.observe( chart );
|
|
259
|
+
return () => observer.disconnect();
|
|
260
|
+
}, [
|
|
261
|
+
fitting,
|
|
262
|
+
groupLayout,
|
|
263
|
+
rows,
|
|
264
|
+
hasColumnLabels,
|
|
265
|
+
hasGroups,
|
|
266
|
+
compactCellGap,
|
|
267
|
+
compactCellSize,
|
|
268
|
+
groupGap,
|
|
269
|
+
] );
|
|
270
|
+
const wrapped = useMemo(
|
|
271
|
+
() => ( fit ? wrapColumnGroups( groupLayout, fit.bands ) : null ),
|
|
272
|
+
[ fit, groupLayout ]
|
|
273
|
+
);
|
|
182
274
|
|
|
183
275
|
const buildTooltipData = useCallback(
|
|
184
276
|
( columnIndex: number, rowIndex: number ): HeatmapTooltipData => {
|
|
@@ -366,12 +458,18 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
366
458
|
const rowTrack = compact
|
|
367
459
|
? 'var(--a8c-charts-dimension-heatmap-cell-size)'
|
|
368
460
|
: `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
|
|
369
|
-
const hasColumnLabels = data.some( column => Boolean( column.label ) );
|
|
370
|
-
const hasGroups = groupLayout.groups.length > 0;
|
|
371
461
|
// Every item is placed by hand rather than auto-flowed, so the template can
|
|
372
462
|
// carry gap tracks that hold no cell.
|
|
373
|
-
const
|
|
374
|
-
const
|
|
463
|
+
const placed = wrapped ?? groupLayout;
|
|
464
|
+
const columnLine = ( columnIndex: number ) => placed.columns[ columnIndex ].line;
|
|
465
|
+
const bandOf = ( columnIndex: number ) => placed.columns[ columnIndex ].band;
|
|
466
|
+
const { bands } = placed;
|
|
467
|
+
// Each band repeats the label row, the data rows and the group-label row, and
|
|
468
|
+
// the next starts one group gap below.
|
|
469
|
+
const bandRowCount = ( hasColumnLabels ? 1 : 0 ) + rows + ( hasGroups ? 1 : 0 ) + 1;
|
|
470
|
+
const labelRowOf = ( band: number ) => 1 + band * bandRowCount;
|
|
471
|
+
const dataRowOf = ( band: number, rowIndex: number ) =>
|
|
472
|
+
labelRowOf( band ) + ( hasColumnLabels ? 1 : 0 ) + rowIndex;
|
|
375
473
|
// A summary column takes a content-sized track: a roll-up is wider than a
|
|
376
474
|
// cell, and a shared track would stretch every cell to fit it. `max-content`
|
|
377
475
|
// as the max keeps the leftover width out of it once the data tracks hit
|
|
@@ -382,23 +480,33 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
382
480
|
// minmax track the way it widens the summary's auto track. Compact cells
|
|
383
481
|
// are fixed, so there the gaps share the leftover width instead.
|
|
384
482
|
const gapTrack = compact ? `minmax(${ groupGap }px, 1fr)` : `${ groupGap }px`;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
483
|
+
// Wrapped bands share one set of slots, each as wide as its widest group.
|
|
484
|
+
const columnTracks = wrapped
|
|
485
|
+
? wrapped.slotSpans
|
|
486
|
+
.map( ( span, slot ) =>
|
|
487
|
+
[ ...( slot > 0 ? [ gapTrack ] : [] ), ...Array( span ).fill( columnTrack ) ].join( ' ' )
|
|
488
|
+
)
|
|
489
|
+
.join( ' ' )
|
|
490
|
+
: data
|
|
491
|
+
.map( ( column, columnIndex ) =>
|
|
492
|
+
groupLayout.columns[ columnIndex ].gapBefore
|
|
493
|
+
? `${ gapTrack } ${ dataTrack( column ) }`
|
|
494
|
+
: dataTrack( column )
|
|
495
|
+
)
|
|
496
|
+
.join( ' ' );
|
|
497
|
+
const bandTracks = `${ hasColumnLabels ? 'auto ' : '' }repeat(${ rows }, ${ rowTrack })${
|
|
498
|
+
hasGroups ? ' auto' : ''
|
|
499
|
+
}`;
|
|
392
500
|
const gridStyle: Record< string, string | number > = {
|
|
393
|
-
|
|
501
|
+
...fillVars,
|
|
394
502
|
gridTemplateColumns: `auto ${ columnTracks }`,
|
|
395
|
-
gridTemplateRows:
|
|
396
|
-
hasGroups ? ' auto' : ''
|
|
397
|
-
}`,
|
|
503
|
+
gridTemplateRows: Array( bands ).fill( bandTracks ).join( ` ${ groupGap }px ` ),
|
|
398
504
|
};
|
|
399
505
|
if ( compact ) {
|
|
400
506
|
gridStyle[ '--a8c-charts-dimension-heatmap-cell-gap' ] = `${ compactCellGap }px`;
|
|
401
|
-
gridStyle[ '--a8c-charts-dimension-heatmap-cell-size' ] = `${
|
|
507
|
+
gridStyle[ '--a8c-charts-dimension-heatmap-cell-size' ] = `${
|
|
508
|
+
fit?.cellSize ?? compactCellSize
|
|
509
|
+
}px`;
|
|
402
510
|
}
|
|
403
511
|
|
|
404
512
|
// A summary column sits one gap apart from the data on either side; two
|
|
@@ -435,6 +543,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
435
543
|
legendChildren={ [] }
|
|
436
544
|
trailingContent={ nonLegendChildren }
|
|
437
545
|
gap={ gap }
|
|
546
|
+
rootRef={ chartRef }
|
|
438
547
|
className={ clsx( 'heatmap-chart', styles[ 'heatmap-chart' ], className, {
|
|
439
548
|
[ styles[ 'heatmap-chart--height-capped' ] ]: heightCapped,
|
|
440
549
|
} ) }
|
|
@@ -465,11 +574,14 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
465
574
|
{ /* Decorative: cell aria-labels already carry the column name. */ }
|
|
466
575
|
{ hasColumnLabels && (
|
|
467
576
|
<div role="row" aria-hidden="true" className={ styles[ 'heatmap-chart__row' ] }>
|
|
468
|
-
<span style={ { gridColumn: 1, gridRow:
|
|
577
|
+
<span style={ { gridColumn: 1, gridRow: labelRowOf( 0 ) } } />
|
|
469
578
|
{ data.map( ( column, columnIndex ) => (
|
|
470
579
|
<span
|
|
471
580
|
key={ `col-${ columnIndex }` }
|
|
472
|
-
style={ {
|
|
581
|
+
style={ {
|
|
582
|
+
gridColumn: columnLine( columnIndex ),
|
|
583
|
+
gridRow: labelRowOf( bandOf( columnIndex ) ),
|
|
584
|
+
} }
|
|
473
585
|
className={ clsx( styles[ 'heatmap-chart__col-label' ], {
|
|
474
586
|
[ styles[ 'heatmap-chart__col-label--summary' ] ]: column.summary,
|
|
475
587
|
...summaryGaps( columnIndex ),
|
|
@@ -483,7 +595,6 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
483
595
|
|
|
484
596
|
{ Array.from( { length: rows } ).map( ( _row, rowIndex ) => {
|
|
485
597
|
const labelVisible = ! compact || rowIndex % 2 === 0;
|
|
486
|
-
const gridRow = firstDataRow + rowIndex;
|
|
487
598
|
return (
|
|
488
599
|
<div
|
|
489
600
|
key={ `row-${ rowIndex }` }
|
|
@@ -491,16 +602,22 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
491
602
|
aria-rowindex={ rowIndex + 1 }
|
|
492
603
|
className={ styles[ 'heatmap-chart__row' ] }
|
|
493
604
|
>
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
605
|
+
{ Array.from( { length: bands } ).map( ( _band, band ) => (
|
|
606
|
+
<span
|
|
607
|
+
key={ `row-label-${ band }` }
|
|
608
|
+
aria-hidden="true"
|
|
609
|
+
className={ styles[ 'heatmap-chart__row-label' ] }
|
|
610
|
+
style={ { gridColumn: 1, gridRow: dataRowOf( band, rowIndex ) } }
|
|
611
|
+
>
|
|
612
|
+
{ labelVisible ? ( rowLabels[ rowIndex ] ?? '' ) : '' }
|
|
613
|
+
</span>
|
|
614
|
+
) ) }
|
|
501
615
|
{ data.map( ( column, columnIndex ) => {
|
|
502
616
|
const cell = column.data[ rowIndex ];
|
|
503
|
-
const placement = {
|
|
617
|
+
const placement = {
|
|
618
|
+
gridColumn: columnLine( columnIndex ),
|
|
619
|
+
gridRow: dataRowOf( bandOf( columnIndex ), rowIndex ),
|
|
620
|
+
};
|
|
504
621
|
|
|
505
622
|
// A hidden cell keeps its grid slot (so the rest of the
|
|
506
623
|
// column doesn't shift) but paints nothing and takes no
|
|
@@ -542,7 +659,7 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
542
659
|
const value = cell?.value ?? null;
|
|
543
660
|
const present = isPresent( value );
|
|
544
661
|
// A summary cell is on another scale, so it takes no fill.
|
|
545
|
-
const filled = present && ! column.summary;
|
|
662
|
+
const filled = present && ! column.summary && ! isEmptyValue( value, extent );
|
|
546
663
|
const normalized = filled ? getNormalizedValue( value, extent ) : 0;
|
|
547
664
|
const info = buildTooltipData( columnIndex, rowIndex );
|
|
548
665
|
const accessibleLabel = `${ cellName( info ) }: ${
|
|
@@ -601,14 +718,14 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
|
|
|
601
718
|
} ) }
|
|
602
719
|
{ hasGroups && (
|
|
603
720
|
<div role="row" aria-hidden="true" className={ styles[ 'heatmap-chart__row' ] }>
|
|
604
|
-
{
|
|
721
|
+
{ placed.groups.map( ( group, groupIndex ) => (
|
|
605
722
|
<span
|
|
606
723
|
key={ `group-${ groupIndex }` }
|
|
607
724
|
data-testid="heatmap-group-label"
|
|
608
725
|
className={ styles[ 'heatmap-chart__group-label' ] }
|
|
609
726
|
style={ {
|
|
610
727
|
gridColumn: `${ group.line } / span ${ group.span }`,
|
|
611
|
-
gridRow:
|
|
728
|
+
gridRow: dataRowOf( group.band, rows ),
|
|
612
729
|
} }
|
|
613
730
|
>
|
|
614
731
|
{ group.label }
|
|
@@ -2,10 +2,12 @@ import { warnOnce } from '../../../utils/warn-once';
|
|
|
2
2
|
import type { HeatmapColumnGroup } from '../types';
|
|
3
3
|
|
|
4
4
|
export type ColumnGroupLayout = {
|
|
5
|
-
/** One per data column: its grid column line, whether a gap track precedes it, and its group's index. */
|
|
6
|
-
columns: { line: number; gapBefore: boolean; group?: number }[];
|
|
7
|
-
/** The groups as given, each with the grid column line its label
|
|
8
|
-
groups: ( HeatmapColumnGroup & { line: number } )[];
|
|
5
|
+
/** One per data column: its grid column line and band, whether a gap track precedes it, and its group's index. */
|
|
6
|
+
columns: { line: number; band: number; gapBefore: boolean; group?: number }[];
|
|
7
|
+
/** The groups as given, each with the grid column line and band its label sits on; none when any was unusable. */
|
|
8
|
+
groups: ( HeatmapColumnGroup & { line: number; band: number } )[];
|
|
9
|
+
/** Rows of groups the columns are laid out on. */
|
|
10
|
+
bands: number;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
const isSpan = ( span: number ) => Number.isInteger( span ) && span > 0;
|
|
@@ -26,9 +28,11 @@ export const resolveColumnGroups = (
|
|
|
26
28
|
const ungrouped = (): ColumnGroupLayout => ( {
|
|
27
29
|
columns: Array.from( { length: columnCount }, ( _, index ) => ( {
|
|
28
30
|
line: firstLine + index,
|
|
31
|
+
band: 0,
|
|
29
32
|
gapBefore: false,
|
|
30
33
|
} ) ),
|
|
31
34
|
groups: [],
|
|
35
|
+
bands: 1,
|
|
32
36
|
} );
|
|
33
37
|
|
|
34
38
|
// An empty grid draws nothing, so static groups awaiting data are not a misuse.
|
|
@@ -55,17 +59,84 @@ export const resolveColumnGroups = (
|
|
|
55
59
|
if ( groupIndex > 0 ) {
|
|
56
60
|
line += 1;
|
|
57
61
|
}
|
|
58
|
-
laidOut.push( { ...group, line } );
|
|
62
|
+
laidOut.push( { ...group, line, band: 0 } );
|
|
59
63
|
for ( let offset = 0; offset < group.span; offset++ ) {
|
|
60
|
-
columns.push( {
|
|
64
|
+
columns.push( {
|
|
65
|
+
line,
|
|
66
|
+
band: 0,
|
|
67
|
+
gapBefore: groupIndex > 0 && offset === 0,
|
|
68
|
+
group: groupIndex,
|
|
69
|
+
} );
|
|
61
70
|
line += 1;
|
|
62
71
|
}
|
|
63
72
|
} );
|
|
64
73
|
// An ungrouped tail opens no further gap.
|
|
65
74
|
while ( columns.length < columnCount ) {
|
|
66
|
-
columns.push( { line, gapBefore: false } );
|
|
75
|
+
columns.push( { line, band: 0, gapBefore: false } );
|
|
67
76
|
line += 1;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
return { columns, groups: laidOut };
|
|
79
|
+
return { columns, groups: laidOut, bands: 1 };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type WrappedColumnGroupLayout = ColumnGroupLayout & {
|
|
83
|
+
/** Columns per group slot: the widest group in that position on any band. */
|
|
84
|
+
slotSpans: number[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Wraps the groups onto bands of equal group count, each band starting back at
|
|
89
|
+
* the first data line so the groups line up in slots down the bands.
|
|
90
|
+
*
|
|
91
|
+
* @param layout - The unwrapped layout from `resolveColumnGroups`.
|
|
92
|
+
* @param bands - Bands to aim for; the groups split as evenly as whole bands allow.
|
|
93
|
+
* @return The wrapped layout, or null when there is nothing to wrap: one band, one group, or a column outside every group.
|
|
94
|
+
*/
|
|
95
|
+
export const wrapColumnGroups = (
|
|
96
|
+
layout: ColumnGroupLayout,
|
|
97
|
+
bands: number
|
|
98
|
+
): WrappedColumnGroupLayout | null => {
|
|
99
|
+
const { columns, groups } = layout;
|
|
100
|
+
if ( bands < 2 || groups.length < 2 || columns.some( column => column.group === undefined ) ) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const perBand = Math.ceil( groups.length / bands );
|
|
105
|
+
const slotSpans: number[] = Array( perBand ).fill( 0 );
|
|
106
|
+
groups.forEach( ( group, index ) => {
|
|
107
|
+
const slot = index % perBand;
|
|
108
|
+
slotSpans[ slot ] = Math.max( slotSpans[ slot ], group.span );
|
|
109
|
+
} );
|
|
110
|
+
const slotLines: number[] = [];
|
|
111
|
+
let line = columns[ 0 ].line;
|
|
112
|
+
slotSpans.forEach( ( span, slot ) => {
|
|
113
|
+
if ( slot > 0 ) {
|
|
114
|
+
line += 1;
|
|
115
|
+
}
|
|
116
|
+
slotLines.push( line );
|
|
117
|
+
line += span;
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
const wrappedGroups = groups.map( ( group, index ) => ( {
|
|
121
|
+
...group,
|
|
122
|
+
line: slotLines[ index % perBand ],
|
|
123
|
+
band: Math.floor( index / perBand ),
|
|
124
|
+
} ) );
|
|
125
|
+
const wrappedColumns = columns.map( column => {
|
|
126
|
+
const group = column.group as number;
|
|
127
|
+
const offset = column.line - groups[ group ].line;
|
|
128
|
+
return {
|
|
129
|
+
...column,
|
|
130
|
+
line: wrappedGroups[ group ].line + offset,
|
|
131
|
+
band: wrappedGroups[ group ].band,
|
|
132
|
+
gapBefore: offset === 0 && group % perBand > 0,
|
|
133
|
+
};
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
columns: wrappedColumns,
|
|
138
|
+
groups: wrappedGroups,
|
|
139
|
+
bands: Math.ceil( groups.length / perBand ),
|
|
140
|
+
slotSpans,
|
|
141
|
+
};
|
|
71
142
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { wrapColumnGroups } from './column-groups';
|
|
2
|
+
import type { ColumnGroupLayout } from './column-groups';
|
|
3
|
+
|
|
4
|
+
export type CompactCellFitInput = {
|
|
5
|
+
/** Width and height (px) the grid may take. */
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
/** The unwrapped layout; every band count is tried through `wrapColumnGroups` on it. */
|
|
9
|
+
layout: ColumnGroupLayout;
|
|
10
|
+
rows: number;
|
|
11
|
+
cellGap: number;
|
|
12
|
+
groupGap: number;
|
|
13
|
+
/** Label track and row sizes (px); null for a label row the grid does not draw. */
|
|
14
|
+
rowLabelWidth: number;
|
|
15
|
+
columnLabelHeight: number | null;
|
|
16
|
+
groupLabelHeight: number | null;
|
|
17
|
+
/** Smallest cell worth drawing; below it the grid stays on one band and overflows. */
|
|
18
|
+
minCellSize: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type CompactCellFit = { bands: number; cellSize: number };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Largest whole-pixel square cell, and the band count giving it, for a compact grid in a box.
|
|
25
|
+
*
|
|
26
|
+
* @param input - The box, the grid's layout, and its fixed gaps and label sizes.
|
|
27
|
+
* @return The fit; one band at `minCellSize` when no band count reaches it.
|
|
28
|
+
*/
|
|
29
|
+
export const fitCompactCells = ( input: CompactCellFitInput ): CompactCellFit => {
|
|
30
|
+
const { width, height, layout, rows, cellGap, groupGap, minCellSize } = input;
|
|
31
|
+
const columns = layout.columns.length;
|
|
32
|
+
if ( columns === 0 || rows === 0 ) {
|
|
33
|
+
return { bands: 1, cellSize: minCellSize };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const maxBands = Math.max( 1, layout.groups.length );
|
|
37
|
+
const labelHeights = ( input.columnLabelHeight ?? 0 ) + ( input.groupLabelHeight ?? 0 );
|
|
38
|
+
const bandTracks =
|
|
39
|
+
( input.columnLabelHeight === null ? 0 : 1 ) +
|
|
40
|
+
rows +
|
|
41
|
+
( input.groupLabelHeight === null ? 0 : 1 );
|
|
42
|
+
let best: CompactCellFit | null = null;
|
|
43
|
+
|
|
44
|
+
for ( let target = 1; target <= maxBands; target++ ) {
|
|
45
|
+
const wrapped = target > 1 ? wrapColumnGroups( layout, target ) : null;
|
|
46
|
+
const { bands } = wrapped ?? layout;
|
|
47
|
+
const cellColumns = wrapped
|
|
48
|
+
? wrapped.slotSpans.reduce( ( sum, span ) => sum + span, 0 )
|
|
49
|
+
: columns;
|
|
50
|
+
const gapTracks = wrapped
|
|
51
|
+
? wrapped.slotSpans.length - 1
|
|
52
|
+
: layout.columns.filter( column => column.gapBefore ).length;
|
|
53
|
+
// The row-label track, the cell tracks and the group-gap tracks, one cell gap apart.
|
|
54
|
+
const columnTracks = 1 + cellColumns + gapTracks;
|
|
55
|
+
const byWidth =
|
|
56
|
+
( width - input.rowLabelWidth - gapTracks * groupGap - ( columnTracks - 1 ) * cellGap ) /
|
|
57
|
+
cellColumns;
|
|
58
|
+
|
|
59
|
+
// Each band's tracks, plus one group-gap track between bands.
|
|
60
|
+
const rowTracks = bands * bandTracks + ( bands - 1 );
|
|
61
|
+
const fixedHeight =
|
|
62
|
+
bands * labelHeights + ( bands - 1 ) * groupGap + ( rowTracks - 1 ) * cellGap;
|
|
63
|
+
const byHeight = ( height - fixedHeight ) / ( bands * rows );
|
|
64
|
+
|
|
65
|
+
const cellSize = Math.floor( Math.min( byWidth, byHeight ) );
|
|
66
|
+
if ( cellSize >= minCellSize && ( ! best || cellSize > best.cellSize ) ) {
|
|
67
|
+
best = { bands, cellSize };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return best ?? { bands: 1, cellSize: minCellSize };
|
|
72
|
+
};
|
|
@@ -2,8 +2,8 @@ import { createContext } from 'react';
|
|
|
2
2
|
|
|
3
3
|
export type HeatmapContextValue = {
|
|
4
4
|
extent: [ number, number ];
|
|
5
|
-
/** The
|
|
6
|
-
|
|
5
|
+
/** The custom properties the fill reads, set on the grid and on each legend swatch. */
|
|
6
|
+
fillVars: Record< string, string >;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
/** Shared by the chart and legend without importing back from `heatmap-chart.tsx`. */
|