@perspective-dev/viewer-charts 4.3.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 (258) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/cdn/perspective-viewer-charts.js +3 -0
  3. package/dist/cdn/perspective-viewer-charts.js.map +7 -0
  4. package/dist/esm/axis/axis-primitives.d.ts +24 -0
  5. package/dist/esm/axis/bar-axis.d.ts +51 -0
  6. package/dist/esm/axis/canvas.d.ts +24 -0
  7. package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
  8. package/dist/esm/axis/categorical-axis.d.ts +27 -0
  9. package/dist/esm/axis/facet-chrome.d.ts +13 -0
  10. package/dist/esm/axis/label-geometry.d.ts +41 -0
  11. package/dist/esm/axis/legend.d.ts +44 -0
  12. package/dist/esm/axis/numeric-axis.d.ts +20 -0
  13. package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
  14. package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
  15. package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
  16. package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
  17. package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
  18. package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
  19. package/dist/esm/charts/canvas-types.d.ts +15 -0
  20. package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
  21. package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
  22. package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
  23. package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
  24. package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
  25. package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
  26. package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
  27. package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
  28. package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
  29. package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
  30. package/dist/esm/charts/chart-base.d.ts +402 -0
  31. package/dist/esm/charts/chart.d.ts +338 -0
  32. package/dist/esm/charts/common/band-layout.d.ts +32 -0
  33. package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
  34. package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
  35. package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
  36. package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
  37. package/dist/esm/charts/common/leaf-color.d.ts +33 -0
  38. package/dist/esm/charts/common/node-store.d.ts +81 -0
  39. package/dist/esm/charts/common/tree-chart.d.ts +48 -0
  40. package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
  41. package/dist/esm/charts/common/tree-data.d.ts +54 -0
  42. package/dist/esm/charts/common/visible-extent.d.ts +51 -0
  43. package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
  44. package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
  45. package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
  46. package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
  47. package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
  48. package/dist/esm/charts/map/map.d.ts +67 -0
  49. package/dist/esm/charts/registry.d.ts +14 -0
  50. package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
  51. package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
  52. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
  53. package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
  54. package/dist/esm/charts/series/series-build.d.ts +228 -0
  55. package/dist/esm/charts/series/series-interact.d.ts +35 -0
  56. package/dist/esm/charts/series/series-render.d.ts +41 -0
  57. package/dist/esm/charts/series/series-type.d.ts +49 -0
  58. package/dist/esm/charts/series/series.d.ts +317 -0
  59. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
  60. package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
  61. package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
  62. package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
  63. package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
  64. package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
  65. package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
  66. package/dist/esm/charts/treemap/treemap.d.ts +74 -0
  67. package/dist/esm/config.d.ts +27 -0
  68. package/dist/esm/data/lazy-row.d.ts +32 -0
  69. package/dist/esm/data/split-groups.d.ts +20 -0
  70. package/dist/esm/data/view-reader.d.ts +35 -0
  71. package/dist/esm/event-detail.d.ts +28 -0
  72. package/dist/esm/index.d.ts +3 -0
  73. package/dist/esm/interaction/hit-test.d.ts +30 -0
  74. package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
  75. package/dist/esm/interaction/host-sink-message.d.ts +46 -0
  76. package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
  77. package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
  78. package/dist/esm/interaction/spatial-grid.d.ts +15 -0
  79. package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
  80. package/dist/esm/interaction/zoom-controller.d.ts +106 -0
  81. package/dist/esm/interaction/zoom-router.d.ts +48 -0
  82. package/dist/esm/layout/facet-grid.d.ts +126 -0
  83. package/dist/esm/layout/plot-layout.d.ts +104 -0
  84. package/dist/esm/layout/ticks.d.ts +17 -0
  85. package/dist/esm/map/mercator.d.ts +102 -0
  86. package/dist/esm/map/tile-cache.d.ts +38 -0
  87. package/dist/esm/map/tile-layer.d.ts +66 -0
  88. package/dist/esm/map/tile-loader.d.ts +52 -0
  89. package/dist/esm/map/tile-source.d.ts +66 -0
  90. package/dist/esm/perspective-viewer-charts.js +3 -0
  91. package/dist/esm/perspective-viewer-charts.js.map +7 -0
  92. package/dist/esm/plugin/charts.d.ts +40 -0
  93. package/dist/esm/plugin/plugin.d.ts +95 -0
  94. package/dist/esm/render/scheduler.d.ts +41 -0
  95. package/dist/esm/theme/gradient.d.ts +48 -0
  96. package/dist/esm/theme/palette.d.ts +13 -0
  97. package/dist/esm/theme/theme-snapshot.d.ts +7 -0
  98. package/dist/esm/theme/theme.d.ts +53 -0
  99. package/dist/esm/transport/protocol.d.ts +430 -0
  100. package/dist/esm/transport/renderer-transport.d.ts +201 -0
  101. package/dist/esm/utils/css.d.ts +1 -0
  102. package/dist/esm/utils/font-snapshot.d.ts +50 -0
  103. package/dist/esm/webgl/buffer-pool.d.ts +62 -0
  104. package/dist/esm/webgl/context-manager.d.ts +184 -0
  105. package/dist/esm/webgl/gradient-texture.d.ts +17 -0
  106. package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
  107. package/dist/esm/webgl/plot-frame.d.ts +39 -0
  108. package/dist/esm/webgl/program-cache.d.ts +13 -0
  109. package/dist/esm/webgl/shader-manifest.d.ts +53 -0
  110. package/dist/esm/webgl/shader-registry.d.ts +22 -0
  111. package/dist/esm/worker/boot.d.ts +0 -0
  112. package/dist/esm/worker/dispatch.d.ts +9 -0
  113. package/dist/esm/worker/font-loader.d.ts +2 -0
  114. package/dist/esm/worker/renderer.worker.d.ts +115 -0
  115. package/dist/esm/worker/session-host.d.ts +26 -0
  116. package/package.json +47 -0
  117. package/src/css/perspective-viewer-charts.css +95 -0
  118. package/src/ts/axis/axis-primitives.ts +125 -0
  119. package/src/ts/axis/bar-axis.ts +345 -0
  120. package/src/ts/axis/canvas.ts +64 -0
  121. package/src/ts/axis/categorical-axis-core.ts +125 -0
  122. package/src/ts/axis/categorical-axis.ts +716 -0
  123. package/src/ts/axis/facet-chrome.ts +42 -0
  124. package/src/ts/axis/label-geometry.ts +188 -0
  125. package/src/ts/axis/legend.ts +218 -0
  126. package/src/ts/axis/numeric-axis.ts +353 -0
  127. package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
  128. package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
  129. package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
  130. package/src/ts/charts/candlestick/candlestick.ts +367 -0
  131. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
  132. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
  133. package/src/ts/charts/canvas-types.ts +30 -0
  134. package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
  135. package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
  136. package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
  137. package/src/ts/charts/cartesian/cartesian.ts +469 -0
  138. package/src/ts/charts/cartesian/glyph.ts +81 -0
  139. package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
  140. package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
  141. package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
  142. package/src/ts/charts/cartesian/label-interner.ts +56 -0
  143. package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
  144. package/src/ts/charts/chart-base.ts +840 -0
  145. package/src/ts/charts/chart.ts +427 -0
  146. package/src/ts/charts/common/band-layout.ts +63 -0
  147. package/src/ts/charts/common/categorical-y-chart.ts +81 -0
  148. package/src/ts/charts/common/category-axis-resolver.ts +314 -0
  149. package/src/ts/charts/common/chrome-cache.ts +79 -0
  150. package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
  151. package/src/ts/charts/common/leaf-color.ts +92 -0
  152. package/src/ts/charts/common/node-store.ts +235 -0
  153. package/src/ts/charts/common/tree-chart.ts +76 -0
  154. package/src/ts/charts/common/tree-chrome.ts +123 -0
  155. package/src/ts/charts/common/tree-data.ts +623 -0
  156. package/src/ts/charts/common/visible-extent.ts +112 -0
  157. package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
  158. package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
  159. package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
  160. package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
  161. package/src/ts/charts/heatmap/heatmap.ts +368 -0
  162. package/src/ts/charts/map/map.ts +201 -0
  163. package/src/ts/charts/registry.ts +65 -0
  164. package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
  165. package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
  166. package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
  167. package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
  168. package/src/ts/charts/series/series-build.ts +848 -0
  169. package/src/ts/charts/series/series-interact.ts +604 -0
  170. package/src/ts/charts/series/series-render.ts +1109 -0
  171. package/src/ts/charts/series/series-type.ts +99 -0
  172. package/src/ts/charts/series/series.ts +794 -0
  173. package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
  174. package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
  175. package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
  176. package/src/ts/charts/sunburst/sunburst.ts +248 -0
  177. package/src/ts/charts/treemap/treemap-interact.ts +445 -0
  178. package/src/ts/charts/treemap/treemap-layout.ts +328 -0
  179. package/src/ts/charts/treemap/treemap-render.ts +886 -0
  180. package/src/ts/charts/treemap/treemap.ts +247 -0
  181. package/src/ts/config.ts +41 -0
  182. package/src/ts/data/lazy-row.ts +140 -0
  183. package/src/ts/data/split-groups.ts +97 -0
  184. package/src/ts/data/view-reader.ts +107 -0
  185. package/src/ts/event-detail.ts +44 -0
  186. package/src/ts/index.ts +53 -0
  187. package/src/ts/interaction/hit-test.ts +106 -0
  188. package/src/ts/interaction/host-sink-dom.ts +85 -0
  189. package/src/ts/interaction/host-sink-message.ts +75 -0
  190. package/src/ts/interaction/lazy-tooltip.ts +102 -0
  191. package/src/ts/interaction/raw-event-forwarder.ts +175 -0
  192. package/src/ts/interaction/spatial-grid.ts +100 -0
  193. package/src/ts/interaction/tooltip-controller.ts +407 -0
  194. package/src/ts/interaction/zoom-controller.ts +468 -0
  195. package/src/ts/interaction/zoom-router.ts +230 -0
  196. package/src/ts/layout/facet-grid.ts +346 -0
  197. package/src/ts/layout/plot-layout.ts +277 -0
  198. package/src/ts/layout/ticks.ts +168 -0
  199. package/src/ts/map/mercator.ts +204 -0
  200. package/src/ts/map/tile-cache.ts +96 -0
  201. package/src/ts/map/tile-layer.ts +382 -0
  202. package/src/ts/map/tile-loader.ts +143 -0
  203. package/src/ts/map/tile-source.ts +156 -0
  204. package/src/ts/plugin/charts.ts +286 -0
  205. package/src/ts/plugin/plugin.ts +668 -0
  206. package/src/ts/render/scheduler.ts +339 -0
  207. package/src/ts/shaders/area.frag.glsl +20 -0
  208. package/src/ts/shaders/area.vert.glsl +19 -0
  209. package/src/ts/shaders/bar.frag.glsl +25 -0
  210. package/src/ts/shaders/bar.vert.glsl +60 -0
  211. package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
  212. package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
  213. package/src/ts/shaders/density-extreme.frag.glsl +30 -0
  214. package/src/ts/shaders/density-mrt.frag.glsl +44 -0
  215. package/src/ts/shaders/density-mrt.vert.glsl +48 -0
  216. package/src/ts/shaders/density-resolve.frag.glsl +89 -0
  217. package/src/ts/shaders/density-resolve.vert.glsl +23 -0
  218. package/src/ts/shaders/density-splat.frag.glsl +34 -0
  219. package/src/ts/shaders/density-splat.vert.glsl +52 -0
  220. package/src/ts/shaders/gridline.frag.glsl +18 -0
  221. package/src/ts/shaders/gridline.vert.glsl +18 -0
  222. package/src/ts/shaders/heatmap.frag.glsl +23 -0
  223. package/src/ts/shaders/heatmap.vert.glsl +42 -0
  224. package/src/ts/shaders/line-uniform.frag.glsl +26 -0
  225. package/src/ts/shaders/line-uniform.vert.glsl +54 -0
  226. package/src/ts/shaders/line.frag.glsl +28 -0
  227. package/src/ts/shaders/line.vert.glsl +87 -0
  228. package/src/ts/shaders/scatter.frag.glsl +39 -0
  229. package/src/ts/shaders/scatter.vert.glsl +67 -0
  230. package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
  231. package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
  232. package/src/ts/shaders/tile.frag.glsl +27 -0
  233. package/src/ts/shaders/tile.vert.glsl +35 -0
  234. package/src/ts/shaders/treemap.frag.glsl +19 -0
  235. package/src/ts/shaders/treemap.vert.glsl +25 -0
  236. package/src/ts/shaders/y-scatter.frag.glsl +30 -0
  237. package/src/ts/shaders/y-scatter.vert.glsl +31 -0
  238. package/src/ts/theme/gradient.ts +312 -0
  239. package/src/ts/theme/palette.ts +64 -0
  240. package/src/ts/theme/theme-snapshot.ts +66 -0
  241. package/src/ts/theme/theme.ts +166 -0
  242. package/src/ts/transport/protocol.ts +497 -0
  243. package/src/ts/transport/renderer-transport.ts +788 -0
  244. package/src/ts/utils/css.ts +36 -0
  245. package/src/ts/utils/font-snapshot.ts +159 -0
  246. package/src/ts/webgl/buffer-pool.ts +163 -0
  247. package/src/ts/webgl/context-manager.ts +414 -0
  248. package/src/ts/webgl/gradient-texture.ts +84 -0
  249. package/src/ts/webgl/instanced-attrs.ts +139 -0
  250. package/src/ts/webgl/plot-frame.ts +91 -0
  251. package/src/ts/webgl/program-cache.ts +46 -0
  252. package/src/ts/webgl/shader-manifest.ts +148 -0
  253. package/src/ts/webgl/shader-registry.ts +97 -0
  254. package/src/ts/worker/boot.ts +22 -0
  255. package/src/ts/worker/dispatch.ts +99 -0
  256. package/src/ts/worker/font-loader.ts +89 -0
  257. package/src/ts/worker/renderer.worker.ts +734 -0
  258. package/src/ts/worker/session-host.ts +118 -0
