@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,794 @@
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 { ColumnDataMap } from "../../data/view-reader";
14
+ import type { WebGLContextManager } from "../../webgl/context-manager";
15
+ import type { ZoomConfig } from "../../interaction/zoom-controller";
16
+ import { CategoricalYChart } from "../common/categorical-y-chart";
17
+ import { type PlotRect } from "../../layout/plot-layout";
18
+ import { type AxisDomain } from "../../axis/numeric-axis";
19
+ import {
20
+ buildSeriesPipeline,
21
+ readBarRecord,
22
+ type SeriesChartRecord,
23
+ type NumericCategoryDomain,
24
+ type SeriesInfo,
25
+ type BarColumns,
26
+ emptyBarColumns,
27
+ } from "./series-build";
28
+ import {
29
+ renderBarFrame,
30
+ uploadBarInstances,
31
+ invalidateGlyphBuffers,
32
+ rebuildGlyphBuffers,
33
+ } from "./series-render";
34
+ import {
35
+ handleBarHover,
36
+ handleBarLegendClick,
37
+ showBarPinnedTooltip,
38
+ showBarPinnedTooltipForSample,
39
+ } from "./series-interact";
40
+ import { resolvePalette } from "../../theme/palette";
41
+ import { LineGlyph } from "./glyphs/draw-lines";
42
+ import { ScatterGlyph } from "./glyphs/draw-scatter";
43
+ import { AreaGlyph } from "./glyphs/draw-areas";
44
+ import barVert from "../../shaders/bar.vert.glsl";
45
+ import barFrag from "../../shaders/bar.frag.glsl";
46
+
47
+ /**
48
+ * Per-frame memo of the auto-fit value extent for a {@link SeriesChart},
49
+ * keyed on the visible categorical window. Two axis slots (`left*` /
50
+ * `right*`) because dual-axis bar charts refit independently.
51
+ */
52
+ export interface SeriesAutoFitCache {
53
+ catMin: number;
54
+ catMax: number;
55
+ hidden: Set<number>;
56
+ leftMin: number;
57
+ leftMax: number;
58
+ hasLeft: boolean;
59
+ rightMin: number;
60
+ rightMax: number;
61
+ hasRight: boolean;
62
+ }
63
+
64
+ export interface CachedLocations {
65
+ u_proj_left: WebGLUniformLocation | null;
66
+ u_proj_right: WebGLUniformLocation | null;
67
+ u_hover_series: WebGLUniformLocation | null;
68
+ u_horizontal: WebGLUniformLocation | null;
69
+ a_corner: number;
70
+ a_x_center: number;
71
+ a_half_width: number;
72
+ a_y0: number;
73
+ a_y1: number;
74
+ a_color: number;
75
+ a_series_id: number;
76
+ a_axis: number;
77
+ }
78
+
79
+ /**
80
+ * Bar chart. Fields are package-internal (no `private`) so helper modules
81
+ * in this folder can read/write them.
82
+ *
83
+ * Orientation: vertical (Y Bar) is the default — categorical X, numeric
84
+ * Y. When `_isHorizontal` is true (X Bar) the roles swap: numeric X,
85
+ * categorical Y reading top-to-bottom. The data pipeline + instance
86
+ * attributes stay in *logical* coordinates (xCenter = category center,
87
+ * y0/y1 = value extent); the swap happens in three places only:
88
+ * 1. Projection matrix (`bar-render.ts`) — args reordered, Y flipped.
89
+ * 2. Vertex shader — `u_horizontal` uniform transposes position.
90
+ * 3. Chrome (`bar-axis.ts`) — categorical axis moves from bottom to
91
+ * left; numeric axis from left to bottom.
92
+ * Hit-testing reads the swapped pixel→data mapping via the projected
93
+ * `PlotLayout`, so its logical comparisons don't need changes.
94
+ */
95
+ export class SeriesChart extends CategoricalYChart {
96
+ readonly _isHorizontal: boolean;
97
+
98
+ constructor(orientation: "vertical" | "horizontal" = "vertical") {
99
+ super();
100
+ this._isHorizontal = orientation === "horizontal";
101
+ }
102
+
103
+ /**
104
+ * Lock the categorical axis — scrolling through category indices
105
+ * isn't meaningful, and the layout code assumes all categories are
106
+ * always present. The value axis stays freely zoomable.
107
+ */
108
+ protected override getZoomConfig(): ZoomConfig {
109
+ return { lockAxis: this._isHorizontal ? "x" : "y" };
110
+ }
111
+
112
+ _locations: CachedLocations | null = null;
113
+
114
+ // Series-specific categorical-axis bookkeeping. `_rowPaths`,
115
+ // `_numCategories`, `_rowOffset`, `_program`, `_cornerBuffer`,
116
+ // `_lastLayout`, `_lastXDomain`, `_lastYDomain`, `_lastYTicks`, and
117
+ // `_autoFitValue` all live on `CategoricalYChart`.
118
+ _aggregates: string[] = [];
119
+ _splitPrefixes: string[] = [];
120
+ _series: SeriesInfo[] = [];
121
+
122
+ /**
123
+ * Columnar bar/area record storage. Indexed by bar slot in
124
+ * `[0, _bars.count)`. Replaces the legacy `SeriesChartRecord[]` to
125
+ * avoid per-record POJO allocation on data load.
126
+ */
127
+ _bars: BarColumns = emptyBarColumns();
128
+
129
+ /**
130
+ * Pre-partitioned series indices by glyph type — populated at the end
131
+ * of `uploadAndRender` and reused across frames. Eliminates per-glyph
132
+ * `chart._series.filter(...)` allocations in the render loop. Each
133
+ * holds the full list of that type (including hidden series); the
134
+ * draw paths still skip hidden via `_hiddenSeries` lookup.
135
+ */
136
+ _barSeries: SeriesInfo[] = [];
137
+ _lineSeries: SeriesInfo[] = [];
138
+ _scatterSeries: SeriesInfo[] = [];
139
+ _areaSeries: SeriesInfo[] = [];
140
+
141
+ /**
142
+ * Cached primary / secondary axis labels — `_series.filter().map().
143
+ * dedupe().join()` per axis, recomputed only on series-set change.
144
+ */
145
+ _primaryValueLabel = "";
146
+ _altValueLabel = "";
147
+
148
+ /**
149
+ * (seriesId * 1e9 + catIdx) → bar-record index in `_bars`. Built once
150
+ * per pipeline run for area-strip lookups; rebuilt on hidden-toggle
151
+ * is unnecessary because the index keys don't depend on hidden state.
152
+ */
153
+ _areaBarIndex: Map<number, number> | null = null;
154
+
155
+ /**
156
+ * Cached Y-color buffer state for `uploadBarColors` short-circuit.
157
+ * `_lastUploadedColors` mirrors the bytes last shipped to the GPU;
158
+ * `uploadBarColors` skips when the new buffer matches byte-for-byte.
159
+ * Reset (set to `null`) on data load or palette change.
160
+ */
161
+ _lastUploadedColors: Float32Array | null = null;
162
+
163
+ /**
164
+ * Cached palette + identity-keys for short-circuiting per-frame
165
+ * resolution. Inputs (`seriesPalette` ref, `gradientStops` ref,
166
+ * `series.length`) only change on data load or `restyle()`.
167
+ */
168
+ _paletteCache: [number, number, number][] | null = null;
169
+ _paletteCacheKey: {
170
+ seriesPalette: [number, number, number][] | null;
171
+ gradientStops: unknown;
172
+ seriesLength: number;
173
+ } | null = null;
174
+
175
+ /**
176
+ * Reusable scratch for the build pipeline — keeps the stack ladder
177
+ * `Float64Array(N*M)` capacity hot across data reloads. The pipeline
178
+ * resizes if the new build's footprint exceeds capacity.
179
+ */
180
+ _posStackScratch: Float64Array | null = null;
181
+ _negStackScratch: Float64Array | null = null;
182
+ _leftDomain: { min: number; max: number } = { min: 0, max: 1 };
183
+ _rightDomain: { min: number; max: number } | null = null;
184
+ _hasRightAxis = false;
185
+
186
+ /**
187
+ * `domain_mode: "expand"` accumulators. Hold the running union of
188
+ * every prior build's value-axis (and, in numeric-category mode,
189
+ * category-axis) extent for as long as the option is active.
190
+ * Cleared in `resetExpandedDomain` — wired from the worker's
191
+ * `resetAllZooms` and from view-config mutations on the base
192
+ * class. `null` whenever the option is `"fit"` or the accumulator
193
+ * has just been cleared; the next build re-seeds.
194
+ */
195
+ _expandedLeftDomain: { min: number; max: number } | null = null;
196
+ _expandedRightDomain: { min: number; max: number } | null = null;
197
+ _expandedCategoryDomain: { min: number; max: number } | null = null;
198
+
199
+ /**
200
+ * Numeric category-axis state. Populated only when `group_by` has
201
+ * exactly one level and that level is `date | datetime | integer |
202
+ * float` (boolean → category). When set, `_bars[].xCenter` lives in
203
+ * real data units (not logical category indices), and the
204
+ * categorical-side axis renders as a numeric axis instead of the
205
+ * stringified-category one.
206
+ */
207
+ _categoryAxisMode: "category" | "numeric" = "category";
208
+ _numericCategoryDomain: NumericCategoryDomain | null = null;
209
+
210
+ /**
211
+ * Origin used to rebase category positions before f32 narrowing.
212
+ * Datetime numeric category axes carry ~1.7e12-magnitude values
213
+ * which f32 cannot represent below ~256ms; the GPU buffers store
214
+ * `(xCenter - _categoryOrigin)` and the projection matrix is built
215
+ * with the same origin so its `tx` term stays small. Leftover
216
+ * absolute coords are still available via `_numericCategoryDomain`
217
+ * for axis-tick formatting and `dataToPixel`. `0` in category mode
218
+ * (where positions are small integer indices) and in non-datetime
219
+ * numeric modes (integer / float categories also fit in f32).
220
+ */
221
+ _categoryOrigin = 0;
222
+
223
+ /**
224
+ * Cached numeric category-axis ticks for the last frame.
225
+ */
226
+ _lastCatTicks: number[] | null = null;
227
+
228
+ /**
229
+ * Per-category X coordinate in real data units (numeric axis mode
230
+ * only). `null` in category mode — line/scatter/area glyphs fall
231
+ * back to using `catIdx` directly as the X coordinate.
232
+ */
233
+ _categoryPositions: Float64Array | null = null;
234
+
235
+ _hiddenSeries: Set<number> = new Set();
236
+ _hoveredBarIdx = -1;
237
+ _pinnedBarIdx = -1;
238
+
239
+ /**
240
+ * Synthetic bar record for hover hits on line / scatter glyphs that
241
+ * don't have a real `BarRecord` in `_bars`. At most one of
242
+ * `_hoveredBarIdx` and `_hoveredSample` is populated per frame; see
243
+ * {@link ./bar-interact.getHoveredBar}.
244
+ */
245
+ _hoveredSample: SeriesChartRecord | null = null;
246
+
247
+ // Unstacked sample grid produced by buildBarPipeline: samples[catI * S + seriesId].
248
+ _samples: Float32Array = new Float32Array(0);
249
+ _sampleValid: Uint8Array = new Uint8Array(0);
250
+
251
+ /**
252
+ * Typed glyph composition. Each glyph (line / scatter / area) owns
253
+ * its program cache and persistent vertex buffers privately; the
254
+ * chart routes draw / rebuild / invalidate via `_glyphs`. Bar
255
+ * glyph state lives on the chart directly (shared bar program +
256
+ * `_locations` + buffer pool), so it's a free function rather than
257
+ * a class.
258
+ */
259
+ readonly _glyphs = {
260
+ lines: new LineGlyph(),
261
+ scatter: new ScatterGlyph(),
262
+ areas: new AreaGlyph(),
263
+ } as const;
264
+
265
+ // Dual-axis bar charts keep a secondary Y-axis domain + ticks for
266
+ // the right-side axis chrome.
267
+ _lastAltYDomain: AxisDomain | null = null;
268
+ _lastAltYTicks: number[] | null = null;
269
+
270
+ _uploadedBars = 0;
271
+
272
+ /**
273
+ * Bar-record indices uploaded to the instance buffers, in dispatch
274
+ * order. `_uploadedBars` is the active prefix length; the trailing
275
+ * capacity is reused across data reloads / legend toggles.
276
+ */
277
+ _visibleBarIndices: Int32Array = new Int32Array(0);
278
+
279
+ _legendRects: { seriesId: number; rect: PlotRect }[] = [];
280
+
281
+ /**
282
+ * Cached legend layout — recomputed only on series-set / palette /
283
+ * hidden-set / theme change. Frame-rate redraws read from this
284
+ * directly; otherwise `ctx.measureText` would run per series each
285
+ * frame. `null` flags an invalidation; `_legendRects` is rebuilt
286
+ * lazily on the next chrome pass.
287
+ */
288
+ _legendCacheValid = false;
289
+
290
+ /**
291
+ * Per-frame memo of the auto-fit value extent keyed on the visible
292
+ * categorical window. Two comparisons per hit → no walk. Reset to
293
+ * null on any mutation that would change the outcome (data reload,
294
+ * legend toggle).
295
+ *
296
+ * Two axis slots because dual-axis bar charts refit left and right
297
+ * independently.
298
+ *
299
+ * TODO(perf): when the visible window shrinks from a large N, the
300
+ * linear walk over `_bars` dominates for N > ~100K. `_bars` is
301
+ * already ordered by `catIdx`, so a binary-search pair to find the
302
+ * visible slice drops this to O(log N + K_visible). Deferred until
303
+ * profiling shows the walk in the hot path — current scale caps
304
+ * keep it below 1% of frame time.
305
+ */
306
+ _autoFitCache: SeriesAutoFitCache | null = null;
307
+
308
+ /**
309
+ * Per-category extent buckets. Built once per data load (and
310
+ * rebuilt when `_hiddenSeries` changes), then read per-frame by
311
+ * `computeVisibleValueExtent` to compute the auto-fit window over
312
+ * the visible cat range in O(visibleCats) instead of
313
+ * O(`bars.count`). Capacity reused across builds via
314
+ * length-checked grow.
315
+ *
316
+ * Memory: 4 × Float64 + 2 × Uint8 = 34 bytes per category. For
317
+ * typical N (≤ 1000 cats) this is < 35 KB; for high-cardinality
318
+ * N = 100k it's 3.4 MB. Acceptable trade for eliminating the
319
+ * O(N×M×P) per-frame walk during pan/zoom animations.
320
+ */
321
+ _catExtents: {
322
+ leftMin: Float64Array;
323
+ leftMax: Float64Array;
324
+ rightMin: Float64Array;
325
+ rightMax: Float64Array;
326
+ hasLeft: Uint8Array;
327
+ hasRight: Uint8Array;
328
+ n: number;
329
+ } | null = null;
330
+
331
+ /**
332
+ * Identity of the `_hiddenSeries` set baked into `_catExtents`.
333
+ * Pointer-compares to detect legend-toggle invalidations.
334
+ */
335
+ _catExtentsHidden: Set<number> | null = null;
336
+
337
+ protected override tooltipCallbacks() {
338
+ return {
339
+ onHover: (mx: number, my: number) => handleBarHover(this, mx, my),
340
+ onLeave: () => {
341
+ if (this._hoveredBarIdx !== -1 || this._hoveredSample) {
342
+ this._hoveredBarIdx = -1;
343
+ this._hoveredSample = null;
344
+ if (this._glManager) {
345
+ renderBarFrame(this, this._glManager);
346
+ }
347
+ }
348
+ },
349
+ onClickPre: (mx: number, my: number) =>
350
+ handleBarLegendClick(this, mx, my),
351
+ onPin: (mx: number, my: number) => {
352
+ // Refresh the hit-test at the click coords directly:
353
+ // `dispatchHover` is RAF-throttled in the worker, so a
354
+ // click that follows a mousemove in the same task may
355
+ // arrive at `onPin` before the prior hover RAF has
356
+ // updated `_hoveredBarIdx`. Re-running the hit-test
357
+ // here makes the pin path independent of hover timing.
358
+ handleBarHover(this, mx, my);
359
+ if (this._hoveredBarIdx >= 0) {
360
+ const barIdx = this._hoveredBarIdx;
361
+ showBarPinnedTooltip(this, barIdx);
362
+ const rec = readBarRecord(
363
+ this._bars,
364
+ barIdx,
365
+ this._splitPrefixes.length,
366
+ this._samples,
367
+ this._series.length,
368
+ );
369
+ void this._emitSeriesClickSelect(rec);
370
+ } else if (this._hoveredSample) {
371
+ const rec = this._hoveredSample;
372
+ showBarPinnedTooltipForSample(this, rec);
373
+ void this._emitSeriesClickSelect(rec);
374
+ }
375
+ },
376
+ onUnpin: () => {
377
+ this.emitUnselect();
378
+ },
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Resolve a clicked bar / point into a `PerspectiveClickDetail`
384
+ * (via `buildClickDetail`) and emit both
385
+ * `perspective-click` and `perspective-global-filter` to the host.
386
+ *
387
+ * `rowIdx` derivation: the series pipeline emits one record per
388
+ * (catIdx, agg, split) tuple, and a pivoted view has one view row
389
+ * per category — so `catIdx + _rowOffset` is the source-view row.
390
+ * `_aggregates[aggIdx]` is the *base* column name (no split
391
+ * prefix). Group-by values come from per-level `_rowPaths`, split-by
392
+ * values are recovered by splitting `_splitPrefixes[splitIdx]` on
393
+ * the `|` delimiter the engine uses for pivoted column names.
394
+ */
395
+ private async _emitSeriesClickSelect(b: SeriesChartRecord): Promise<void> {
396
+ if (!this._aggregates[b.aggIdx]) {
397
+ return;
398
+ }
399
+
400
+ const groupByValues: (string | null)[] = this._rowPaths.map(
401
+ (level) => level.labels[b.catIdx] ?? null,
402
+ );
403
+ const splitKey = this._splitPrefixes[b.splitIdx] ?? "";
404
+ const splitByValues =
405
+ this._splitBy.length > 0 && splitKey !== ""
406
+ ? splitKey.split("|")
407
+ : [];
408
+
409
+ await this.emitClickAndSelect({
410
+ rowIdx: b.catIdx + this._rowOffset,
411
+ columnName: this._aggregates[b.aggIdx],
412
+ groupByValues,
413
+ splitByValues,
414
+ });
415
+ }
416
+
417
+ async uploadAndRender(
418
+ glManager: WebGLContextManager,
419
+ columns: ColumnDataMap,
420
+ startRow: number,
421
+ endRow: number,
422
+ ): Promise<void> {
423
+ this._glManager = glManager;
424
+ const gl = glManager.gl;
425
+
426
+ if (startRow !== 0) {
427
+ // Bar charts render a single consolidated pass — the viewer
428
+ // should not chunk this, but guard defensively.
429
+ return;
430
+ }
431
+
432
+ if (!this._program) {
433
+ this._program = glManager.shaders.getOrCreate(
434
+ "bar",
435
+ barVert,
436
+ barFrag,
437
+ );
438
+ const p = this._program;
439
+ this._locations = {
440
+ u_proj_left: gl.getUniformLocation(p, "u_proj_left"),
441
+ u_proj_right: gl.getUniformLocation(p, "u_proj_right"),
442
+ u_hover_series: gl.getUniformLocation(p, "u_hover_series"),
443
+ u_horizontal: gl.getUniformLocation(p, "u_horizontal"),
444
+ a_corner: gl.getAttribLocation(p, "a_corner"),
445
+ a_x_center: gl.getAttribLocation(p, "a_x_center"),
446
+ a_half_width: gl.getAttribLocation(p, "a_half_width"),
447
+ a_y0: gl.getAttribLocation(p, "a_y0"),
448
+ a_y1: gl.getAttribLocation(p, "a_y1"),
449
+ a_color: gl.getAttribLocation(p, "a_color"),
450
+ a_series_id: gl.getAttribLocation(p, "a_series_id"),
451
+ a_axis: gl.getAttribLocation(p, "a_axis"),
452
+ };
453
+
454
+ this._cornerBuffer = gl.createBuffer()!;
455
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._cornerBuffer);
456
+ gl.bufferData(
457
+ gl.ARRAY_BUFFER,
458
+ new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
459
+ gl.STATIC_DRAW,
460
+ );
461
+ }
462
+
463
+ const result = buildSeriesPipeline({
464
+ columns,
465
+ numRows: endRow,
466
+ columnSlots: this._columnSlots,
467
+ groupBy: this._groupBy,
468
+ splitBy: this._splitBy,
469
+ groupByTypes: this._groupByTypes,
470
+ columnsConfig: this._columnsConfig,
471
+ defaultChartType: this._defaultChartType as
472
+ | "bar"
473
+ | "line"
474
+ | "scatter"
475
+ | "area"
476
+ | undefined,
477
+ autoAltYAxis: this._pluginConfig.auto_alt_y_axis,
478
+ bandInnerFrac: this._pluginConfig.band_inner_frac,
479
+ barInnerPad: this._pluginConfig.bar_inner_pad,
480
+ includeZero: this._pluginConfig.include_zero,
481
+ scratchBars: this._bars,
482
+ scratchPosStack: this._posStackScratch,
483
+ scratchNegStack: this._negStackScratch,
484
+ });
485
+
486
+ // `domain_mode: "expand"` post-build union. Mutate the pipeline
487
+ // result struct in place so every downstream assignment below
488
+ // (`_leftDomain`, `_rightDomain`, `_numericCategoryDomain`,
489
+ // `_categoryOrigin`) automatically picks up the grown extent.
490
+ // `"fit"` (or a fresh reset) leaves the result untouched and
491
+ // clears the accumulators so the next toggle starts fresh.
492
+ if (this._pluginConfig.domain_mode === "expand") {
493
+ if (this._expandedLeftDomain) {
494
+ result.leftDomain.min = Math.min(
495
+ this._expandedLeftDomain.min,
496
+ result.leftDomain.min,
497
+ );
498
+ result.leftDomain.max = Math.max(
499
+ this._expandedLeftDomain.max,
500
+ result.leftDomain.max,
501
+ );
502
+ }
503
+
504
+ this._expandedLeftDomain = { ...result.leftDomain };
505
+
506
+ if (result.rightDomain) {
507
+ if (this._expandedRightDomain) {
508
+ result.rightDomain.min = Math.min(
509
+ this._expandedRightDomain.min,
510
+ result.rightDomain.min,
511
+ );
512
+ result.rightDomain.max = Math.max(
513
+ this._expandedRightDomain.max,
514
+ result.rightDomain.max,
515
+ );
516
+ }
517
+
518
+ this._expandedRightDomain = { ...result.rightDomain };
519
+ }
520
+
521
+ if (result.numericCategoryDomain) {
522
+ if (this._expandedCategoryDomain) {
523
+ result.numericCategoryDomain.min = Math.min(
524
+ this._expandedCategoryDomain.min,
525
+ result.numericCategoryDomain.min,
526
+ );
527
+ result.numericCategoryDomain.max = Math.max(
528
+ this._expandedCategoryDomain.max,
529
+ result.numericCategoryDomain.max,
530
+ );
531
+ }
532
+
533
+ this._expandedCategoryDomain = {
534
+ min: result.numericCategoryDomain.min,
535
+ max: result.numericCategoryDomain.max,
536
+ };
537
+ }
538
+ } else {
539
+ this._expandedLeftDomain = null;
540
+ this._expandedRightDomain = null;
541
+ this._expandedCategoryDomain = null;
542
+ }
543
+
544
+ this._aggregates = result.aggregates;
545
+ this._splitPrefixes = result.splitPrefixes;
546
+ this._rowPaths = result.rowPaths;
547
+ this._numCategories = result.numCategories;
548
+ this._rowOffset = result.rowOffset;
549
+ this._categoryAxisMode = result.axisMode.mode;
550
+ this._numericCategoryDomain = result.numericCategoryDomain;
551
+ this._categoryPositions = result.categoryPositions;
552
+
553
+ // Rebase origin for the category axis. Pin to the domain min so
554
+ // every bar/sample can be uploaded as `(xCenter - origin)` and
555
+ // the f32 GPU pipeline never sees the full ~1.7e12 timestamp.
556
+ // Non-numeric modes (categorical, no domain) leave origin at 0.
557
+ this._categoryOrigin = result.numericCategoryDomain?.min ?? 0;
558
+ this._series = result.series;
559
+ this._bars = result.bars;
560
+ this._posStackScratch = result.posStack;
561
+ this._negStackScratch = result.negStack;
562
+ this._samples = result.samples;
563
+
564
+ // Pre-partition `_series` by glyph type once per build. Frame
565
+ // paths read these directly instead of `_series.filter(...)`.
566
+ // Single bucket-push pass over the source array — replaces
567
+ // four `Array.filter` allocations with in-place `length = 0`
568
+ // resets on the chart-owned arrays. Same total memory in
569
+ // steady state, but skips three array-header allocations and
570
+ // one redundant pass over `result.series` per data load.
571
+ this._barSeries.length = 0;
572
+ this._lineSeries.length = 0;
573
+ this._scatterSeries.length = 0;
574
+ this._areaSeries.length = 0;
575
+ for (const s of result.series) {
576
+ switch (s.chartType) {
577
+ case "bar":
578
+ this._barSeries.push(s);
579
+ break;
580
+ case "line":
581
+ this._lineSeries.push(s);
582
+ break;
583
+ case "scatter":
584
+ this._scatterSeries.push(s);
585
+ break;
586
+ case "area":
587
+ this._areaSeries.push(s);
588
+ break;
589
+ }
590
+ }
591
+
592
+ // Cache the per-axis label string. Recomputing the dedupe-and-
593
+ // join per frame allocated four arrays + a string, all stable
594
+ // between data loads.
595
+ this._primaryValueLabel = uniqueAggLabels(result.series, 0);
596
+ this._altValueLabel = uniqueAggLabels(result.series, 1);
597
+
598
+ // Pre-build the area-strip lookup index (seriesId * 1e9 + catIdx
599
+ // → bar slot). Legacy code rebuilt this every frame inside
600
+ // `drawAreas`. The index is derived purely from `_bars` and is
601
+ // valid for the lifetime of this build.
602
+ this._areaBarIndex = buildAreaBarIndex(this._bars);
603
+
604
+ // New bar records invalidate downstream caches — auto-fit extent,
605
+ // legend layout (text widths can shift on series-set change),
606
+ // palette + color upload (palette length changes), and persistent
607
+ // glyph buffers (vertex data is rebuilt below). Also drop the
608
+ // per-category extent identity so the bucket rebuilds on
609
+ // next read.
610
+ this._autoFitCache = null;
611
+ this._legendCacheValid = false;
612
+ this._paletteCache = null;
613
+ this._paletteCacheKey = null;
614
+ this._catExtentsHidden = null;
615
+ this._lastUploadedColors = null;
616
+ this._sampleValid = result.sampleValid;
617
+ this._leftDomain = result.leftDomain;
618
+ this._rightDomain = result.rightDomain;
619
+ this._hasRightAxis = result.hasRightAxis;
620
+
621
+ // Resolve the palette eagerly. Both `uploadBarInstances` (color
622
+ // attribute) and `rebuildGlyphBuffers` (per-series RGB capture)
623
+ // read `_series[i].color`, so the stamp has to happen first.
624
+ ensurePalette(this);
625
+
626
+ uploadBarInstances(this, glManager);
627
+
628
+ invalidateGlyphBuffers(this);
629
+ rebuildGlyphBuffers(this, glManager);
630
+
631
+ await this.requestRender(glManager);
632
+ }
633
+
634
+ _fullRender(glManager: WebGLContextManager): void {
635
+ if (!this._program) {
636
+ return;
637
+ }
638
+
639
+ this._glManager = glManager;
640
+ renderBarFrame(this, glManager);
641
+ }
642
+
643
+ override resetExpandedDomain(): void {
644
+ this._expandedLeftDomain = null;
645
+ this._expandedRightDomain = null;
646
+ this._expandedCategoryDomain = null;
647
+ }
648
+
649
+ protected destroyInternal(): void {
650
+ if (this._glManager) {
651
+ const gl = this._glManager.gl;
652
+ if (this._cornerBuffer) {
653
+ gl.deleteBuffer(this._cornerBuffer);
654
+ }
655
+
656
+ destroyGlyphBuffers(this);
657
+ }
658
+
659
+ this._program = null;
660
+ this._locations = null;
661
+ this._cornerBuffer = null;
662
+ this._bars = emptyBarColumns();
663
+ this._series = [];
664
+ this._barSeries = [];
665
+ this._lineSeries = [];
666
+ this._scatterSeries = [];
667
+ this._areaSeries = [];
668
+ this._areaBarIndex = null;
669
+ this._paletteCache = null;
670
+ this._paletteCacheKey = null;
671
+ this._lastUploadedColors = null;
672
+ this._posStackScratch = null;
673
+ this._negStackScratch = null;
674
+ this._rowPaths = [];
675
+ this._numCategories = 0;
676
+ this._hiddenSeries.clear();
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Build the `(seriesId * 1e9 + catIdx) → bar-record-index` lookup for
682
+ * area glyphs. Areas read y0/y1 by (seriesId, catIdx) on every strip;
683
+ * legacy code rebuilt this map per frame from the bars list. Invariant:
684
+ * 1e9 is safe since category counts never approach it.
685
+ */
686
+ function buildAreaBarIndex(bars: BarColumns): Map<number, number> {
687
+ const m = new Map<number, number>();
688
+ for (let i = 0; i < bars.count; i++) {
689
+ if (bars.chartType[i] !== 1 /* AREA */) {
690
+ continue;
691
+ }
692
+
693
+ m.set(bars.seriesId[i] * 1_000_000_000 + bars.catIdx[i], i);
694
+ }
695
+
696
+ return m;
697
+ }
698
+
699
+ /**
700
+ * Dedupe + join the aggregate names for series on a given axis. Stable
701
+ * across pan/zoom — caches on the chart so the legacy O(S²) `indexOf`-
702
+ * based dedupe doesn't run per frame.
703
+ */
704
+ function uniqueAggLabels(series: SeriesInfo[], axis: 0 | 1): string {
705
+ const seen = new Set<string>();
706
+ const ordered: string[] = [];
707
+ for (const s of series) {
708
+ if (s.axis !== axis) {
709
+ continue;
710
+ }
711
+
712
+ if (seen.has(s.aggName)) {
713
+ continue;
714
+ }
715
+
716
+ seen.add(s.aggName);
717
+ ordered.push(s.aggName);
718
+ }
719
+
720
+ return ordered.join(", ");
721
+ }
722
+
723
+ /**
724
+ * Resolve the per-series palette and stamp it onto `_series[i].color`.
725
+ * Cached on `_paletteCache` keyed by reference identity of the theme
726
+ * inputs + series count — only `restyle()` (which clears `_paletteCache`
727
+ * via `invalidateTheme`) or a data load (which clears it explicitly)
728
+ * forces re-resolution.
729
+ *
730
+ * Returns true when the cache changed (caller invalidates color upload).
731
+ */
732
+ export function ensurePalette(chart: SeriesChart): boolean {
733
+ const theme = chart._resolveTheme();
734
+ const seriesPalette = theme.seriesPalette;
735
+ const gradientStops = theme.gradientStops;
736
+ const seriesLength = chart._series.length;
737
+
738
+ const key = chart._paletteCacheKey;
739
+ if (
740
+ chart._paletteCache &&
741
+ key &&
742
+ key.seriesPalette === seriesPalette &&
743
+ key.gradientStops === gradientStops &&
744
+ key.seriesLength === seriesLength
745
+ ) {
746
+ return false;
747
+ }
748
+
749
+ const palette = resolvePaletteCached(
750
+ seriesPalette,
751
+ gradientStops,
752
+ seriesLength,
753
+ );
754
+ chart._paletteCache = palette;
755
+ chart._paletteCacheKey = { seriesPalette, gradientStops, seriesLength };
756
+
757
+ for (let i = 0; i < chart._series.length; i++) {
758
+ chart._series[i].color = palette[i];
759
+ }
760
+
761
+ return true;
762
+ }
763
+
764
+ /**
765
+ * Module-local indirection so `series.ts` can call into the palette
766
+ * resolver without pulling the entire `series-render.ts` import graph
767
+ * into its file scope. Re-exported through `series-render.ts`.
768
+ */
769
+ function resolvePaletteCached(
770
+ seriesPalette: [number, number, number][],
771
+ gradientStops: import("../../theme/gradient").GradientStop[],
772
+ seriesLength: number,
773
+ ): [number, number, number][] {
774
+ return resolvePalette(seriesPalette, gradientStops, seriesLength);
775
+ }
776
+
777
+ /**
778
+ * Tear down per-glyph GPU resources. Each glyph instance owns its own
779
+ * program cache + persistent buffers and frees both in `destroy`.
780
+ */
781
+ function destroyGlyphBuffers(chart: SeriesChart): void {
782
+ chart._glyphs.lines.destroy(chart);
783
+ chart._glyphs.scatter.destroy(chart);
784
+ chart._glyphs.areas.destroy(chart);
785
+ }
786
+
787
+ /**
788
+ * Horizontal bar chart — numeric X, categorical Y.
789
+ */
790
+ export class XBarChart extends SeriesChart {
791
+ constructor() {
792
+ super("horizontal");
793
+ }
794
+ }