@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,948 @@
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 { Canvas2D } from "../canvas-types";
14
+ import { drawFacetTitle } from "../../axis/facet-chrome";
15
+ import type { WebGLContextManager } from "../../webgl/context-manager";
16
+ import type { CartesianChart } from "./cartesian";
17
+ import { PlotLayout } from "../../layout/plot-layout";
18
+ import {
19
+ buildFacetGrid,
20
+ bottomRowLayouts,
21
+ leftColumnLayouts,
22
+ type FacetGrid,
23
+ } from "../../layout/facet-grid";
24
+ import { type Theme } from "../../theme/theme";
25
+ import { resolvePalette } from "../../theme/palette";
26
+ import { paletteToStops } from "../../theme/gradient";
27
+ import {
28
+ renderInPlotFrame,
29
+ clearAndSetupFrame,
30
+ withScissor,
31
+ } from "../../webgl/plot-frame";
32
+ import { ensureGradientTexture } from "../../webgl/gradient-texture";
33
+ import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
34
+ import {
35
+ computeTicks,
36
+ renderGridlines,
37
+ renderAxesChrome,
38
+ renderCellXAxis,
39
+ renderCellYAxis,
40
+ renderOuterXAxis,
41
+ renderOuterYAxis,
42
+ type AxisDomain,
43
+ } from "../../axis/numeric-axis";
44
+ import { initCanvas, getScaledContext } from "../../axis/canvas";
45
+ import {
46
+ renderLegend,
47
+ renderLegendAt,
48
+ renderCategoricalLegend,
49
+ renderCategoricalLegendAt,
50
+ } from "../../axis/legend";
51
+
52
+ /**
53
+ * NaN guard: `_xOrigin`/`_yOrigin` start as NaN before the first valid sample.
54
+ */
55
+ function rebaseOrigin(o: number): number {
56
+ return isNaN(o) ? 0 : o;
57
+ }
58
+
59
+ /**
60
+ * Full-frame render: gridlines → glyph draw inside the plot-frame
61
+ * scissor → chrome overlay (axes + legend + tooltip).
62
+ *
63
+ * Branches on `_facetConfig.facet_mode`:
64
+ *
65
+ * - `"overlay"` (legacy): a single plot rect; all split series are
66
+ * drawn together, distinguished by color. This is the pre-facet
67
+ * behavior, preserved for manual opt-in via `plugin_config.facet_mode`.
68
+ * - `"grid"` (default): when splits are present, `_splitGroups` laid
69
+ * out as a grid of sub-plots by {@link buildFacetGrid}. When splits
70
+ * are absent, falls through to the single-plot path — identical to
71
+ * the `"overlay"` case with 0 splits, so the non-split render path
72
+ * is byte-for-byte unchanged from before this feature.
73
+ */
74
+ export function renderCartesianFrame(
75
+ chart: CartesianChart,
76
+ glManager: WebGLContextManager,
77
+ ): void {
78
+ const gl = glManager.gl;
79
+ const dpr = glManager.dpr;
80
+ const cssWidth = gl.canvas.width / dpr;
81
+ const cssHeight = gl.canvas.height / dpr;
82
+ if (cssWidth <= 0 || cssHeight <= 0) {
83
+ return;
84
+ }
85
+
86
+ const hasSplits = chart._splitGroups.length > 0;
87
+ const facetMode = chart._facetConfig.facet_mode;
88
+ const useGrid = hasSplits && facetMode === "grid";
89
+
90
+ chart.computeEffectiveFacetFlags();
91
+
92
+ // Legend appears only when the user wired a color column with a
93
+ // non-degenerate range. `split_by` alone no longer forces a
94
+ // legend — faceting is the axis of splitting, not coloring.
95
+ const hasColorCol =
96
+ chart._colorName !== "" && chart._colorMin < chart._colorMax;
97
+
98
+ // Overall domain = current viewport in shared-zoom mode, full data
99
+ // extents in independent-zoom mode (each facet consults its own
100
+ // controller inside `renderFacetedFrame`).
101
+ const independent =
102
+ useGrid && chart._facetConfig.zoom_mode === "independent";
103
+ let domain: { xMin: number; xMax: number; yMin: number; yMax: number };
104
+ if (chart._zoomController && !independent) {
105
+ domain = chart._zoomController.getVisibleDomain();
106
+ } else {
107
+ domain = {
108
+ xMin: chart._xMin,
109
+ xMax: chart._xMax,
110
+ yMin: chart._yMin,
111
+ yMax: chart._yMax,
112
+ };
113
+ }
114
+
115
+ if (!isFinite(domain.xMin) || !isFinite(domain.yMin)) {
116
+ return;
117
+ }
118
+
119
+ const theme = chart._resolveTheme();
120
+ const seriesPalette = theme.seriesPalette;
121
+
122
+ const xType = chart._columnTypes[chart._xLabel] || "";
123
+ const yType = chart._columnTypes[chart._yLabel] || "";
124
+ const xIsDate = xType === "date" || xType === "datetime";
125
+ const yIsDate = yType === "date" || yType === "datetime";
126
+
127
+ // Prepare the shared gradient LUT once (used by all facets).
128
+ //
129
+ // Three color sources map to three LUT types:
130
+ // - split_by or string color column → multi-entry series palette
131
+ // keyed by `_uniqueColorLabels.size`.
132
+ // - no color source at all → single-entry series palette
133
+ // (`palette[0]`). Points are stored with `a_color_value = 0.5`
134
+ // in the build; a 1-color LUT returns the same RGB for every
135
+ // sample so the default value is harmless.
136
+ // - numeric color column → continuous theme gradient.
137
+ // Categorical only when a string color column was wired —
138
+ // `split_by` alone no longer implies categorical coloring.
139
+ const isCategorical = chart._colorIsString;
140
+ const hasNoColorSource = !isCategorical && !chart._colorName;
141
+ let lutStops = theme.gradientStops;
142
+ if (isCategorical || hasNoColorSource) {
143
+ const labelCount = hasNoColorSource
144
+ ? Math.max(1, chart._splitGroups.length)
145
+ : Math.max(1, chart._uniqueColorLabels.size);
146
+
147
+ // Cache key carries the `seriesPalette` reference (changes per
148
+ // theme — `_resolveTheme` returns a fresh `Theme` after
149
+ // `invalidateTheme()`) plus `labelCount`. Reference compare
150
+ // catches theme switches that the prior length-only key
151
+ // missed.
152
+ if (
153
+ chart._lastLutStops &&
154
+ chart._lastLutSeriesPalette === seriesPalette &&
155
+ chart._lastLutLabelCount === labelCount
156
+ ) {
157
+ lutStops = chart._lastLutStops;
158
+ } else {
159
+ const palette = resolvePalette(
160
+ seriesPalette,
161
+ theme.gradientStops,
162
+ labelCount,
163
+ );
164
+ lutStops = paletteToStops(palette);
165
+ chart._lastLutStops = lutStops;
166
+ chart._lastLutSeriesPalette = seriesPalette;
167
+ chart._lastLutLabelCount = labelCount;
168
+ }
169
+ } else {
170
+ chart._lastLutStops = null;
171
+ chart._lastLutSeriesPalette = null;
172
+ chart._lastLutLabelCount = -1;
173
+ }
174
+
175
+ chart._gradientCache = ensureGradientTexture(
176
+ glManager,
177
+ chart._gradientCache,
178
+ lutStops,
179
+ );
180
+
181
+ if (useGrid) {
182
+ renderFacetedFrame(chart, glManager, domain, theme, {
183
+ xIsDate,
184
+ yIsDate,
185
+ cssWidth,
186
+ cssHeight,
187
+ });
188
+ } else {
189
+ // Single-plot path (no splits, or `"overlay"` mode).
190
+ chart._facetGrid = null;
191
+ renderSinglePlotFrame(chart, glManager, domain, theme, {
192
+ xIsDate,
193
+ yIsDate,
194
+ cssWidth,
195
+ cssHeight,
196
+ hasColorCol,
197
+ });
198
+ }
199
+
200
+ renderCartesianChromeOverlay(chart);
201
+ }
202
+
203
+ interface RenderFrameCtx {
204
+ xIsDate: boolean;
205
+ yIsDate: boolean;
206
+ cssWidth: number;
207
+ cssHeight: number;
208
+ }
209
+
210
+ interface SinglePlotCtx extends RenderFrameCtx {
211
+ hasColorCol: boolean;
212
+ }
213
+
214
+ function buildXDomain(
215
+ chart: CartesianChart,
216
+ min: number,
217
+ max: number,
218
+ isDate: boolean,
219
+ ): AxisDomain {
220
+ return {
221
+ min,
222
+ max,
223
+ label:
224
+ chart._xLabel || (chart._xIsRowIndex ? "Row" : chart._xName || ""),
225
+ isDate,
226
+ };
227
+ }
228
+
229
+ function buildYDomain(
230
+ chart: CartesianChart,
231
+ min: number,
232
+ max: number,
233
+ isDate: boolean,
234
+ ): AxisDomain {
235
+ return {
236
+ min,
237
+ max,
238
+ label: chart._yLabel || chart._yName,
239
+ isDate,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Original single-plot render path — all series drawn into one
245
+ * `PlotLayout` with one projection matrix. Used when splits are absent
246
+ * or when `facet_mode === "overlay"`.
247
+ */
248
+ function renderSinglePlotFrame(
249
+ chart: CartesianChart,
250
+ glManager: WebGLContextManager,
251
+ domain: { xMin: number; xMax: number; yMin: number; yMax: number },
252
+ theme: Theme,
253
+ ctx: SinglePlotCtx,
254
+ ): void {
255
+ const gl = glManager.gl;
256
+ const { cssWidth, cssHeight, xIsDate, yIsDate, hasColorCol } = ctx;
257
+
258
+ const layout = new PlotLayout(cssWidth, cssHeight, {
259
+ hasXLabel: !!chart._xLabel,
260
+ hasYLabel: !!chart._yLabel,
261
+ hasLegend: hasColorCol,
262
+ });
263
+ chart._lastLayout = layout;
264
+ if (chart._zoomController) {
265
+ chart._zoomController.updateLayout(layout);
266
+ }
267
+
268
+ const projection = layout.buildProjectionMatrix(
269
+ domain.xMin,
270
+ domain.xMax,
271
+ domain.yMin,
272
+ domain.yMax,
273
+ undefined,
274
+ undefined,
275
+ undefined,
276
+ rebaseOrigin(chart._xOrigin),
277
+ rebaseOrigin(chart._yOrigin),
278
+ );
279
+
280
+ const xDomain = buildXDomain(chart, domain.xMin, domain.xMax, xIsDate);
281
+ const yDomain = buildYDomain(chart, domain.yMin, domain.yMax, yIsDate);
282
+ const { xTicks, yTicks } = computeTicks(xDomain, yDomain, layout);
283
+
284
+ const isMap = chart._renderMode === "map";
285
+
286
+ if (chart._gridlineCanvas && !isMap) {
287
+ // One-shot destructive prep (resizes + clears + scales to DPR).
288
+ // `renderGridlines` itself is non-destructive.
289
+ const dpr = glManager.dpr;
290
+ initCanvas(chart._gridlineCanvas, layout, dpr);
291
+ renderGridlines(
292
+ chart._gridlineCanvas,
293
+ layout,
294
+ xTicks,
295
+ yTicks,
296
+ theme,
297
+ dpr,
298
+ );
299
+ } else if (chart._gridlineCanvas && isMap) {
300
+ // Map mode draws no cartesian gridlines, but the gridline
301
+ // canvas may carry stale ink from a prior cartesian chart
302
+ // type. Reset it to a clean transparent surface so the
303
+ // basemap (rendered into the GL canvas below) reads as the
304
+ // only background layer.
305
+ initCanvas(chart._gridlineCanvas, layout, glManager.dpr);
306
+ }
307
+
308
+ renderInPlotFrame(gl, layout, glManager.dpr, () => {
309
+ if (isMap) {
310
+ chart.renderBackground(
311
+ glManager,
312
+ layout,
313
+ projection,
314
+ domain,
315
+ rebaseOrigin(chart._xOrigin),
316
+ rebaseOrigin(chart._yOrigin),
317
+ );
318
+ }
319
+
320
+ chart.glyph.draw(chart, glManager, projection);
321
+ });
322
+
323
+ chart._lastXDomain = xDomain;
324
+ chart._lastYDomain = yDomain;
325
+ chart._lastXTicks = xTicks;
326
+ chart._lastYTicks = yTicks;
327
+ chart._lastGradientStops = theme.gradientStops;
328
+ chart._lastHasColorCol = hasColorCol;
329
+ }
330
+
331
+ /**
332
+ * Faceted render path — one sub-plot per split, laid out in a grid.
333
+ * Each facet gets its own `PlotLayout` (with canvas-absolute margins),
334
+ * its own projection matrix, and one `drawSeries(s)` dispatch inside
335
+ * its scissor rect. Shader, buffers, gradient texture, and zoom
336
+ * controller state are all shared.
337
+ *
338
+ * Shared-zoom mode uses one global domain for every facet's projection
339
+ * (current default). Independent-zoom mode (Stage 6) will consult a
340
+ * per-facet `ZoomController`.
341
+ */
342
+ function renderFacetedFrame(
343
+ chart: CartesianChart,
344
+ glManager: WebGLContextManager,
345
+ domain: { xMin: number; xMax: number; yMin: number; yMax: number },
346
+ theme: Theme,
347
+ ctx: RenderFrameCtx,
348
+ ): void {
349
+ const gl = glManager.gl;
350
+ const { cssWidth, cssHeight, xIsDate, yIsDate } = ctx;
351
+
352
+ const labels = chart._splitGroups.map((g) => g.prefix);
353
+
354
+ // Legend: reserve space only when the user wired a color column.
355
+ // - string column: categorical swatches from `_uniqueColorLabels`.
356
+ // - numeric column: gradient bar from `_colorMin/_colorMax`.
357
+ // - no color column: no legend (facets alone don't warrant one).
358
+ const hasCategoricalLegend =
359
+ chart._colorIsString && chart._uniqueColorLabels.size > 1;
360
+ const hasGradientLegend =
361
+ !!chart._colorName &&
362
+ !chart._colorIsString &&
363
+ chart._colorMin < chart._colorMax;
364
+ const hasLegend = hasCategoricalLegend || hasGradientLegend;
365
+
366
+ // Use the frame-local effective flags (set in
367
+ // `renderCartesianFrame`) so independent-zoom mode falls through
368
+ // to per-cell axes without mutating the user's stored
369
+ // `_facetConfig.shared_x_axis` / `shared_y_axis`. Continuous
370
+ // charts always have both axes, so the false branch maps to
371
+ // per-cell mode (never to "none", which is reserved for tree
372
+ // charts).
373
+ const grid: FacetGrid = buildFacetGrid(labels, {
374
+ cssWidth,
375
+ cssHeight,
376
+ xAxis: chart._lastEffectiveSharedX ? "outer" : "cell",
377
+ yAxis: chart._lastEffectiveSharedY ? "outer" : "cell",
378
+ hasLegend,
379
+ hasXLabel: !!chart._xLabel,
380
+ hasYLabel: !!chart._yLabel,
381
+ gap: chart._facetConfig.facet_padding,
382
+ });
383
+ chart._facetGrid = grid;
384
+
385
+ // Grid invariant: every cell has the same plot rect dimensions.
386
+ // Downstream code (tick sampling, projection math) depends on
387
+ // this. The O(N) comparison runs at most once per frame and bails
388
+ // at the first mismatch — cheap enough to leave on unconditionally.
389
+ if (grid.cells.length > 1) {
390
+ const r0 = grid.cells[0].layout.plotRect;
391
+ for (let i = 1; i < grid.cells.length; i++) {
392
+ const r = grid.cells[i].layout.plotRect;
393
+ if (r.width !== r0.width || r.height !== r0.height) {
394
+ console.warn(
395
+ `facet-grid: cell ${i} size (${r.width}×${r.height}) ` +
396
+ `differs from cell 0 (${r0.width}×${r0.height})`,
397
+ );
398
+ break;
399
+ }
400
+ }
401
+ }
402
+
403
+ // `_lastLayout` backs the hover hit-test in `continuous-interact.ts`.
404
+ // In faceted mode the hover routine resolves the facet under the
405
+ // cursor and consults that cell's layout directly; for legacy
406
+ // fallback (shouldn't fire), publish the first cell's layout.
407
+ chart._lastLayout = grid.cells[0]?.layout ?? null;
408
+
409
+ // Keep every controller's layout pointer fresh for wheel/pan math.
410
+ chart.syncFacetZoomLayouts(grid.cells);
411
+ const independent = chart._facetConfig.zoom_mode === "independent";
412
+
413
+ const xDomain = buildXDomain(chart, domain.xMin, domain.xMax, xIsDate);
414
+ const yDomain = buildYDomain(chart, domain.yMin, domain.yMax, yIsDate);
415
+
416
+ // Gridlines + per-facet axes use the first cell's layout for tick
417
+ // sampling (all cells have identical plotRect dimensions). Per-facet
418
+ // rendering then reuses the same tick arrays.
419
+ const sampleLayout = grid.cells[0]?.layout;
420
+ const { xTicks, yTicks } = sampleLayout
421
+ ? computeTicks(xDomain, yDomain, sampleLayout)
422
+ : { xTicks: [], yTicks: [] };
423
+
424
+ // One-shot destructive prep for the gridline + WebGL canvases.
425
+ // Both phases below are per-facet; calling their destructive
426
+ // helpers (initCanvas / renderInPlotFrame) in the loop would wipe
427
+ // every previously-drawn facet, leaving only the last cell
428
+ // visible.
429
+ if (chart._gridlineCanvas && sampleLayout) {
430
+ initCanvas(chart._gridlineCanvas, sampleLayout, glManager.dpr);
431
+ }
432
+
433
+ clearAndSetupFrame(gl);
434
+
435
+ for (let i = 0; i < grid.cells.length; i++) {
436
+ const cell = grid.cells[i];
437
+ const zc = chart.getZoomControllerForFacet(i);
438
+ const facetDomain = independent && zc ? zc.getVisibleDomain() : domain;
439
+
440
+ // `buildProjectionMatrix` must run before `renderGridlines`:
441
+ // it seeds the padded-domain fields on `cell.layout` that
442
+ // `dataToPixel` (used by gridline tick → pixel mapping) reads.
443
+ // Skipping this order leaves the layout on its default
444
+ // `[0, 1]` padded domain, and every tick pixel falls outside
445
+ // the cell's `plotRect`, so `drawGridlinesX/Y` filters them
446
+ // all out and the gridline canvas stays blank.
447
+ const projection = cell.layout.buildProjectionMatrix(
448
+ facetDomain.xMin,
449
+ facetDomain.xMax,
450
+ facetDomain.yMin,
451
+ facetDomain.yMax,
452
+ undefined,
453
+ undefined,
454
+ undefined,
455
+ rebaseOrigin(chart._xOrigin),
456
+ rebaseOrigin(chart._yOrigin),
457
+ );
458
+
459
+ // Per-facet gridlines: reuse shared ticks in shared-zoom mode,
460
+ // compute fresh ticks in independent mode (each facet has its
461
+ // own domain). Map mode skips gridlines entirely; the
462
+ // basemap layer is rendered into the GL canvas inside the
463
+ // facet's scissor below.
464
+ const isMap = chart._renderMode === "map";
465
+ if (chart._gridlineCanvas && !isMap) {
466
+ const localXTicks = independent
467
+ ? computeTicks(
468
+ buildXDomain(
469
+ chart,
470
+ facetDomain.xMin,
471
+ facetDomain.xMax,
472
+ xIsDate,
473
+ ),
474
+ buildYDomain(
475
+ chart,
476
+ facetDomain.yMin,
477
+ facetDomain.yMax,
478
+ yIsDate,
479
+ ),
480
+ cell.layout,
481
+ ).xTicks
482
+ : xTicks;
483
+ const localYTicks = independent
484
+ ? computeTicks(
485
+ buildXDomain(
486
+ chart,
487
+ facetDomain.xMin,
488
+ facetDomain.xMax,
489
+ xIsDate,
490
+ ),
491
+ buildYDomain(
492
+ chart,
493
+ facetDomain.yMin,
494
+ facetDomain.yMax,
495
+ yIsDate,
496
+ ),
497
+ cell.layout,
498
+ ).yTicks
499
+ : yTicks;
500
+ renderGridlines(
501
+ chart._gridlineCanvas,
502
+ cell.layout,
503
+ localXTicks,
504
+ localYTicks,
505
+ theme,
506
+ glManager.dpr,
507
+ );
508
+ }
509
+
510
+ withScissor(gl, cell.layout, glManager.dpr, () => {
511
+ if (isMap) {
512
+ chart.renderBackground(
513
+ glManager,
514
+ cell.layout,
515
+ projection,
516
+ facetDomain,
517
+ rebaseOrigin(chart._xOrigin),
518
+ rebaseOrigin(chart._yOrigin),
519
+ );
520
+ }
521
+
522
+ chart.glyph.drawSeries(chart, glManager, projection, i);
523
+ });
524
+ }
525
+
526
+ chart._lastXDomain = xDomain;
527
+ chart._lastYDomain = yDomain;
528
+ chart._lastXTicks = xTicks;
529
+ chart._lastYTicks = yTicks;
530
+ chart._lastGradientStops = theme.gradientStops;
531
+ chart._lastHasColorCol = hasLegend;
532
+ }
533
+
534
+ /**
535
+ * Redraw the chrome canvas only. Used for lightweight hover updates.
536
+ */
537
+ export function renderCartesianChromeOverlay(chart: CartesianChart): void {
538
+ if (
539
+ !chart._chromeCanvas ||
540
+ !chart._lastLayout ||
541
+ !chart._lastXDomain ||
542
+ !chart._lastYDomain ||
543
+ !chart._glManager
544
+ ) {
545
+ return;
546
+ }
547
+
548
+ // One-shot destructive prep for the chrome canvas — resizes to
549
+ // CSS × DPR and scales the transform. Per-facet calls below read
550
+ // the already-prepared context via `getScaledContext` so the
551
+ // bitmap persists across the loop.
552
+ initCanvas(chart._chromeCanvas, chart._lastLayout, chart._glManager.dpr);
553
+ if (chart._facetGrid) {
554
+ renderFacetedChromeOverlay(chart);
555
+ } else {
556
+ renderSinglePlotChromeOverlay(chart);
557
+ }
558
+ }
559
+
560
+ function renderSinglePlotChromeOverlay(chart: CartesianChart): void {
561
+ const layout = chart._lastLayout!;
562
+ const theme = chart._resolveTheme();
563
+ const dpr = chart._glManager?.dpr ?? 1;
564
+ const isMap = chart._renderMode === "map";
565
+
566
+ if (isMap) {
567
+ chart.renderMapChrome(chart._chromeCanvas!, layout, theme, dpr);
568
+ } else {
569
+ renderAxesChrome(
570
+ chart._chromeCanvas!,
571
+ chart._lastXDomain!,
572
+ chart._lastYDomain!,
573
+ layout,
574
+ chart._lastXTicks!,
575
+ chart._lastYTicks!,
576
+ theme,
577
+ dpr,
578
+ chart.getColumnFormatter(chart._xName, "tick"),
579
+ chart.getColumnFormatter(chart._yName, "tick"),
580
+ );
581
+ }
582
+
583
+ if (chart._lastHasColorCol) {
584
+ const stops = chart._lastGradientStops ?? theme.gradientStops;
585
+ if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
586
+ const palette = resolvePalette(
587
+ theme.seriesPalette,
588
+ stops,
589
+ chart._uniqueColorLabels.size,
590
+ );
591
+ renderCategoricalLegend(
592
+ chart._chromeCanvas!,
593
+ layout,
594
+ chart._uniqueColorLabels,
595
+ palette,
596
+ theme,
597
+ );
598
+ } else if (chart._colorName) {
599
+ renderLegend(
600
+ chart._chromeCanvas!,
601
+ layout,
602
+ {
603
+ min: chart._colorMin,
604
+ max: chart._colorMax,
605
+ label: chart._colorName,
606
+ },
607
+ stops,
608
+ theme,
609
+ chart.getColumnFormatter(chart._colorName, "value"),
610
+ );
611
+ }
612
+ }
613
+
614
+ renderScatterLabels(chart, chart._chromeCanvas!, layout, 0, 1);
615
+
616
+ if (chart._hoveredIndex >= 0 && chart._xData && chart._yData) {
617
+ renderTooltip(chart, chart._chromeCanvas!, layout);
618
+ }
619
+ }
620
+
621
+ function renderFacetedChromeOverlay(chart: CartesianChart): void {
622
+ const grid = chart._facetGrid!;
623
+ const canvas = chart._chromeCanvas!;
624
+ const theme = chart._resolveTheme();
625
+ const dpr = chart._glManager?.dpr ?? 1;
626
+ const sharedXTicks = chart._lastXTicks!;
627
+ const sharedYTicks = chart._lastYTicks!;
628
+ const xDomain = chart._lastXDomain!;
629
+ const yDomain = chart._lastYDomain!;
630
+ const isMap = chart._renderMode === "map";
631
+
632
+ // Read the frame-local effective flags set by `renderCartesianFrame`
633
+ // — these already fold in the independent-zoom override (outer
634
+ // axes are incompatible with per-cell viewports), so `sharedX` /
635
+ // `sharedY` true here implies shared-zoom too.
636
+ const sharedX = chart._lastEffectiveSharedX;
637
+ const sharedY = chart._lastEffectiveSharedY;
638
+ const independent = chart._facetConfig.zoom_mode === "independent";
639
+
640
+ // Shared X axis: one outer band across the bottom of the grid,
641
+ // with ticks painted per-column (one pass per bottom-row cell).
642
+ // Shared Y axis: one outer band down the left, ticks per-row
643
+ // (one pass per leftmost-column cell). Map mode replaces both
644
+ // with `renderMapChrome` (attribution + scale bar), painted once
645
+ // over the whole facet grid.
646
+ if (isMap) {
647
+ chart.renderMapChrome(canvas, chart._lastLayout!, theme, dpr);
648
+ }
649
+
650
+ if (!isMap && sharedX && grid.outerXAxisRect) {
651
+ renderOuterXAxis(
652
+ canvas,
653
+ grid.outerXAxisRect,
654
+ xDomain,
655
+ sharedXTicks,
656
+ bottomRowLayouts(grid),
657
+ theme,
658
+ !!chart._xLabel,
659
+ dpr,
660
+ chart.getColumnFormatter(chart._xName, "tick"),
661
+ );
662
+ }
663
+
664
+ if (!isMap && sharedY && grid.outerYAxisRect) {
665
+ renderOuterYAxis(
666
+ canvas,
667
+ grid.outerYAxisRect,
668
+ yDomain,
669
+ sharedYTicks,
670
+ leftColumnLayouts(grid),
671
+ theme,
672
+ !!chart._yLabel,
673
+ dpr,
674
+ chart.getColumnFormatter(chart._yName, "tick"),
675
+ );
676
+ }
677
+
678
+ // Per-facet axes for the non-shared sides + title strips.
679
+ // Map mode skips per-cell axis rendering (no cartesian axes
680
+ // belong on a map) but still paints facet titles and labels.
681
+ for (let i = 0; i < grid.cells.length; i++) {
682
+ const cell = grid.cells[i];
683
+ const zc = independent ? chart.getZoomControllerForFacet(i) : null;
684
+ const d = zc ? zc.getVisibleDomain() : null;
685
+ const localX = d ? { ...xDomain, min: d.xMin, max: d.xMax } : xDomain;
686
+ const localY = d ? { ...yDomain, min: d.yMin, max: d.yMax } : yDomain;
687
+ const ticks = independent
688
+ ? computeTicks(localX, localY, cell.layout)
689
+ : { xTicks: sharedXTicks, yTicks: sharedYTicks };
690
+
691
+ if (!isMap && !sharedX) {
692
+ renderCellXAxis(
693
+ canvas,
694
+ localX,
695
+ cell.layout,
696
+ ticks.xTicks,
697
+ theme,
698
+ !!chart._xLabel,
699
+ dpr,
700
+ chart.getColumnFormatter(chart._xName, "tick"),
701
+ );
702
+ }
703
+
704
+ if (!isMap && !sharedY) {
705
+ renderCellYAxis(
706
+ canvas,
707
+ localY,
708
+ cell.layout,
709
+ ticks.yTicks,
710
+ theme,
711
+ !!chart._yLabel,
712
+ dpr,
713
+ chart.getColumnFormatter(chart._yName, "tick"),
714
+ );
715
+ }
716
+
717
+ if (cell.titleRect) {
718
+ drawFacetTitle(canvas, cell.label, cell.titleRect, theme, dpr);
719
+ }
720
+
721
+ renderScatterLabels(chart, canvas, cell.layout, i, i + 1);
722
+ }
723
+
724
+ // Shared legend: categorical (string color) or gradient
725
+ // (numeric color). Position derives from `grid.legendRect`
726
+ // which `buildFacetGrid` populates when `hasLegend` was set.
727
+ if (chart._lastHasColorCol && grid.legendRect) {
728
+ const stops = chart._lastGradientStops ?? theme.gradientStops;
729
+ if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
730
+ const palette = resolvePalette(
731
+ theme.seriesPalette,
732
+ stops,
733
+ Math.max(1, chart._uniqueColorLabels.size),
734
+ );
735
+ renderCategoricalLegendAt(
736
+ canvas,
737
+ grid.legendRect,
738
+ chart._uniqueColorLabels,
739
+ palette,
740
+ theme,
741
+ );
742
+ } else if (chart._colorName) {
743
+ // Numeric gradient legend in the shared outer rect. The
744
+ // label sits above the bar, so inset the rect's top by
745
+ // the usual 20 px that `renderLegend` reserves.
746
+ renderLegendAt(
747
+ canvas,
748
+ {
749
+ x: grid.legendRect.x,
750
+ y: grid.legendRect.y + 20,
751
+ width: grid.legendRect.width,
752
+ height: grid.legendRect.height - 20,
753
+ },
754
+ {
755
+ min: chart._colorMin,
756
+ max: chart._colorMax,
757
+ label: chart._colorName,
758
+ },
759
+ stops,
760
+ theme,
761
+ chart.getColumnFormatter(chart._colorName, "value"),
762
+ );
763
+ }
764
+ }
765
+
766
+ // Coordinated hover / click indicators across facets. The tooltip
767
+ // lines are whatever the last resolved lazy fetch produced (or
768
+ // null while a fetch is still in flight); `renderCanvasTooltip`
769
+ // paints crosshair + ring regardless, but skips the text box
770
+ // until lines are available. See `handleCartesianHover`.
771
+ if (chart._hoveredIndex >= 0 && chart._xData && chart._yData) {
772
+ // `_xData`/`_yData` are rebased; `dataToPixel` expects absolute
773
+ // domain coords (matching `paddedXMin`/`paddedXMax`), so undo
774
+ // the rebase before mapping.
775
+ const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
776
+ const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
777
+ const dataX = chart._xData[chart._hoveredIndex] + xOrigin;
778
+ const dataY = chart._yData[chart._hoveredIndex] + yOrigin;
779
+ const sourceFacet = seriesFromIndex(chart, chart._hoveredIndex);
780
+ const opts = chart.glyph.tooltipOptions();
781
+ const tooltipLines = chart._lazyTooltip.lines ?? [];
782
+
783
+ for (let i = 0; i < grid.cells.length; i++) {
784
+ const cell = grid.cells[i];
785
+ const isSource = i === sourceFacet;
786
+
787
+ // Pixel position inside this facet for the source point's
788
+ // data coordinate — ghost indicator in non-source facets.
789
+ const pos = cell.layout.dataToPixel(dataX, dataY);
790
+ const plot = cell.layout.plotRect;
791
+ if (
792
+ pos.px < plot.x ||
793
+ pos.px > plot.x + plot.width ||
794
+ pos.py < plot.y ||
795
+ pos.py > plot.y + plot.height
796
+ ) {
797
+ continue;
798
+ }
799
+
800
+ const coordinated = chart._facetConfig.coordinated_tooltip;
801
+ const lines = isSource || coordinated ? tooltipLines : [];
802
+ renderCanvasTooltip(canvas, pos, lines, cell.layout, theme, dpr, {
803
+ crosshair: opts.crosshair,
804
+ highlightRadius: isSource ? opts.highlightRadius : 0,
805
+ });
806
+ }
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Map a flat slotted index back to its series (facet) index.
812
+ */
813
+ export function seriesFromIndex(
814
+ chart: CartesianChart,
815
+ flatIdx: number,
816
+ ): number {
817
+ if (chart._seriesCapacity <= 0) {
818
+ return 0;
819
+ }
820
+
821
+ return Math.floor(flatIdx / chart._seriesCapacity);
822
+ }
823
+
824
+ /**
825
+ * Maximum scatter labels painted in a single chrome pass. Beyond this
826
+ * we sample with a fixed stride so the canvas pass stays bounded as
827
+ * the user zooms out. The chrome overlay redraws on hover, so an
828
+ * unbounded `fillText` loop would stutter on every mouse move.
829
+ */
830
+ const MAX_SCATTER_LABELS = 5_000;
831
+
832
+ /**
833
+ * Draw the scatter-label column (slot 4) as 2D text next to each
834
+ * visible point. Labels are anchored slightly to the right of the
835
+ * point and vertically centered on it, painted in the theme's
836
+ * `labelColor`. Caller scopes us to a series range so faceted mode
837
+ * draws only the cell's own labels.
838
+ */
839
+ function renderScatterLabels(
840
+ chart: CartesianChart,
841
+ canvas: Canvas2D,
842
+ layout: PlotLayout,
843
+ seriesStart: number,
844
+ seriesEnd: number,
845
+ ): void {
846
+ if (!chart._labels || !chart._xData || !chart._yData) {
847
+ return;
848
+ }
849
+
850
+ const dict = chart._labels.dictionary;
851
+ const labelData = chart._labels.data;
852
+ const xData = chart._xData;
853
+ const yData = chart._yData;
854
+ const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
855
+ const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
856
+ const cap = chart._seriesCapacity;
857
+ if (cap <= 0) {
858
+ return;
859
+ }
860
+
861
+ let visibleCount = 0;
862
+ for (let s = seriesStart; s < seriesEnd; s++) {
863
+ visibleCount += chart._seriesUploadedCounts[s] ?? 0;
864
+ }
865
+
866
+ if (visibleCount === 0) {
867
+ return;
868
+ }
869
+
870
+ const dpr = chart._glManager?.dpr ?? 1;
871
+ const ctx = getScaledContext(canvas, dpr);
872
+ if (!ctx) {
873
+ return;
874
+ }
875
+
876
+ const theme = chart._resolveTheme();
877
+ const plot = layout.plotRect;
878
+ const stride = Math.max(1, Math.ceil(visibleCount / MAX_SCATTER_LABELS));
879
+
880
+ ctx.save();
881
+ ctx.font = `11px ${theme.fontFamily}`;
882
+ ctx.fillStyle = theme.labelColor;
883
+ ctx.textAlign = "left";
884
+ ctx.textBaseline = "middle";
885
+
886
+ for (let s = seriesStart; s < seriesEnd; s++) {
887
+ const count = chart._seriesUploadedCounts[s] ?? 0;
888
+ const base = s * cap;
889
+ for (let j = 0; j < count; j += stride) {
890
+ const idx = base + j;
891
+ const dictIdx = labelData[idx];
892
+ if (dictIdx < 0) {
893
+ continue;
894
+ }
895
+
896
+ const { px, py } = layout.dataToPixel(
897
+ xData[idx] + xOrigin,
898
+ yData[idx] + yOrigin,
899
+ );
900
+ if (
901
+ px < plot.x ||
902
+ px > plot.x + plot.width ||
903
+ py < plot.y ||
904
+ py > plot.y + plot.height
905
+ ) {
906
+ continue;
907
+ }
908
+
909
+ ctx.fillText(dict[dictIdx], px + 8, py - 4);
910
+ }
911
+ }
912
+
913
+ ctx.restore();
914
+ }
915
+
916
+ function renderTooltip(
917
+ chart: CartesianChart,
918
+ canvas: Canvas2D,
919
+ layout: PlotLayout,
920
+ ): void {
921
+ const idx = chart._hoveredIndex;
922
+ if (idx < 0 || !chart._xData || !chart._yData) {
923
+ return;
924
+ }
925
+
926
+ const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
927
+ const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
928
+ const pos = layout.dataToPixel(
929
+ chart._xData[idx] + xOrigin,
930
+ chart._yData[idx] + yOrigin,
931
+ );
932
+
933
+ // Lines come from the async lazy tooltip fetch kicked off in
934
+ // `handleCartesianHover`. While a fetch is in flight this is
935
+ // `null`; the canvas tooltip helper still paints the crosshair /
936
+ // highlight ring but skips the text box.
937
+ const lines = chart._lazyTooltip.lines ?? [];
938
+ const theme = chart._resolveTheme();
939
+ renderCanvasTooltip(
940
+ canvas,
941
+ pos,
942
+ lines,
943
+ layout,
944
+ theme,
945
+ chart._glManager?.dpr ?? 1,
946
+ chart.glyph.tooltipOptions(),
947
+ );
948
+ }