@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,1109 @@
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 { Context2D } from "../canvas-types";
14
+ import type { WebGLContextManager } from "../../webgl/context-manager";
15
+ import {
16
+ ensurePalette,
17
+ type SeriesChart,
18
+ type SeriesAutoFitCache,
19
+ } from "./series";
20
+ import type { PlotRect } from "../../layout/plot-layout";
21
+ import { PlotLayout } from "../../layout/plot-layout";
22
+ import { renderInPlotFrame } from "../../webgl/plot-frame";
23
+ import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
24
+ import { drawBars, BAR_TYPE_BAR_VAL as BAR_TYPE_BAR } from "./glyphs/draw-bars";
25
+ import { getHoveredBar } from "./series-interact";
26
+ import { computeNiceTicks } from "../../layout/ticks";
27
+ import { type AxisDomain } from "../../axis/numeric-axis";
28
+ import {
29
+ renderBarAxesChrome,
30
+ renderBarGridlines,
31
+ type BarCategoryAxis,
32
+ } from "../../axis/bar-axis";
33
+ import {
34
+ measureCategoricalAxisHeight,
35
+ measureCategoricalAxisWidth,
36
+ type CategoricalDomain,
37
+ } from "../../axis/categorical-axis";
38
+ import { buildBarTooltipLines } from "./series-interact";
39
+
40
+ /**
41
+ * Reusable scratch for bar instance uploads. Sized lazily at the first
42
+ * use; grown on demand. Avoids `new Float32Array(n)` × 7 buffers per
43
+ * legend-toggle / data-load; size is bounded by the bar-typed subset
44
+ * of `_bars.count`.
45
+ */
46
+ interface BarInstanceScratch {
47
+ xCenters: Float32Array;
48
+ halfWidths: Float32Array;
49
+ y0s: Float32Array;
50
+ y1s: Float32Array;
51
+ seriesIds: Float32Array;
52
+ axes: Float32Array;
53
+ colors: Float32Array;
54
+ }
55
+
56
+ let _barInstanceScratch: BarInstanceScratch | null = null;
57
+
58
+ function ensureBarInstanceScratch(n: number): BarInstanceScratch {
59
+ if (
60
+ _barInstanceScratch &&
61
+ _barInstanceScratch.xCenters.length >= n &&
62
+ _barInstanceScratch.colors.length >= n * 3
63
+ ) {
64
+ return _barInstanceScratch;
65
+ }
66
+
67
+ const cap = Math.max(n, _barInstanceScratch?.xCenters.length ?? 0);
68
+ _barInstanceScratch = {
69
+ xCenters: new Float32Array(cap),
70
+ halfWidths: new Float32Array(cap),
71
+ y0s: new Float32Array(cap),
72
+ y1s: new Float32Array(cap),
73
+ seriesIds: new Float32Array(cap),
74
+ axes: new Float32Array(cap),
75
+ colors: new Float32Array(cap * 3),
76
+ };
77
+ return _barInstanceScratch;
78
+ }
79
+
80
+ /**
81
+ * Upload bar instance buffers from the columnar `_bars` storage. Filters
82
+ * to bar-typed records only (areas draw as triangle strips). Skips
83
+ * hidden series. Re-called from data-load and legend-toggle paths; the
84
+ * scratch buffers and `_visibleBarIndices` are reused across calls.
85
+ */
86
+ export function uploadBarInstances(
87
+ chart: SeriesChart,
88
+ glManager: WebGLContextManager,
89
+ ): void {
90
+ const bars = chart._bars;
91
+ const total = bars.count;
92
+ let n = 0;
93
+
94
+ if (total > 0) {
95
+ const scratch = ensureBarInstanceScratch(total);
96
+ if (
97
+ !chart._visibleBarIndices ||
98
+ chart._visibleBarIndices.length < total
99
+ ) {
100
+ chart._visibleBarIndices = new Int32Array(total);
101
+ }
102
+
103
+ const indices = chart._visibleBarIndices;
104
+
105
+ // Rebase each xCenter by `_categoryOrigin` before f32 narrowing.
106
+ // For datetime numeric category axes the absolute xCenter is
107
+ // ~1.7e12 and f32 narrowing collapses adjacent bars onto the
108
+ // same value; subtracting the origin brings every value into
109
+ // the seconds range where f32 has full precision. The matching
110
+ // projection matrix is built with the same origin so the shader
111
+ // math is consistent.
112
+ const xOrigin = chart._categoryOrigin;
113
+ const series = chart._series;
114
+ const hidden = chart._hiddenSeries;
115
+ const ct = bars.chartType;
116
+ const sid = bars.seriesId;
117
+ const xC = bars.xCenter;
118
+ const hw = bars.halfWidth;
119
+ const by0 = bars.y0;
120
+ const by1 = bars.y1;
121
+ const ax = bars.axis;
122
+ for (let i = 0; i < total; i++) {
123
+ if (ct[i] !== BAR_TYPE_BAR) {
124
+ continue;
125
+ }
126
+
127
+ const seriesId = sid[i];
128
+ if (hidden.has(seriesId)) {
129
+ continue;
130
+ }
131
+
132
+ scratch.xCenters[n] = xC[i] - xOrigin;
133
+ scratch.halfWidths[n] = hw[i];
134
+ scratch.y0s[n] = by0[i];
135
+ scratch.y1s[n] = by1[i];
136
+ scratch.seriesIds[n] = seriesId;
137
+ scratch.axes[n] = ax[i];
138
+ const color = series[seriesId].color;
139
+ scratch.colors[n * 3] = color[0];
140
+ scratch.colors[n * 3 + 1] = color[1];
141
+ scratch.colors[n * 3 + 2] = color[2];
142
+ indices[n] = i;
143
+ n++;
144
+ }
145
+ }
146
+
147
+ chart._uploadedBars = n;
148
+ if (n === 0) {
149
+ chart._lastUploadedColors = null;
150
+ return;
151
+ }
152
+
153
+ const scratch = _barInstanceScratch!;
154
+ glManager.bufferPool.ensureCapacity(n);
155
+ // `subarray(0, n)` slices the scratch to the current frame's
156
+ // valid-data length. The scratch grows monotonically across
157
+ // frames (see `ensureBarInstanceScratch`) so its `.length` reflects
158
+ // historical peak, not current `n` — passing it whole would
159
+ // overflow the GPU buffer after any session reset.
160
+ glManager.bufferPool.upload("bar_x", scratch.xCenters.subarray(0, n), 0, 1);
161
+ glManager.bufferPool.upload(
162
+ "bar_hw",
163
+ scratch.halfWidths.subarray(0, n),
164
+ 0,
165
+ 1,
166
+ );
167
+ glManager.bufferPool.upload("bar_y0", scratch.y0s.subarray(0, n), 0, 1);
168
+ glManager.bufferPool.upload("bar_y1", scratch.y1s.subarray(0, n), 0, 1);
169
+ glManager.bufferPool.upload(
170
+ "bar_sid",
171
+ scratch.seriesIds.subarray(0, n),
172
+ 0,
173
+ 1,
174
+ );
175
+ glManager.bufferPool.upload("bar_axis", scratch.axes.subarray(0, n), 0, 1);
176
+ glManager.bufferPool.upload(
177
+ "bar_color",
178
+ scratch.colors.subarray(0, n * 3),
179
+ 0,
180
+ 3,
181
+ );
182
+
183
+ // Snapshot the uploaded color bytes so subsequent palette-only
184
+ // changes can detect a no-op and skip the GPU write.
185
+ if (
186
+ !chart._lastUploadedColors ||
187
+ chart._lastUploadedColors.length < n * 3
188
+ ) {
189
+ chart._lastUploadedColors = new Float32Array(
190
+ Math.max(n * 3, chart._lastUploadedColors?.length ?? 0),
191
+ );
192
+ }
193
+
194
+ chart._lastUploadedColors.set(scratch.colors.subarray(0, n * 3));
195
+ }
196
+
197
+ /**
198
+ * Re-upload the per-bar color attribute. Short-circuits when the new
199
+ * colors match the last-uploaded snapshot byte-for-byte. Legacy code
200
+ * ran this every frame regardless; with the cached palette now stable
201
+ * across pan/zoom this becomes a no-op except after data load /
202
+ * `restyle()`.
203
+ */
204
+ export function uploadBarColors(
205
+ chart: SeriesChart,
206
+ glManager: WebGLContextManager,
207
+ ): void {
208
+ const n = chart._uploadedBars;
209
+ if (n === 0) {
210
+ return;
211
+ }
212
+
213
+ const indices = chart._visibleBarIndices;
214
+ const series = chart._series;
215
+ const sid = chart._bars.seriesId;
216
+ const scratch = ensureBarInstanceScratch(n);
217
+ for (let i = 0; i < n; i++) {
218
+ const color = series[sid[indices[i]]].color;
219
+ scratch.colors[i * 3] = color[0];
220
+ scratch.colors[i * 3 + 1] = color[1];
221
+ scratch.colors[i * 3 + 2] = color[2];
222
+ }
223
+
224
+ const last = chart._lastUploadedColors;
225
+ if (last && last.length >= n * 3) {
226
+ let same = true;
227
+ for (let i = 0; i < n * 3; i++) {
228
+ if (last[i] !== scratch.colors[i]) {
229
+ same = false;
230
+ break;
231
+ }
232
+ }
233
+
234
+ if (same) {
235
+ return;
236
+ }
237
+ }
238
+
239
+ glManager.bufferPool.upload(
240
+ "bar_color",
241
+ scratch.colors.subarray(0, n * 3),
242
+ 0,
243
+ 3,
244
+ );
245
+ if (!last || last.length < n * 3) {
246
+ chart._lastUploadedColors = new Float32Array(n * 3);
247
+ }
248
+
249
+ chart._lastUploadedColors!.set(scratch.colors.subarray(0, n * 3));
250
+ }
251
+
252
+ /**
253
+ * Drop persistent vertex buffers for line / scatter / area glyphs.
254
+ * Called from `uploadAndRender` before {@link rebuildGlyphBuffers}.
255
+ */
256
+ export function invalidateGlyphBuffers(chart: SeriesChart): void {
257
+ chart._glyphs.lines.invalidateBuffers(chart);
258
+ chart._glyphs.scatter.invalidateBuffers(chart);
259
+ chart._glyphs.areas.invalidateBuffers(chart);
260
+ }
261
+
262
+ /**
263
+ * Build persistent vertex buffers for line / scatter / area glyphs.
264
+ * The legacy renderers rebuilt and re-uploaded these every frame inside
265
+ * the per-glyph draw functions; with stable post-build geometry the
266
+ * uploads now happen exactly once per data-load / palette change.
267
+ */
268
+ export function rebuildGlyphBuffers(
269
+ chart: SeriesChart,
270
+ glManager: WebGLContextManager,
271
+ ): void {
272
+ chart._glyphs.lines.rebuildBuffers(chart, glManager);
273
+ chart._glyphs.scatter.rebuildBuffers(chart, glManager);
274
+ chart._glyphs.areas.rebuildBuffers(chart, glManager);
275
+ }
276
+
277
+ /**
278
+ * Full-frame render: gridlines → WebGL bars (instanced) → chrome overlay.
279
+ */
280
+ export function renderBarFrame(
281
+ chart: SeriesChart,
282
+ glManager: WebGLContextManager,
283
+ ): void {
284
+ const gl = glManager.gl;
285
+ const dpr = glManager.dpr;
286
+ const cssWidth = gl.canvas.width / dpr;
287
+ const cssHeight = gl.canvas.height / dpr;
288
+ if (cssWidth <= 0 || cssHeight <= 0) {
289
+ return;
290
+ }
291
+
292
+ if (chart._numCategories === 0) {
293
+ return;
294
+ }
295
+
296
+ // Resolve the theme + palette. `ensurePalette` is a no-op when the
297
+ // palette inputs (theme refs + series count) are unchanged — under
298
+ // pan/zoom this short-circuits, leaving frame work to the GPU draw
299
+ // calls only. After data load / `restyle()` it stamps fresh RGB
300
+ // onto `_series[i].color`, and the color upload path detects the
301
+ // change and re-uploads the bar instance colors.
302
+ const theme = chart._resolveTheme();
303
+ if (ensurePalette(chart) && chart._uploadedBars > 0) {
304
+ uploadBarColors(chart, glManager);
305
+ }
306
+
307
+ const horizontal = chart._isHorizontal;
308
+ const numericCat = chart._categoryAxisMode === "numeric";
309
+
310
+ // Category axis bounds. Category mode runs [-0.5, N-0.5] in logical
311
+ // units; numeric mode reads min/max from the data-unit
312
+ // `_numericCategoryDomain`. Horizontal mode flips the Y domain so
313
+ // catIdx=0 sits at the top (handled below in the projection call).
314
+ const catMin = numericCat ? chart._numericCategoryDomain!.min : -0.5;
315
+ const catMax = numericCat
316
+ ? chart._numericCategoryDomain!.max
317
+ : chart._numCategories - 0.5;
318
+
319
+ const valMin = chart._leftDomain.min;
320
+ const valMax = chart._leftDomain.max;
321
+ if (chart._zoomController) {
322
+ if (horizontal) {
323
+ chart._zoomController.setBaseDomain(valMin, valMax, catMin, catMax);
324
+ } else {
325
+ chart._zoomController.setBaseDomain(catMin, catMax, valMin, valMax);
326
+ }
327
+ }
328
+
329
+ // `visCat*` and `visVal*` always describe the currently-visible window
330
+ // in logical (category/value) coords regardless of orientation.
331
+ let visCatMin = catMin;
332
+ let visCatMax = catMax;
333
+ let visValMin = valMin;
334
+ let visValMax = valMax;
335
+ let visRightMin = chart._rightDomain?.min ?? 0;
336
+ let visRightMax = chart._rightDomain?.max ?? 1;
337
+ if (chart._zoomController) {
338
+ const vd = chart._zoomController.getVisibleDomain();
339
+ if (horizontal) {
340
+ visValMin = vd.xMin;
341
+ visValMax = vd.xMax;
342
+ visCatMin = vd.yMin;
343
+ visCatMax = vd.yMax;
344
+ } else {
345
+ visCatMin = vd.xMin;
346
+ visCatMax = vd.xMax;
347
+ visValMin = vd.yMin;
348
+ visValMax = vd.yMax;
349
+ }
350
+ }
351
+
352
+ // Auto-fit the value axis to the visible categorical window. Gated
353
+ // on `_autoFitValue` + non-default zoom: at default zoom the refit
354
+ // result always equals `_leftDomain`/`_rightDomain`, so walking
355
+ // would be wasted work (and would shift test baselines).
356
+ if (
357
+ chart._autoFitValue &&
358
+ chart._zoomController &&
359
+ !chart._zoomController.isDefault()
360
+ ) {
361
+ const fit = computeVisibleValueExtent(chart, visCatMin, visCatMax);
362
+ if (fit.hasLeft) {
363
+ visValMin = fit.leftMin;
364
+ visValMax = fit.leftMax;
365
+ }
366
+
367
+ if (chart._rightDomain && fit.hasRight) {
368
+ visRightMin = fit.rightMin;
369
+ visRightMax = fit.rightMax;
370
+ }
371
+ }
372
+
373
+ // `include_zero` is absolute — zero must stay inside the rendered
374
+ // domain even after a dynamic-zoom refit (`computeVisibleValueExtent`
375
+ // returns the data-only extent, which can drop the baseline).
376
+ // Without this, tick computation sees the refit window while the
377
+ // projection's `requireZero` snap silently re-anchors to zero, so
378
+ // ticks crowd one edge of an otherwise zero-anchored plot.
379
+ if (chart._pluginConfig.include_zero) {
380
+ if (visValMin > 0) {
381
+ visValMin = 0;
382
+ }
383
+
384
+ if (visValMax < 0) {
385
+ visValMax = 0;
386
+ }
387
+
388
+ if (chart._rightDomain) {
389
+ if (visRightMin > 0) {
390
+ visRightMin = 0;
391
+ }
392
+
393
+ if (visRightMax < 0) {
394
+ visRightMax = 0;
395
+ }
396
+ }
397
+ }
398
+
399
+ const hasLegend = chart._series.length > 1;
400
+ const hasCatLabel = chart._groupBy.length > 0;
401
+
402
+ const provisionalDomain: CategoricalDomain = {
403
+ levels: chart._rowPaths,
404
+ numRows: chart._numCategories,
405
+ levelLabels: chart._groupBy.slice(),
406
+ };
407
+
408
+ let layout: PlotLayout;
409
+ if (horizontal) {
410
+ // Numeric category axis on the Y side: the gutter just needs
411
+ // standard numeric tick width (~55px), no per-row label
412
+ // measurement.
413
+ const leftExtra = numericCat
414
+ ? 55
415
+ : measureCategoricalAxisWidth(provisionalDomain);
416
+
417
+ layout = new PlotLayout(cssWidth, cssHeight, {
418
+ hasXLabel: true,
419
+ hasYLabel: hasCatLabel,
420
+ hasLegend,
421
+ leftExtra,
422
+ });
423
+ } else if (numericCat) {
424
+ // Numeric category axis on the X side: bottom gutter is a
425
+ // fixed numeric-axis row (~24px), no leaf-rotation measurement.
426
+ layout = new PlotLayout(cssWidth, cssHeight, {
427
+ hasXLabel: hasCatLabel,
428
+ hasYLabel: true,
429
+ hasLegend,
430
+ bottomExtra: 24,
431
+ });
432
+ } else {
433
+ const estLeft = 55 + 16;
434
+ const estRight = hasLegend ? 80 : 16;
435
+ const estPlotWidth = Math.max(1, cssWidth - estLeft - estRight);
436
+ const bottomExtra = measureCategoricalAxisHeight(
437
+ provisionalDomain,
438
+ estPlotWidth,
439
+ );
440
+ layout = new PlotLayout(cssWidth, cssHeight, {
441
+ hasXLabel: hasCatLabel,
442
+ hasYLabel: true,
443
+ hasLegend,
444
+ bottomExtra,
445
+ });
446
+ }
447
+
448
+ chart._lastLayout = layout;
449
+ if (chart._zoomController) {
450
+ chart._zoomController.updateLayout(layout);
451
+ }
452
+
453
+ // Build the primary projection. `clamp` names the axis that carries
454
+ // the *value* data (Y for Y Bar, X for X Bar). `requireZero` pins
455
+ // the baseline at zero so bar / area glyphs grow from the axis
456
+ // line; it must track `include_zero` so the projection's padded
457
+ // domain matches the build pipeline's `leftDomain` (otherwise the
458
+ // tick computation and the WebGL geometry use different scales).
459
+ const requireZero = chart._pluginConfig.include_zero;
460
+ const projLeft = horizontal
461
+ ? layout.buildProjectionMatrix(
462
+ visValMin,
463
+ visValMax,
464
+
465
+ // Flip so catIdx=0 renders at the top.
466
+ visCatMax,
467
+ visCatMin,
468
+ "x",
469
+ requireZero,
470
+ undefined,
471
+ 0,
472
+ chart._categoryOrigin,
473
+ )
474
+ : layout.buildProjectionMatrix(
475
+ visCatMin,
476
+ visCatMax,
477
+ visValMin,
478
+ visValMax,
479
+ "y",
480
+ requireZero,
481
+ undefined,
482
+ chart._categoryOrigin,
483
+ 0,
484
+ );
485
+
486
+ let projRight: Float32Array;
487
+ if (chart._hasRightAxis && chart._rightDomain && !horizontal) {
488
+ const savedPadXMin = layout.paddedXMin;
489
+ const savedPadXMax = layout.paddedXMax;
490
+ const savedPadYMin = layout.paddedYMin;
491
+ const savedPadYMax = layout.paddedYMax;
492
+ projRight = layout.buildProjectionMatrix(
493
+ visCatMin,
494
+ visCatMax,
495
+ visRightMin,
496
+ visRightMax,
497
+ "y",
498
+ requireZero,
499
+ undefined,
500
+ chart._categoryOrigin,
501
+ 0,
502
+ );
503
+ layout.paddedXMin = savedPadXMin;
504
+ layout.paddedXMax = savedPadXMax;
505
+ layout.paddedYMin = savedPadYMin;
506
+ layout.paddedYMax = savedPadYMax;
507
+ } else {
508
+ // Dual-axis horizontal is not supported in this iteration; fall
509
+ // through to a single axis when horizontal + _hasRightAxis.
510
+ projRight = projLeft;
511
+ }
512
+
513
+ const leftValueTicks = computeNiceTicks(visValMin, visValMax, 6);
514
+ const rightValueTicks =
515
+ chart._hasRightAxis && chart._rightDomain && !horizontal
516
+ ? computeNiceTicks(visRightMin, visRightMax, 6)
517
+ : null;
518
+
519
+ const catDomain: CategoricalDomain = provisionalDomain;
520
+ const valueDomain: AxisDomain = {
521
+ min: visValMin,
522
+ max: visValMax,
523
+ label: chart._primaryValueLabel,
524
+ };
525
+ const altValueDomain: AxisDomain | null =
526
+ chart._rightDomain && !horizontal
527
+ ? {
528
+ min: visRightMin,
529
+ max: visRightMax,
530
+ label: chart._altValueLabel,
531
+ }
532
+ : null;
533
+
534
+ if (chart._gridlineCanvas) {
535
+ renderBarGridlines(
536
+ chart._gridlineCanvas,
537
+ layout,
538
+ leftValueTicks,
539
+ theme,
540
+ glManager.dpr,
541
+ horizontal,
542
+ );
543
+ }
544
+
545
+ renderInPlotFrame(gl, layout, glManager.dpr, () => {
546
+ // Paint order: areas behind bars (so bar borders stay crisp),
547
+ // bars above, lines above those, scatter points on top. X Bar
548
+ // only paints bars — the other glyphs bake in vertical geometry
549
+ // and aren't supported for horizontal orientation.
550
+ if (!horizontal) {
551
+ chart._glyphs.areas.draw(
552
+ chart,
553
+ gl,
554
+ glManager,
555
+ projLeft,
556
+ projRight,
557
+ theme.areaOpacity,
558
+ );
559
+ }
560
+
561
+ gl.useProgram(chart._program!);
562
+ const loc = chart._locations!;
563
+ gl.uniformMatrix4fv(loc.u_proj_left, false, projLeft);
564
+ gl.uniformMatrix4fv(loc.u_proj_right, false, projRight);
565
+ gl.uniform1f(loc.u_horizontal, horizontal ? 1.0 : 0.0);
566
+ const hovered = chart._series.length > 1 ? getHoveredBar(chart) : null;
567
+ gl.uniform1f(loc.u_hover_series, hovered ? hovered.seriesId : -1);
568
+ drawBars(chart, gl, glManager);
569
+
570
+ if (!horizontal) {
571
+ chart._glyphs.lines.draw(chart, gl, glManager, projLeft, projRight);
572
+ chart._glyphs.scatter.draw(
573
+ chart,
574
+ gl,
575
+ glManager,
576
+ projLeft,
577
+ projRight,
578
+ );
579
+ }
580
+ });
581
+
582
+ chart._lastXDomain = catDomain;
583
+ chart._lastYDomain = valueDomain;
584
+ chart._lastYTicks = leftValueTicks;
585
+ chart._lastAltYDomain = altValueDomain;
586
+ chart._lastAltYTicks = rightValueTicks;
587
+ chart._lastCatTicks = numericCat
588
+ ? computeNiceTicks(visCatMin, visCatMax, 6)
589
+ : null;
590
+ renderBarChromeOverlay(chart);
591
+ }
592
+
593
+ /**
594
+ * Draw axes chrome + legend + tooltip onto the overlay canvas.
595
+ */
596
+ export function renderBarChromeOverlay(chart: SeriesChart): void {
597
+ if (
598
+ !chart._chromeCanvas ||
599
+ !chart._lastLayout ||
600
+ !chart._lastYDomain ||
601
+ !chart._lastYTicks
602
+ ) {
603
+ return;
604
+ }
605
+
606
+ const theme = chart._resolveTheme();
607
+ let catAxis: BarCategoryAxis;
608
+ if (
609
+ chart._categoryAxisMode === "numeric" &&
610
+ chart._numericCategoryDomain &&
611
+ chart._lastCatTicks
612
+ ) {
613
+ catAxis = {
614
+ mode: "numeric",
615
+ domain: {
616
+ min: chart._numericCategoryDomain.min,
617
+ max: chart._numericCategoryDomain.max,
618
+ isDate: chart._numericCategoryDomain.isDate,
619
+ label: chart._numericCategoryDomain.label,
620
+ },
621
+ ticks: chart._lastCatTicks,
622
+ };
623
+ } else if (chart._lastXDomain) {
624
+ catAxis = { mode: "category", domain: chart._lastXDomain };
625
+ } else {
626
+ return;
627
+ }
628
+
629
+ // Y axis columns: the primary axis aggregates the unique Y column
630
+ // shared by all series on it. With `auto_alt_y_axis`, series can
631
+ // split across primary/secondary by `_series[i].onAltAxis`; the
632
+ // primary formatter follows the first non-alt series, alt follows
633
+ // the first alt series (falls back to the formatter's own type-
634
+ // aware fallback if no such series exists).
635
+ const primarySeries = chart._series.find((s) => s.axis === 0);
636
+ const altSeries = chart._series.find((s) => s.axis === 1);
637
+ const xColumn = chart._groupBy[0];
638
+ renderBarAxesChrome(
639
+ chart._chromeCanvas,
640
+ catAxis,
641
+ chart._lastYDomain,
642
+ chart._lastYTicks,
643
+ chart._lastLayout,
644
+ theme,
645
+ chart._glManager?.dpr ?? 1,
646
+ chart._lastAltYDomain ?? undefined,
647
+ chart._lastAltYTicks ?? undefined,
648
+ chart._isHorizontal,
649
+ {
650
+ value: chart.getColumnFormatter(
651
+ primarySeries?.aggName ?? null,
652
+ "tick",
653
+ ),
654
+ alt: chart.getColumnFormatter(altSeries?.aggName ?? null, "tick"),
655
+ category: chart.getColumnFormatter(xColumn, "tick"),
656
+ },
657
+ );
658
+
659
+ renderBarLegend(chart);
660
+
661
+ if (getHoveredBar(chart)) {
662
+ renderBarTooltipCanvas(chart);
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Cached parallel array of measured legend text widths. The legend
668
+ * renderer reads from this each frame instead of re-running
669
+ * `ctx.measureText` per series; the widths only change on series-set
670
+ * or theme change. `_legendCacheValid` gates rebuild.
671
+ */
672
+ let _legendTextWidths: Float64Array = new Float64Array(0);
673
+
674
+ function ensureLegendLayout(
675
+ chart: SeriesChart,
676
+ ctx: Context2D,
677
+ fontFamily: string,
678
+ ): void {
679
+ if (chart._legendCacheValid) {
680
+ return;
681
+ }
682
+
683
+ const series = chart._series;
684
+ if (_legendTextWidths.length < series.length) {
685
+ _legendTextWidths = new Float64Array(series.length);
686
+ }
687
+
688
+ ctx.save();
689
+ ctx.font = `11px ${fontFamily}`;
690
+ for (let i = 0; i < series.length; i++) {
691
+ _legendTextWidths[i] = ctx.measureText(series[i].label).width;
692
+ }
693
+
694
+ ctx.restore();
695
+ chart._legendCacheValid = true;
696
+ }
697
+
698
+ function renderBarLegend(chart: SeriesChart): void {
699
+ chart._legendRects = [];
700
+ if (!chart._chromeCanvas || !chart._lastLayout) {
701
+ return;
702
+ }
703
+
704
+ if (chart._series.length <= 1) {
705
+ return;
706
+ }
707
+
708
+ const ctx = chart._chromeCanvas.getContext("2d") as Context2D | null;
709
+ if (!ctx) {
710
+ return;
711
+ }
712
+
713
+ ctx.save();
714
+
715
+ const theme = chart._resolveTheme();
716
+ const textColor = theme.legendText;
717
+ const fontFamily = theme.fontFamily;
718
+
719
+ ensureLegendLayout(chart, ctx, fontFamily);
720
+
721
+ const layout = chart._lastLayout;
722
+ const swatchSize = 10;
723
+ const lineHeight = 18;
724
+ const x = layout.plotRect.x + layout.plotRect.width + 12;
725
+ let y = layout.margins.top + 10;
726
+
727
+ ctx.font = `11px ${fontFamily}`;
728
+ ctx.textAlign = "left";
729
+ ctx.textBaseline = "middle";
730
+
731
+ const series = chart._series;
732
+ const widths = _legendTextWidths;
733
+ for (let i = 0; i < series.length; i++) {
734
+ const s = series[i];
735
+ const hidden = chart._hiddenSeries.has(s.seriesId);
736
+ const r = Math.round(s.color[0] * 255);
737
+ const g = Math.round(s.color[1] * 255);
738
+ const b = Math.round(s.color[2] * 255);
739
+
740
+ ctx.globalAlpha = hidden ? 0.3 : 1.0;
741
+ ctx.fillStyle = `rgb(${r},${g},${b})`;
742
+ ctx.fillRect(x, y - swatchSize / 2, swatchSize, swatchSize);
743
+
744
+ ctx.fillStyle = textColor;
745
+ ctx.fillText(s.label, x + swatchSize + 6, y);
746
+
747
+ const textW = widths[i];
748
+ if (hidden) {
749
+ ctx.strokeStyle = textColor;
750
+ ctx.lineWidth = 1;
751
+ ctx.beginPath();
752
+ ctx.moveTo(x + swatchSize + 6, y);
753
+ ctx.lineTo(x + swatchSize + 6 + textW, y);
754
+ ctx.stroke();
755
+ }
756
+
757
+ ctx.globalAlpha = 1.0;
758
+
759
+ const rect: PlotRect = {
760
+ x: x - 2,
761
+ y: y - lineHeight / 2,
762
+ width: swatchSize + 6 + textW + 4,
763
+ height: lineHeight,
764
+ };
765
+ chart._legendRects.push({ seriesId: s.seriesId, rect });
766
+
767
+ y += lineHeight;
768
+ }
769
+
770
+ ctx.restore();
771
+ }
772
+
773
+ function renderBarTooltipCanvas(chart: SeriesChart): void {
774
+ if (!chart._chromeCanvas || !chart._lastLayout) {
775
+ return;
776
+ }
777
+
778
+ const b = getHoveredBar(chart);
779
+ if (!b) {
780
+ return;
781
+ }
782
+
783
+ const layout = chart._lastLayout;
784
+
785
+ // Bar glyphs anchor the tooltip at the midpoint of the bar body so
786
+ // it reads against a solid swatch. Line / scatter / area glyphs
787
+ // have no body — the data point sits at `y1`, so anchor there
788
+ // (the tooltip visually hovers *over* the point). Hit records
789
+ // synthesized from line/scatter hover tag themselves as "bar" in
790
+ // `_hoveredSample` for rendering purposes, so we pull the true
791
+ // glyph from the series info instead.
792
+ const glyph = chart._series[b.seriesId]?.chartType ?? "bar";
793
+ const anchorV = glyph === "bar" ? (b.y0 + b.y1) / 2 : b.y1;
794
+
795
+ const pos =
796
+ b.axis === 0
797
+ ? chart._isHorizontal
798
+ ? layout.dataToPixel(anchorV, b.xCenter)
799
+ : layout.dataToPixel(b.xCenter, anchorV)
800
+ : rightAxisDataToPixel(chart, b.xCenter, anchorV);
801
+
802
+ const lines = buildBarTooltipLines(chart, b);
803
+ const theme = chart._resolveTheme();
804
+ renderCanvasTooltip(
805
+ chart._chromeCanvas,
806
+ pos,
807
+ lines,
808
+ layout,
809
+ theme,
810
+ chart._glManager?.dpr ?? 1,
811
+ );
812
+ }
813
+
814
+ export function rightAxisDataToPixel(
815
+ chart: SeriesChart,
816
+ x: number,
817
+ y: number,
818
+ ): { px: number; py: number } {
819
+ const layout = chart._lastLayout!;
820
+ const { x: px, y: py, width, height } = layout.plotRect;
821
+ const tx =
822
+ (x - layout.paddedXMin) / (layout.paddedXMax - layout.paddedXMin);
823
+ const r = chart._rightDomain!;
824
+ const ty = (y - r.min) / (r.max - r.min);
825
+ return { px: px + tx * width, py: py + (1 - ty) * height };
826
+ }
827
+
828
+ /**
829
+ * Compute per-axis value extent over bars whose `catIdx` falls inside
830
+ * `[visCatMin, visCatMax]`. Skips hidden series. Returns a cached
831
+ * result on `chart._autoFitCache` when `(visCatMin, visCatMax,
832
+ * _hiddenSeries)` match the previous call — hover-only redraws hit
833
+ * the cache every time.
834
+ *
835
+ * Value source is `min(y0, y1)`/`max(y0, y1)` per bar, which handles
836
+ * stacked + negative-value bars uniformly.
837
+ *
838
+ * TODO(perf): O(|_bars|) linear scan. `_bars` is already ordered by
839
+ * `catIdx`, so a binary-search pair to locate the visible slice would
840
+ * drop this to O(log N + K_visible). Deferred — under current
841
+ * `max_cells` ceilings the scan is <1% of frame time.
842
+ *
843
+ * Cache lifetime: reset on data upload ([bar.ts] `uploadAndRender`)
844
+ * and legend toggle ([bar-interact.ts] `handleBarLegendClick`). Any
845
+ * other mutation that affects the bar set must also null the cache.
846
+ */
847
+ function computeVisibleValueExtent(
848
+ chart: SeriesChart,
849
+ visCatMin: number,
850
+ visCatMax: number,
851
+ ): {
852
+ leftMin: number;
853
+ leftMax: number;
854
+ hasLeft: boolean;
855
+ rightMin: number;
856
+ rightMax: number;
857
+ hasRight: boolean;
858
+ } {
859
+ const cache = chart._autoFitCache;
860
+ if (
861
+ cache &&
862
+ cache.catMin === visCatMin &&
863
+ cache.catMax === visCatMax &&
864
+ cache.hidden === chart._hiddenSeries
865
+ ) {
866
+ return cache;
867
+ }
868
+
869
+ // Pre-bucketed extent table — built once per data load (and on
870
+ // hidden-series mutation) — turns the per-frame walk from
871
+ // O(`bars.count` = N×M×P) into O(visibleCats). The original
872
+ // O(`bars.count`) walk now runs only inside `ensureCatExtents`.
873
+ const buckets = ensureCatExtents(chart);
874
+
875
+ let leftMin = Infinity;
876
+ let leftMax = -Infinity;
877
+ let hasLeft = false;
878
+ let rightMin = Infinity;
879
+ let rightMax = -Infinity;
880
+ let hasRight = false;
881
+
882
+ if (buckets.n > 0) {
883
+ // Clamp to the populated [0, n-1] range. `visCat*` is in
884
+ // continuous coords (numeric or category index space), so
885
+ // floor/ceil to integer bucket indices.
886
+ const lo = Math.max(0, Math.floor(visCatMin));
887
+ const hi = Math.min(buckets.n - 1, Math.ceil(visCatMax));
888
+ const lMin = buckets.leftMin;
889
+ const lMax = buckets.leftMax;
890
+ const rMin = buckets.rightMin;
891
+ const rMax = buckets.rightMax;
892
+ const hL = buckets.hasLeft;
893
+ const hR = buckets.hasRight;
894
+ for (let i = lo; i <= hi; i++) {
895
+ if (hL[i]) {
896
+ if (lMin[i] < leftMin) {
897
+ leftMin = lMin[i];
898
+ }
899
+
900
+ if (lMax[i] > leftMax) {
901
+ leftMax = lMax[i];
902
+ }
903
+
904
+ hasLeft = true;
905
+ }
906
+
907
+ if (hR[i]) {
908
+ if (rMin[i] < rightMin) {
909
+ rightMin = rMin[i];
910
+ }
911
+
912
+ if (rMax[i] > rightMax) {
913
+ rightMax = rMax[i];
914
+ }
915
+
916
+ hasRight = true;
917
+ }
918
+ }
919
+ }
920
+
921
+ // Reuse the same cache object to avoid per-frame allocation.
922
+ // `hidden` stored by reference — identity comparison in the cache
923
+ // hit path catches set-content changes because the legend-click
924
+ // handler swaps / mutates the set in ways that invalidate the
925
+ // cache via the explicit null-out.
926
+ const next = cache ?? newSeriesAutoFitCache();
927
+ next.catMin = visCatMin;
928
+ next.catMax = visCatMax;
929
+ next.hidden = chart._hiddenSeries;
930
+ next.leftMin = leftMin;
931
+ next.leftMax = leftMax;
932
+ next.hasLeft = hasLeft;
933
+ next.rightMin = rightMin;
934
+ next.rightMax = rightMax;
935
+ next.hasRight = hasRight;
936
+ chart._autoFitCache = next;
937
+ return next;
938
+ }
939
+
940
+ function newSeriesAutoFitCache(): SeriesAutoFitCache {
941
+ return {
942
+ catMin: 0,
943
+ catMax: 0,
944
+ hidden: new Set(),
945
+ leftMin: 0,
946
+ leftMax: 0,
947
+ hasLeft: false,
948
+ rightMin: 0,
949
+ rightMax: 0,
950
+ hasRight: false,
951
+ };
952
+ }
953
+
954
+ /**
955
+ * Build (or rebuild) the per-category extent buckets for the current
956
+ * `_bars` set plus the line / scatter sample grid, filtered by the
957
+ * current `_hiddenSeries` set. The buckets answer "what's the value
958
+ * range across this category?" in O(1) per category, replacing the
959
+ * O(`bars.count` + N × |line+scatter|) per-frame walk.
960
+ *
961
+ * Bar / area glyphs contribute via `_bars` (min/max of `y0`,`y1`, so
962
+ * stacking and negative values are handled uniformly). Line / scatter
963
+ * glyphs have no `_bars` records — they contribute the raw sample
964
+ * value `v` as the single-point extent `[v, v]`; without this pass
965
+ * `series_zoom_mode === "dynamic"` would silently behave as `"fixed"`
966
+ * on any pure line/scatter chart.
967
+ *
968
+ * Capacity-reused: typed arrays grown only when `_numCategories`
969
+ * exceeds prior capacity. Amortizes across pan/zoom frames — runs
970
+ * once per data load + once per legend toggle, not per frame.
971
+ */
972
+ function ensureCatExtents(
973
+ chart: SeriesChart,
974
+ ): NonNullable<SeriesChart["_catExtents"]> {
975
+ const N = chart._numCategories;
976
+ let buckets = chart._catExtents;
977
+
978
+ const sameCapacity = buckets && buckets.leftMin.length >= N;
979
+ if (
980
+ buckets &&
981
+ sameCapacity &&
982
+ chart._catExtentsHidden === chart._hiddenSeries
983
+ ) {
984
+ return buckets;
985
+ }
986
+
987
+ if (!buckets || !sameCapacity) {
988
+ buckets = {
989
+ leftMin: new Float64Array(N),
990
+ leftMax: new Float64Array(N),
991
+ rightMin: new Float64Array(N),
992
+ rightMax: new Float64Array(N),
993
+ hasLeft: new Uint8Array(N),
994
+ hasRight: new Uint8Array(N),
995
+ n: N,
996
+ };
997
+ chart._catExtents = buckets;
998
+ } else {
999
+ buckets.n = N;
1000
+ }
1001
+
1002
+ // Initialize every per-cat slot to the empty extent. `Infinity` /
1003
+ // `-Infinity` so that the first contributing bar wins on
1004
+ // min/max comparisons.
1005
+ for (let i = 0; i < N; i++) {
1006
+ buckets.leftMin[i] = Infinity;
1007
+ buckets.leftMax[i] = -Infinity;
1008
+ buckets.rightMin[i] = Infinity;
1009
+ buckets.rightMax[i] = -Infinity;
1010
+ buckets.hasLeft[i] = 0;
1011
+ buckets.hasRight[i] = 0;
1012
+ }
1013
+
1014
+ const bars = chart._bars;
1015
+ const hidden = chart._hiddenSeries;
1016
+ const catIdxArr = bars.catIdx;
1017
+ const seriesIdArr = bars.seriesId;
1018
+ const y0Arr = bars.y0;
1019
+ const y1Arr = bars.y1;
1020
+ const axisArr = bars.axis;
1021
+ for (let i = 0; i < bars.count; i++) {
1022
+ if (hidden.has(seriesIdArr[i])) {
1023
+ continue;
1024
+ }
1025
+
1026
+ const ci = catIdxArr[i];
1027
+ if (ci < 0 || ci >= N) {
1028
+ continue;
1029
+ }
1030
+
1031
+ const y0 = y0Arr[i];
1032
+ const y1 = y1Arr[i];
1033
+ const lo = y0 < y1 ? y0 : y1;
1034
+ const hi = y0 < y1 ? y1 : y0;
1035
+ if (axisArr[i] === 1) {
1036
+ if (lo < buckets.rightMin[ci]) {
1037
+ buckets.rightMin[ci] = lo;
1038
+ }
1039
+
1040
+ if (hi > buckets.rightMax[ci]) {
1041
+ buckets.rightMax[ci] = hi;
1042
+ }
1043
+
1044
+ buckets.hasRight[ci] = 1;
1045
+ } else {
1046
+ if (lo < buckets.leftMin[ci]) {
1047
+ buckets.leftMin[ci] = lo;
1048
+ }
1049
+
1050
+ if (hi > buckets.leftMax[ci]) {
1051
+ buckets.leftMax[ci] = hi;
1052
+ }
1053
+
1054
+ buckets.hasLeft[ci] = 1;
1055
+ }
1056
+ }
1057
+
1058
+ // Line / scatter glyphs route through `_samples`, not `_bars`, so
1059
+ // fold their per-cat values in here. Bar / area series are already
1060
+ // covered by the loop above (including non-stacking bar/area, which
1061
+ // emit `_bars` records with `y0=0`, `y1=v`); line / scatter never
1062
+ // stack, so the sample grid is their only contribution.
1063
+ const samplingSeries = [chart._lineSeries, chart._scatterSeries];
1064
+ const samples = chart._samples;
1065
+ const sampleValid = chart._sampleValid;
1066
+ const S = chart._series.length;
1067
+ for (const seriesArr of samplingSeries) {
1068
+ for (const s of seriesArr) {
1069
+ if (hidden.has(s.seriesId)) {
1070
+ continue;
1071
+ }
1072
+
1073
+ const onRight = s.axis === 1;
1074
+ const sid = s.seriesId;
1075
+ for (let ci = 0; ci < N; ci++) {
1076
+ const sampleIdx = ci * S + sid;
1077
+ if (!((sampleValid[sampleIdx >> 3] >> (sampleIdx & 7)) & 1)) {
1078
+ continue;
1079
+ }
1080
+
1081
+ const v = samples[sampleIdx];
1082
+ if (onRight) {
1083
+ if (v < buckets.rightMin[ci]) {
1084
+ buckets.rightMin[ci] = v;
1085
+ }
1086
+
1087
+ if (v > buckets.rightMax[ci]) {
1088
+ buckets.rightMax[ci] = v;
1089
+ }
1090
+
1091
+ buckets.hasRight[ci] = 1;
1092
+ } else {
1093
+ if (v < buckets.leftMin[ci]) {
1094
+ buckets.leftMin[ci] = v;
1095
+ }
1096
+
1097
+ if (v > buckets.leftMax[ci]) {
1098
+ buckets.leftMax[ci] = v;
1099
+ }
1100
+
1101
+ buckets.hasLeft[ci] = 1;
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ chart._catExtentsHidden = hidden;
1108
+ return buckets;
1109
+ }