@czi-sds/mcp 0.16.0 → 0.17.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/README.md +2 -1
- package/data/component-docs/Accordion.md +291 -0
- package/data/component-docs/Autocomplete.md +525 -0
- package/data/component-docs/Banner.md +231 -0
- package/data/component-docs/Button.md +224 -0
- package/data/component-docs/ButtonDropdown.md +256 -0
- package/data/component-docs/ButtonGroup.md +320 -0
- package/data/component-docs/ButtonToggle.md +397 -0
- package/data/component-docs/Callout.md +268 -0
- package/data/component-docs/CellBasic.md +122 -0
- package/data/component-docs/CellComponent.md +63 -0
- package/data/component-docs/CellHeader.md +128 -0
- package/data/component-docs/ComplexFilter.md +294 -0
- package/data/component-docs/ContentCard.md +406 -0
- package/data/component-docs/Dialog.md +651 -0
- package/data/component-docs/Dropdown.md +155 -0
- package/data/component-docs/DropdownMenu.md +949 -0
- package/data/component-docs/HeatmapChart.md +782 -0
- package/data/component-docs/Hero.md +363 -0
- package/data/component-docs/Icon.md +283 -0
- package/data/component-docs/InputCheckbox.md +205 -0
- package/data/component-docs/InputDropdown.md +482 -0
- package/data/component-docs/InputRadio.md +173 -0
- package/data/component-docs/InputSearch.md +81 -0
- package/data/component-docs/InputSlider.md +189 -0
- package/data/component-docs/InputText.md +110 -0
- package/data/component-docs/InputToggle.md +141 -0
- package/data/component-docs/IntentMessage.md +293 -0
- package/data/component-docs/Legend.md +442 -0
- package/data/component-docs/Link.md +23 -0
- package/data/component-docs/List.md +300 -0
- package/data/component-docs/LoadingIndicator.md +198 -0
- package/data/component-docs/Menu.md +298 -0
- package/data/component-docs/MenuItem.md +100 -0
- package/data/component-docs/NavigationFooter.md +165 -0
- package/data/component-docs/NavigationHeader.md +500 -0
- package/data/component-docs/NavigationJumpTo.md +414 -0
- package/data/component-docs/Notification.md +288 -0
- package/data/component-docs/Pagination.md +67 -0
- package/data/component-docs/Panel.md +636 -0
- package/data/component-docs/Popover.md +448 -0
- package/data/component-docs/PreComposedTable.md +2947 -0
- package/data/component-docs/SegmentedControl.md +396 -0
- package/data/component-docs/StackedBarChart.md +475 -0
- package/data/component-docs/Table.md +2000 -0
- package/data/component-docs/TableHeader.md +53 -0
- package/data/component-docs/TableRow.md +74 -0
- package/data/component-docs/Tabs.md +455 -0
- package/data/component-docs/Tag.md +647 -0
- package/data/component-docs/TagFilter.md +131 -0
- package/data/component-docs/Tooltip.md +561 -0
- package/data/component-docs/TooltipCondensed.md +203 -0
- package/data/component-docs/TooltipTable.md +145 -0
- package/data/component-docs/index.json +262 -0
- package/data/component-list.json +2 -5
- package/data/component-props/Accordion.json +31 -11
- package/data/component-props/Alert.json +3 -3
- package/data/component-props/Autocomplete.json +150 -15
- package/data/component-props/Banner.json +31 -17
- package/data/component-props/Button.json +45 -20
- package/data/component-props/ButtonDropdown.json +36 -17
- package/data/component-props/ButtonDropdownLegacy.json +11 -11
- package/data/component-props/ButtonGroup.json +32 -11
- package/data/component-props/ButtonIcon.json +10 -10
- package/data/component-props/ButtonLegacy.json +19 -19
- package/data/component-props/ButtonToggle.json +32 -19
- package/data/component-props/ButtonToggleLegacy.json +11 -11
- package/data/component-props/Callout.json +58 -17
- package/data/component-props/CellBasic.json +94 -41
- package/data/component-props/CellComponent.json +23 -7
- package/data/component-props/CellHeader.json +62 -19
- package/data/component-props/ComplexFilter.json +52 -21
- package/data/component-props/ContentCard.json +70 -47
- package/data/component-props/Dialog.json +47 -14
- package/data/component-props/Dropdown.json +68 -31
- package/data/component-props/DropdownMenu.json +128 -23
- package/data/component-props/HeatmapChart.json +88 -12
- package/data/component-props/Hero.json +80 -39
- package/data/component-props/Icon.json +20 -9
- package/data/component-props/InputCheckbox.json +36 -14
- package/data/component-props/InputDropdown.json +75 -27
- package/data/component-props/InputRadio.json +31 -13
- package/data/component-props/InputSearch.json +41 -18
- package/data/component-props/InputSlider.json +46 -19
- package/data/component-props/InputText.json +26 -19
- package/data/component-props/InputToggle.json +26 -9
- package/data/component-props/IntentMessage.json +38 -9
- package/data/component-props/Link.json +13 -7
- package/data/component-props/LoadingIndicator.json +11 -3
- package/data/component-props/MenuItem.json +38 -19
- package/data/component-props/MenuSelect.json +8 -8
- package/data/component-props/NavigationFooter.json +49 -19
- package/data/component-props/NavigationHeader.json +150 -37
- package/data/component-props/NavigationJumpTo.json +33 -7
- package/data/component-props/Notification.json +39 -21
- package/data/component-props/Pagination.json +39 -11
- package/data/component-props/Panel.json +62 -9
- package/data/component-props/Popover.json +80 -7
- package/data/component-props/PreComposedTable.json +99 -27
- package/data/component-props/SegmentedControl.json +42 -3
- package/data/component-props/StackedBarChart.json +116 -42
- package/data/component-props/TableRow.json +38 -19
- package/data/component-props/Tabs.json +66 -9
- package/data/component-props/Tag.json +34 -17
- package/data/component-props/TagFilter.json +10 -3
- package/data/component-props/Tooltip.json +49 -18
- package/data/component-props/TooltipCondensed.json +18 -9
- package/data/component-props/TooltipTable.json +17 -7
- package/data/resources/{zeroheight-rules.md → documentation-rules.md} +43 -30
- package/data/resources/figma-rules.md +2 -2
- package/dist/stdio.js +112 -208
- package/package.json +9 -5
- package/data/component-props/ButtonDropdownV2.json +0 -34
- package/data/component-props/ButtonNew.json +0 -47
- package/data/component-props/ButtonToggleV2.json +0 -44
- package/data/component-props/ButtonV2.json +0 -43
- package/data/zeroheight-cache.json +0 -10259
- package/data/zeroheight-exports/Accordion.md +0 -774
- package/data/zeroheight-exports/Banner.md +0 -691
- package/data/zeroheight-exports/Borders.md +0 -117
- package/data/zeroheight-exports/Buttons.md +0 -1854
- package/data/zeroheight-exports/Callout.md +0 -931
- package/data/zeroheight-exports/Colors.md +0 -1755
- package/data/zeroheight-exports/Content-Card.md +0 -309
- package/data/zeroheight-exports/Contributing-to-SDS.md +0 -98
- package/data/zeroheight-exports/Control-Inputs.md +0 -2818
- package/data/zeroheight-exports/Corners.md +0 -104
- package/data/zeroheight-exports/Cover-page.md +0 -3
- package/data/zeroheight-exports/Dialog.md +0 -1367
- package/data/zeroheight-exports/Downloads.md +0 -236
- package/data/zeroheight-exports/Drop-Shadows.md +0 -79
- package/data/zeroheight-exports/Dropdown-Input.md +0 -1776
- package/data/zeroheight-exports/Dropdown-Menus.md +0 -913
- package/data/zeroheight-exports/Element-Status-Tracker.md +0 -267
- package/data/zeroheight-exports/Field-Inputs.md +0 -1405
- package/data/zeroheight-exports/Filters.md +0 -1111
- package/data/zeroheight-exports/Getting-Started.md +0 -1157
- package/data/zeroheight-exports/Hero.md +0 -414
- package/data/zeroheight-exports/Icons.md +0 -2067
- package/data/zeroheight-exports/Intent.md +0 -86
- package/data/zeroheight-exports/Introduction-to-Bases.md +0 -35
- package/data/zeroheight-exports/Introduction-to-Cells.md +0 -35
- package/data/zeroheight-exports/Introduction-to-DNA.md +0 -35
- package/data/zeroheight-exports/Introduction-to-Genes.md +0 -35
- package/data/zeroheight-exports/Introduction.md +0 -115
- package/data/zeroheight-exports/Link.md +0 -432
- package/data/zeroheight-exports/Lists.md +0 -986
- package/data/zeroheight-exports/Loading-Indicator.md +0 -41
- package/data/zeroheight-exports/Navigation.md +0 -1190
- package/data/zeroheight-exports/Notification.md +0 -1038
- package/data/zeroheight-exports/Panel.md +0 -1030
- package/data/zeroheight-exports/README.md +0 -122
- package/data/zeroheight-exports/Responsive-Design.md +0 -330
- package/data/zeroheight-exports/Search-Input.md +0 -91
- package/data/zeroheight-exports/Search.md +0 -822
- package/data/zeroheight-exports/Segmented-Control.md +0 -609
- package/data/zeroheight-exports/Spacing.md +0 -297
- package/data/zeroheight-exports/Table.md +0 -5345
- package/data/zeroheight-exports/Tabs.md +0 -1324
- package/data/zeroheight-exports/Tags.md +0 -1545
- package/data/zeroheight-exports/Text-Link.md +0 -268
- package/data/zeroheight-exports/Theming.md +0 -362
- package/data/zeroheight-exports/Tooltips.md +0 -2015
- package/data/zeroheight-exports/Typography.md +0 -800
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# HeatmapChart
|
|
2
|
+
|
|
3
|
+
A grid of cells coloured by value, for matrices too large to read as a table.
|
|
4
|
+
|
|
5
|
+
> **Ships separately:** HeatmapChart comes from `@czi-sds/data-viz`, not `@czi-sds/components`, and it needs ECharts alongside it. See the Data Viz overview for installation and peer dependencies.
|
|
6
|
+
|
|
7
|
+
## Source Code
|
|
8
|
+
|
|
9
|
+
The component's source code in the SDS codebase can be found [here](https://github.com/chanzuckerberg/sci-components/blob/main/packages/data-viz/src/core/HeatmapChart/index.tsx).
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
**React TypeScript**
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Code examples
|
|
20
|
+
|
|
21
|
+
### **Default HeatmapChart**
|
|
22
|
+
|
|
23
|
+
The smallest complete heatmap: cells, two axes, a size, and an `itemStyle` that turns each value into a color.
|
|
24
|
+
|
|
25
|
+
**Example: DefaultHeatmapChart**
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// A heatmap needs four things: the cells, the two axes they are indexed
|
|
29
|
+
// against, and pixel dimensions for the canvas they are drawn on. Each cell is
|
|
30
|
+
// an object of whatever shape suits the data, and encode says which of its
|
|
31
|
+
// fields places it on each axis. Here they are x and y, but they could just as
|
|
32
|
+
// well be geneIndex and cellTypeIndex.
|
|
33
|
+
//
|
|
34
|
+
// Nothing about a cell's value reaches the chart on its own. The value becomes
|
|
35
|
+
// visible because itemStyle colors each symbol from it, which is also why the
|
|
36
|
+
// scale below is the example's own rather than something the chart provides.
|
|
37
|
+
|
|
38
|
+
import { type ScatterSeriesOption } from "echarts";
|
|
39
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
40
|
+
|
|
41
|
+
const COLUMNS = 12;
|
|
42
|
+
const ROWS = 8;
|
|
43
|
+
const CELL_PX = 24;
|
|
44
|
+
|
|
45
|
+
interface Cell {
|
|
46
|
+
value: number;
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DATA: Cell[] = [];
|
|
52
|
+
for (let x = 0; x < COLUMNS; x += 1) {
|
|
53
|
+
for (let y = 0; y < ROWS; y += 1) {
|
|
54
|
+
DATA.push({
|
|
55
|
+
value: Math.round(50 + 50 * Math.sin(x / 2.5) * Math.cos(y / 3)),
|
|
56
|
+
x,
|
|
57
|
+
y,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A sequential scale, low to high. ECharts has no opinion about this.
|
|
63
|
+
const SCALE = [
|
|
64
|
+
"#eff6fc",
|
|
65
|
+
"#cfe3f6",
|
|
66
|
+
"#a5cbee",
|
|
67
|
+
"#74aee3",
|
|
68
|
+
"#4a8fd4",
|
|
69
|
+
"#2c6fb8",
|
|
70
|
+
"#175193",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const ITEM_STYLE = {
|
|
74
|
+
borderColor: "white",
|
|
75
|
+
borderType: "solid",
|
|
76
|
+
borderWidth: 1,
|
|
77
|
+
color: ({ data }: { data: Cell }) =>
|
|
78
|
+
SCALE[
|
|
79
|
+
Math.min(SCALE.length - 1, Math.floor((data.value / 100) * SCALE.length))
|
|
80
|
+
],
|
|
81
|
+
} as ScatterSeriesOption["itemStyle"];
|
|
82
|
+
|
|
83
|
+
function App() {
|
|
84
|
+
return (
|
|
85
|
+
<div className="app">
|
|
86
|
+
<HeatmapChart
|
|
87
|
+
data={DATA}
|
|
88
|
+
encode={{ x: "x", y: "y" }}
|
|
89
|
+
height={ROWS * CELL_PX}
|
|
90
|
+
itemStyle={ITEM_STYLE}
|
|
91
|
+
symbol="rect"
|
|
92
|
+
symbolSize={CELL_PX}
|
|
93
|
+
width={COLUMNS * CELL_PX}
|
|
94
|
+
xAxisData={Array.from({ length: COLUMNS }, (_, index) => index)}
|
|
95
|
+
yAxisData={Array.from({ length: ROWS }, (_, index) => index)}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default App;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Dot plot
|
|
105
|
+
|
|
106
|
+
Circles sized by one measurement and coloured by another, which is the form this chart is most often used in: expression by cell type, with the diameter carrying the share of cells expressing the gene.
|
|
107
|
+
|
|
108
|
+
**Example: HeatmapChartDotPlot**
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// Cells do not have to be squares that fill their slot. With symbol="circle"
|
|
112
|
+
// and a symbolSize callback, the grid becomes a dot plot, where a cell can
|
|
113
|
+
// carry two measurements at once: one in the color and one in the diameter.
|
|
114
|
+
//
|
|
115
|
+
// symbolSize is given the data item, so the arithmetic that turns a value into
|
|
116
|
+
// a diameter is yours. Keep the largest dot at or below the cell size, or
|
|
117
|
+
// neighbouring dots start to overlap.
|
|
118
|
+
|
|
119
|
+
import { type ScatterSeriesOption } from "echarts";
|
|
120
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
121
|
+
|
|
122
|
+
const GENES = ["CD3E", "CD8A", "MS4A1", "NKG7", "LYZ", "PPBP", "FCGR3A"];
|
|
123
|
+
const CELL_TYPES = ["T cell", "B cell", "NK cell", "Monocyte", "Platelet"];
|
|
124
|
+
const CELL_PX = 32;
|
|
125
|
+
|
|
126
|
+
interface Cell {
|
|
127
|
+
cellTypeIndex: number;
|
|
128
|
+
geneIndex: number;
|
|
129
|
+
// Color carries mean expression, diameter the share of cells expressing.
|
|
130
|
+
percentage: number;
|
|
131
|
+
value: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const DATA: Cell[] = [];
|
|
135
|
+
GENES.forEach((_, geneIndex) => {
|
|
136
|
+
CELL_TYPES.forEach((__, cellTypeIndex) => {
|
|
137
|
+
DATA.push({
|
|
138
|
+
cellTypeIndex,
|
|
139
|
+
geneIndex,
|
|
140
|
+
percentage: Math.round(
|
|
141
|
+
20 + 80 * Math.abs(Math.sin(geneIndex + cellTypeIndex))
|
|
142
|
+
),
|
|
143
|
+
value: Math.round(
|
|
144
|
+
50 + 50 * Math.sin(geneIndex / 1.7) * Math.cos(cellTypeIndex / 1.3)
|
|
145
|
+
),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const SCALE = ["#e9e3f5", "#c9bce8", "#a694d8", "#7f68c4", "#57419f"];
|
|
151
|
+
|
|
152
|
+
const ITEM_STYLE = {
|
|
153
|
+
color: ({ data }: { data: Cell }) =>
|
|
154
|
+
SCALE[
|
|
155
|
+
Math.min(SCALE.length - 1, Math.floor((data.value / 100) * SCALE.length))
|
|
156
|
+
],
|
|
157
|
+
opacity: 1,
|
|
158
|
+
} as ScatterSeriesOption["itemStyle"];
|
|
159
|
+
|
|
160
|
+
const symbolSize = (cell: Cell) => (cell.percentage / 100) * CELL_PX;
|
|
161
|
+
|
|
162
|
+
function App() {
|
|
163
|
+
return (
|
|
164
|
+
<div className="app">
|
|
165
|
+
<HeatmapChart
|
|
166
|
+
data={DATA}
|
|
167
|
+
encode={{ x: "geneIndex", y: "cellTypeIndex" }}
|
|
168
|
+
height={CELL_TYPES.length * CELL_PX}
|
|
169
|
+
itemStyle={ITEM_STYLE}
|
|
170
|
+
symbol="circle"
|
|
171
|
+
symbolSize={symbolSize as ScatterSeriesOption["symbolSize"]}
|
|
172
|
+
width={GENES.length * CELL_PX}
|
|
173
|
+
xAxisData={GENES}
|
|
174
|
+
yAxisData={CELL_TYPES}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default App;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Axis labels and a tooltip
|
|
184
|
+
|
|
185
|
+
Labels turned back on through `options`, with the grid inset to make room for them, and an ECharts tooltip naming the cell under the pointer.
|
|
186
|
+
|
|
187
|
+
**Example: HeatmapChartWithAxesAndTooltip**
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// Axis labels are drawn at font size 0 by default, because the charts this was
|
|
191
|
+
// built for render their axes as separate, scrollable components beside the
|
|
192
|
+
// grid. For a heatmap small enough to label in place, turn them back on
|
|
193
|
+
// through options.xAxis and options.yAxis.
|
|
194
|
+
//
|
|
195
|
+
// The labels need somewhere to go. The grid fills the whole container unless
|
|
196
|
+
// told otherwise, so inset it and make the container big enough to hold both
|
|
197
|
+
// the grid and the gutters the labels sit in.
|
|
198
|
+
//
|
|
199
|
+
// options is passed to ECharts more or less directly, which is also how the
|
|
200
|
+
// tooltip gets here: there is no tooltip prop, just ECharts' own.
|
|
201
|
+
|
|
202
|
+
import { type EChartsOption, type ScatterSeriesOption } from "echarts";
|
|
203
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
204
|
+
|
|
205
|
+
const GENES = ["CD3E", "CD8A", "MS4A1", "NKG7", "LYZ", "PPBP"];
|
|
206
|
+
const CELL_TYPES = ["T cell", "B cell", "NK cell", "Monocyte"];
|
|
207
|
+
|
|
208
|
+
const CELL_PX = 28;
|
|
209
|
+
const GRID_WIDTH = GENES.length * CELL_PX;
|
|
210
|
+
const GRID_HEIGHT = CELL_TYPES.length * CELL_PX;
|
|
211
|
+
const LABEL_GUTTER = 72;
|
|
212
|
+
|
|
213
|
+
interface Cell {
|
|
214
|
+
cellTypeIndex: number;
|
|
215
|
+
geneIndex: number;
|
|
216
|
+
value: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const DATA: Cell[] = [];
|
|
220
|
+
GENES.forEach((_, geneIndex) => {
|
|
221
|
+
CELL_TYPES.forEach((__, cellTypeIndex) => {
|
|
222
|
+
DATA.push({
|
|
223
|
+
cellTypeIndex,
|
|
224
|
+
geneIndex,
|
|
225
|
+
value: Math.round(
|
|
226
|
+
50 + 50 * Math.sin(geneIndex / 1.4) * Math.cos(cellTypeIndex / 1.6)
|
|
227
|
+
),
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const SCALE = ["#eff6fc", "#c3ddf3", "#8ab8e6", "#4a8fd4", "#175193"];
|
|
233
|
+
|
|
234
|
+
const ITEM_STYLE = {
|
|
235
|
+
borderColor: "white",
|
|
236
|
+
borderWidth: 1,
|
|
237
|
+
color: ({ data }: { data: Cell }) =>
|
|
238
|
+
SCALE[
|
|
239
|
+
Math.min(SCALE.length - 1, Math.floor((data.value / 100) * SCALE.length))
|
|
240
|
+
],
|
|
241
|
+
} as ScatterSeriesOption["itemStyle"];
|
|
242
|
+
|
|
243
|
+
const TOOLTIP: EChartsOption["tooltip"] = {
|
|
244
|
+
formatter: (params) => {
|
|
245
|
+
const { data } = params as unknown as { data: Cell };
|
|
246
|
+
|
|
247
|
+
return `${GENES[data.geneIndex]} in ${CELL_TYPES[data.cellTypeIndex]}<br/><strong>${data.value}</strong>`;
|
|
248
|
+
},
|
|
249
|
+
show: true,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function App() {
|
|
253
|
+
return (
|
|
254
|
+
<div className="app">
|
|
255
|
+
<HeatmapChart
|
|
256
|
+
data={DATA}
|
|
257
|
+
encode={{ x: "geneIndex", y: "cellTypeIndex" }}
|
|
258
|
+
grid={{
|
|
259
|
+
height: `${GRID_HEIGHT}px`,
|
|
260
|
+
left: LABEL_GUTTER,
|
|
261
|
+
top: 0,
|
|
262
|
+
width: `${GRID_WIDTH}px`,
|
|
263
|
+
}}
|
|
264
|
+
height={GRID_HEIGHT + LABEL_GUTTER}
|
|
265
|
+
itemStyle={ITEM_STYLE}
|
|
266
|
+
options={{
|
|
267
|
+
tooltip: TOOLTIP,
|
|
268
|
+
xAxis: { axisLabel: { fontSize: 11, rotate: 45 } },
|
|
269
|
+
yAxis: { axisLabel: { fontSize: 11 } },
|
|
270
|
+
}}
|
|
271
|
+
symbolSize={CELL_PX}
|
|
272
|
+
width={GRID_WIDTH + LABEL_GUTTER}
|
|
273
|
+
xAxisData={GENES}
|
|
274
|
+
yAxisData={CELL_TYPES}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default App;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Large grids and the camera
|
|
284
|
+
|
|
285
|
+
A 4,800-cell grid rendered 240 cells at a time. Drag to move the window, or use the wheel to travel down it.
|
|
286
|
+
|
|
287
|
+
**Example: HeatmapChartWithCamera**
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
// 4,800 cells, of which 240 are drawn. The camera is a window onto the grid:
|
|
291
|
+
// only the cells inside it are rendered, and dragging moves the window rather
|
|
292
|
+
// than the chart. It is what makes a heatmap of tens of thousands of cells
|
|
293
|
+
// affordable, and it is worth turning on well before the browser starts to
|
|
294
|
+
// struggle.
|
|
295
|
+
//
|
|
296
|
+
// The container is sized to the window, not to the data: camera.width and
|
|
297
|
+
// camera.height are counted in cells, so the pixel size is that count times
|
|
298
|
+
// whatever a cell should measure. Get this wrong and the cells stretch: the
|
|
299
|
+
// grid always fills the container it is given.
|
|
300
|
+
|
|
301
|
+
import { type ScatterSeriesOption } from "echarts";
|
|
302
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
303
|
+
|
|
304
|
+
const COLUMNS = 80;
|
|
305
|
+
const ROWS = 60;
|
|
306
|
+
const CELL_PX = 20;
|
|
307
|
+
|
|
308
|
+
const CAMERA = { active: true, height: 12, width: 20 };
|
|
309
|
+
|
|
310
|
+
interface Cell {
|
|
311
|
+
value: number;
|
|
312
|
+
x: number;
|
|
313
|
+
y: number;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const DATA: Cell[] = [];
|
|
317
|
+
for (let x = 0; x < COLUMNS; x += 1) {
|
|
318
|
+
for (let y = 0; y < ROWS; y += 1) {
|
|
319
|
+
DATA.push({
|
|
320
|
+
value: Math.round(50 + 50 * Math.sin(x / 6) * Math.cos(y / 5)),
|
|
321
|
+
x,
|
|
322
|
+
y,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const SCALE = ["#fff5cc", "#ffd99b", "#fdae61", "#f46d43", "#c9302c"];
|
|
328
|
+
|
|
329
|
+
const ITEM_STYLE = {
|
|
330
|
+
borderColor: "white",
|
|
331
|
+
borderWidth: 1,
|
|
332
|
+
color: ({ data }: { data: Cell }) =>
|
|
333
|
+
SCALE[
|
|
334
|
+
Math.min(SCALE.length - 1, Math.floor((data.value / 100) * SCALE.length))
|
|
335
|
+
],
|
|
336
|
+
} as ScatterSeriesOption["itemStyle"];
|
|
337
|
+
|
|
338
|
+
function App() {
|
|
339
|
+
return (
|
|
340
|
+
<div className="app">
|
|
341
|
+
<HeatmapChart
|
|
342
|
+
camera={CAMERA}
|
|
343
|
+
data={DATA}
|
|
344
|
+
encode={{ x: "x", y: "y" }}
|
|
345
|
+
height={CAMERA.height * CELL_PX}
|
|
346
|
+
itemStyle={ITEM_STYLE}
|
|
347
|
+
symbolSize={CELL_PX}
|
|
348
|
+
width={CAMERA.width * CELL_PX}
|
|
349
|
+
xAxisData={Array.from({ length: COLUMNS }, (_, index) => index)}
|
|
350
|
+
yAxisData={Array.from({ length: ROWS }, (_, index) => index)}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default App;
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Downloading
|
|
360
|
+
|
|
361
|
+
A download button that exports the chart itself, and a renderer switch to show the format following it: SVG out of the SVG renderer, PNG out of the canvas one.
|
|
362
|
+
|
|
363
|
+
**Example: DownloadHeatmapChart**
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
// Exporting is ECharts' job, so the work is getting hold of the instance: the
|
|
367
|
+
// ref HeatmapChart forwards points at the container ECharts was initialised
|
|
368
|
+
// on, and getInstanceByDom trades that element for the chart.
|
|
369
|
+
//
|
|
370
|
+
// Look it up when the button is clicked rather than caching it. Changing the
|
|
371
|
+
// renderer disposes the instance and builds a new one on the same element, so
|
|
372
|
+
// a cached reference would be exporting a chart that no longer exists.
|
|
373
|
+
//
|
|
374
|
+
// The renderer decides the format: an SVG chart can only produce SVG, a canvas
|
|
375
|
+
// chart can only produce a raster. Keeping the two in step is the whole reason
|
|
376
|
+
// the file type is derived from the renderer below.
|
|
377
|
+
|
|
378
|
+
import { useRef, useState } from "react";
|
|
379
|
+
import { getInstanceByDom, type ScatterSeriesOption } from "echarts";
|
|
380
|
+
import {
|
|
381
|
+
Button,
|
|
382
|
+
Icon,
|
|
383
|
+
SegmentedControl,
|
|
384
|
+
getSpaces,
|
|
385
|
+
type CommonThemeProps,
|
|
386
|
+
} from "@czi-sds/components";
|
|
387
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
388
|
+
import styled from "@emotion/styled";
|
|
389
|
+
|
|
390
|
+
const COLUMNS = 12;
|
|
391
|
+
const ROWS = 8;
|
|
392
|
+
const CELL_PX = 24;
|
|
393
|
+
|
|
394
|
+
interface Cell {
|
|
395
|
+
value: number;
|
|
396
|
+
x: number;
|
|
397
|
+
y: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const DATA: Cell[] = [];
|
|
401
|
+
for (let x = 0; x < COLUMNS; x += 1) {
|
|
402
|
+
for (let y = 0; y < ROWS; y += 1) {
|
|
403
|
+
DATA.push({
|
|
404
|
+
value: Math.round(50 + 50 * Math.sin(x / 2.5) * Math.cos(y / 3)),
|
|
405
|
+
x,
|
|
406
|
+
y,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const SCALE = ["#eff6fc", "#c3ddf3", "#8ab8e6", "#4a8fd4", "#175193"];
|
|
412
|
+
|
|
413
|
+
const ITEM_STYLE = {
|
|
414
|
+
borderColor: "white",
|
|
415
|
+
borderWidth: 1,
|
|
416
|
+
color: ({ data }: { data: Cell }) =>
|
|
417
|
+
SCALE[
|
|
418
|
+
Math.min(SCALE.length - 1, Math.floor((data.value / 100) * SCALE.length))
|
|
419
|
+
],
|
|
420
|
+
} as ScatterSeriesOption["itemStyle"];
|
|
421
|
+
|
|
422
|
+
const Controls = styled.div<CommonThemeProps>`
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
|
|
426
|
+
${(props) => {
|
|
427
|
+
const spaces = getSpaces(props);
|
|
428
|
+
|
|
429
|
+
return `
|
|
430
|
+
gap: ${spaces?.m}px;
|
|
431
|
+
margin-bottom: ${spaces?.l}px;
|
|
432
|
+
`;
|
|
433
|
+
}}
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
function App() {
|
|
437
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
438
|
+
const [renderer, setRenderer] = useState<"svg" | "canvas">("svg");
|
|
439
|
+
|
|
440
|
+
const fileType = renderer === "svg" ? "svg" : "png";
|
|
441
|
+
|
|
442
|
+
function download() {
|
|
443
|
+
const container = containerRef.current;
|
|
444
|
+
const chart = container && getInstanceByDom(container);
|
|
445
|
+
|
|
446
|
+
if (!chart) return;
|
|
447
|
+
|
|
448
|
+
const url = chart.getConnectedDataURL({
|
|
449
|
+
// Both are ignored on the SVG path, which has no pixels to scale and
|
|
450
|
+
// keeps the chart's own background.
|
|
451
|
+
backgroundColor: "white",
|
|
452
|
+
pixelRatio: 2,
|
|
453
|
+
type: fileType,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (!url) return;
|
|
457
|
+
|
|
458
|
+
const link = document.createElement("a");
|
|
459
|
+
link.download = `heatmap.${fileType}`;
|
|
460
|
+
link.href = url;
|
|
461
|
+
link.click();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<div className="app">
|
|
466
|
+
<Controls>
|
|
467
|
+
<SegmentedControl
|
|
468
|
+
buttonDefinition={[
|
|
469
|
+
{ label: "SVG", value: "svg" },
|
|
470
|
+
{ label: "Canvas", value: "canvas" },
|
|
471
|
+
]}
|
|
472
|
+
value={renderer}
|
|
473
|
+
onChange={(_, value) => {
|
|
474
|
+
if (value) setRenderer(value as "svg" | "canvas");
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
477
|
+
|
|
478
|
+
<Button
|
|
479
|
+
sdsStyle="solid"
|
|
480
|
+
sdsType="primary"
|
|
481
|
+
endIcon={<Icon sdsIcon="Download" sdsSize="l" />}
|
|
482
|
+
onClick={download}
|
|
483
|
+
>
|
|
484
|
+
Download .{fileType}
|
|
485
|
+
</Button>
|
|
486
|
+
</Controls>
|
|
487
|
+
|
|
488
|
+
<HeatmapChart
|
|
489
|
+
ref={containerRef}
|
|
490
|
+
data={DATA}
|
|
491
|
+
echartsRendererMode={renderer}
|
|
492
|
+
encode={{ x: "x", y: "y" }}
|
|
493
|
+
height={ROWS * CELL_PX}
|
|
494
|
+
itemStyle={ITEM_STYLE}
|
|
495
|
+
symbolSize={CELL_PX}
|
|
496
|
+
width={COLUMNS * CELL_PX}
|
|
497
|
+
xAxisData={Array.from({ length: COLUMNS }, (_, index) => index)}
|
|
498
|
+
yAxisData={Array.from({ length: ROWS }, (_, index) => index)}
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export default App;
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Events
|
|
508
|
+
|
|
509
|
+
A click handler through `onEvents`, reading the cell back out of the ECharts event.
|
|
510
|
+
|
|
511
|
+
**Example: HeatmapChartWithEvents**
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
// onEvents is a map of ECharts event names to handlers, each called with the
|
|
515
|
+
// raw ECharts event and the chart instance. The event's data property is the
|
|
516
|
+
// data item the interaction landed on, in the shape you passed in, so a click
|
|
517
|
+
// hands back the cell itself rather than a pair of indices.
|
|
518
|
+
//
|
|
519
|
+
// Because the value arrives as unknown, narrowing it is the caller's job.
|
|
520
|
+
|
|
521
|
+
import { useState } from "react";
|
|
522
|
+
import { type ScatterSeriesOption } from "echarts";
|
|
523
|
+
import {
|
|
524
|
+
fontBodyXs,
|
|
525
|
+
getSemanticColors,
|
|
526
|
+
getSpaces,
|
|
527
|
+
type CommonThemeProps,
|
|
528
|
+
} from "@czi-sds/components";
|
|
529
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
530
|
+
import styled from "@emotion/styled";
|
|
531
|
+
|
|
532
|
+
const COLUMNS = 10;
|
|
533
|
+
const ROWS = 6;
|
|
534
|
+
const CELL_PX = 28;
|
|
535
|
+
|
|
536
|
+
interface Cell {
|
|
537
|
+
value: number;
|
|
538
|
+
x: number;
|
|
539
|
+
y: number;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const DATA: Cell[] = [];
|
|
543
|
+
for (let x = 0; x < COLUMNS; x += 1) {
|
|
544
|
+
for (let y = 0; y < ROWS; y += 1) {
|
|
545
|
+
DATA.push({
|
|
546
|
+
value: Math.round(50 + 50 * Math.sin(x / 2) * Math.cos(y / 2.5)),
|
|
547
|
+
x,
|
|
548
|
+
y,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const SCALE = ["#e6f4ea", "#b7e0c4", "#7fc79b", "#43a56f", "#1b7a48"];
|
|
554
|
+
|
|
555
|
+
const ITEM_STYLE = {
|
|
556
|
+
borderColor: "white",
|
|
557
|
+
borderWidth: 1,
|
|
558
|
+
color: ({ data }: { data: Cell }) =>
|
|
559
|
+
SCALE[
|
|
560
|
+
Math.min(SCALE.length - 1, Math.floor((data.value / 100) * SCALE.length))
|
|
561
|
+
],
|
|
562
|
+
} as ScatterSeriesOption["itemStyle"];
|
|
563
|
+
|
|
564
|
+
const Readout = styled.p<CommonThemeProps>`
|
|
565
|
+
${fontBodyXs}
|
|
566
|
+
|
|
567
|
+
${(props) => {
|
|
568
|
+
const semanticColors = getSemanticColors(props);
|
|
569
|
+
const spaces = getSpaces(props);
|
|
570
|
+
|
|
571
|
+
return `
|
|
572
|
+
color: ${semanticColors?.base?.textSecondary};
|
|
573
|
+
margin: 0 0 ${spaces?.m}px;
|
|
574
|
+
`;
|
|
575
|
+
}}
|
|
576
|
+
`;
|
|
577
|
+
|
|
578
|
+
function App() {
|
|
579
|
+
const [selected, setSelected] = useState<Cell | null>(null);
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<div className="app">
|
|
583
|
+
<Readout>
|
|
584
|
+
{selected
|
|
585
|
+
? `Column ${selected.x}, row ${selected.y}: ${selected.value}`
|
|
586
|
+
: "Click a cell."}
|
|
587
|
+
</Readout>
|
|
588
|
+
|
|
589
|
+
<HeatmapChart
|
|
590
|
+
data={DATA}
|
|
591
|
+
encode={{ x: "x", y: "y" }}
|
|
592
|
+
height={ROWS * CELL_PX}
|
|
593
|
+
itemStyle={ITEM_STYLE}
|
|
594
|
+
onEvents={{
|
|
595
|
+
click: (event) => setSelected((event as { data: Cell }).data),
|
|
596
|
+
}}
|
|
597
|
+
symbolSize={CELL_PX}
|
|
598
|
+
width={COLUMNS * CELL_PX}
|
|
599
|
+
xAxisData={Array.from({ length: COLUMNS }, (_, index) => index)}
|
|
600
|
+
yAxisData={Array.from({ length: ROWS }, (_, index) => index)}
|
|
601
|
+
/>
|
|
602
|
+
</div>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export default App;
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## How it is built
|
|
610
|
+
|
|
611
|
+
HeatmapChart is a thin React wrapper around an [Apache ECharts](https://echarts.apache.org/en/index.html) instance. It is not ECharts' heatmap series: it is a scatter series drawn on two category axes, one symbol per cell. That is what lets a cell be a square, a rounded square, or a circle whose diameter carries a second measurement, as a dot plot.
|
|
612
|
+
|
|
613
|
+
Three consequences of that design are worth knowing before you start:
|
|
614
|
+
|
|
615
|
+
- **The props are ECharts' vocabulary, not the SDS one.** `itemStyle`, `emphasis`, `symbolSize`, `grid`, and `axisPointer` are passed through to the underlying option object, and the ECharts documentation is the reference for each of them. The chart takes nothing from the SDS theme: color scales are yours to supply.
|
|
616
|
+
|
|
617
|
+
- **It renders to SVG by default.** That keeps text crisp and elements inspectable, and is the right choice up to a few thousand cells. Beyond that, switch `echartsRendererMode` to `"canvas"`, and reach for the camera below.
|
|
618
|
+
|
|
619
|
+
- **Animation is off and updates are throttled to 100ms.** The component is memoised and resizes the chart before each update, so passing new data on every frame is safe; it will coalesce.
|
|
620
|
+
|
|
621
|
+
## Sizing
|
|
622
|
+
|
|
623
|
+
`width` and `height` are required, in pixels, and the component throws if either is missing or zero: ECharts cannot initialise into a box with no area. The chart does not measure its container, so a responsive heatmap means measuring the container yourself and passing the result down.
|
|
624
|
+
|
|
625
|
+
The grid fills those dimensions exactly, which makes cell size a matter of arithmetic: pick the pixel size a cell should be and multiply by the number of categories on each axis. Sizing the container to anything else stretches the cells rather than clipping them.
|
|
626
|
+
|
|
627
|
+
## Data and encoding
|
|
628
|
+
|
|
629
|
+
`data` is a flat array of cells, one per position in the grid, and each cell is an object of whatever shape suits the data. `encode` names the two fields that place it:
|
|
630
|
+
|
|
631
|
+
**React TypeScript**
|
|
632
|
+
|
|
633
|
+
```tsx
|
|
634
|
+
const data = [
|
|
635
|
+
{ geneIndex: 0, cellTypeIndex: 0, percentage: 0.5 },
|
|
636
|
+
{ geneIndex: 1, cellTypeIndex: 0, percentage: 0.8 },
|
|
637
|
+
];
|
|
638
|
+
|
|
639
|
+
<HeatmapChart
|
|
640
|
+
data={data}
|
|
641
|
+
encode={{ x: "geneIndex", y: "cellTypeIndex" }}
|
|
642
|
+
xAxisData={genes}
|
|
643
|
+
yAxisData={cellTypes}
|
|
644
|
+
width={genes.length * 20}
|
|
645
|
+
height={cellTypes.length * 20}
|
|
646
|
+
/>;
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
`xAxisData` and `yAxisData` are the categories themselves, as strings, numbers, or objects with a `value` and a `textStyle`. The values a cell is encoded on are indices into them, so the two must line up: a cell whose `geneIndex` is 4 lands on the fifth entry of `xAxisData`.
|
|
650
|
+
|
|
651
|
+
Everything else a cell carries (an expression level, a count, a p-value) is invisible until something draws it. That is the job of `itemStyle` and `symbolSize`, both of which accept a callback that receives the data item, so the color scale and the size scale are yours to define. Without an `itemStyle` every cell is black, and `symbolSize` defaults to 5 pixels.
|
|
652
|
+
|
|
653
|
+
## The camera
|
|
654
|
+
|
|
655
|
+
A heatmap of any real size cannot be rendered whole. `camera` turns on a window over the grid, given in cells rather than pixels, and only the cells inside it are drawn:
|
|
656
|
+
|
|
657
|
+
**React TypeScript**
|
|
658
|
+
|
|
659
|
+
```tsx
|
|
660
|
+
<HeatmapChart
|
|
661
|
+
camera={{ active: true, width: 40, height: 20 }}
|
|
662
|
+
width={40 * CELL_PX}
|
|
663
|
+
height={20 * CELL_PX}
|
|
664
|
+
{...rest}
|
|
665
|
+
/>
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Underneath it is ECharts' `dataZoom` with the zoom locked, which is why the interactions are the ones `dataZoom` provides: dragging pans the window, and the wheel scrolls it vertically. The window starts at the origin of the grid, and defaults to 40 cells wide by 20 tall when the sizes are left out. Size the container to the window, not to the data.
|
|
669
|
+
|
|
670
|
+
## Escape hatches
|
|
671
|
+
|
|
672
|
+
The named props cover the common cases; everything else in ECharts is reached through `options`, which is merged into the generated option object. Tooltips, visual maps, and axis labels all arrive this way. The merge is shallow and treats a few keys specially, which is worth knowing when an option appears to be ignored:
|
|
673
|
+
|
|
674
|
+
- `xAxis`, `yAxis` and `series` are merged one level deep over the chart's defaults, and only the first entry of each is read. Passing an object replaces the matching default key outright, so `axisLabel` given to `xAxis` replaces the whole default `axisLabel` rather than adding to it.
|
|
675
|
+
|
|
676
|
+
- `axisPointer` and `dataZoom` accept either a single object, applied to both axes, or a two-entry array for the x and y axes separately. `dataZoom` is only applied when the camera is active, since otherwise there is no window for it to describe.
|
|
677
|
+
|
|
678
|
+
- `grid` has a prop of its own, which takes either an option object or a function receiving the computed default. That is useful for insetting the grid to make room for axis labels while keeping the sizing the chart worked out.
|
|
679
|
+
|
|
680
|
+
- Every other key in `options` is spread over the result untouched, and wins.
|
|
681
|
+
|
|
682
|
+
> **dataZoom and options.dataZoom both work.** They are merged in that order over the window's own defaults, so `options` wins where the two overlap. Either way the camera has to be active for the merge to reach the chart.
|
|
683
|
+
|
|
684
|
+
## Downloading the chart
|
|
685
|
+
|
|
686
|
+
There is no download prop. Exporting is something the ECharts instance does, so the work is getting hold of it: the ref the component forwards points at the container ECharts was initialised on, and `getInstanceByDom` trades that element for the chart. From there, `getDataURL` returns the picture as a data URL and an anchor turns it into a file:
|
|
687
|
+
|
|
688
|
+
**React TypeScript**
|
|
689
|
+
|
|
690
|
+
```tsx
|
|
691
|
+
import { useRef } from "react";
|
|
692
|
+
import { getInstanceByDom } from "echarts";
|
|
693
|
+
import { HeatmapChart } from "@czi-sds/data-viz";
|
|
694
|
+
|
|
695
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
696
|
+
|
|
697
|
+
function download() {
|
|
698
|
+
const container = containerRef.current;
|
|
699
|
+
const chart = container && getInstanceByDom(container);
|
|
700
|
+
|
|
701
|
+
if (!chart) return;
|
|
702
|
+
|
|
703
|
+
const url = chart.getDataURL({ pixelRatio: 2, type: "png" });
|
|
704
|
+
|
|
705
|
+
if (!url) return;
|
|
706
|
+
|
|
707
|
+
const link = document.createElement("a");
|
|
708
|
+
link.download = "heatmap.png";
|
|
709
|
+
link.href = url;
|
|
710
|
+
link.click();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
<HeatmapChart ref={containerRef} {...rest} />;
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Look the instance up when the download is triggered rather than caching it. Changing `echartsRendererMode` disposes the chart and builds a new one on the same element, and a cached reference would be exporting one that no longer exists.
|
|
717
|
+
|
|
718
|
+
Three things decide what comes out:
|
|
719
|
+
|
|
720
|
+
- **The renderer decides the format, not the type option.** An SVG chart can only export SVG, and a canvas chart can only export a raster; asking a canvas chart for `type: "svg"` produces a PNG with a misleading name. Derive the file extension from `echartsRendererMode`, as the example below does, or fix the renderer and the extension together.
|
|
721
|
+
|
|
722
|
+
- `pixelRatio` and `backgroundColor` only apply on the canvas path: 2 or 3 for a print-resolution PNG, and a colour if the default transparent background will not do. An SVG export ignores both and keeps the chart's own background. `excludeComponents` takes the names of components to hide while exporting, such as a toolbox.
|
|
723
|
+
|
|
724
|
+
- **Only what is rendered is exported.** With the camera active, the export contains the window, not the whole grid. Exporting everything means rendering everything: turn the camera off for the export, and expect it to cost what drawing the full grid costs.
|
|
725
|
+
|
|
726
|
+
A heatmap is often assembled from more than one chart: the grid, plus the separate charts drawing its axis labels. ECharts can composite them into a single image, but only once they are in a connected group:
|
|
727
|
+
|
|
728
|
+
**React TypeScript**
|
|
729
|
+
|
|
730
|
+
```tsx
|
|
731
|
+
import { connect, getInstanceByDom } from "echarts";
|
|
732
|
+
|
|
733
|
+
// Once, after every chart has been initialised.
|
|
734
|
+
connect([heatmap, xAxisChart, yAxisChart]);
|
|
735
|
+
|
|
736
|
+
// Lays the group out by where each chart sits on screen.
|
|
737
|
+
const url = heatmap.getConnectedDataURL({
|
|
738
|
+
connectedBackgroundColor: "white",
|
|
739
|
+
pixelRatio: 2,
|
|
740
|
+
type: "png",
|
|
741
|
+
});
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
`getConnectedDataURL` falls back to `getDataURL` when the chart is not in a connected group, so a single chart can call either. That is what the heatmap demo does: its download button calls `getConnectedDataURL` on the grid alone.
|
|
745
|
+
|
|
746
|
+
## Behavior and accessibility
|
|
747
|
+
|
|
748
|
+
- The chart is a single SVG or canvas element with no accessible name, no roles, and no tab stop. Assistive technology can see nothing in it, and a keyboard cannot reach a cell, so a heatmap needs a text equivalent alongside it: a summary of what it shows, or the same numbers reachable as a table.
|
|
749
|
+
|
|
750
|
+
- Axis labels are drawn at font size 0 by default, and the x axis is rotated 90 degrees. The charts this was built for pair the grid with their own scrollable axis components, which is why the built-in labels start hidden; a small heatmap can turn them back on through `options`, as the example below does.
|
|
751
|
+
|
|
752
|
+
- Hover draws a border around the cell under the pointer (white and 4px on a square, black and 2px on a circle) and never scales it. Pass `emphasis` to change that.
|
|
753
|
+
|
|
754
|
+
- Tooltips are ECharts' own HTML tooltips, positioned over the chart and built from a formatter you write. They are pointer-only, and they are not the SDS Tooltip.
|
|
755
|
+
|
|
756
|
+
- Handlers in `onEvents` are rebound whenever the chart updates, and rebinding removes every listener registered for that event name. If you also attach listeners to the ECharts instance yourself, expect them to be dropped; attach them through `onEvents` instead.
|
|
757
|
+
|
|
758
|
+
- Color is the whole message in a heatmap. Pick a sequential scale that survives being read by someone with a color vision deficiency, keep it consistent between charts that will be compared, and make the scale itself visible (a legend or a `visualMap` through `options`), since the component does not draw one.
|
|
759
|
+
|
|
760
|
+
## Props
|
|
761
|
+
|
|
762
|
+
Any remaining props are spread onto the container div, so `className`, `id`, and `data-testid` work as usual, and the component forwards its ref to that div. Every option below that names an ECharts concept links to the ECharts documentation for the shape it takes.
|
|
763
|
+
|
|
764
|
+
| Name | Type | Default | Description |
|
|
765
|
+
| --------------------- | --------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
766
|
+
| `data` | `DatasetComponentOption["source"]` | - (required) | The cells, as a flat array of objects whose fields are named by `encode`. |
|
|
767
|
+
| `xAxisData` | `(string \| number \| { value, textStyle })[]` | - (required) | The categories along the x axis. A cell's encoded x value is an index into this array. |
|
|
768
|
+
| `yAxisData` | `(string \| number \| { value, textStyle })[]` | - (required) | The categories along the y axis. |
|
|
769
|
+
| `width` | `number` | - (required) | Width of the chart in pixels. Must be greater than zero; the component throws otherwise. |
|
|
770
|
+
| `height` | `number` | - (required) | Height of the chart in pixels, under the same rule. |
|
|
771
|
+
| `encode` | `{ x: string; y: string }` | - | Which fields of a data item place it on each axis. Omitting it leaves ECharts to guess from the field order. |
|
|
772
|
+
| `symbol` | `"rect"` \| `"roundRect"` \| `"circle"` | `"rect"` | The shape of a cell. Circles turn the grid into a dot plot, where diameter can carry a second measurement. |
|
|
773
|
+
| `symbolSize` | `number` \| `number[]` \| `((value, params) => number \| number[])` | `5` | Cell size in pixels: one number, a `[width, height]` pair, or a callback given the data item. Set it to the cell pitch for a solid grid. |
|
|
774
|
+
| `itemStyle` | `ScatterSeriesOption["itemStyle"]` | solid black | How a cell is painted. Its `color` accepts a callback receiving the data item, which is how a value becomes a color. |
|
|
775
|
+
| `emphasis` | `ScatterSeriesOption["emphasis"]` | a 2–4px border | How a cell is drawn while the pointer is over it. |
|
|
776
|
+
| `camera` | `{ active: boolean; width: number; height: number }` | 40 × 20 when active | Renders only a window of the grid, measured in cells, and lets the pointer pan it. Sizes are ignored unless `active` is `true`. |
|
|
777
|
+
| `dataZoom` | `EChartsOption["dataZoom"]` | the camera window | ECharts' dataZoom configuration for that window, merged over the defaults it sets. One object applies to both axes; a two-entry array configures them separately. It reaches the chart only while `camera.active` is `true`. |
|
|
778
|
+
| `axisPointer` | `EChartsOption["axisPointer"]` | hidden | The crosshair following the pointer. One object applies to both axes; a two-entry array configures them separately. |
|
|
779
|
+
| `grid` | `EChartsOption["grid"]` \| `((defaultGrid) => EChartsOption["grid"])` | fills the container | Where the plotting area sits inside the container. The function form receives the computed default, for insetting it without recomputing the size. |
|
|
780
|
+
| `options` | `EChartsOption` | - | Anything else ECharts understands, merged over the generated options. See the merge rules above. |
|
|
781
|
+
| `onEvents` | `Record<string, (event: unknown,` `chart: ECharts) => void>` | - | ECharts event listeners by name. Each handler gets the raw event, whose `data` property is the cell, and the chart instance. |
|
|
782
|
+
| `echartsRendererMode` | `"svg"` \| `"canvas"` | `"svg"` | Which renderer ECharts initialises with. Canvas copes better with very large grids. Changing it rebuilds the chart. |
|