@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,840 @@
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 { View } from "@perspective-dev/client";
14
+ import {
15
+ createNumberFormatter,
16
+ createDatetimeFormatter,
17
+ createDateFormatter,
18
+ sourceColumn,
19
+ type NumberFormatConfig,
20
+ type DateFormatConfig,
21
+ } from "@perspective-dev/viewer/src/ts/column-format.js";
22
+ import type { ColumnDataMap } from "../data/view-reader";
23
+ import { LazyRowFetcher } from "../data/lazy-row";
24
+ import { formatTickValue, formatDateTickValue } from "../layout/ticks";
25
+ import type { WebGLContextManager } from "../webgl/context-manager";
26
+ import {
27
+ ZoomController,
28
+ type ZoomConfig,
29
+ } from "../interaction/zoom-controller";
30
+ import {
31
+ DEFAULT_FACET_CONFIG,
32
+ DEFAULT_PLUGIN_CONFIG,
33
+ type ChartImplementation,
34
+ type FacetConfig,
35
+ type PluginConfig,
36
+ } from "./chart";
37
+ import {
38
+ TooltipController,
39
+ type HostSink,
40
+ type TooltipCallbacks,
41
+ type UserClickPayload,
42
+ type UserSelectPayload,
43
+ } from "../interaction/tooltip-controller";
44
+ import type { PerspectiveClickDetail } from "../event-detail";
45
+ import type { ViewConfig } from "@perspective-dev/client";
46
+ import { resolveThemeFromVars, type Theme } from "../theme/theme";
47
+ import { requestRender as scheduleRender } from "../render/scheduler";
48
+
49
+ /**
50
+ * Locale-aware fallback formatter applied to numeric tooltip / legend
51
+ * values when the column has no `number_format` configured. Two
52
+ * fractional digits matches the legacy datagrid default and gives
53
+ * tooltips a stable display width.
54
+ */
55
+ const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
56
+ v: number,
57
+ ) => string) => {
58
+ return formatTickValue;
59
+ // const intl = createNumberFormatter("float");
60
+ // return (v) => intl.format(v);
61
+ })();
62
+
63
+ /**
64
+ * Locale-aware fallback formatter for datetime tooltip / legend values
65
+ * when the column has no `date_format` configured. Uses the locale
66
+ * default (no `dateStyle` / `timeStyle`) to match what most users
67
+ * expect from an `Intl.DateTimeFormat()` constructed with no options.
68
+ */
69
+ const DEFAULT_DATETIME_FORMATTER: (v: number) => string = ((): ((
70
+ v: number,
71
+ ) => string) => {
72
+ return formatDateTickValue;
73
+ // const intl = createDatetimeFormatter();
74
+ // return (v) => intl.format(v);
75
+ })();
76
+
77
+ /**
78
+ * Base class for WebGL chart implementations. Owns the common lifecycle
79
+ * plumbing (canvas wiring, viewer config setters, tooltip controller)
80
+ * so each concrete chart only implements data pipeline, rendering, and
81
+ * destruction hooks.
82
+ *
83
+ * ## Frame lifecycle (three phases)
84
+ *
85
+ * Every render of a chart passes through three phases:
86
+ *
87
+ * 1. `uploadAndRender(glManager, columns, startRow, endRow)`.
88
+ * Driven by the plugin wrapper once per data chunk. The subclass
89
+ * runs its build pipeline (axis/series resolution, record
90
+ * generation, domain accumulation) and pushes typed-array results
91
+ * into GPU buffers via `glManager.bufferPool`. Most charts also
92
+ * compile their shaders lazily here on first call.
93
+ *
94
+ * 2. `requestRender(glManager)` — single entrypoint for triggering a
95
+ * paint. Routes through the module-level scheduler
96
+ * ([render/scheduler.ts]) which coalesces by glManager and runs
97
+ * `_fullRender` + `awaitGpuFence` + `endFrame` on the next RAF.
98
+ * Concurrent requests collapse to one `_fullRender` per frame and
99
+ * fence waits across charts run in parallel, so per-chart latency
100
+ * is bounded by that chart's own GPU work.
101
+ *
102
+ * 3. `_fullRender(glManager)` — the subclass implements its own draw
103
+ * loop: resolve visible domains from the zoom controller, build
104
+ * projection matrices, call into its glyph draw helpers, and paint
105
+ * the chrome overlay (axes, legend, tooltip).
106
+ *
107
+ * `destroy()` is called by the plugin wrapper on teardown. It detaches
108
+ * tooltip listeners, then invokes the subclass's `destroyInternal()`
109
+ * to free chart-specific GL resources.
110
+ *
111
+ * ## What subclasses implement
112
+ * - `uploadAndRender` — phase 1; ends by `await this.requestRender(glManager)`.
113
+ * - `tooltipCallbacks()` — return chart-specific hover/click handlers.
114
+ * - `_fullRender` — phase 3; must be safe to call with no data
115
+ * (subclass guards on its own state machine — empty trees, missing
116
+ * programs, etc — and returns early without touching GL).
117
+ * - `destroyInternal` — release chart-specific resources.
118
+ *
119
+ * `getZoomConfig()` is an optional override; default = both axes
120
+ * zoom-unlocked. See {@link ZoomConfig}.
121
+ */
122
+ export abstract class AbstractChart implements ChartImplementation {
123
+ // Access is `public` so the per-chart helper modules
124
+ // (e.g. `./bar/bar-build.ts`) can read/write these without fighting
125
+ // TypeScript's `protected` check. The underscore prefix marks them
126
+ // as internal by convention.
127
+ _glManager: WebGLContextManager | null = null;
128
+ _gridlineCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
129
+ _chromeCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
130
+
131
+ /**
132
+ * Host-supplied CSS-variable map. The host snapshots its DOM via
133
+ * `snapshotThemeVars(el)` and ships it over the control channel;
134
+ * the chart decodes via `resolveThemeFromVars` lazily in
135
+ * `_resolveTheme()`. The chart never reads the DOM itself (it
136
+ * always runs inside `WorkerRenderer`, possibly off-thread).
137
+ */
138
+ _themeVars: Record<string, string> = {};
139
+ _zoomController: ZoomController | null = null;
140
+
141
+ /**
142
+ * Per-facet zoom controllers. Populated when `zoom_mode ===
143
+ * "independent"` and the chart enters faceted mode; each facet's
144
+ * render path reads its own viewport from the matching entry.
145
+ *
146
+ * Shared-zoom mode leaves this empty; `_zoomController` is the
147
+ * single domain used for every facet.
148
+ */
149
+ _facetZoomControllers: ZoomController[] = [];
150
+
151
+ _columnSlots: (string | null)[] = [];
152
+ _groupBy: string[] = [];
153
+ _splitBy: string[] = [];
154
+ _columnTypes: Record<string, string> = {};
155
+
156
+ /**
157
+ * Effective shared-axis flags for the most recent faceted frame.
158
+ * Derived per-frame from `_facetConfig.shared_x_axis` /
159
+ * `shared_y_axis` and `zoom_mode` via
160
+ * {@link computeEffectiveFacetFlags} — independent-zoom mode forces
161
+ * both off because an outer axis band has no single domain it could
162
+ * display. Stored here (rather than mutated back onto
163
+ * `_facetConfig`) so the user's configured shared-axis preferences
164
+ * survive a "shared → independent → shared" round-trip. Read by
165
+ * chrome-overlay code (e.g. `renderFacetedChromeOverlay`,
166
+ * `renderFacetedHeatmapChromeOverlay`) after the main render pass
167
+ * sets them.
168
+ */
169
+ _lastEffectiveSharedX = false;
170
+ _lastEffectiveSharedY = false;
171
+
172
+ /**
173
+ * Source-column types for `group_by` columns — sourced from
174
+ * `table.schema()` (plain columns) merged with `view.expression_schema()`
175
+ * (expression-typed group_bys). Distinct from `_columnTypes` (which
176
+ * is the post-aggregation `view.schema()` map): the level-type
177
+ * lookup for `__ROW_PATH_N__` columns must use the unaggregated
178
+ * type, since `view.schema()` doesn't key these synthetic columns.
179
+ */
180
+ _groupByTypes: Record<string, string> = {};
181
+ _columnsConfig: Record<string, any> = {};
182
+
183
+ /**
184
+ * Pre-compiled per-column value formatters, keyed by the **source**
185
+ * column name (synthetic split-by paths are normalized via
186
+ * `sourceColumn`). Rebuilt by `setColumnsConfig` from the active
187
+ * plugin's `column_config_schema` output, then consulted by axis /
188
+ * tooltip / legend paths via {@link getColumnFormatter}.
189
+ *
190
+ * `undefined` means "no configured formatter for this column" — the
191
+ * caller falls back to the chart's hand-rolled tick formatter.
192
+ */
193
+ _columnFormatters: Map<string, (v: number) => string> = new Map();
194
+ _defaultChartType: string | undefined = undefined;
195
+ _facetConfig: FacetConfig = { ...DEFAULT_FACET_CONFIG };
196
+
197
+ /**
198
+ * Plugin-scoped global configuration. Updated by `setPluginConfig`
199
+ * (driven from the host's `plugin.restore()`) and read by render-
200
+ * path glyphs (`line_width_px`, `point_size_px`, etc.) and by the
201
+ * build pipelines (`auto_alt_y_axis`, `band_inner_frac`,
202
+ * `bar_inner_pad`). Defaults preserve the previous compile-time
203
+ * constants so first-frame rendering before `restore()` matches
204
+ * the pre-refactor output.
205
+ */
206
+ _pluginConfig: PluginConfig = { ...DEFAULT_PLUGIN_CONFIG };
207
+
208
+ _tooltip = new TooltipController();
209
+
210
+ /**
211
+ * Reference to the active host sink, captured in {@link attachTooltip}.
212
+ * Used to emit `perspective-click` / `perspective-global-filter` user
213
+ * events back to the host. Distinct from `_tooltip._host` to avoid
214
+ * reaching into the tooltip controller's internals.
215
+ */
216
+ _hostSink: HostSink | null = null;
217
+
218
+ /**
219
+ * Promise chain that serializes user-event emissions so a rapid
220
+ * pin → unpin sequence stays in order even when `buildClickDetail`
221
+ * awaits `_lazyRows.fetchRow`. Without the queue, click 1's async
222
+ * row fetch could resolve AFTER click 2's synchronous `emitUnselect`
223
+ * — flipping the host's observed event order. All emit helpers
224
+ * (`emitClickAndSelect`, `emitUserClick`, `emitUserSelect`,
225
+ * `emitUnselect`) chain through this.
226
+ */
227
+ _emitQueue: Promise<void> = Promise.resolve();
228
+
229
+ /**
230
+ * Cached resolved theme — populated on first `_resolveTheme()` call,
231
+ * cleared by `invalidateTheme()` (driven from `plugin.restyle()`).
232
+ * `getComputedStyle` / `getPropertyValue` reads cost ~100µs each;
233
+ * zoom/hover dispatch redraws at 60Hz so we resolve once and reuse.
234
+ */
235
+ _theme: Theme | null = null;
236
+
237
+ /**
238
+ * On-demand single-row fetcher used by lazy tooltip column
239
+ * lookups. Reset on every `setView` call; subclasses read
240
+ * `_lazyRows.fetchRow(rowIdx)` from their hover/pin paths and
241
+ * compare a captured serial against the current hovered/pinned
242
+ * state at resolution time, so stale fetches never paint.
243
+ *
244
+ * Can be `null` on chart types that don't surface the View
245
+ * (unit-tested charts) or before the first `draw`.
246
+ */
247
+ _lazyRows: LazyRowFetcher | null = null;
248
+
249
+ // ChartImplementation setters (trivial stores)
250
+
251
+ setGridlineCanvas(canvas: HTMLCanvasElement | OffscreenCanvas): void {
252
+ this._gridlineCanvas = canvas;
253
+ }
254
+
255
+ setChromeCanvas(canvas: HTMLCanvasElement | OffscreenCanvas): void {
256
+ this._chromeCanvas = canvas;
257
+ }
258
+
259
+ setTheme(vars: Record<string, string>): void {
260
+ this._themeVars = vars;
261
+ this._theme = null;
262
+ }
263
+
264
+ setZoomController(zc: ZoomController): void {
265
+ this._zoomController = zc;
266
+ zc.configure(this.getZoomConfig());
267
+ }
268
+
269
+ /**
270
+ * Resolve the zoom controller that owns facet `idx`. In shared-zoom
271
+ * mode (default) this is always the chart's single `_zoomController`.
272
+ * In independent-zoom mode the router provisions one controller per
273
+ * facet; this returns the matching entry, allocating on demand so
274
+ * the render path never has to check `zoom_mode` itself.
275
+ */
276
+ getZoomControllerForFacet(idx: number): ZoomController | null {
277
+ if (this._facetConfig.zoom_mode === "shared") {
278
+ return this._zoomController;
279
+ }
280
+
281
+ if (!this._zoomController) {
282
+ return null;
283
+ }
284
+
285
+ let zc = this._facetZoomControllers[idx];
286
+ if (!zc) {
287
+ zc = new ZoomController();
288
+ zc.configure(this.getZoomConfig());
289
+ this._facetZoomControllers[idx] = zc;
290
+ }
291
+
292
+ return zc;
293
+ }
294
+
295
+ /**
296
+ * Derive the effective shared-X / shared-Y flags for the current
297
+ * frame and stamp them onto `_lastEffectiveSharedX/Y` for downstream
298
+ * chrome-overlay code to consume. Independent-zoom mode forces both
299
+ * shared flags off — the outer axis band cannot display per-cell
300
+ * viewports — without mutating the user's stored `_facetConfig`.
301
+ *
302
+ * Returns `{ independentZoom, effectiveSharedX, effectiveSharedY }`
303
+ * for callers that need the values immediately (e.g. to pass
304
+ * `xAxis: "outer" | "cell"` into `buildFacetGrid`).
305
+ */
306
+ computeEffectiveFacetFlags(): {
307
+ independentZoom: boolean;
308
+ effectiveSharedX: boolean;
309
+ effectiveSharedY: boolean;
310
+ } {
311
+ const independentZoom = this._facetConfig.zoom_mode === "independent";
312
+ const effectiveSharedX =
313
+ !independentZoom && this._facetConfig.shared_x_axis;
314
+ const effectiveSharedY =
315
+ !independentZoom && this._facetConfig.shared_y_axis;
316
+ this._lastEffectiveSharedX = effectiveSharedX;
317
+ this._lastEffectiveSharedY = effectiveSharedY;
318
+ return { independentZoom, effectiveSharedX, effectiveSharedY };
319
+ }
320
+
321
+ /**
322
+ * Wire every active zoom controller's layout pointer for the
323
+ * supplied facet cells. In shared-zoom mode every
324
+ * `getZoomControllerForFacet(i)` returns the same `_zoomController`,
325
+ * so iterating past the first cell would just re-write the same
326
+ * pointer — `break`-on-shared keeps the cost O(1) and avoids the
327
+ * subtle bug where every facet's `updateLayout` overwrites the
328
+ * previous one with the last cell's layout.
329
+ */
330
+ syncFacetZoomLayouts(
331
+ cells: ReadonlyArray<{
332
+ layout: import("../layout/plot-layout").PlotLayout;
333
+ }>,
334
+ ): void {
335
+ const independent = this._facetConfig.zoom_mode === "independent";
336
+ for (let i = 0; i < cells.length; i++) {
337
+ this.getZoomControllerForFacet(i)?.updateLayout(cells[i].layout);
338
+ if (!independent) {
339
+ return;
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Set base domain on every zoom controller owned by this chart.
346
+ */
347
+ setZoomBaseDomain(
348
+ xMin: number,
349
+ xMax: number,
350
+ yMin: number,
351
+ yMax: number,
352
+ ): void {
353
+ if (this._zoomController) {
354
+ this._zoomController.setBaseDomain(xMin, xMax, yMin, yMax);
355
+ }
356
+
357
+ for (const zc of this._facetZoomControllers) {
358
+ if (zc) {
359
+ zc.setBaseDomain(xMin, xMax, yMin, yMax);
360
+ }
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Zoom-controller config for this chart type. Subclasses override to
366
+ * pin an axis (e.g. bar charts pin the categorical axis). Default:
367
+ * both axes freely zoomable.
368
+ */
369
+ protected getZoomConfig(): ZoomConfig {
370
+ return {};
371
+ }
372
+
373
+ setColumnSlots(slots: (string | null)[]): void {
374
+ this._columnSlots = slots;
375
+ }
376
+
377
+ setViewPivots(groupBy: string[], splitBy: string[]): void {
378
+ this._groupBy = groupBy;
379
+ this._splitBy = splitBy;
380
+ }
381
+
382
+ setColumnTypes(schema: Record<string, string>): void {
383
+ this._columnTypes = schema;
384
+ this._rebuildColumnFormatters();
385
+ }
386
+
387
+ /**
388
+ * Clear any `domain_mode: "expand"` accumulator state. Driven by
389
+ * `plugin.draw()` (a fresh `draw` always indicates a view-level
390
+ * change — viewer config, filters, sorts, etc. — that invalidates
391
+ * the previously-accumulated extent) and by the worker's
392
+ * `resetAllZooms` path (user clicked "Reset Zoom"). `plugin.update()`
393
+ * deliberately does *not* call this — same view, more data, the
394
+ * accumulator should keep growing. No-op on the base; chart
395
+ * families that hold accumulator fields override.
396
+ */
397
+ resetExpandedDomain(): void {}
398
+
399
+ setGroupByTypes(schema: Record<string, string>): void {
400
+ this._groupByTypes = schema;
401
+ }
402
+
403
+ setColumnsConfig(cfg: Record<string, any>): void {
404
+ this._columnsConfig = cfg ?? {};
405
+ this._rebuildColumnFormatters();
406
+ }
407
+
408
+ /**
409
+ * Rebuild {@link _columnFormatters} from `_columnsConfig` +
410
+ * `_columnTypes`. Called from both `setColumnsConfig` and
411
+ * `setColumnTypes` since either side of the (config, types) pair
412
+ * can arrive first depending on the host's restore order. Idempotent.
413
+ */
414
+ private _rebuildColumnFormatters(): void {
415
+ this._columnFormatters = new Map();
416
+ for (const [name, columnCfg] of Object.entries(this._columnsConfig)) {
417
+ // `_columnTypes` is the post-aggregation `view.schema()` map
418
+ // and doesn't key group_by source columns; fall back to
419
+ // `_groupByTypes` so a configured `date_format` on a
420
+ // group_by column (e.g. an "Order Date" pivot) still
421
+ // compiles to an `Intl.DateTimeFormat` rather than being
422
+ // silently dropped.
423
+ const type = this._columnTypes[name] ?? this._groupByTypes[name];
424
+ const fmt = this._compileColumnFormatter(type, columnCfg);
425
+ if (fmt) {
426
+ this._columnFormatters.set(name, fmt);
427
+ }
428
+ }
429
+ }
430
+
431
+ private _compileColumnFormatter(
432
+ type: string | undefined,
433
+ cfg: Record<string, any> | undefined,
434
+ ): ((v: number) => string) | undefined {
435
+ if (!type || !cfg) {
436
+ return undefined;
437
+ }
438
+
439
+ if (type === "integer" || type === "float") {
440
+ const numberFormat = cfg.number_format as
441
+ | NumberFormatConfig
442
+ | undefined;
443
+ if (!numberFormat) {
444
+ return undefined;
445
+ }
446
+
447
+ const intl = createNumberFormatter(type, numberFormat);
448
+ return (v) => intl.format(v);
449
+ }
450
+
451
+ if (type === "datetime") {
452
+ const dateFormat = cfg.date_format as DateFormatConfig | undefined;
453
+ if (!dateFormat) {
454
+ return undefined;
455
+ }
456
+
457
+ const intl = createDatetimeFormatter(dateFormat);
458
+ return (v) => intl.format(v);
459
+ }
460
+
461
+ if (type === "date") {
462
+ const dateFormat = cfg.date_format as DateFormatConfig | undefined;
463
+ if (!dateFormat) {
464
+ return undefined;
465
+ }
466
+
467
+ const intl = createDateFormatter(dateFormat);
468
+ return (v) => intl.format(v);
469
+ }
470
+
471
+ return undefined;
472
+ }
473
+
474
+ /**
475
+ * Returns the formatter for `columnName` if one has been configured
476
+ * (via `column_config_schema` + the user's sidebar choices), else a
477
+ * type-appropriate fallback for the chart context.
478
+ *
479
+ * @param columnName May be a synthetic split-by path
480
+ * (`<split_val>|...|<source_col>`); the source column is recovered
481
+ * internally before lookup.
482
+ * @param context `"tick"` returns `undefined` when no per-column
483
+ * formatter is configured, so the receiving axis renderer can
484
+ * apply its own step-aware default (adaptive date precision from
485
+ * tick spacing, K/M/B suffixes for numerics). `"value"` returns
486
+ * a precise `Intl.NumberFormat` / `Intl.DateTimeFormat` fallback —
487
+ * appropriate for tooltips, legends, overlays where the caller
488
+ * invokes the formatter directly and needs a guaranteed function.
489
+ */
490
+ getColumnFormatter(
491
+ columnName: string | null | undefined,
492
+ context: "tick",
493
+ ): ((v: number) => string) | undefined;
494
+ getColumnFormatter(
495
+ columnName: string | null | undefined,
496
+ context?: "value",
497
+ ): (v: number) => string;
498
+ getColumnFormatter(
499
+ columnName: string | null | undefined,
500
+ context: "tick" | "value" = "value",
501
+ ): ((v: number) => string) | undefined {
502
+ if (columnName) {
503
+ const formatter = this._columnFormatters.get(
504
+ sourceColumn(columnName),
505
+ );
506
+ if (formatter) {
507
+ return formatter;
508
+ }
509
+ }
510
+
511
+ if (context === "tick") {
512
+ return undefined;
513
+ }
514
+
515
+ // `_columnTypes` is the post-aggregation schema and doesn't
516
+ // key group_by source columns (their post-aggregate form is
517
+ // `__ROW_PATH_N__`); fall back to `_groupByTypes` so date /
518
+ // datetime group_by axes don't get formatted as numbers.
519
+ const sourceName = columnName ? sourceColumn(columnName) : undefined;
520
+ const type = sourceName
521
+ ? (this._columnTypes[sourceName] ?? this._groupByTypes[sourceName])
522
+ : undefined;
523
+
524
+ if (type === "date" || type === "datetime") {
525
+ return DEFAULT_DATETIME_FORMATTER;
526
+ }
527
+
528
+ return DEFAULT_VALUE_FORMATTER;
529
+ }
530
+
531
+ setDefaultChartType(chartType: string): void {
532
+ this._defaultChartType = chartType;
533
+ }
534
+
535
+ setFacetConfig(cfg: FacetConfig): void {
536
+ this._facetConfig = { ...cfg };
537
+ }
538
+
539
+ /**
540
+ * Apply plugin-scoped global config. Stores `cfg` for later reads
541
+ * and mirrors the overlapping fields onto adjacent state so deep
542
+ * render code keeps reading the single struct it already does:
543
+ *
544
+ * - `facet_mode` / `facet_zoom_mode` sync into `_facetConfig` so
545
+ * `cartesian-render.ts` (and the treemap/sunburst grid checks)
546
+ * keep working unchanged.
547
+ * - `series_zoom_mode` toggles the `_autoFitValue` flag declared
548
+ * on `CategoricalYChart` ("dynamic" = refit on zoom, "fixed" =
549
+ * pinned to full extent). Harmless write on charts that don't
550
+ * expose the field.
551
+ *
552
+ * Render-path uniform fields (`line_width_px`, `point_size_px`,
553
+ * `wick_width_px`, `ohlc_line_width_px`) are read directly from
554
+ * `_pluginConfig` by their respective glyphs on each draw — no
555
+ * sync needed. Build-time fields (`auto_alt_y_axis`,
556
+ * `band_inner_frac`, `bar_inner_pad`) are read by the pipeline
557
+ * inputs in `uploadAndRender`; they take effect on next data load.
558
+ */
559
+ setPluginConfig(cfg: PluginConfig): void {
560
+ this._pluginConfig = { ...cfg };
561
+ this._facetConfig = {
562
+ ...this._facetConfig,
563
+ facet_mode: cfg.facet_mode,
564
+ zoom_mode: cfg.facet_zoom_mode,
565
+ };
566
+
567
+ (this as { _autoFitValue?: boolean })._autoFitValue =
568
+ cfg.series_zoom_mode === "dynamic";
569
+ }
570
+
571
+ /**
572
+ * Lazily decode the host-supplied theme vars. Subsequent calls hit
573
+ * the cache until `invalidateTheme()` clears it. Render-path
574
+ * callers should always read theme values through this method so
575
+ * the parsed `Theme` (gradient stops, palette, etc.) amortizes
576
+ * across an entire frame.
577
+ */
578
+ _resolveTheme(): Theme {
579
+ if (!this._theme) {
580
+ this._theme = resolveThemeFromVars(this._themeVars);
581
+ }
582
+
583
+ return this._theme;
584
+ }
585
+
586
+ /**
587
+ * Drop the cached theme so the next `_resolveTheme()` call re-decodes
588
+ * from `_themeVars`. Wired to `plugin.restyle()` — the host pushes
589
+ * a fresh var snapshot before invalidating.
590
+ */
591
+ invalidateTheme(): void {
592
+ this._theme = null;
593
+ }
594
+
595
+ /**
596
+ * Install a new view for lazy row fetches. Disposes any prior
597
+ * fetcher and dismisses the pinned tooltip — the prior pinned
598
+ * row index has no guaranteed correspondence in the new view
599
+ * (pivot / filter / sort changes can all reshuffle rows).
600
+ */
601
+ setView(view: View): void {
602
+ if (this._lazyRows) {
603
+ this._lazyRows.dispose();
604
+ }
605
+
606
+ this._lazyRows = new LazyRowFetcher(view);
607
+ // A view change (filter / pivot / sort / schema) implicitly
608
+ // dismisses any active pin — the prior row index has no
609
+ // guaranteed correspondence in the new view. Emit a matching
610
+ // `selected: false` so downstream filter-coordinated consumers
611
+ // can roll back their derived state.
612
+ const wasPinned = this._tooltip.isPinned;
613
+ this._tooltip.dismiss();
614
+ if (wasPinned) {
615
+ this.emitUnselect();
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Build the chart-specific {@link TooltipCallbacks} object — the
621
+ * `onHover` / `onLeave` / `onClickPre` / `onPin` / `onDblClick`
622
+ * surface that mediates between the cursor and chart state.
623
+ * Subclasses override this; the base returns a no-op pair.
624
+ */
625
+ protected tooltipCallbacks(): TooltipCallbacks {
626
+ return {
627
+ onHover: () => {},
628
+ onLeave: () => {},
629
+ };
630
+ }
631
+
632
+ /**
633
+ * Wire the chart's `TooltipController` for virtual-dispatch
634
+ * `InteractionEvent`s forwarded from the host, and install the
635
+ * host sink that materializes pinned tooltips and cursor changes
636
+ * host-side.
637
+ */
638
+ attachTooltip(host: HostSink): void {
639
+ this._tooltip.attach(this.tooltipCallbacks());
640
+ this._tooltip.setHost(host);
641
+ this._hostSink = host;
642
+ }
643
+
644
+ /**
645
+ * Build a `PerspectiveClickDetail` payload from a per-family
646
+ * resolved click target. Fetches the source-view row via
647
+ * `_lazyRows` (returns `row: {}` if the row can't be resolved —
648
+ * e.g., aggregate / density cells), and concatenates the
649
+ * `group_by` and `split_by` pivot values into a
650
+ * `viewer.restore({ filter })`-shaped patch.
651
+ *
652
+ * Mirrors the filter-building logic in datagrid's
653
+ * `getCellConfig` ([packages/viewer-datagrid/src/ts/get_cell_config.ts]),
654
+ * but operates on `AbstractChart` state rather than a `DatagridModel`.
655
+ */
656
+ async buildClickDetail(target: {
657
+ rowIdx: number | null;
658
+ columnName: string;
659
+ groupByValues: (string | number | null)[];
660
+ splitByValues: (string | number | null)[];
661
+ }): Promise<PerspectiveClickDetail> {
662
+ let row: Record<string, unknown> = {};
663
+ if (target.rowIdx != null && target.rowIdx >= 0 && this._lazyRows) {
664
+ try {
665
+ const r = await this._lazyRows.fetchRow(target.rowIdx);
666
+ row = Object.fromEntries(r);
667
+ } catch {
668
+ // Fetcher may have been disposed mid-flight; treat as
669
+ // "no row" and emit the filter-only detail anyway.
670
+ row = {};
671
+ }
672
+ }
673
+
674
+ const filter: Array<[string, "==", string | number]> = [];
675
+ for (let i = 0; i < this._groupBy.length; i++) {
676
+ const v = target.groupByValues[i];
677
+ if (v != null && v !== "") {
678
+ filter.push([this._groupBy[i], "==", v]);
679
+ }
680
+ }
681
+
682
+ for (let i = 0; i < this._splitBy.length; i++) {
683
+ const v = target.splitByValues[i];
684
+ if (v != null && v !== "") {
685
+ filter.push([this._splitBy[i], "==", v]);
686
+ }
687
+ }
688
+
689
+ return {
690
+ row,
691
+ column_names: [target.columnName],
692
+ config: { filter } as Partial<ViewConfig>,
693
+ };
694
+ }
695
+
696
+ /**
697
+ * Forward a `perspective-click` to the host. No-op when the chart
698
+ * has not been wired to a host sink (e.g., unit-tested charts).
699
+ * Synchronous; callers needing ordering with async emits should
700
+ * chain through `_emitQueue`.
701
+ */
702
+ emitUserClick(detail: PerspectiveClickDetail): void {
703
+ const payload: UserClickPayload = {
704
+ row: detail.row,
705
+ column_names: detail.column_names,
706
+ config: detail.config as { filter?: unknown[] },
707
+ };
708
+ this._hostSink?.emitUserClick?.(payload);
709
+ }
710
+
711
+ /**
712
+ * Forward a `perspective-global-filter` to the host. The host
713
+ * transport materializes a `PerspectiveSelectDetail` from this plus
714
+ * its cached previous-insert config and dispatches. Synchronous.
715
+ */
716
+ emitUserSelect(args: {
717
+ selected: boolean;
718
+ row: Record<string, unknown>;
719
+ column_names: string[];
720
+ insertConfig: Partial<ViewConfig>;
721
+ }): void {
722
+ const payload: UserSelectPayload = {
723
+ selected: args.selected,
724
+ row: args.row,
725
+ column_names: args.column_names,
726
+ insertConfig: args.insertConfig as { filter?: unknown[] },
727
+ };
728
+ this._hostSink?.emitUserSelect?.(payload);
729
+ }
730
+
731
+ /**
732
+ * Convenience: fire both `perspective-click` and
733
+ * `perspective-global-filter` (`selected: true`) from a resolved
734
+ * click target. Used by chart families where every click both
735
+ * "selects" and "filters" (series, heatmap, candlestick, scatter,
736
+ * treemap-leaf, etc.). Treemap branch / breadcrumb gestures use
737
+ * the lower-level helpers directly.
738
+ *
739
+ * Chains through `_emitQueue` so the row-fetch await can't reorder
740
+ * this emit behind a follow-up `emitUnselect`.
741
+ */
742
+ emitClickAndSelect(target: {
743
+ rowIdx: number | null;
744
+ columnName: string;
745
+ groupByValues: (string | number | null)[];
746
+ splitByValues: (string | number | null)[];
747
+ }): Promise<void> {
748
+ const next = this._emitQueue.then(async () => {
749
+ const detail = await this.buildClickDetail(target);
750
+ this.emitUserClick(detail);
751
+ this.emitUserSelect({
752
+ selected: true,
753
+ row: detail.row,
754
+ column_names: detail.column_names,
755
+ insertConfig: detail.config,
756
+ });
757
+ });
758
+ // Swallow errors on the chain so a single failure doesn't
759
+ // poison subsequent emits; surface to console for debugging.
760
+ this._emitQueue = next.catch((e) => {
761
+ console.error("emitClickAndSelect failed", e);
762
+ });
763
+ return next;
764
+ }
765
+
766
+ /**
767
+ * Fire a `perspective-global-filter` with `selected: false`. Used
768
+ * by treemap / sunburst breadcrumb navigation and by chart-base's
769
+ * own `setView` when a view change implicitly dismisses any active
770
+ * pin. Chains through `_emitQueue` so it lands AFTER any in-flight
771
+ * `emitClickAndSelect`.
772
+ */
773
+ emitUnselect(
774
+ args: {
775
+ row?: Record<string, unknown>;
776
+ column_names?: string[];
777
+ } = {},
778
+ ): void {
779
+ const next = this._emitQueue.then(() => {
780
+ this.emitUserSelect({
781
+ selected: false,
782
+ row: args.row ?? {},
783
+ column_names: args.column_names ?? [],
784
+ insertConfig: { filter: [] },
785
+ });
786
+ });
787
+ this._emitQueue = next.catch((e) => {
788
+ console.error("emitUnselect failed", e);
789
+ });
790
+ }
791
+
792
+ // Render entrypoint
793
+
794
+ /**
795
+ * Public coalesced render. Routes through the module-level
796
+ * scheduler so concurrent calls collapse to one `_fullRender` per
797
+ * RAF and the host blitter receives one bitmap per frame. The
798
+ * returned promise resolves after this chart's `awaitGpuFence` +
799
+ * `endFrame` chain — independent of other charts in the same
800
+ * RAF, which run their fence waits in parallel.
801
+ *
802
+ * Every render-triggering caller — upload chunks, zoom / pan,
803
+ * resize, theme invalidation, host-driven redraws — calls this.
804
+ * The only sanctioned bypass is `snapshotPng`, which calls
805
+ * `_fullRender` directly to keep the GL backbuffer intact for
806
+ * `gl.readPixels`.
807
+ */
808
+ requestRender(glManager: WebGLContextManager): Promise<void> {
809
+ return scheduleRender(glManager, () => this._fullRender(glManager));
810
+ }
811
+
812
+ // Lifecycle
813
+
814
+ destroy(): void {
815
+ this._tooltip.detach();
816
+ this._tooltip.dismiss();
817
+ if (this._lazyRows) {
818
+ this._lazyRows.dispose();
819
+ this._lazyRows = null;
820
+ }
821
+
822
+ this.destroyInternal();
823
+ }
824
+
825
+ // Abstract surface
826
+
827
+ abstract uploadAndRender(
828
+ glManager: WebGLContextManager,
829
+ columns: ColumnDataMap,
830
+ startRow: number,
831
+ endRow: number,
832
+ ): Promise<void>;
833
+
834
+ abstract _fullRender(glManager: WebGLContextManager): void;
835
+
836
+ /**
837
+ * Release chart-specific GL/CPU resources. `destroy` calls this.
838
+ */
839
+ protected abstract destroyInternal(): void;
840
+ }