@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,1263 @@
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 { CartesianChart } from "../cartesian";
15
+ import type { Glyph } from "../glyph";
16
+ import { bindGradientTexture } from "../../../webgl/gradient-texture";
17
+ import { getInstancing } from "../../../webgl/instanced-attrs";
18
+ import { buildPointRowTooltipLines } from "../tooltip-lines";
19
+ import splatVert from "../../../shaders/density-splat.vert.glsl";
20
+ import splatFrag from "../../../shaders/density-splat.frag.glsl";
21
+ import extremeFrag from "../../../shaders/density-extreme.frag.glsl";
22
+ import mrtVert from "../../../shaders/density-mrt.vert.glsl";
23
+ import mrtFrag from "../../../shaders/density-mrt.frag.glsl";
24
+ import resolveVert from "../../../shaders/density-resolve.vert.glsl";
25
+ import resolveFrag from "../../../shaders/density-resolve.frag.glsl";
26
+
27
+ /**
28
+ * Integer mode identifiers shared with the resolve shader's
29
+ * `u_color_mode` branch ladder. Keep these in sync with the
30
+ * comparisons in `density-resolve.frag.glsl`.
31
+ */
32
+ const MODE_DENSITY = 0;
33
+ const MODE_MEAN = 1;
34
+ const MODE_EXTREME = 2;
35
+ const MODE_SIGNED = 3;
36
+
37
+ type ColorMode = "mean" | "density" | "extreme" | "signed";
38
+
39
+ /**
40
+ * Subset of `OES_draw_buffers_indexed` we touch. The official type
41
+ * isn't in `lib.dom.d.ts`; everything we use is `iOES`-suffixed.
42
+ */
43
+ interface IndexedBlendExt {
44
+ blendEquationiOES(buf: number, mode: number): void;
45
+ blendFunciOES(buf: number, src: number, dst: number): void;
46
+ enableiOES(target: number, index: number): void;
47
+ disableiOES(target: number, index: number): void;
48
+ }
49
+
50
+ interface SplatProgramCache {
51
+ program: WebGLProgram;
52
+ u_projection: WebGLUniformLocation | null;
53
+ u_radius_ndc: WebGLUniformLocation | null;
54
+ u_intensity: WebGLUniformLocation | null;
55
+ u_color_range: WebGLUniformLocation | null;
56
+ a_corner: number;
57
+ a_position: number;
58
+ a_color_value: number;
59
+ }
60
+
61
+ interface DensityCache {
62
+ splat: SplatProgramCache;
63
+
64
+ /**
65
+ * Single-target splat program writing `(w, w·t, 0, 0)` into the
66
+ * extreme FBO with MAX blend. Lazily compiled on first
67
+ * `extreme`-mode render (when MRT is unavailable).
68
+ */
69
+ extremeSplat: SplatProgramCache | null;
70
+
71
+ /**
72
+ * Two-target MRT splat program for the `extreme` path on hardware
73
+ * that advertises `OES_draw_buffers_indexed`. `gl_FragData[0]`
74
+ * routes to the heat FBO (ADD blend), `gl_FragData[1]` to the
75
+ * extreme FBO (MAX blend). Lazily compiled on first
76
+ * `extreme`-mode render after the indexed-blend extension is
77
+ * confirmed.
78
+ */
79
+ mrtSplat: SplatProgramCache | null;
80
+
81
+ resolve: {
82
+ program: WebGLProgram;
83
+ u_heat: WebGLUniformLocation | null;
84
+ u_extreme: WebGLUniformLocation | null;
85
+ u_gradient_lut: WebGLUniformLocation | null;
86
+ u_heat_max: WebGLUniformLocation | null;
87
+ u_color_mode: WebGLUniformLocation | null;
88
+ a_corner: number;
89
+ };
90
+
91
+ quadCornerBuffer: WebGLBuffer;
92
+ tripleCornerBuffer: WebGLBuffer;
93
+
94
+ /**
95
+ * Heat (density / weighted-color) framebuffer + texture. R = Σw,
96
+ * G = Σ(w·t). Always allocated.
97
+ */
98
+ heatTexture: WebGLTexture;
99
+ heatFramebuffer: WebGLFramebuffer;
100
+
101
+ /**
102
+ * Extreme (signed-max deviation) framebuffer + texture. R holds the
103
+ * MAX of positive deviation, G holds the MAX of negative deviation
104
+ * magnitude. Lazily allocated the first time `extreme` mode runs;
105
+ * `null` otherwise so the common case doesn't pay for a 4MB
106
+ * float texture it never reads.
107
+ */
108
+ extremeTexture: WebGLTexture | null;
109
+ extremeFramebuffer: WebGLFramebuffer | null;
110
+
111
+ /**
112
+ * MRT framebuffer with both `heatTexture` and `extremeTexture`
113
+ * attached. Used only on the indexed-blend fast path; `null`
114
+ * otherwise. Lazily allocated alongside `extremeTexture`.
115
+ */
116
+ mrtFramebuffer: WebGLFramebuffer | null;
117
+
118
+ heatWidth: number;
119
+ heatHeight: number;
120
+ heatType: number;
121
+ heatInternalFormat: number;
122
+ heatFormat: number;
123
+
124
+ /**
125
+ * `true` when the heat FBO uses a true float (or half-float)
126
+ * accumulation format. `signed` mode requires this; on the
127
+ * `UNSIGNED_BYTE` fallback the signed-sum math is meaningless
128
+ * (R and G saturate to 1 independently, so `G - 0.5·R` collapses
129
+ * to a constant 0.5) and the glyph silently degrades to `mean`.
130
+ */
131
+ floatFbo: boolean;
132
+
133
+ /**
134
+ * Cached probe result for `OES_draw_buffers_indexed`. `null` until
135
+ * the first `extreme`-mode draw, then either the extension object
136
+ * (MRT path) or `false` (two-pass fallback).
137
+ */
138
+ indexedBlend: IndexedBlendExt | null | false;
139
+
140
+ /**
141
+ * `true` after `console.warn` has fired once for a `signed`-mode
142
+ * downgrade on this glyph. Suppresses repeat noise across the
143
+ * 60Hz render loop.
144
+ */
145
+ signedDowngradeWarned: boolean;
146
+
147
+ robustBounds: {
148
+ lo: number;
149
+ hi: number;
150
+ dataCount: number;
151
+ colorName: string;
152
+ colorIsString: boolean;
153
+ } | null;
154
+ }
155
+
156
+ /**
157
+ * Density-field glyph. Each cartesian row is rasterized as an additive
158
+ * radial splat into an RGBA float FBO; a fullscreen pass resolves the
159
+ * accumulated density (and optional color-weighted average) through the
160
+ * chart's gradient LUT and composites the result inside the plot rect.
161
+ *
162
+ * The user-facing `gradient_color_mode` plugin field selects between:
163
+ *
164
+ * - `density` — alpha and hue from density alone.
165
+ * - `mean` — density-weighted average of color-t (default).
166
+ * - `extreme` — sign-aware MAX of per-point color deviation. Requires
167
+ * a second accumulation target; uses `OES_draw_buffers_indexed`
168
+ * MRT in one pass when available, otherwise falls back to two
169
+ * sequential splat passes.
170
+ * - `signed` — net positive vs. negative accumulation via the
171
+ * `G - 0.5·R` identity. Requires a float-capable framebuffer; on
172
+ * `UNSIGNED_BYTE` fallback the glyph silently degrades to `mean`
173
+ * with a one-line console warning.
174
+ */
175
+ export class DensityGlyph implements Glyph {
176
+ readonly name = "density" as const;
177
+ private _cache: DensityCache | null = null;
178
+
179
+ ensureProgram(chart: CartesianChart, glManager: WebGLContextManager): void {
180
+ if (this._cache) {
181
+ this.ensureHeatTarget(chart, glManager);
182
+ return;
183
+ }
184
+
185
+ const gl = glManager.gl;
186
+ const splatProgram = glManager.shaders.getOrCreate(
187
+ "density-splat",
188
+ splatVert,
189
+ splatFrag,
190
+ );
191
+ const resolveProgram = glManager.shaders.getOrCreate(
192
+ "density-resolve",
193
+ resolveVert,
194
+ resolveFrag,
195
+ );
196
+
197
+ const quadCornerBuffer = gl.createBuffer()!;
198
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadCornerBuffer);
199
+ gl.bufferData(
200
+ gl.ARRAY_BUFFER,
201
+ new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
202
+ gl.STATIC_DRAW,
203
+ );
204
+
205
+ const tripleCornerBuffer = gl.createBuffer()!;
206
+ gl.bindBuffer(gl.ARRAY_BUFFER, tripleCornerBuffer);
207
+ gl.bufferData(
208
+ gl.ARRAY_BUFFER,
209
+ new Float32Array([-1, -1, 3, -1, -1, 3]),
210
+ gl.STATIC_DRAW,
211
+ );
212
+
213
+ const { internalFormat, format, type, isFloat } =
214
+ pickHeatFormat(glManager);
215
+
216
+ const heatTexture = createAccumTexture(gl);
217
+ const heatFramebuffer = gl.createFramebuffer()!;
218
+
219
+ this._cache = {
220
+ splat: extractSplatLocations(gl, splatProgram),
221
+ extremeSplat: null,
222
+ mrtSplat: null,
223
+ resolve: {
224
+ program: resolveProgram,
225
+ u_heat: gl.getUniformLocation(resolveProgram, "u_heat"),
226
+ u_extreme: gl.getUniformLocation(resolveProgram, "u_extreme"),
227
+ u_gradient_lut: gl.getUniformLocation(
228
+ resolveProgram,
229
+ "u_gradient_lut",
230
+ ),
231
+ u_heat_max: gl.getUniformLocation(resolveProgram, "u_heat_max"),
232
+ u_color_mode: gl.getUniformLocation(
233
+ resolveProgram,
234
+ "u_color_mode",
235
+ ),
236
+ a_corner: gl.getAttribLocation(resolveProgram, "a_corner"),
237
+ },
238
+ quadCornerBuffer,
239
+ tripleCornerBuffer,
240
+ heatTexture,
241
+ heatFramebuffer,
242
+ extremeTexture: null,
243
+ extremeFramebuffer: null,
244
+ mrtFramebuffer: null,
245
+ heatWidth: 0,
246
+ heatHeight: 0,
247
+ heatType: type,
248
+ heatInternalFormat: internalFormat,
249
+ heatFormat: format,
250
+ floatFbo: isFloat,
251
+ indexedBlend: null,
252
+ signedDowngradeWarned: false,
253
+ robustBounds: null,
254
+ };
255
+
256
+ this.ensureHeatTarget(chart, glManager);
257
+ }
258
+
259
+ draw(
260
+ chart: CartesianChart,
261
+ glManager: WebGLContextManager,
262
+ projection: Float32Array,
263
+ ): void {
264
+ const cache = this._cache;
265
+ if (!cache || !ensurePointBuffers(glManager)) {
266
+ return;
267
+ }
268
+
269
+ const numSeries = Math.max(1, chart._splitGroups.length);
270
+ const cap = chart._seriesCapacity;
271
+ let total = 0;
272
+ for (let s = 0; s < numSeries; s++) {
273
+ total += chart._seriesUploadedCounts[s] ?? 0;
274
+ }
275
+
276
+ if (total === 0) {
277
+ return;
278
+ }
279
+
280
+ this.runSplatAndResolve(chart, glManager, cache, projection, (cb) => {
281
+ for (let s = 0; s < numSeries; s++) {
282
+ const count = chart._seriesUploadedCounts[s] ?? 0;
283
+ if (count <= 0) {
284
+ continue;
285
+ }
286
+
287
+ cb(s * cap, count);
288
+ }
289
+ });
290
+ }
291
+
292
+ drawSeries(
293
+ chart: CartesianChart,
294
+ glManager: WebGLContextManager,
295
+ projection: Float32Array,
296
+ seriesIdx: number,
297
+ ): void {
298
+ const cache = this._cache;
299
+ if (!cache || !ensurePointBuffers(glManager)) {
300
+ return;
301
+ }
302
+
303
+ const count = chart._seriesUploadedCounts[seriesIdx] ?? 0;
304
+ if (count <= 0) {
305
+ return;
306
+ }
307
+
308
+ const cap = chart._seriesCapacity;
309
+ this.runSplatAndResolve(chart, glManager, cache, projection, (cb) =>
310
+ cb(seriesIdx * cap, count),
311
+ );
312
+ }
313
+
314
+ buildTooltipLines(
315
+ chart: CartesianChart,
316
+ flatIdx: number,
317
+ ): Promise<string[]> {
318
+ return buildPointRowTooltipLines(chart, flatIdx);
319
+ }
320
+
321
+ tooltipOptions() {
322
+ return { crosshair: true, highlightRadius: 0 };
323
+ }
324
+
325
+ destroy(chart: CartesianChart): void {
326
+ const cache = this._cache;
327
+ if (!cache || !chart._glManager) {
328
+ this._cache = null;
329
+ return;
330
+ }
331
+
332
+ const gl = chart._glManager.gl;
333
+ gl.deleteBuffer(cache.quadCornerBuffer);
334
+ gl.deleteBuffer(cache.tripleCornerBuffer);
335
+ gl.deleteTexture(cache.heatTexture);
336
+ gl.deleteFramebuffer(cache.heatFramebuffer);
337
+ if (cache.extremeTexture) {
338
+ gl.deleteTexture(cache.extremeTexture);
339
+ }
340
+
341
+ if (cache.extremeFramebuffer) {
342
+ gl.deleteFramebuffer(cache.extremeFramebuffer);
343
+ }
344
+
345
+ if (cache.mrtFramebuffer) {
346
+ gl.deleteFramebuffer(cache.mrtFramebuffer);
347
+ }
348
+
349
+ this._cache = null;
350
+ }
351
+
352
+ /**
353
+ * Resize the heat (and, when allocated, extreme + MRT) targets to
354
+ * the current canvas bitmap size. The canvas backing store changes
355
+ * on DPR or layout updates, so we compare cached dimensions and
356
+ * re-allocate when stale.
357
+ */
358
+ private ensureHeatTarget(
359
+ _chart: CartesianChart,
360
+ glManager: WebGLContextManager,
361
+ ): void {
362
+ const cache = this._cache;
363
+ if (!cache) {
364
+ return;
365
+ }
366
+
367
+ const gl = glManager.gl;
368
+ const w = gl.canvas.width;
369
+ const h = gl.canvas.height;
370
+ if (w === cache.heatWidth && h === cache.heatHeight) {
371
+ return;
372
+ }
373
+
374
+ if (w <= 0 || h <= 0) {
375
+ return;
376
+ }
377
+
378
+ this.allocAccumTexture(gl, cache, cache.heatTexture, w, h);
379
+
380
+ gl.bindFramebuffer(gl.FRAMEBUFFER, cache.heatFramebuffer);
381
+ gl.framebufferTexture2D(
382
+ gl.FRAMEBUFFER,
383
+ gl.COLOR_ATTACHMENT0,
384
+ gl.TEXTURE_2D,
385
+ cache.heatTexture,
386
+ 0,
387
+ );
388
+
389
+ if (cache.extremeTexture) {
390
+ this.allocAccumTexture(gl, cache, cache.extremeTexture, w, h);
391
+ if (cache.extremeFramebuffer) {
392
+ gl.bindFramebuffer(gl.FRAMEBUFFER, cache.extremeFramebuffer);
393
+ gl.framebufferTexture2D(
394
+ gl.FRAMEBUFFER,
395
+ gl.COLOR_ATTACHMENT0,
396
+ gl.TEXTURE_2D,
397
+ cache.extremeTexture,
398
+ 0,
399
+ );
400
+ }
401
+
402
+ if (cache.mrtFramebuffer) {
403
+ // MRT FBO only exists when indexed-blend was probed
404
+ // successfully, which is gated on WebGL2.
405
+ const gl2 = gl as WebGL2RenderingContext;
406
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, cache.mrtFramebuffer);
407
+ gl2.framebufferTexture2D(
408
+ gl2.FRAMEBUFFER,
409
+ gl2.COLOR_ATTACHMENT0,
410
+ gl2.TEXTURE_2D,
411
+ cache.heatTexture,
412
+ 0,
413
+ );
414
+ gl2.framebufferTexture2D(
415
+ gl2.FRAMEBUFFER,
416
+ gl2.COLOR_ATTACHMENT1,
417
+ gl2.TEXTURE_2D,
418
+ cache.extremeTexture,
419
+ 0,
420
+ );
421
+ }
422
+ }
423
+
424
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
425
+
426
+ cache.heatWidth = w;
427
+ cache.heatHeight = h;
428
+ }
429
+
430
+ /**
431
+ * Re-allocate the storage for one accumulation texture using the
432
+ * cached format triple. Called both at first draw and on every
433
+ * canvas-size change.
434
+ */
435
+ private allocAccumTexture(
436
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
437
+ cache: DensityCache,
438
+ tex: WebGLTexture,
439
+ w: number,
440
+ h: number,
441
+ ): void {
442
+ gl.bindTexture(gl.TEXTURE_2D, tex);
443
+ gl.texImage2D(
444
+ gl.TEXTURE_2D,
445
+ 0,
446
+ cache.heatInternalFormat,
447
+ w,
448
+ h,
449
+ 0,
450
+ cache.heatFormat,
451
+ cache.heatType,
452
+ null,
453
+ );
454
+ }
455
+
456
+ /**
457
+ * Lazily allocate the extreme-mode accumulation texture + its
458
+ * framebuffers. Sized to match the heat target. Also probes
459
+ * `OES_draw_buffers_indexed` once per cache; if available, builds
460
+ * the MRT framebuffer with both textures attached.
461
+ */
462
+ private ensureExtremeTarget(
463
+ glManager: WebGLContextManager,
464
+ cache: DensityCache,
465
+ ): void {
466
+ const gl = glManager.gl;
467
+ if (cache.extremeTexture) {
468
+ return;
469
+ }
470
+
471
+ const tex = createAccumTexture(gl);
472
+ cache.extremeTexture = tex;
473
+ cache.extremeFramebuffer = gl.createFramebuffer()!;
474
+
475
+ this.allocAccumTexture(
476
+ gl,
477
+ cache,
478
+ tex,
479
+ cache.heatWidth,
480
+ cache.heatHeight,
481
+ );
482
+
483
+ gl.bindFramebuffer(gl.FRAMEBUFFER, cache.extremeFramebuffer);
484
+ gl.framebufferTexture2D(
485
+ gl.FRAMEBUFFER,
486
+ gl.COLOR_ATTACHMENT0,
487
+ gl.TEXTURE_2D,
488
+ tex,
489
+ 0,
490
+ );
491
+
492
+ if (cache.indexedBlend === null) {
493
+ // First chance to probe: only attempt MRT on WebGL2 where
494
+ // `gl.drawBuffers` is in core. On WebGL1 we'd also need
495
+ // `WEBGL_draw_buffers` for the JS function, but the
496
+ // indexed-blend extension itself doesn't ship there.
497
+ const ext = glManager.isWebGL2
498
+ ? (gl.getExtension(
499
+ "OES_draw_buffers_indexed",
500
+ ) as IndexedBlendExt | null)
501
+ : null;
502
+ cache.indexedBlend = ext ?? false;
503
+ }
504
+
505
+ if (cache.indexedBlend) {
506
+ // Indexed blend is gated on `isWebGL2`, so `gl` is a
507
+ // WebGL2 context here — cast for `COLOR_ATTACHMENT1`,
508
+ // which isn't on the WebGL1 type.
509
+ const gl2 = gl as WebGL2RenderingContext;
510
+ cache.mrtFramebuffer = gl2.createFramebuffer()!;
511
+ gl2.bindFramebuffer(gl2.FRAMEBUFFER, cache.mrtFramebuffer);
512
+ gl2.framebufferTexture2D(
513
+ gl2.FRAMEBUFFER,
514
+ gl2.COLOR_ATTACHMENT0,
515
+ gl2.TEXTURE_2D,
516
+ cache.heatTexture,
517
+ 0,
518
+ );
519
+ gl2.framebufferTexture2D(
520
+ gl2.FRAMEBUFFER,
521
+ gl2.COLOR_ATTACHMENT1,
522
+ gl2.TEXTURE_2D,
523
+ tex,
524
+ 0,
525
+ );
526
+ }
527
+
528
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
529
+ }
530
+
531
+ /**
532
+ * Compile (and cache) the single-target extreme splat program — the
533
+ * fallback two-pass path's second pass. Reuses the splat vertex
534
+ * shader so `v_color_t` semantics match the heat pass.
535
+ */
536
+ private ensureExtremeSplatProgram(
537
+ glManager: WebGLContextManager,
538
+ cache: DensityCache,
539
+ ): SplatProgramCache {
540
+ if (cache.extremeSplat) {
541
+ return cache.extremeSplat;
542
+ }
543
+
544
+ const program = glManager.shaders.getOrCreate(
545
+ "density-extreme",
546
+ splatVert,
547
+ extremeFrag,
548
+ );
549
+ cache.extremeSplat = extractSplatLocations(glManager.gl, program);
550
+ return cache.extremeSplat;
551
+ }
552
+
553
+ /**
554
+ * Compile (and cache) the MRT splat program. Only safe to call
555
+ * after `cache.indexedBlend` resolves truthy — the program's
556
+ * `#extension GL_EXT_draw_buffers : require` would fail to
557
+ * compile on contexts without multi-render-target support.
558
+ */
559
+ private ensureMrtSplatProgram(
560
+ glManager: WebGLContextManager,
561
+ cache: DensityCache,
562
+ ): SplatProgramCache {
563
+ if (cache.mrtSplat) {
564
+ return cache.mrtSplat;
565
+ }
566
+
567
+ // The MRT frag is GLSL ES 3.00 (`layout(location=N) out vec4`);
568
+ // the legacy GLSL 100 splat vert can't link against it because
569
+ // a program's shaders must share a version. Use the paired
570
+ // `density-mrt.vert.glsl` instead — same math, 300 ES dialect.
571
+ const program = glManager.shaders.getOrCreate(
572
+ "density-mrt",
573
+ mrtVert,
574
+ mrtFrag,
575
+ );
576
+ cache.mrtSplat = extractSplatLocations(glManager.gl, program);
577
+ return cache.mrtSplat;
578
+ }
579
+
580
+ /**
581
+ * Resolve the active mode for this frame. Folds in the silent
582
+ * downgrades to `mean` with a one-shot console warning:
583
+ *
584
+ * - `signed` requires a float-capable framebuffer
585
+ * (`EXT_color_buffer_float` on WebGL2 in practice).
586
+ * - `extreme` requires `gl.MAX` blend and a second color
587
+ * attachment, both of which are WebGL2-only here. On WebGL1
588
+ * we could probe `EXT_blend_minmax` + `WEBGL_draw_buffers`
589
+ * but degrading is simpler and the context manager prefers
590
+ * WebGL2 already.
591
+ */
592
+ private activeMode(
593
+ glManager: WebGLContextManager,
594
+ chart: CartesianChart,
595
+ cache: DensityCache,
596
+ ): ColorMode {
597
+ const requested = chart._pluginConfig.gradient_color_mode;
598
+ if (requested === "signed" && !cache.floatFbo) {
599
+ this.warnDowngradeOnce(
600
+ cache,
601
+ "signed mode requires a float framebuffer (EXT_color_buffer_float); falling back to mean.",
602
+ );
603
+ return "mean";
604
+ }
605
+
606
+ if (requested === "extreme" && !glManager.isWebGL2) {
607
+ this.warnDowngradeOnce(
608
+ cache,
609
+ "extreme mode requires WebGL2 (for MAX blend and a second color attachment); falling back to mean.",
610
+ );
611
+ return "mean";
612
+ }
613
+
614
+ return requested;
615
+ }
616
+
617
+ private warnDowngradeOnce(cache: DensityCache, message: string): void {
618
+ if (cache.signedDowngradeWarned) {
619
+ return;
620
+ }
621
+
622
+ cache.signedDowngradeWarned = true;
623
+ console.warn(`Density: ${message}`);
624
+ }
625
+
626
+ /**
627
+ * Shared splat → resolve pipeline. `dispatchSplats(cb)` iterates
628
+ * the series ranges the caller wants drawn, invoking
629
+ * `cb(slotOffset, count)` per range — `drawSeries` passes a single
630
+ * range, `draw` iterates every series. Internally branches on the
631
+ * active color mode: density/mean/signed share the single-target
632
+ * heat-only pass, `extreme` runs either an MRT single-pass or two
633
+ * sequential passes depending on extension support.
634
+ */
635
+ private runSplatAndResolve(
636
+ chart: CartesianChart,
637
+ glManager: WebGLContextManager,
638
+ cache: DensityCache,
639
+ projection: Float32Array,
640
+ dispatchSplats: (
641
+ cb: (slotOffset: number, count: number) => void,
642
+ ) => void,
643
+ ): void {
644
+ this.ensureHeatTarget(chart, glManager);
645
+ if (cache.heatWidth === 0 || cache.heatHeight === 0) {
646
+ return;
647
+ }
648
+
649
+ if (!chart._gradientCache) {
650
+ return;
651
+ }
652
+
653
+ const mode = this.activeMode(glManager, chart, cache);
654
+
655
+ // Resolve the color range we want the splat shader to use for
656
+ // its per-point `t` mapping. Robust bounds apply to modes that
657
+ // consume `t` directly (`mean`, `extreme`); `signed` actively
658
+ // benefits from raw extents so outlier influence accumulates;
659
+ // `density` ignores color entirely.
660
+ const hasColor =
661
+ chart._colorMin < chart._colorMax &&
662
+ (!!chart._colorName || chart._splitGroups.length > 1);
663
+ let cmin = 0.0;
664
+ let cmax = 0.0;
665
+ if (mode !== "density" && hasColor) {
666
+ cmin = chart._colorMin;
667
+ cmax = chart._colorMax;
668
+ const useRobust =
669
+ !chart._colorIsString &&
670
+ (mode === "mean" || mode === "extreme");
671
+ if (useRobust) {
672
+ const robust = ensureRobustBounds(chart, cache);
673
+ if (robust) {
674
+ cmin = robust.lo;
675
+ cmax = robust.hi;
676
+ }
677
+ }
678
+ }
679
+
680
+ if (mode === "extreme") {
681
+ this.ensureExtremeTarget(glManager, cache);
682
+ }
683
+
684
+ if (mode === "extreme" && cache.indexedBlend) {
685
+ this.runMrtExtremePass(
686
+ glManager,
687
+ cache,
688
+ projection,
689
+ chart._pluginConfig.gradient_intensity,
690
+ glManager.dpr * chart._pluginConfig.gradient_radius_px,
691
+ cmin,
692
+ cmax,
693
+ dispatchSplats,
694
+ );
695
+ } else {
696
+ this.runHeatPass(
697
+ glManager,
698
+ cache,
699
+ projection,
700
+ chart._pluginConfig.gradient_intensity,
701
+ glManager.dpr * chart._pluginConfig.gradient_radius_px,
702
+ cmin,
703
+ cmax,
704
+ dispatchSplats,
705
+ );
706
+
707
+ if (mode === "extreme") {
708
+ this.runExtremePass(
709
+ glManager,
710
+ cache,
711
+ projection,
712
+ chart._pluginConfig.gradient_intensity,
713
+ glManager.dpr * chart._pluginConfig.gradient_radius_px,
714
+ cmin,
715
+ cmax,
716
+ dispatchSplats,
717
+ );
718
+ }
719
+ }
720
+
721
+ this.runResolvePass(glManager, cache, chart, mode);
722
+ }
723
+
724
+ /**
725
+ * Single-target accumulation into the heat FBO. ADD blend; writes
726
+ * `(w, w·t, 0, 0)`. Used by every mode except `extreme` on the
727
+ * MRT path (which does this work and the extreme pass in one go).
728
+ */
729
+ private runHeatPass(
730
+ glManager: WebGLContextManager,
731
+ cache: DensityCache,
732
+ projection: Float32Array,
733
+ intensity: number,
734
+ radiusPx: number,
735
+ cmin: number,
736
+ cmax: number,
737
+ dispatchSplats: (
738
+ cb: (slotOffset: number, count: number) => void,
739
+ ) => void,
740
+ ): void {
741
+ const gl = glManager.gl;
742
+ const wasScissor = !!gl.getParameter(gl.SCISSOR_TEST);
743
+
744
+ gl.bindFramebuffer(gl.FRAMEBUFFER, cache.heatFramebuffer);
745
+ gl.viewport(0, 0, cache.heatWidth, cache.heatHeight);
746
+ this.clearTarget(gl, wasScissor);
747
+
748
+ gl.blendFunc(gl.ONE, gl.ONE);
749
+ gl.blendEquation(gl.FUNC_ADD);
750
+
751
+ this.bindSplatProgram(
752
+ gl,
753
+ cache.splat,
754
+ projection,
755
+ intensity,
756
+ radiusPx,
757
+ cache.heatWidth,
758
+ cache.heatHeight,
759
+ cmin,
760
+ cmax,
761
+ );
762
+
763
+ this.bindAndDispatchInstanced(
764
+ glManager,
765
+ cache,
766
+ cache.splat,
767
+ dispatchSplats,
768
+ );
769
+ this.unbindSplatInstancing(glManager, cache.splat);
770
+ }
771
+
772
+ /**
773
+ * Second pass of the two-pass extreme path. MAX blend; writes
774
+ * sign-split deviation magnitudes into the extreme FBO. Skipped
775
+ * entirely on the MRT fast path.
776
+ */
777
+ private runExtremePass(
778
+ glManager: WebGLContextManager,
779
+ cache: DensityCache,
780
+ projection: Float32Array,
781
+ intensity: number,
782
+ radiusPx: number,
783
+ cmin: number,
784
+ cmax: number,
785
+ dispatchSplats: (
786
+ cb: (slotOffset: number, count: number) => void,
787
+ ) => void,
788
+ ): void {
789
+ const gl = glManager.gl;
790
+ const wasScissor = !!gl.getParameter(gl.SCISSOR_TEST);
791
+ const program = this.ensureExtremeSplatProgram(glManager, cache);
792
+
793
+ gl.bindFramebuffer(gl.FRAMEBUFFER, cache.extremeFramebuffer!);
794
+ gl.viewport(0, 0, cache.heatWidth, cache.heatHeight);
795
+ this.clearTarget(gl, wasScissor);
796
+
797
+ gl.blendFunc(gl.ONE, gl.ONE);
798
+ // `MAX` is WebGL2-only on the type; `activeMode` gates the
799
+ // extreme path on WebGL2 so the cast is safe at runtime.
800
+ const gl2 = gl as WebGL2RenderingContext;
801
+ gl2.blendEquation(gl2.MAX);
802
+
803
+ this.bindSplatProgram(
804
+ gl,
805
+ program,
806
+ projection,
807
+ intensity,
808
+ radiusPx,
809
+ cache.heatWidth,
810
+ cache.heatHeight,
811
+ cmin,
812
+ cmax,
813
+ );
814
+
815
+ this.bindAndDispatchInstanced(
816
+ glManager,
817
+ cache,
818
+ program,
819
+ dispatchSplats,
820
+ );
821
+ this.unbindSplatInstancing(glManager, program);
822
+
823
+ // Restore default ADD equation for downstream callers.
824
+ gl.blendEquation(gl.FUNC_ADD);
825
+ }
826
+
827
+ /**
828
+ * MRT fast path: one splat draw writes density (ADD) and extreme
829
+ * (MAX) in the same invocation by routing `gl_FragData[0]` /
830
+ * `gl_FragData[1]` to attachments 0 and 1 with per-attachment
831
+ * blend equations.
832
+ */
833
+ private runMrtExtremePass(
834
+ glManager: WebGLContextManager,
835
+ cache: DensityCache,
836
+ projection: Float32Array,
837
+ intensity: number,
838
+ radiusPx: number,
839
+ cmin: number,
840
+ cmax: number,
841
+ dispatchSplats: (
842
+ cb: (slotOffset: number, count: number) => void,
843
+ ) => void,
844
+ ): void {
845
+ const gl = glManager.gl as WebGL2RenderingContext;
846
+ const ext = cache.indexedBlend as IndexedBlendExt;
847
+ const wasScissor = !!gl.getParameter(gl.SCISSOR_TEST);
848
+ const program = this.ensureMrtSplatProgram(glManager, cache);
849
+
850
+ gl.bindFramebuffer(gl.FRAMEBUFFER, cache.mrtFramebuffer!);
851
+ gl.viewport(0, 0, cache.heatWidth, cache.heatHeight);
852
+ gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
853
+ this.clearTarget(gl, wasScissor);
854
+
855
+ // Per-attachment blend: ADD for density, MAX for extreme.
856
+ ext.enableiOES(gl.BLEND, 0);
857
+ ext.enableiOES(gl.BLEND, 1);
858
+ ext.blendEquationiOES(0, gl.FUNC_ADD);
859
+ ext.blendFunciOES(0, gl.ONE, gl.ONE);
860
+ ext.blendEquationiOES(1, gl.MAX);
861
+ ext.blendFunciOES(1, gl.ONE, gl.ONE);
862
+
863
+ this.bindSplatProgram(
864
+ gl,
865
+ program,
866
+ projection,
867
+ intensity,
868
+ radiusPx,
869
+ cache.heatWidth,
870
+ cache.heatHeight,
871
+ cmin,
872
+ cmax,
873
+ );
874
+
875
+ this.bindAndDispatchInstanced(
876
+ glManager,
877
+ cache,
878
+ program,
879
+ dispatchSplats,
880
+ );
881
+ this.unbindSplatInstancing(glManager, program);
882
+
883
+ // Restore the global default for both attachments — subsequent
884
+ // single-target draws (resolve, other charts) rely on it. The
885
+ // indexed extension leaks state across attachments otherwise.
886
+ ext.blendEquationiOES(0, gl.FUNC_ADD);
887
+ ext.blendEquationiOES(1, gl.FUNC_ADD);
888
+ ext.blendFunciOES(0, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
889
+ ext.blendFunciOES(1, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
890
+ gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
891
+ }
892
+
893
+ /**
894
+ * Clear the currently bound framebuffer's color attachment(s) to
895
+ * fully transparent, bypassing scissor so leftovers from a prior
896
+ * facet's region don't bleed into this pass's full sample range.
897
+ * Restores the scissor state on exit.
898
+ */
899
+ private clearTarget(
900
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
901
+ wasScissor: boolean,
902
+ ): void {
903
+ if (wasScissor) {
904
+ gl.disable(gl.SCISSOR_TEST);
905
+ }
906
+
907
+ gl.clearColor(0, 0, 0, 0);
908
+ gl.clear(gl.COLOR_BUFFER_BIT);
909
+
910
+ if (wasScissor) {
911
+ gl.enable(gl.SCISSOR_TEST);
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Upload the per-frame splat-program uniforms (projection, splat
917
+ * radius, intensity, color range). Shared by the heat-only pass,
918
+ * the extreme single-target pass, and the MRT pass since each
919
+ * program exposes the same uniform layout.
920
+ */
921
+ private bindSplatProgram(
922
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
923
+ cache: SplatProgramCache,
924
+ projection: Float32Array,
925
+ intensity: number,
926
+ radiusPx: number,
927
+ targetWidth: number,
928
+ targetHeight: number,
929
+ cmin: number,
930
+ cmax: number,
931
+ ): void {
932
+ gl.useProgram(cache.program);
933
+ gl.uniformMatrix4fv(cache.u_projection, false, projection);
934
+ gl.uniform1f(cache.u_intensity, intensity);
935
+
936
+ const radiusNdcX = (2 * radiusPx) / Math.max(1, targetWidth);
937
+ const radiusNdcY = (2 * radiusPx) / Math.max(1, targetHeight);
938
+ gl.uniform2f(cache.u_radius_ndc, radiusNdcX, radiusNdcY);
939
+ gl.uniform2f(cache.u_color_range, cmin, cmax);
940
+ }
941
+
942
+ /**
943
+ * Bind the static unit-quad corner buffer (divisor 0) and per-
944
+ * instance position + color attributes (divisor 1), then iterate
945
+ * the caller's series ranges issuing one instanced draw each.
946
+ */
947
+ private bindAndDispatchInstanced(
948
+ glManager: WebGLContextManager,
949
+ cache: DensityCache,
950
+ program: SplatProgramCache,
951
+ dispatchSplats: (
952
+ cb: (slotOffset: number, count: number) => void,
953
+ ) => void,
954
+ ): void {
955
+ const gl = glManager.gl;
956
+
957
+ gl.bindBuffer(gl.ARRAY_BUFFER, cache.quadCornerBuffer);
958
+ gl.enableVertexAttribArray(program.a_corner);
959
+ gl.vertexAttribPointer(program.a_corner, 2, gl.FLOAT, false, 0, 0);
960
+
961
+ const instancing = getInstancing(glManager);
962
+ instancing.setDivisor(program.a_corner, 0);
963
+ instancing.setDivisor(program.a_position, 1);
964
+ instancing.setDivisor(program.a_color_value, 1);
965
+
966
+ const posBuf = glManager.bufferPool.peek("a_position")!;
967
+ const colorBuf = glManager.bufferPool.peek("a_color_value")!;
968
+
969
+ dispatchSplats((slotOffset, count) => {
970
+ const posStride = 2 * Float32Array.BYTES_PER_ELEMENT;
971
+ const scalarStride = Float32Array.BYTES_PER_ELEMENT;
972
+
973
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf.buffer);
974
+ gl.enableVertexAttribArray(program.a_position);
975
+ gl.vertexAttribPointer(
976
+ program.a_position,
977
+ 2,
978
+ gl.FLOAT,
979
+ false,
980
+ posStride,
981
+ slotOffset * posStride,
982
+ );
983
+
984
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf.buffer);
985
+ gl.enableVertexAttribArray(program.a_color_value);
986
+ gl.vertexAttribPointer(
987
+ program.a_color_value,
988
+ 1,
989
+ gl.FLOAT,
990
+ false,
991
+ scalarStride,
992
+ slotOffset * scalarStride,
993
+ );
994
+
995
+ instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
996
+ });
997
+ }
998
+
999
+ /**
1000
+ * Reset the per-instance divisors so subsequent draws (in this or
1001
+ * another chart) don't inherit the instanced bindings.
1002
+ */
1003
+ private unbindSplatInstancing(
1004
+ glManager: WebGLContextManager,
1005
+ program: SplatProgramCache,
1006
+ ): void {
1007
+ const instancing = getInstancing(glManager);
1008
+ instancing.setDivisor(program.a_position, 0);
1009
+ instancing.setDivisor(program.a_color_value, 0);
1010
+ }
1011
+
1012
+ /**
1013
+ * Resolve pass on the canvas FBO. Standard alpha composite. Reads
1014
+ * the heat FBO (always) and, in `extreme` mode, the extreme FBO.
1015
+ * Uploads the mode int that the resolve frag branches on.
1016
+ */
1017
+ private runResolvePass(
1018
+ glManager: WebGLContextManager,
1019
+ cache: DensityCache,
1020
+ chart: CartesianChart,
1021
+ mode: ColorMode,
1022
+ ): void {
1023
+ const gl = glManager.gl;
1024
+
1025
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1026
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
1027
+ gl.blendEquation(gl.FUNC_ADD);
1028
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1029
+
1030
+ const resolve = cache.resolve;
1031
+ gl.useProgram(resolve.program);
1032
+ gl.uniform1f(resolve.u_heat_max, chart._pluginConfig.gradient_heat_max);
1033
+ gl.uniform1i(resolve.u_color_mode, modeToInt(mode));
1034
+
1035
+ gl.activeTexture(gl.TEXTURE0);
1036
+ gl.bindTexture(gl.TEXTURE_2D, cache.heatTexture);
1037
+ gl.uniform1i(resolve.u_heat, 0);
1038
+
1039
+ // The shader unconditionally samples `u_extreme` in the extreme
1040
+ // branch. Bind whatever we have (the heat texture as a no-op
1041
+ // bind in non-extreme modes) so the unit stays defined and
1042
+ // texture-completeness checks pass.
1043
+ gl.activeTexture(gl.TEXTURE1);
1044
+ gl.bindTexture(
1045
+ gl.TEXTURE_2D,
1046
+ cache.extremeTexture ?? cache.heatTexture,
1047
+ );
1048
+ gl.uniform1i(resolve.u_extreme, 1);
1049
+
1050
+ bindGradientTexture(
1051
+ glManager,
1052
+ chart._gradientCache!.texture,
1053
+ resolve.u_gradient_lut,
1054
+ 2,
1055
+ );
1056
+
1057
+ gl.bindBuffer(gl.ARRAY_BUFFER, cache.tripleCornerBuffer);
1058
+ gl.enableVertexAttribArray(resolve.a_corner);
1059
+ gl.vertexAttribPointer(resolve.a_corner, 2, gl.FLOAT, false, 0, 0);
1060
+
1061
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
1062
+ }
1063
+ }
1064
+
1065
+ function modeToInt(mode: ColorMode): number {
1066
+ switch (mode) {
1067
+ case "density":
1068
+ return MODE_DENSITY;
1069
+ case "extreme":
1070
+ return MODE_EXTREME;
1071
+ case "signed":
1072
+ return MODE_SIGNED;
1073
+ case "mean":
1074
+ default:
1075
+ return MODE_MEAN;
1076
+ }
1077
+ }
1078
+
1079
+ function createAccumTexture(
1080
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
1081
+ ): WebGLTexture {
1082
+ const tex = gl.createTexture()!;
1083
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1084
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1085
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1086
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1087
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1088
+ return tex;
1089
+ }
1090
+
1091
+ function extractSplatLocations(
1092
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
1093
+ program: WebGLProgram,
1094
+ ): SplatProgramCache {
1095
+ return {
1096
+ program,
1097
+ u_projection: gl.getUniformLocation(program, "u_projection"),
1098
+ u_radius_ndc: gl.getUniformLocation(program, "u_radius_ndc"),
1099
+ u_intensity: gl.getUniformLocation(program, "u_intensity"),
1100
+ u_color_range: gl.getUniformLocation(program, "u_color_range"),
1101
+ a_corner: gl.getAttribLocation(program, "a_corner"),
1102
+ a_position: gl.getAttribLocation(program, "a_position"),
1103
+ a_color_value: gl.getAttribLocation(program, "a_color_value"),
1104
+ };
1105
+ }
1106
+
1107
+ /**
1108
+ * Resolve the highest-precision float color buffer the running GL
1109
+ * context will accept. WebGL2 + `EXT_color_buffer_float` gives
1110
+ * RGBA16F; otherwise fall back to RGBA8. The fallback compresses
1111
+ * density into [0, 1] and saturates earlier; `signed` mode degrades
1112
+ * to `mean` on this path because its `G - 0.5·R` math depends on
1113
+ * unclamped accumulation.
1114
+ */
1115
+ function pickHeatFormat(glManager: WebGLContextManager): {
1116
+ internalFormat: number;
1117
+ format: number;
1118
+ type: number;
1119
+ isFloat: boolean;
1120
+ } {
1121
+ const gl = glManager.gl;
1122
+ if (glManager.isWebGL2) {
1123
+ const gl2 = gl as WebGL2RenderingContext;
1124
+ if (gl2.getExtension("EXT_color_buffer_float")) {
1125
+ return {
1126
+ internalFormat: gl2.RGBA16F,
1127
+ format: gl2.RGBA,
1128
+ type: gl2.HALF_FLOAT,
1129
+ isFloat: true,
1130
+ };
1131
+ }
1132
+ }
1133
+
1134
+ return {
1135
+ internalFormat: gl.RGBA,
1136
+ format: gl.RGBA,
1137
+ type: gl.UNSIGNED_BYTE,
1138
+ isFloat: false,
1139
+ };
1140
+ }
1141
+
1142
+ /**
1143
+ * Verify the shared cartesian position + color attribute buffers exist.
1144
+ * The cartesian build pipeline uploads them on each chunk; render-path
1145
+ * callers must use `peek` (never `getOrCreate`) so a pan/zoom render
1146
+ * landing between an `ensureBufferCapacity` and its `uploadChunk`
1147
+ * doesn't recreate the buffer with zeros.
1148
+ */
1149
+ function ensurePointBuffers(glManager: WebGLContextManager): boolean {
1150
+ const pos = glManager.bufferPool.peek("a_position");
1151
+ const color = glManager.bufferPool.peek("a_color_value");
1152
+ return !!pos && !!color;
1153
+ }
1154
+
1155
+ /**
1156
+ * Cap for the strided sample used to compute the 5th/95th percentile
1157
+ * color-column bounds. A larger sample tightens the quantile estimate
1158
+ * but costs O(n log n) sort time. At 50k the sort runs ~10ms once per
1159
+ * data refresh; subsequent renders hit the cache.
1160
+ */
1161
+ const ROBUST_SAMPLE_MAX = 50_000;
1162
+
1163
+ /**
1164
+ * Resolve the robust (5th/95th percentile) bounds for the color column,
1165
+ * reading from the cache when `(dataCount, colorName, colorIsString)`
1166
+ * hasn't changed since the last compute. Returns `null` when robust
1167
+ * clipping doesn't apply — no color column, categorical column (exact
1168
+ * palette indices), or a degenerate sample.
1169
+ */
1170
+ function ensureRobustBounds(
1171
+ chart: CartesianChart,
1172
+ cache: DensityCache,
1173
+ ): { lo: number; hi: number } | null {
1174
+ if (!chart._colorName || chart._colorIsString) {
1175
+ cache.robustBounds = null;
1176
+ return null;
1177
+ }
1178
+
1179
+ const cur = cache.robustBounds;
1180
+ if (
1181
+ cur &&
1182
+ cur.dataCount === chart._dataCount &&
1183
+ cur.colorName === chart._colorName &&
1184
+ cur.colorIsString === chart._colorIsString
1185
+ ) {
1186
+ return { lo: cur.lo, hi: cur.hi };
1187
+ }
1188
+
1189
+ const computed = computeRobustBounds(chart);
1190
+ if (!computed) {
1191
+ cache.robustBounds = null;
1192
+ return null;
1193
+ }
1194
+
1195
+ cache.robustBounds = {
1196
+ lo: computed.lo,
1197
+ hi: computed.hi,
1198
+ dataCount: chart._dataCount,
1199
+ colorName: chart._colorName,
1200
+ colorIsString: chart._colorIsString,
1201
+ };
1202
+ return computed;
1203
+ }
1204
+
1205
+ /**
1206
+ * Sample `chart._colorData` along its slotted per-series ranges, sort
1207
+ * the strided sample, and return the 5th/95th percentile values. The
1208
+ * sample skips unused tail slots (per-series `_seriesUploadedCounts`
1209
+ * cap) so split mode doesn't pollute the distribution with default
1210
+ * `0.5` placeholders.
1211
+ *
1212
+ * Falls back to raw `_colorMin`/`_colorMax` when the quantile sample
1213
+ * collapses to a single value — otherwise a zero-width range would
1214
+ * trip the splat shader's `cmax <= cmin` branch and paint every
1215
+ * point at t=0.5.
1216
+ */
1217
+ function computeRobustBounds(
1218
+ chart: CartesianChart,
1219
+ ): { lo: number; hi: number } | null {
1220
+ if (!chart._colorData || chart._dataCount < 2) {
1221
+ return null;
1222
+ }
1223
+
1224
+ const cap = chart._seriesCapacity;
1225
+ const numSeries = Math.max(1, chart._splitGroups.length);
1226
+ const stride = Math.max(1, Math.ceil(chart._dataCount / ROBUST_SAMPLE_MAX));
1227
+
1228
+ const samples: number[] = [];
1229
+ const data = chart._colorData;
1230
+ for (let s = 0; s < numSeries; s++) {
1231
+ const count = chart._seriesUploadedCounts[s] ?? 0;
1232
+ const base = s * cap;
1233
+ for (let j = 0; j < count; j += stride) {
1234
+ const v = data[base + j];
1235
+ if (Number.isFinite(v)) {
1236
+ samples.push(v);
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ if (samples.length < 2) {
1242
+ return null;
1243
+ }
1244
+
1245
+ samples.sort((a, b) => a - b);
1246
+ const loIdx = Math.floor(samples.length * 0.05);
1247
+ const hiIdx = Math.min(
1248
+ samples.length - 1,
1249
+ Math.ceil(samples.length * 0.95),
1250
+ );
1251
+
1252
+ const lo = samples[loIdx];
1253
+ const hi = samples[hiIdx];
1254
+ if (!(hi > lo)) {
1255
+ if (chart._colorMax > chart._colorMin) {
1256
+ return { lo: chart._colorMin, hi: chart._colorMax };
1257
+ }
1258
+
1259
+ return null;
1260
+ }
1261
+
1262
+ return { lo, hi };
1263
+ }