@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.
Files changed (164) hide show
  1. package/README.md +2 -1
  2. package/data/component-docs/Accordion.md +291 -0
  3. package/data/component-docs/Autocomplete.md +525 -0
  4. package/data/component-docs/Banner.md +231 -0
  5. package/data/component-docs/Button.md +224 -0
  6. package/data/component-docs/ButtonDropdown.md +256 -0
  7. package/data/component-docs/ButtonGroup.md +320 -0
  8. package/data/component-docs/ButtonToggle.md +397 -0
  9. package/data/component-docs/Callout.md +268 -0
  10. package/data/component-docs/CellBasic.md +122 -0
  11. package/data/component-docs/CellComponent.md +63 -0
  12. package/data/component-docs/CellHeader.md +128 -0
  13. package/data/component-docs/ComplexFilter.md +294 -0
  14. package/data/component-docs/ContentCard.md +406 -0
  15. package/data/component-docs/Dialog.md +651 -0
  16. package/data/component-docs/Dropdown.md +155 -0
  17. package/data/component-docs/DropdownMenu.md +949 -0
  18. package/data/component-docs/HeatmapChart.md +782 -0
  19. package/data/component-docs/Hero.md +363 -0
  20. package/data/component-docs/Icon.md +283 -0
  21. package/data/component-docs/InputCheckbox.md +205 -0
  22. package/data/component-docs/InputDropdown.md +482 -0
  23. package/data/component-docs/InputRadio.md +173 -0
  24. package/data/component-docs/InputSearch.md +81 -0
  25. package/data/component-docs/InputSlider.md +189 -0
  26. package/data/component-docs/InputText.md +110 -0
  27. package/data/component-docs/InputToggle.md +141 -0
  28. package/data/component-docs/IntentMessage.md +293 -0
  29. package/data/component-docs/Legend.md +442 -0
  30. package/data/component-docs/Link.md +23 -0
  31. package/data/component-docs/List.md +300 -0
  32. package/data/component-docs/LoadingIndicator.md +198 -0
  33. package/data/component-docs/Menu.md +298 -0
  34. package/data/component-docs/MenuItem.md +100 -0
  35. package/data/component-docs/NavigationFooter.md +165 -0
  36. package/data/component-docs/NavigationHeader.md +500 -0
  37. package/data/component-docs/NavigationJumpTo.md +414 -0
  38. package/data/component-docs/Notification.md +288 -0
  39. package/data/component-docs/Pagination.md +67 -0
  40. package/data/component-docs/Panel.md +636 -0
  41. package/data/component-docs/Popover.md +448 -0
  42. package/data/component-docs/PreComposedTable.md +2947 -0
  43. package/data/component-docs/SegmentedControl.md +396 -0
  44. package/data/component-docs/StackedBarChart.md +475 -0
  45. package/data/component-docs/Table.md +2000 -0
  46. package/data/component-docs/TableHeader.md +53 -0
  47. package/data/component-docs/TableRow.md +74 -0
  48. package/data/component-docs/Tabs.md +455 -0
  49. package/data/component-docs/Tag.md +647 -0
  50. package/data/component-docs/TagFilter.md +131 -0
  51. package/data/component-docs/Tooltip.md +561 -0
  52. package/data/component-docs/TooltipCondensed.md +203 -0
  53. package/data/component-docs/TooltipTable.md +145 -0
  54. package/data/component-docs/index.json +262 -0
  55. package/data/component-list.json +2 -5
  56. package/data/component-props/Accordion.json +31 -11
  57. package/data/component-props/Alert.json +3 -3
  58. package/data/component-props/Autocomplete.json +150 -15
  59. package/data/component-props/Banner.json +31 -17
  60. package/data/component-props/Button.json +45 -20
  61. package/data/component-props/ButtonDropdown.json +36 -17
  62. package/data/component-props/ButtonDropdownLegacy.json +11 -11
  63. package/data/component-props/ButtonGroup.json +32 -11
  64. package/data/component-props/ButtonIcon.json +10 -10
  65. package/data/component-props/ButtonLegacy.json +19 -19
  66. package/data/component-props/ButtonToggle.json +32 -19
  67. package/data/component-props/ButtonToggleLegacy.json +11 -11
  68. package/data/component-props/Callout.json +58 -17
  69. package/data/component-props/CellBasic.json +94 -41
  70. package/data/component-props/CellComponent.json +23 -7
  71. package/data/component-props/CellHeader.json +62 -19
  72. package/data/component-props/ComplexFilter.json +52 -21
  73. package/data/component-props/ContentCard.json +70 -47
  74. package/data/component-props/Dialog.json +47 -14
  75. package/data/component-props/Dropdown.json +68 -31
  76. package/data/component-props/DropdownMenu.json +128 -23
  77. package/data/component-props/HeatmapChart.json +88 -12
  78. package/data/component-props/Hero.json +80 -39
  79. package/data/component-props/Icon.json +20 -9
  80. package/data/component-props/InputCheckbox.json +36 -14
  81. package/data/component-props/InputDropdown.json +75 -27
  82. package/data/component-props/InputRadio.json +31 -13
  83. package/data/component-props/InputSearch.json +41 -18
  84. package/data/component-props/InputSlider.json +46 -19
  85. package/data/component-props/InputText.json +26 -19
  86. package/data/component-props/InputToggle.json +26 -9
  87. package/data/component-props/IntentMessage.json +38 -9
  88. package/data/component-props/Link.json +13 -7
  89. package/data/component-props/LoadingIndicator.json +11 -3
  90. package/data/component-props/MenuItem.json +38 -19
  91. package/data/component-props/MenuSelect.json +8 -8
  92. package/data/component-props/NavigationFooter.json +49 -19
  93. package/data/component-props/NavigationHeader.json +150 -37
  94. package/data/component-props/NavigationJumpTo.json +33 -7
  95. package/data/component-props/Notification.json +39 -21
  96. package/data/component-props/Pagination.json +39 -11
  97. package/data/component-props/Panel.json +62 -9
  98. package/data/component-props/Popover.json +80 -7
  99. package/data/component-props/PreComposedTable.json +99 -27
  100. package/data/component-props/SegmentedControl.json +42 -3
  101. package/data/component-props/StackedBarChart.json +116 -42
  102. package/data/component-props/TableRow.json +38 -19
  103. package/data/component-props/Tabs.json +66 -9
  104. package/data/component-props/Tag.json +34 -17
  105. package/data/component-props/TagFilter.json +10 -3
  106. package/data/component-props/Tooltip.json +49 -18
  107. package/data/component-props/TooltipCondensed.json +18 -9
  108. package/data/component-props/TooltipTable.json +17 -7
  109. package/data/resources/{zeroheight-rules.md → documentation-rules.md} +43 -30
  110. package/data/resources/figma-rules.md +2 -2
  111. package/dist/stdio.js +112 -208
  112. package/package.json +9 -5
  113. package/data/component-props/ButtonDropdownV2.json +0 -34
  114. package/data/component-props/ButtonNew.json +0 -47
  115. package/data/component-props/ButtonToggleV2.json +0 -44
  116. package/data/component-props/ButtonV2.json +0 -43
  117. package/data/zeroheight-cache.json +0 -10259
  118. package/data/zeroheight-exports/Accordion.md +0 -774
  119. package/data/zeroheight-exports/Banner.md +0 -691
  120. package/data/zeroheight-exports/Borders.md +0 -117
  121. package/data/zeroheight-exports/Buttons.md +0 -1854
  122. package/data/zeroheight-exports/Callout.md +0 -931
  123. package/data/zeroheight-exports/Colors.md +0 -1755
  124. package/data/zeroheight-exports/Content-Card.md +0 -309
  125. package/data/zeroheight-exports/Contributing-to-SDS.md +0 -98
  126. package/data/zeroheight-exports/Control-Inputs.md +0 -2818
  127. package/data/zeroheight-exports/Corners.md +0 -104
  128. package/data/zeroheight-exports/Cover-page.md +0 -3
  129. package/data/zeroheight-exports/Dialog.md +0 -1367
  130. package/data/zeroheight-exports/Downloads.md +0 -236
  131. package/data/zeroheight-exports/Drop-Shadows.md +0 -79
  132. package/data/zeroheight-exports/Dropdown-Input.md +0 -1776
  133. package/data/zeroheight-exports/Dropdown-Menus.md +0 -913
  134. package/data/zeroheight-exports/Element-Status-Tracker.md +0 -267
  135. package/data/zeroheight-exports/Field-Inputs.md +0 -1405
  136. package/data/zeroheight-exports/Filters.md +0 -1111
  137. package/data/zeroheight-exports/Getting-Started.md +0 -1157
  138. package/data/zeroheight-exports/Hero.md +0 -414
  139. package/data/zeroheight-exports/Icons.md +0 -2067
  140. package/data/zeroheight-exports/Intent.md +0 -86
  141. package/data/zeroheight-exports/Introduction-to-Bases.md +0 -35
  142. package/data/zeroheight-exports/Introduction-to-Cells.md +0 -35
  143. package/data/zeroheight-exports/Introduction-to-DNA.md +0 -35
  144. package/data/zeroheight-exports/Introduction-to-Genes.md +0 -35
  145. package/data/zeroheight-exports/Introduction.md +0 -115
  146. package/data/zeroheight-exports/Link.md +0 -432
  147. package/data/zeroheight-exports/Lists.md +0 -986
  148. package/data/zeroheight-exports/Loading-Indicator.md +0 -41
  149. package/data/zeroheight-exports/Navigation.md +0 -1190
  150. package/data/zeroheight-exports/Notification.md +0 -1038
  151. package/data/zeroheight-exports/Panel.md +0 -1030
  152. package/data/zeroheight-exports/README.md +0 -122
  153. package/data/zeroheight-exports/Responsive-Design.md +0 -330
  154. package/data/zeroheight-exports/Search-Input.md +0 -91
  155. package/data/zeroheight-exports/Search.md +0 -822
  156. package/data/zeroheight-exports/Segmented-Control.md +0 -609
  157. package/data/zeroheight-exports/Spacing.md +0 -297
  158. package/data/zeroheight-exports/Table.md +0 -5345
  159. package/data/zeroheight-exports/Tabs.md +0 -1324
  160. package/data/zeroheight-exports/Tags.md +0 -1545
  161. package/data/zeroheight-exports/Text-Link.md +0 -268
  162. package/data/zeroheight-exports/Theming.md +0 -362
  163. package/data/zeroheight-exports/Tooltips.md +0 -2015
  164. 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. |