@@ -0,0 +1,815 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import type { WebGLContextManager } from "../../webgl/context-manager";
14
+ import type { HeatmapChart } from "./heatmap";
15
+ import { PlotLayout } from "../../layout/plot-layout";
16
+ import { drawFacetTitle } from "../../axis/facet-chrome";
17
+ import {
18
+ renderInPlotFrame,
19
+ clearAndSetupFrame,
20
+ withScissor,
21
+ } from "../../webgl/plot-frame";
22
+ import { getInstancing } from "../../webgl/instanced-attrs";
23
+ import { initCanvas } from "../../axis/canvas";
24
+ import { buildFacetGrid } from "../../layout/facet-grid";
25
+ import {
26
+ measureCategoricalAxisHeight,
27
+ renderCategoricalXTicks,
28
+ type CategoricalDomain,
29
+ } from "../../axis/categorical-axis";
30
+ import {
31
+ measureCategoricalAxisWidth,
32
+ renderCategoricalYTicks,
33
+ type CategoricalYAxisOptions,
34
+ } from "./heatmap-y-axis";
35
+ import {
36
+ drawNumericCategoryX,
37
+ drawNumericCategoryY,
38
+ } from "../../axis/bar-axis";
39
+ import { computeNiceTicks } from "../../layout/ticks";
40
+
41
+ // The heatmap's Y-axis column names end with the (single, externally
42
+ // enforced) aggregate name. That leaf column is a redundant constant and
43
+ // doesn't belong on the axis — promote the deepest split prefix to the
44
+ // leaf position instead.
45
+ const HEATMAP_Y_AXIS_OPTS: CategoricalYAxisOptions = {
46
+ skipLeafLevel: true,
47
+ };
48
+
49
+ import { renderLegend, renderLegendAt } from "../../axis/legend";
50
+ import heatmapVert from "../../shaders/heatmap.vert.glsl";
51
+ import heatmapFrag from "../../shaders/heatmap.frag.glsl";
52
+ import { colorValueToT } from "../../theme/gradient";
53
+ import {
54
+ bindGradientTexture,
55
+ ensureGradientTexture,
56
+ } from "../../webgl/gradient-texture";
57
+ import { renderHeatmapTooltip } from "./heatmap-interact";
58
+
59
+ export interface HeatmapLocations {
60
+ u_projection: WebGLUniformLocation | null;
61
+ u_cell_inset: WebGLUniformLocation | null;
62
+ u_cell_size: WebGLUniformLocation | null;
63
+ u_gradient_lut: WebGLUniformLocation | null;
64
+ a_corner: number;
65
+ a_cell: number;
66
+ a_color_t: number;
67
+ }
68
+
69
+ /**
70
+ * Full-frame render: WebGL cells → chrome overlay.
71
+ */
72
+ export function renderHeatmapFrame(
73
+ chart: HeatmapChart,
74
+ glManager: WebGLContextManager,
75
+ ): void {
76
+ const gl = glManager.gl;
77
+ const dpr = glManager.dpr;
78
+ const cssWidth = gl.canvas.width / dpr;
79
+ const cssHeight = gl.canvas.height / dpr;
80
+ if (cssWidth <= 0 || cssHeight <= 0) {
81
+ return;
82
+ }
83
+
84
+ if (chart._facets.length > 0) {
85
+ renderFacetedHeatmap(chart, glManager, cssWidth, cssHeight);
86
+ return;
87
+ }
88
+
89
+ if (chart._numX === 0 || chart._numY === 0) {
90
+ return;
91
+ }
92
+
93
+ const theme = chart._resolveTheme();
94
+
95
+ const xDomain: CategoricalDomain = {
96
+ levels: chart._xLevels,
97
+ numRows: chart._numX,
98
+ levelLabels: chart._groupBy.slice(),
99
+ };
100
+ const yDomain: CategoricalDomain = {
101
+ levels: chart._yLevels,
102
+ numRows: chart._numY,
103
+ levelLabels: [],
104
+ };
105
+
106
+ const xNumeric = chart._xAxisMode.mode === "numeric";
107
+ const yNumeric = chart._yAxisMode.mode === "numeric";
108
+
109
+ // Measure both hierarchical axes *before* building the layout so the
110
+ // plot rect accounts for their footprints. Numeric axes get fixed
111
+ // gutters matching bar's branch (24px bottom, 55px left).
112
+ const estLeft = yNumeric
113
+ ? 55
114
+ : measureCategoricalAxisWidth(yDomain, HEATMAP_Y_AXIS_OPTS);
115
+ const bottomExtra = xNumeric
116
+ ? 24
117
+ : measureCategoricalAxisHeight(
118
+ xDomain,
119
+ Math.max(1, cssWidth - estLeft - 110),
120
+ );
121
+
122
+ const layout = new PlotLayout(cssWidth, cssHeight, {
123
+ hasXLabel: chart._groupBy.length > 0,
124
+ hasYLabel: false,
125
+ hasLegend: true,
126
+ bottomExtra,
127
+ leftExtra: estLeft,
128
+ });
129
+ chart._lastLayout = layout;
130
+ if (chart._zoomController) {
131
+ chart._zoomController.updateLayout(layout);
132
+ }
133
+
134
+ // Domain depends on axis mode. Category mode: cell grid
135
+ // `[-0.5, N-0.5]` so cells sit at integer coordinates. Numeric mode:
136
+ // pre-padded `numericDomain` already includes a half-band on each
137
+ // edge so cells stay flush with the axis.
138
+ const xDomainMin = xNumeric ? chart._xNumericDomain!.min : -0.5;
139
+ const xDomainMax = xNumeric
140
+ ? chart._xNumericDomain!.max
141
+ : chart._numX - 0.5;
142
+ const yDomainMin = yNumeric ? chart._yNumericDomain!.min : -0.5;
143
+ const yDomainMax = yNumeric
144
+ ? chart._yNumericDomain!.max
145
+ : chart._numY - 0.5;
146
+ if (chart._zoomController) {
147
+ chart._zoomController.setBaseDomain(
148
+ xDomainMin,
149
+ xDomainMax,
150
+ yDomainMin,
151
+ yDomainMax,
152
+ );
153
+ }
154
+
155
+ const vis = chart._zoomController
156
+ ? chart._zoomController.getVisibleDomain()
157
+ : {
158
+ xMin: xDomainMin,
159
+ xMax: xDomainMax,
160
+ yMin: yDomainMin,
161
+ yMax: yDomainMax,
162
+ };
163
+
164
+ // Heatmap cell rects span the exact domain edge-to-edge, so any
165
+ // cosmetic padding leaves a visible sliver between the outermost
166
+ // cells and the axis chrome. Force `padRatio: 0` for flush edges.
167
+ const projection = layout.buildProjectionMatrix(
168
+ vis.xMin,
169
+ vis.xMax,
170
+ vis.yMin,
171
+ vis.yMax,
172
+ undefined,
173
+ undefined,
174
+ 0,
175
+ chart._xOrigin,
176
+ chart._yOrigin,
177
+ );
178
+
179
+ // Cell gap is specified in CSS pixels but the shader needs data-space
180
+ // insets. Convert using the plot's data-per-pixel scale; clamp to
181
+ // half a band so the gap can't eat the entire cell.
182
+ const plot = layout.plotRect;
183
+ const pxPerDataX = plot.width / (vis.xMax - vis.xMin);
184
+ const pxPerDataY = plot.height / (vis.yMax - vis.yMin);
185
+ const halfGap = theme.heatmapGapPx * 0.5;
186
+ const cellSizeX = xNumeric ? chart._xNumericDomain!.bandWidth : 1;
187
+ const cellSizeY = yNumeric ? chart._yNumericDomain!.bandWidth : 1;
188
+ const insetX = Math.min(
189
+ cellSizeX * 0.5,
190
+ pxPerDataX > 0 ? halfGap / pxPerDataX : 0,
191
+ );
192
+ const insetY = Math.min(
193
+ cellSizeY * 0.5,
194
+ pxPerDataY > 0 ? halfGap / pxPerDataY : 0,
195
+ );
196
+
197
+ // Gridline canvas isn't used by heatmap — clear it so stale content
198
+ // from a previous plugin doesn't bleed through.
199
+ if (chart._gridlineCanvas) {
200
+ const _gctx = initCanvas(chart._gridlineCanvas, layout, glManager.dpr);
201
+ }
202
+
203
+ ensureProgram(chart, glManager);
204
+ uploadInstanceBuffers(chart, glManager);
205
+
206
+ chart._gradientCache = ensureGradientTexture(
207
+ glManager,
208
+ chart._gradientCache,
209
+ theme.gradientStops,
210
+ );
211
+
212
+ renderInPlotFrame(gl, layout, glManager.dpr, () => {
213
+ gl.useProgram(chart._program!);
214
+ const loc = chart._locations!;
215
+ gl.uniformMatrix4fv(loc.u_projection, false, projection);
216
+ gl.uniform2f(loc.u_cell_inset, insetX, insetY);
217
+ gl.uniform2f(loc.u_cell_size, cellSizeX, cellSizeY);
218
+ bindGradientTexture(
219
+ glManager,
220
+ chart._gradientCache!.texture,
221
+ loc.u_gradient_lut,
222
+ 0,
223
+ );
224
+ drawCellsInstanced(chart, gl, glManager, 0, chart._uploadedCells);
225
+ });
226
+
227
+ renderHeatmapChromeOverlay(chart);
228
+ }
229
+
230
+ function ensureProgram(
231
+ chart: HeatmapChart,
232
+ glManager: WebGLContextManager,
233
+ ): void {
234
+ if (chart._program) {
235
+ return;
236
+ }
237
+
238
+ const gl = glManager.gl;
239
+ const program = glManager.shaders.getOrCreate(
240
+ "heatmap",
241
+ heatmapVert,
242
+ heatmapFrag,
243
+ );
244
+ chart._program = program;
245
+ chart._locations = {
246
+ u_projection: gl.getUniformLocation(program, "u_projection"),
247
+ u_cell_inset: gl.getUniformLocation(program, "u_cell_inset"),
248
+ u_cell_size: gl.getUniformLocation(program, "u_cell_size"),
249
+ u_gradient_lut: gl.getUniformLocation(program, "u_gradient_lut"),
250
+ a_corner: gl.getAttribLocation(program, "a_corner"),
251
+ a_cell: gl.getAttribLocation(program, "a_cell"),
252
+ a_color_t: gl.getAttribLocation(program, "a_color_t"),
253
+ };
254
+
255
+ const cornerBuffer = gl.createBuffer()!;
256
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
257
+
258
+ // Triangle strip: (0,0) (1,0) (0,1) (1,1)
259
+ gl.bufferData(
260
+ gl.ARRAY_BUFFER,
261
+ new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
262
+ gl.STATIC_DRAW,
263
+ );
264
+ chart._cornerBuffer = cornerBuffer;
265
+ }
266
+
267
+ function uploadInstanceBuffers(
268
+ chart: HeatmapChart,
269
+ glManager: WebGLContextManager,
270
+ ): void {
271
+ const n = chart._cells.length;
272
+ chart._uploadedCells = n;
273
+ if (n === 0) {
274
+ return;
275
+ }
276
+
277
+ const cellXY = new Float32Array(n * 2);
278
+ const colorT = new Float32Array(n);
279
+
280
+ // Sign-aware `t`: 0 always lands at 0.5. See `theme/gradient.ts`.
281
+ // Numeric-axis mode pre-multiplies the integer index into the real
282
+ // data position so the shader can apply `u_cell_size` (band width)
283
+ // uniformly without per-instance attrs. Datetime axes need an
284
+ // origin rebase before f32 narrowing — see {@link HeatmapFacet}
285
+ // and `HeatmapChart._xOrigin/_yOrigin`. Origins are 0 for category
286
+ // mode, where the integer index is already small.
287
+ if (chart._facets.length > 0) {
288
+ let i = 0;
289
+ for (const facet of chart._facets) {
290
+ const xPos = facet.pipeline.xPositions;
291
+ const yPos = facet.pipeline.yPositions;
292
+ const xO = facet.xOrigin;
293
+ const yO = facet.yOrigin;
294
+ for (const c of facet.pipeline.cells) {
295
+ cellXY[i * 2] = xPos ? xPos[c.xIdx] - xO : c.xIdx;
296
+ cellXY[i * 2 + 1] = yPos ? yPos[c.yIdx] - yO : c.yIdx;
297
+ colorT[i] = colorValueToT(
298
+ c.value,
299
+ chart._colorMin,
300
+ chart._colorMax,
301
+ );
302
+ i++;
303
+ }
304
+ }
305
+ } else {
306
+ const xPos = chart._xPositions;
307
+ const yPos = chart._yPositions;
308
+ const xO = chart._xOrigin;
309
+ const yO = chart._yOrigin;
310
+ for (let i = 0; i < n; i++) {
311
+ const c = chart._cells[i];
312
+ cellXY[i * 2] = xPos ? xPos[c.xIdx] - xO : c.xIdx;
313
+ cellXY[i * 2 + 1] = yPos ? yPos[c.yIdx] - yO : c.yIdx;
314
+ colorT[i] = colorValueToT(
315
+ c.value,
316
+ chart._colorMin,
317
+ chart._colorMax,
318
+ );
319
+ }
320
+ }
321
+
322
+ glManager.bufferPool.ensureCapacity(n);
323
+ glManager.bufferPool.upload("heatmap_cell", cellXY, 0, 2);
324
+ glManager.bufferPool.upload("heatmap_t", colorT, 0, 1);
325
+ }
326
+
327
+ function drawCellsInstanced(
328
+ chart: HeatmapChart,
329
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
330
+ glManager: WebGLContextManager,
331
+ instanceStart: number,
332
+ instanceCount: number,
333
+ ): void {
334
+ if (instanceCount === 0) {
335
+ return;
336
+ }
337
+
338
+ const loc = chart._locations!;
339
+ const instancing = getInstancing(glManager);
340
+ const { setDivisor } = instancing;
341
+ const f = Float32Array.BYTES_PER_ELEMENT;
342
+
343
+ // Per-vertex corner buffer.
344
+ gl.bindBuffer(gl.ARRAY_BUFFER, chart._cornerBuffer!);
345
+ gl.enableVertexAttribArray(loc.a_corner);
346
+ gl.vertexAttribPointer(loc.a_corner, 2, gl.FLOAT, false, 0, 0);
347
+ setDivisor(loc.a_corner, 0);
348
+
349
+ // Per-instance cell position. Byte offset into the packed buffer
350
+ // advances instance 0 of this draw to slot `instanceStart`.
351
+ //
352
+ // Render-path uses `peek` (not `getOrCreate`); if the buffers
353
+ // haven't been uploaded yet, skip the draw rather than render
354
+ // against a recreated zero buffer.
355
+ const cellBuf = glManager.bufferPool.peek("heatmap_cell");
356
+ const tBuf = glManager.bufferPool.peek("heatmap_t");
357
+ if (!cellBuf || !tBuf) {
358
+ return;
359
+ }
360
+
361
+ gl.bindBuffer(gl.ARRAY_BUFFER, cellBuf.buffer);
362
+ gl.enableVertexAttribArray(loc.a_cell);
363
+ gl.vertexAttribPointer(
364
+ loc.a_cell,
365
+ 2,
366
+ gl.FLOAT,
367
+ false,
368
+ 0,
369
+ instanceStart * 2 * f,
370
+ );
371
+ setDivisor(loc.a_cell, 1);
372
+
373
+ gl.bindBuffer(gl.ARRAY_BUFFER, tBuf.buffer);
374
+ gl.enableVertexAttribArray(loc.a_color_t);
375
+ gl.vertexAttribPointer(
376
+ loc.a_color_t,
377
+ 1,
378
+ gl.FLOAT,
379
+ false,
380
+ 0,
381
+ instanceStart * f,
382
+ );
383
+ setDivisor(loc.a_color_t, 1);
384
+
385
+ instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount);
386
+
387
+ setDivisor(loc.a_cell, 0);
388
+ setDivisor(loc.a_color_t, 0);
389
+ }
390
+
391
+ /**
392
+ * Chrome overlay: X axis + Y axis + color legend + (optional) tooltip.
393
+ */
394
+ export function renderHeatmapChromeOverlay(chart: HeatmapChart): void {
395
+ if (!chart._chromeCanvas) {
396
+ return;
397
+ }
398
+
399
+ if (chart._facets.length > 0) {
400
+ renderFacetedHeatmapChromeOverlay(chart);
401
+ return;
402
+ }
403
+
404
+ if (!chart._lastLayout) {
405
+ return;
406
+ }
407
+
408
+ const layout = chart._lastLayout;
409
+ const theme = chart._resolveTheme();
410
+ const dpr = chart._glManager?.dpr ?? 1;
411
+
412
+ const ctx = initCanvas(chart._chromeCanvas, layout, dpr);
413
+ if (!ctx) {
414
+ return;
415
+ }
416
+
417
+ // L-shaped axis line, same as bar chart chrome.
418
+ ctx.strokeStyle = theme.gridlineColor;
419
+ ctx.lineWidth = 1;
420
+ ctx.beginPath();
421
+ ctx.moveTo(layout.plotRect.x, layout.plotRect.y);
422
+ ctx.lineTo(layout.plotRect.x, layout.plotRect.y + layout.plotRect.height);
423
+ ctx.lineTo(
424
+ layout.plotRect.x + layout.plotRect.width,
425
+ layout.plotRect.y + layout.plotRect.height,
426
+ );
427
+ ctx.stroke();
428
+
429
+ const xDomain: CategoricalDomain = {
430
+ levels: chart._xLevels,
431
+ numRows: chart._numX,
432
+ levelLabels: chart._groupBy.slice(),
433
+ };
434
+
435
+ const yDomain: CategoricalDomain = {
436
+ levels: chart._yLevels,
437
+ numRows: chart._numY,
438
+ levelLabels: [],
439
+ };
440
+
441
+ // Heatmap X axis is the first group_by level; Y axis is the
442
+ // second (when present) or the first split_by level.
443
+ const xColumn = chart._groupBy[0];
444
+ const yColumn = chart._groupBy[1] ?? chart._splitBy[0];
445
+
446
+ if (chart._xAxisMode.mode === "numeric" && chart._xNumericDomain) {
447
+ const ticks = computeNiceTicks(layout.paddedXMin, layout.paddedXMax, 6);
448
+ drawNumericCategoryX(
449
+ ctx,
450
+ layout,
451
+ chart._xNumericDomain,
452
+ ticks,
453
+ theme,
454
+ chart.getColumnFormatter(xColumn, "tick"),
455
+ );
456
+ } else {
457
+ renderCategoricalXTicks(ctx, layout, xDomain, theme);
458
+ }
459
+
460
+ if (chart._yAxisMode.mode === "numeric" && chart._yNumericDomain) {
461
+ const ticks = computeNiceTicks(layout.paddedYMin, layout.paddedYMax, 6);
462
+ drawNumericCategoryY(
463
+ ctx,
464
+ layout,
465
+ chart._yNumericDomain,
466
+ ticks,
467
+ theme,
468
+ chart.getColumnFormatter(yColumn, "tick"),
469
+ );
470
+ } else {
471
+ renderCategoricalYTicks(
472
+ ctx,
473
+ layout,
474
+ yDomain,
475
+ theme,
476
+ HEATMAP_Y_AXIS_OPTS,
477
+ );
478
+ }
479
+
480
+ // Color legend on the right. The aggregate column name is in
481
+ // `_columnSlots[0]` (heatmap's only data column slot is "Color").
482
+ renderLegend(
483
+ chart._chromeCanvas,
484
+ layout,
485
+ {
486
+ min: chart._colorMin,
487
+ max: chart._colorMax,
488
+ label: chart._aggName,
489
+ },
490
+ theme.gradientStops,
491
+ theme,
492
+ chart.getColumnFormatter(chart._columnSlots[0], "value"),
493
+ );
494
+
495
+ if (chart._hoveredCell) {
496
+ renderHeatmapTooltip(chart);
497
+ }
498
+ }
499
+
500
+ /** Multi-facet WebGL render. Packs all facets' cells into one instance
501
+ * buffer and dispatches once per facet with a rebound pointer offset,
502
+ * matching projection, and scissor to the facet's plot rect. */
503
+ function renderFacetedHeatmap(
504
+ chart: HeatmapChart,
505
+ glManager: WebGLContextManager,
506
+ cssWidth: number,
507
+ cssHeight: number,
508
+ ): void {
509
+ const gl = glManager.gl;
510
+ const theme = chart._resolveTheme();
511
+
512
+ // Derive the effective shared-axis flags for this frame. Stamps
513
+ // `_lastEffectiveSharedX/Y` on the chart so
514
+ // `renderFacetedHeatmapChromeOverlay` reads the same values without
515
+ // re-deriving (and without us having to mutate `_facetConfig`).
516
+ const { effectiveSharedX, effectiveSharedY } =
517
+ chart.computeEffectiveFacetFlags();
518
+
519
+ const grid = buildFacetGrid(
520
+ chart._facets.map((f) => f.label),
521
+ {
522
+ cssWidth,
523
+ cssHeight,
524
+ xAxis: effectiveSharedX ? "outer" : "cell",
525
+ yAxis: effectiveSharedY ? "outer" : "cell",
526
+ hasLegend: true,
527
+ hasXLabel: chart._groupBy.length > 0,
528
+ hasYLabel: false,
529
+ gap: 8,
530
+ },
531
+ );
532
+ chart._facetGrid = grid;
533
+
534
+ for (let i = 0; i < chart._facets.length; i++) {
535
+ const cell = grid.cells[i];
536
+ if (cell) {
537
+ chart._facets[i].layout = cell.layout;
538
+ }
539
+ }
540
+
541
+ // Wire every active zoom controller's layout pointer so wheel/pan
542
+ // hit-tests compute correct data deltas.
543
+ chart.syncFacetZoomLayouts(grid.cells);
544
+
545
+ ensureProgram(chart, glManager);
546
+ uploadInstanceBuffers(chart, glManager);
547
+ chart._gradientCache = ensureGradientTexture(
548
+ glManager,
549
+ chart._gradientCache,
550
+ theme.gradientStops,
551
+ );
552
+
553
+ gl.useProgram(chart._program!);
554
+ const loc = chart._locations!;
555
+ bindGradientTexture(
556
+ glManager,
557
+ chart._gradientCache!.texture,
558
+ loc.u_gradient_lut,
559
+ 0,
560
+ );
561
+
562
+ // One clear for the whole frame; per-facet scissor keeps each
563
+ // facet's draw confined to its plot rect without wiping its
564
+ // neighbours.
565
+ clearAndSetupFrame(gl);
566
+
567
+ for (let i = 0; i < chart._facets.length; i++) {
568
+ const facet = chart._facets[i];
569
+ if (facet.instanceCount === 0) {
570
+ continue;
571
+ }
572
+
573
+ const { numX, numY } = facet.pipeline;
574
+ if (numX === 0 || numY === 0) {
575
+ continue;
576
+ }
577
+
578
+ const layout = facet.layout;
579
+ const xNumeric = facet.pipeline.xAxisMode.mode === "numeric";
580
+ const yNumeric = facet.pipeline.yAxisMode.mode === "numeric";
581
+ const xDomainMin = xNumeric ? facet.pipeline.xNumericDomain!.min : -0.5;
582
+ const xDomainMax = xNumeric
583
+ ? facet.pipeline.xNumericDomain!.max
584
+ : numX - 0.5;
585
+ const yDomainMin = yNumeric ? facet.pipeline.yNumericDomain!.min : -0.5;
586
+ const yDomainMax = yNumeric
587
+ ? facet.pipeline.yNumericDomain!.max
588
+ : numY - 0.5;
589
+
590
+ // Anchor the controller's base domain to this facet's data
591
+ // extent so wheel/pan transforms compose against a meaningful
592
+ // identity. In shared mode every facet writes the same base
593
+ // (heatmap facets share group_by → identical X domain, and
594
+ // matching Y shapes from `partitionColumnsPerFacet` → identical
595
+ // Y domain), so last-write-wins is a no-op. In independent
596
+ // mode each facet's own controller gets its own base.
597
+ const zc = chart.getZoomControllerForFacet(i);
598
+ if (zc) {
599
+ zc.setBaseDomain(xDomainMin, xDomainMax, yDomainMin, yDomainMax);
600
+ }
601
+
602
+ const vis = zc
603
+ ? zc.getVisibleDomain()
604
+ : {
605
+ xMin: xDomainMin,
606
+ xMax: xDomainMax,
607
+ yMin: yDomainMin,
608
+ yMax: yDomainMax,
609
+ };
610
+ const projection = layout.buildProjectionMatrix(
611
+ vis.xMin,
612
+ vis.xMax,
613
+ vis.yMin,
614
+ vis.yMax,
615
+ undefined,
616
+ undefined,
617
+ 0,
618
+ facet.xOrigin,
619
+ facet.yOrigin,
620
+ );
621
+
622
+ const plot = layout.plotRect;
623
+ const pxPerDataX = plot.width / (vis.xMax - vis.xMin);
624
+ const pxPerDataY = plot.height / (vis.yMax - vis.yMin);
625
+ const halfGap = theme.heatmapGapPx * 0.5;
626
+ const cellSizeX = xNumeric
627
+ ? facet.pipeline.xNumericDomain!.bandWidth
628
+ : 1;
629
+ const cellSizeY = yNumeric
630
+ ? facet.pipeline.yNumericDomain!.bandWidth
631
+ : 1;
632
+ const insetX = Math.min(
633
+ cellSizeX * 0.5,
634
+ pxPerDataX > 0 ? halfGap / pxPerDataX : 0,
635
+ );
636
+ const insetY = Math.min(
637
+ cellSizeY * 0.5,
638
+ pxPerDataY > 0 ? halfGap / pxPerDataY : 0,
639
+ );
640
+
641
+ withScissor(gl, layout, glManager.dpr, () => {
642
+ gl.uniformMatrix4fv(loc.u_projection, false, projection);
643
+ gl.uniform2f(loc.u_cell_inset, insetX, insetY);
644
+ gl.uniform2f(loc.u_cell_size, cellSizeX, cellSizeY);
645
+ drawCellsInstanced(
646
+ chart,
647
+ gl,
648
+ glManager,
649
+ facet.instanceStart,
650
+ facet.instanceCount,
651
+ );
652
+ });
653
+ }
654
+
655
+ renderHeatmapChromeOverlay(chart);
656
+ }
657
+
658
+ /**
659
+ * Multi-facet chrome: per-facet X/Y axis + title, one shared legend.
660
+ */
661
+ function renderFacetedHeatmapChromeOverlay(chart: HeatmapChart): void {
662
+ if (!chart._chromeCanvas || !chart._facetGrid) {
663
+ return;
664
+ }
665
+
666
+ const theme = chart._resolveTheme();
667
+
668
+ // `initCanvas` wants a `PlotLayout` to sync DPR-aware sizing. The
669
+ // first facet's layout is canvas-sized (cssWidth/cssHeight match
670
+ // the element), so either facet works for the DPR handshake.
671
+ const dpr = chart._glManager?.dpr ?? 1;
672
+ const ctx = initCanvas(chart._chromeCanvas, chart._facets[0].layout, dpr);
673
+ if (!ctx) {
674
+ return;
675
+ }
676
+
677
+ // Shared-axis suppression: when shared-X is active the X tick
678
+ // labels paint just below `cell.layout.plotRect` — which, because
679
+ // `buildFacetGrid` was called with `xAxis: "outer"`, falls into
680
+ // the reserved `outerXAxisRect` band rather than per-cell padding.
681
+ // Painting from a bottom-edge cell's layout is enough; non-edge
682
+ // rows would paint the same labels at the wrong y coordinate, so
683
+ // we skip them. Symmetric for Y. The cleanest way to express
684
+ // "shared = only edge cells render axes" is to gate the per-cell
685
+ // call on `!sharedX || isBottomEdge` (and analogously for Y).
686
+ const sharedX = chart._lastEffectiveSharedX;
687
+ const sharedY = chart._lastEffectiveSharedY;
688
+ const grid = chart._facetGrid;
689
+ for (let i = 0; i < chart._facets.length; i++) {
690
+ const facet = chart._facets[i];
691
+ const cell = grid.cells[i];
692
+ const layout = facet.layout;
693
+ const plot = layout.plotRect;
694
+
695
+ ctx.strokeStyle = theme.gridlineColor;
696
+ ctx.lineWidth = 1;
697
+ ctx.beginPath();
698
+ ctx.moveTo(plot.x, plot.y);
699
+ ctx.lineTo(plot.x, plot.y + plot.height);
700
+ ctx.lineTo(plot.x + plot.width, plot.y + plot.height);
701
+ ctx.stroke();
702
+
703
+ const xDomain: CategoricalDomain = {
704
+ levels: facet.pipeline.xLevels,
705
+ numRows: facet.pipeline.numX,
706
+ levelLabels: chart._groupBy.slice(),
707
+ };
708
+ const yDomain: CategoricalDomain = {
709
+ levels: facet.pipeline.yLevels,
710
+ numRows: facet.pipeline.numY,
711
+ levelLabels: [],
712
+ };
713
+
714
+ const xColumn = chart._groupBy[0];
715
+ const yColumn = chart._groupBy[1] ?? chart._splitBy[0];
716
+
717
+ if (!sharedX || cell.isBottomEdge) {
718
+ if (
719
+ facet.pipeline.xAxisMode.mode === "numeric" &&
720
+ facet.pipeline.xNumericDomain
721
+ ) {
722
+ const ticks = computeNiceTicks(
723
+ layout.paddedXMin,
724
+ layout.paddedXMax,
725
+ 6,
726
+ );
727
+ drawNumericCategoryX(
728
+ ctx,
729
+ layout,
730
+ facet.pipeline.xNumericDomain,
731
+ ticks,
732
+ theme,
733
+ chart.getColumnFormatter(xColumn, "tick"),
734
+ );
735
+ } else {
736
+ renderCategoricalXTicks(ctx, layout, xDomain, theme);
737
+ }
738
+ }
739
+
740
+ if (!sharedY || cell.isLeftEdge) {
741
+ if (
742
+ facet.pipeline.yAxisMode.mode === "numeric" &&
743
+ facet.pipeline.yNumericDomain
744
+ ) {
745
+ const ticks = computeNiceTicks(
746
+ layout.paddedYMin,
747
+ layout.paddedYMax,
748
+ 6,
749
+ );
750
+ drawNumericCategoryY(
751
+ ctx,
752
+ layout,
753
+ facet.pipeline.yNumericDomain,
754
+ ticks,
755
+ theme,
756
+ chart.getColumnFormatter(yColumn, "tick"),
757
+ );
758
+ } else {
759
+ renderCategoricalYTicks(
760
+ ctx,
761
+ layout,
762
+ yDomain,
763
+ theme,
764
+ HEATMAP_Y_AXIS_OPTS,
765
+ );
766
+ }
767
+ }
768
+ }
769
+
770
+ // Per-facet titles sit in the grid cell's titleRect — one strip per
771
+ // facet, above the plot rect. The grid's cells and the chart's
772
+ // facets are parallel arrays by construction.
773
+ for (let i = 0; i < grid.cells.length; i++) {
774
+ const cell = grid.cells[i];
775
+ const facet = chart._facets[i];
776
+ if (!facet || !cell.titleRect) {
777
+ continue;
778
+ }
779
+
780
+ drawFacetTitle(
781
+ chart._chromeCanvas,
782
+ facet.label,
783
+ cell.titleRect,
784
+ theme,
785
+ dpr,
786
+ );
787
+ }
788
+
789
+ // Shared colorbar at `grid.legendRect`. No meaningful single label —
790
+ // the facet titles already name each column, and a combined label
791
+ // would be ambiguous when columns differ.
792
+ if (grid.legendRect) {
793
+ renderLegendAt(
794
+ chart._chromeCanvas,
795
+ {
796
+ x: grid.legendRect.x,
797
+ y: grid.legendRect.y + 20,
798
+ width: grid.legendRect.width,
799
+ height: Math.max(1, grid.legendRect.height - 20),
800
+ },
801
+ {
802
+ min: chart._colorMin,
803
+ max: chart._colorMax,
804
+ label: "",
805
+ },
806
+ theme.gradientStops,
807
+ theme,
808
+ chart.getColumnFormatter(chart._columnSlots[0], "value"),
809
+ );
810
+ }
811
+
812
+ if (chart._hoveredCell) {
813
+ renderHeatmapTooltip(chart);
814
+ }
815
+ }