@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,848 @@
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 { buildSplitGroups } from "../../data/split-groups";
15
+ import type { CategoricalLevel } from "../../axis/categorical-axis";
16
+ import {
17
+ resolveAxisMode,
18
+ resolveCategoryAxis,
19
+ resolveNumericCategoryDomain,
20
+ type AxisMode,
21
+ type NumericCategoryDomain,
22
+ } from "../common/category-axis-resolver";
23
+ import { computeSlotGeometry } from "../common/band-layout";
24
+ import {
25
+ resolveChartType,
26
+ resolveStack,
27
+ resolveAltAxis,
28
+ type ChartType,
29
+ type ColumnChartConfig,
30
+ } from "./series-type";
31
+
32
+ const DUAL_Y_RATIO_THRESHOLD = 50;
33
+
34
+ export interface SeriesInfo {
35
+ seriesId: number;
36
+ aggIdx: number;
37
+ splitIdx: number;
38
+ aggName: string;
39
+ splitKey: string;
40
+ label: string;
41
+ color: [number, number, number];
42
+ axis: 0 | 1;
43
+ chartType: ChartType;
44
+ stack: boolean;
45
+ }
46
+
47
+ /**
48
+ * Logical bar/area record. Synthesized on demand from {@link BarColumns}
49
+ * via {@link readBarRecord} for tooltip / hover paths. The pipeline never
50
+ * materializes these — see `BarColumns` for the columnar storage that
51
+ * replaces the legacy `SeriesChartRecord[]`.
52
+ */
53
+ export interface SeriesChartRecord {
54
+ catIdx: number;
55
+ aggIdx: number;
56
+ splitIdx: number;
57
+ seriesId: number;
58
+ xCenter: number;
59
+ halfWidth: number;
60
+ y0: number;
61
+ y1: number;
62
+ value: number;
63
+ axis: 0 | 1;
64
+
65
+ /**
66
+ * `"bar"` quads or `"area"` strip segments both stack via this record.
67
+ */
68
+ chartType: "bar" | "area";
69
+ }
70
+
71
+ export const BAR_TYPE_BAR = 0;
72
+ export const BAR_TYPE_AREA = 1;
73
+
74
+ /**
75
+ * Columnar storage for the bar/area record set. Replaces the legacy
76
+ * `SeriesChartRecord[]` to avoid per-record POJO allocation at scale —
77
+ * with N×M×P potentially in the millions, the array-of-objects layout
78
+ * was the dominant build-time GC pressure.
79
+ *
80
+ * Records are appended in `(catIdx, aggIdx, splitIdx)` lexicographic
81
+ * order — the outer category loop guarantees `catIdx` is monotonically
82
+ * non-decreasing, which the renderer / hit-test use for binary-search
83
+ * narrowing.
84
+ *
85
+ * `count` is the active record count; the underlying typed arrays may
86
+ * be over-allocated for capacity reuse across builds.
87
+ */
88
+ /**
89
+ * Compact columnar storage for the bar/area record set.
90
+ *
91
+ * Three fields the prior schema carried have been dropped because
92
+ * they're cheaply derivable at hover time:
93
+ * - `aggIdx` ← `seriesId / splitCount` (integer division)
94
+ * - `splitIdx` ← `seriesId % splitCount`
95
+ * - `value` ← `samples[catIdx * S + seriesId]`
96
+ *
97
+ * Per-cell write count drops from 11 to 8 (~27% fewer typed-array
98
+ * stores) and per-record memory drops from 58 B to 42 B (~28% lower
99
+ * footprint at scale). `chartType` is kept (1 B / record) — it's
100
+ * read in tight loops in the render and hit-test paths and a string
101
+ * dispatch via `_series[]` would be slower than the byte compare.
102
+ */
103
+ export interface BarColumns {
104
+ count: number;
105
+ catIdx: Int32Array;
106
+ seriesId: Int32Array;
107
+
108
+ /**
109
+ * 0 = left axis, 1 = right axis.
110
+ */
111
+ axis: Uint8Array;
112
+
113
+ /**
114
+ * {@link BAR_TYPE_BAR} | {@link BAR_TYPE_AREA}.
115
+ */
116
+ chartType: Uint8Array;
117
+ xCenter: Float64Array;
118
+ halfWidth: Float64Array;
119
+ y0: Float64Array;
120
+ y1: Float64Array;
121
+ }
122
+
123
+ export function emptyBarColumns(): BarColumns {
124
+ return {
125
+ count: 0,
126
+ catIdx: new Int32Array(0),
127
+ seriesId: new Int32Array(0),
128
+ axis: new Uint8Array(0),
129
+ chartType: new Uint8Array(0),
130
+ xCenter: new Float64Array(0),
131
+ halfWidth: new Float64Array(0),
132
+ y0: new Float64Array(0),
133
+ y1: new Float64Array(0),
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Reuse `prev`'s typed arrays when capacity is sufficient, else allocate
139
+ * fresh. Resets `count` to 0 either way; pipeline writes from index 0.
140
+ */
141
+ export function ensureBarColumnsCapacity(
142
+ prev: BarColumns | null,
143
+ capacity: number,
144
+ ): BarColumns {
145
+ if (prev && prev.catIdx.length >= capacity) {
146
+ prev.count = 0;
147
+ return prev;
148
+ }
149
+
150
+ return {
151
+ count: 0,
152
+ catIdx: new Int32Array(capacity),
153
+ seriesId: new Int32Array(capacity),
154
+ axis: new Uint8Array(capacity),
155
+ chartType: new Uint8Array(capacity),
156
+ xCenter: new Float64Array(capacity),
157
+ halfWidth: new Float64Array(capacity),
158
+ y0: new Float64Array(capacity),
159
+ y1: new Float64Array(capacity),
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Synthesize a {@link SeriesChartRecord} POJO for record `i`. Used by
165
+ * tooltip / hover paths that hand out a single record reference; not
166
+ * called in any frame-rate hot loop.
167
+ *
168
+ * `splitCount` is `splitPrefixes.length` from the build result (= `P`
169
+ * in pipeline notation). `samples` + `numSeries` recover the raw
170
+ * value from the unstacked sample grid; `samples[catIdx * S + sid]`
171
+ * always carries the same value the pipeline saw when emitting the
172
+ * record (both writes share the same `v` source).
173
+ */
174
+ export function readBarRecord(
175
+ cols: BarColumns,
176
+ i: number,
177
+ splitCount: number,
178
+ samples: Float32Array,
179
+ numSeries: number,
180
+ ): SeriesChartRecord {
181
+ const sid = cols.seriesId[i];
182
+ const ci = cols.catIdx[i];
183
+ return {
184
+ catIdx: ci,
185
+ aggIdx: Math.floor(sid / splitCount),
186
+ splitIdx: sid % splitCount,
187
+ seriesId: sid,
188
+ xCenter: cols.xCenter[i],
189
+ halfWidth: cols.halfWidth[i],
190
+ y0: cols.y0[i],
191
+ y1: cols.y1[i],
192
+ value: samples[ci * numSeries + sid],
193
+ axis: cols.axis[i] as 0 | 1,
194
+ chartType: cols.chartType[i] === BAR_TYPE_BAR ? "bar" : "area",
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Reusable Float64 scratch — chart owns one for `posStack` and one for
200
+ * `negStack`. Pipeline zero-fills the active prefix on entry.
201
+ */
202
+ export function ensureFloat64Scratch(
203
+ prev: Float64Array | null,
204
+ capacity: number,
205
+ ): Float64Array {
206
+ if (prev && prev.length >= capacity) {
207
+ return prev;
208
+ }
209
+
210
+ return new Float64Array(Math.max(capacity, prev?.length ?? 0));
211
+ }
212
+
213
+ export interface SeriesPipelineInput {
214
+ columns: ColumnDataMap;
215
+ numRows: number;
216
+ columnSlots: (string | null)[];
217
+ groupBy: string[];
218
+ splitBy: string[];
219
+
220
+ /**
221
+ * Source-column types for `group_by` columns (table.schema() merged
222
+ * with view.expression_schema()). Used to (a) stringify non-string
223
+ * row-path levels and (b) decide between category and numeric axis
224
+ * mode for single-level group_bys.
225
+ */
226
+ groupByTypes: Record<string, string>;
227
+ columnsConfig: Record<string, ColumnChartConfig> | undefined;
228
+
229
+ /**
230
+ * Plugin-scoped default glyph when a column has no explicit entry.
231
+ */
232
+ defaultChartType?: ChartType;
233
+
234
+ /**
235
+ * Plugin-config knobs consumed by the build pipeline. Pulled from
236
+ * the chart impl's `_pluginConfig` (sourced from the host's
237
+ * `plugin_config_schema` / `restore({ plugin_config })`):
238
+ *
239
+ * - `autoAltYAxis` — auto-split aggregates onto a secondary Y
240
+ * axis when their magnitude ratio exceeds
241
+ * `DUAL_Y_RATIO_THRESHOLD`. Replaces the `AUTO_ALT_Y_AXIS`
242
+ * compile-time toggle.
243
+ * - `bandInnerFrac` / `barInnerPad` — band-slot geometry forwarded
244
+ * to `computeSlotGeometry`. Replace the `BAND_INNER_FRAC` /
245
+ * `BAR_INNER_PAD` constants.
246
+ */
247
+ autoAltYAxis: boolean;
248
+ bandInnerFrac: number;
249
+ barInnerPad: number;
250
+
251
+ /**
252
+ * Anchor value-axis extents to zero. When `true` (bar / area
253
+ * default), `leftDomain` / `rightDomain` are guaranteed to enclose
254
+ * `0` so bars and areas render against their natural baseline.
255
+ * When `false` (line / scatter default), the domain is the raw
256
+ * `min`/`max` of the data — the axis tightens around the visible
257
+ * variation. Maps directly to `PluginConfig.include_zero`.
258
+ */
259
+ includeZero: boolean;
260
+
261
+ /**
262
+ * Reusable scratch — pipeline writes records into these in place
263
+ * and zero-fills the stack ladder. Pass the previous build's
264
+ * outputs to amortize allocation across data reloads.
265
+ */
266
+ scratchBars?: BarColumns | null;
267
+ scratchPosStack?: Float64Array | null;
268
+ scratchNegStack?: Float64Array | null;
269
+ }
270
+
271
+ export type { NumericCategoryDomain };
272
+
273
+ export interface SeriesPipelineResult {
274
+ aggregates: string[];
275
+ splitPrefixes: string[];
276
+ rowPaths: CategoricalLevel[];
277
+ numCategories: number;
278
+ rowOffset: number;
279
+
280
+ /**
281
+ * Axis mode discriminator. `category` is the default (zero or
282
+ * many group_by levels, or a single string/boolean level). `numeric`
283
+ * fires for a single non-string non-boolean group_by — bars are
284
+ * positioned by the underlying data value rather than logical
285
+ * category index.
286
+ */
287
+ axisMode: AxisMode;
288
+
289
+ /**
290
+ * Populated only when `axisMode.mode === "numeric"`.
291
+ */
292
+ numericCategoryDomain: NumericCategoryDomain | null;
293
+
294
+ /**
295
+ * Per-category X coordinate in real data units. Populated only in
296
+ * numeric axis mode — `null` in category mode where catIdx itself
297
+ * is the position. Indexed by `catIdx` (0..numCategories-1).
298
+ */
299
+ categoryPositions: Float64Array | null;
300
+ series: SeriesInfo[];
301
+
302
+ /**
303
+ * Columnar bar/area records, one per (catIdx, agg, split) for series
304
+ * where `stack === true && chartType in ["bar", "area"]` (stacked) or
305
+ * `chartType in ["bar", "area"]` with non-zero value (unstacked).
306
+ */
307
+ bars: BarColumns;
308
+
309
+ /**
310
+ * Reusable scratch passthrough — these own the stack ladder typed
311
+ * arrays so the next build can reuse capacity.
312
+ */
313
+ posStack: Float64Array | null;
314
+ negStack: Float64Array | null;
315
+
316
+ /**
317
+ * Unstacked sample grid: `samples[catIdx * S + seriesId]` is the raw
318
+ * value for that cell. Only valid for non-stacking series (or for
319
+ * stacking series when you need the raw, pre-stack value); the
320
+ * corresponding bit in `sampleValid` indicates whether the cell carries
321
+ * data. `S === series.length`.
322
+ */
323
+ samples: Float32Array;
324
+ sampleValid: Uint8Array;
325
+
326
+ leftDomain: { min: number; max: number };
327
+ rightDomain: { min: number; max: number } | null;
328
+ hasRightAxis: boolean;
329
+ }
330
+
331
+ function setValidBit(valid: Uint8Array, idx: number): void {
332
+ valid[idx >> 3] |= 1 << (idx & 7);
333
+ }
334
+
335
+ /**
336
+ * Pure pipeline: turn a raw `ColumnDataMap` into (a) columnar stacked
337
+ * bar/area records and (b) an unstacked `samples` grid for line/scatter
338
+ * glyphs plus non-stacking bar/area series. Holds row_path data as
339
+ * zero-copy views (no materialization of category strings).
340
+ *
341
+ * Automatically splits aggregates across a secondary Y axis when their
342
+ * extents differ by more than {@link DUAL_Y_RATIO_THRESHOLD}×.
343
+ */
344
+ export function buildSeriesPipeline(
345
+ input: SeriesPipelineInput,
346
+ ): SeriesPipelineResult {
347
+ const {
348
+ columns,
349
+ numRows,
350
+ columnSlots,
351
+ groupBy,
352
+ splitBy,
353
+ groupByTypes,
354
+ columnsConfig,
355
+ defaultChartType,
356
+ autoAltYAxis,
357
+ bandInnerFrac,
358
+ barInnerPad,
359
+ includeZero,
360
+ scratchBars,
361
+ scratchPosStack,
362
+ scratchNegStack,
363
+ } = input;
364
+
365
+ const axisMode = resolveAxisMode(groupBy, groupByTypes);
366
+ const empty: SeriesPipelineResult = {
367
+ aggregates: [],
368
+ splitPrefixes: [],
369
+ rowPaths: [],
370
+ numCategories: 0,
371
+ rowOffset: 0,
372
+ axisMode,
373
+ numericCategoryDomain: null,
374
+ categoryPositions: null,
375
+ series: [],
376
+ bars: emptyBarColumns(),
377
+ posStack: scratchPosStack ?? null,
378
+ negStack: scratchNegStack ?? null,
379
+ samples: new Float32Array(0),
380
+ sampleValid: new Uint8Array(0),
381
+ leftDomain: { min: 0, max: 0 },
382
+ rightDomain: null,
383
+ hasRightAxis: false,
384
+ };
385
+
386
+ const aggregates = columnSlots.filter((s): s is string => !!s);
387
+ if (aggregates.length === 0) {
388
+ return empty;
389
+ }
390
+
391
+ const splitPrefixes: string[] = [];
392
+ if (splitBy.length > 0) {
393
+ for (const g of buildSplitGroups(columns, [], aggregates)) {
394
+ if (g.colNames.size > 0) {
395
+ splitPrefixes.push(g.prefix);
396
+ }
397
+ }
398
+
399
+ if (splitPrefixes.length === 0) {
400
+ splitPrefixes.push("");
401
+ }
402
+ } else {
403
+ splitPrefixes.push("");
404
+ }
405
+
406
+ const levelTypes = groupBy.map((name) => groupByTypes[name] ?? "string");
407
+ const { rowPaths, numCategories, rowOffset } = resolveCategoryAxis(
408
+ columns,
409
+ numRows,
410
+ groupBy.length,
411
+ levelTypes,
412
+ );
413
+
414
+ if (numCategories === 0) {
415
+ return {
416
+ ...empty,
417
+ aggregates,
418
+ splitPrefixes,
419
+ rowPaths,
420
+ rowOffset,
421
+ };
422
+ }
423
+
424
+ const series: SeriesInfo[] = [];
425
+ const M = aggregates.length;
426
+ const P = splitPrefixes.length;
427
+ for (let k = 0; k < M; k++) {
428
+ for (let p = 0; p < P; p++) {
429
+ const aggName = aggregates[k];
430
+ const splitKey = splitPrefixes[p];
431
+ const label =
432
+ splitKey === ""
433
+ ? aggName
434
+ : `${splitKey}${M > 1 ? ` | ${aggName}` : ""}`;
435
+ const chartType = resolveChartType(
436
+ aggName,
437
+ columnsConfig,
438
+ defaultChartType,
439
+ );
440
+ const stack = resolveStack(aggName, chartType, columnsConfig);
441
+ series.push({
442
+ seriesId: k * P + p,
443
+ aggIdx: k,
444
+ splitIdx: p,
445
+ aggName,
446
+ splitKey,
447
+ label,
448
+ color: [0.5, 0.5, 0.5],
449
+ axis: 0,
450
+ chartType,
451
+ stack,
452
+ });
453
+ }
454
+ }
455
+
456
+ // `aggExtents` accumulates per-aggregate value ranges for the
457
+ // dual-axis split heuristic below. `includeZero` decides whether
458
+ // the range starts anchored at zero (bar / area: the bar grows
459
+ // from the zero baseline, so it's part of the natural extent) or
460
+ // open (line / scatter: extent is the raw `min`..`max`).
461
+ const aggExtents: { min: number; max: number }[] = [];
462
+ for (let k = 0; k < M; k++) {
463
+ aggExtents.push(
464
+ includeZero
465
+ ? { min: 0, max: 0 }
466
+ : { min: Infinity, max: -Infinity },
467
+ );
468
+ }
469
+
470
+ const N = numCategories;
471
+ const S = series.length;
472
+
473
+ // Stacking ladder, keyed by (catIdx, aggIdx). Reuse chart-owned
474
+ // scratch when sized; else allocate. Active prefix is zero-filled.
475
+ const stackLen = N * M;
476
+ const posStack = ensureFloat64Scratch(scratchPosStack ?? null, stackLen);
477
+ const negStack = ensureFloat64Scratch(scratchNegStack ?? null, stackLen);
478
+ posStack.fill(0, 0, stackLen);
479
+ negStack.fill(0, 0, stackLen);
480
+
481
+ const samples = new Float32Array(N * S);
482
+ const sampleValid = new Uint8Array((N * S + 7) >> 3);
483
+
484
+ // Numeric-mode category positions: real data values from __ROW_PATH_0__.
485
+ // null in category mode (catIdx is the position).
486
+ let categoryPositions: Float64Array | null = null;
487
+ let numericCategoryDomain: NumericCategoryDomain | null = null;
488
+ let numericBandWidth = 1;
489
+ if (axisMode.mode === "numeric" && N > 0) {
490
+ const rp = columns.get("__ROW_PATH_0__");
491
+ const resolved = resolveNumericCategoryDomain(
492
+ rp?.values,
493
+ N,
494
+ rowOffset,
495
+ groupBy[0] ?? "",
496
+ axisMode.numericType === "date" ||
497
+ axisMode.numericType === "datetime",
498
+ );
499
+ if (resolved) {
500
+ categoryPositions = resolved.categoryPositions;
501
+ numericCategoryDomain = resolved.numericCategoryDomain;
502
+ numericBandWidth = resolved.numericCategoryDomain.bandWidth;
503
+ }
504
+ }
505
+
506
+ // Per-band slot geometry — `computeSlotGeometry` returns values in
507
+ // band-relative units (band width = 1). In numeric mode scale by
508
+ // the data-unit band width derived above.
509
+ const baseSlot = computeSlotGeometry(M, bandInnerFrac, barInnerPad);
510
+ const slotWidth = baseSlot.slotWidth * numericBandWidth;
511
+ const halfWidth = baseSlot.halfWidth * numericBandWidth;
512
+
513
+ // Pre-build per-aggregate slot offsets. The legacy form recomputed
514
+ // `(k - (M - 1) / 2) * slotWidth` for every (catI, k, p) — N×M×P
515
+ // FMA chains in the inner loop. Hoist to a length-M lookup.
516
+ const slotOffsets = new Float64Array(M);
517
+ const halfWidthOffset = (M - 1) / 2;
518
+ for (let k = 0; k < M; k++) {
519
+ slotOffsets[k] = (k - halfWidthOffset) * slotWidth;
520
+ }
521
+
522
+ // Pre-resolve the (k, p) → column reference + valid mask. The legacy
523
+ // form built the column name string and called `columns.get(...)` for
524
+ // every (catI, k, p) cell — N×M×P string allocs + Map lookups, which
525
+ // dominates for dense data. Hoist outside the row loop to an
526
+ // M*P-shaped flat array of (values, valid) tuples.
527
+ const colValues: (ArrayLike<unknown> | null)[] = new Array(M * P);
528
+ const colValid: (Uint8Array | null)[] = new Array(M * P);
529
+ for (let k = 0; k < M; k++) {
530
+ const aggName = aggregates[k];
531
+ for (let p = 0; p < P; p++) {
532
+ const splitKey = splitPrefixes[p];
533
+ const colName =
534
+ splitKey === "" ? aggName : `${splitKey}|${aggName}`;
535
+ const col = columns.get(colName);
536
+ const idx = k * P + p;
537
+ colValues[idx] = col?.values ?? null;
538
+ colValid[idx] = col?.valid ?? null;
539
+ }
540
+ }
541
+
542
+ // Pre-allocate columnar bar storage at N*M*P upper bound. The
543
+ // pipeline emits at most one record per (cat, agg, split) cell;
544
+ // `bars.count` tracks the active prefix.
545
+ const barCap = N * M * P;
546
+ const bars = ensureBarColumnsCapacity(scratchBars ?? null, barCap);
547
+ let barWrite = 0;
548
+
549
+ for (let catI = 0; catI < N; catI++) {
550
+ const row = catI + rowOffset;
551
+
552
+ // Hoist the category center — same value across all (k, p) for
553
+ // the current catI.
554
+ const catCenter = categoryPositions ? categoryPositions[catI] : catI;
555
+
556
+ for (let k = 0; k < M; k++) {
557
+ const slotOffset = slotOffsets[k];
558
+ const xCenter = catCenter + slotOffset;
559
+ const ext = aggExtents[k];
560
+
561
+ for (let p = 0; p < P; p++) {
562
+ const seriesId = k * P + p;
563
+ const s = series[seriesId];
564
+ const colIdx = k * P + p;
565
+ const values = colValues[colIdx];
566
+ if (!values) {
567
+ continue;
568
+ }
569
+
570
+ const valid = colValid[colIdx];
571
+ if (valid) {
572
+ const bit = (valid[row >> 3] >> (row & 7)) & 1;
573
+ if (!bit) {
574
+ continue;
575
+ }
576
+ }
577
+
578
+ const v = values[row] as number;
579
+ if (!isFinite(v)) {
580
+ continue;
581
+ }
582
+
583
+ // Record the raw value in the unstacked grid for every
584
+ // glyph that needs it (line, scatter, non-stacking bar/area).
585
+ const sampleIdx = catI * S + seriesId;
586
+ samples[sampleIdx] = v;
587
+ setValidBit(sampleValid, sampleIdx);
588
+
589
+ // Stacking-glyph path: emit a record with running y0/y1.
590
+ if (
591
+ (s.chartType === "bar" || s.chartType === "area") &&
592
+ s.stack
593
+ ) {
594
+ if (v === 0) {
595
+ continue;
596
+ }
597
+
598
+ const stackIdx = catI * M + k;
599
+ let y0: number;
600
+ let y1: number;
601
+ if (v >= 0) {
602
+ y0 = posStack[stackIdx];
603
+ y1 = y0 + v;
604
+ posStack[stackIdx] = y1;
605
+ } else {
606
+ y0 = negStack[stackIdx];
607
+ y1 = y0 + v;
608
+ negStack[stackIdx] = y1;
609
+ }
610
+
611
+ if (y0 < ext.min) {
612
+ ext.min = y0;
613
+ }
614
+
615
+ if (y1 < ext.min) {
616
+ ext.min = y1;
617
+ }
618
+
619
+ if (y0 > ext.max) {
620
+ ext.max = y0;
621
+ }
622
+
623
+ if (y1 > ext.max) {
624
+ ext.max = y1;
625
+ }
626
+
627
+ bars.catIdx[barWrite] = catI;
628
+ bars.seriesId[barWrite] = seriesId;
629
+ bars.axis[barWrite] = 0;
630
+ bars.chartType[barWrite] =
631
+ s.chartType === "bar" ? BAR_TYPE_BAR : BAR_TYPE_AREA;
632
+ bars.xCenter[barWrite] = xCenter;
633
+ bars.halfWidth[barWrite] = halfWidth;
634
+ bars.y0[barWrite] = y0;
635
+ bars.y1[barWrite] = y1;
636
+ barWrite++;
637
+ } else {
638
+ // Non-stacking: extend extents by raw value against zero
639
+ // baseline so the axis still encloses line/scatter data.
640
+ if (v < ext.min) {
641
+ ext.min = v;
642
+ }
643
+
644
+ if (v > ext.max) {
645
+ ext.max = v;
646
+ }
647
+
648
+ if (includeZero) {
649
+ if (0 < ext.min) {
650
+ ext.min = 0;
651
+ }
652
+
653
+ if (0 > ext.max) {
654
+ ext.max = 0;
655
+ }
656
+ }
657
+
658
+ // Non-stacking bar/area still needs a record so the
659
+ // glyph draw call has a concrete rect. Unstacked: y0=0,
660
+ // y1=v.
661
+ if (s.chartType === "bar" || s.chartType === "area") {
662
+ if (v === 0) {
663
+ continue;
664
+ }
665
+
666
+ bars.catIdx[barWrite] = catI;
667
+ bars.seriesId[barWrite] = seriesId;
668
+ bars.axis[barWrite] = 0;
669
+ bars.chartType[barWrite] =
670
+ s.chartType === "bar"
671
+ ? BAR_TYPE_BAR
672
+ : BAR_TYPE_AREA;
673
+ bars.xCenter[barWrite] = xCenter;
674
+ bars.halfWidth[barWrite] = halfWidth;
675
+ bars.y0[barWrite] = 0;
676
+ bars.y1[barWrite] = v;
677
+ barWrite++;
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+
684
+ bars.count = barWrite;
685
+
686
+ let hasRightAxis = false;
687
+ if (autoAltYAxis && M >= 2) {
688
+ const extents: number[] = new Array(M);
689
+ let maxExt = 0;
690
+ let minExt = Infinity;
691
+ for (let k = 0; k < M; k++) {
692
+ const e = aggExtents[k];
693
+ const ae = Math.max(Math.abs(e.min), Math.abs(e.max), 1e-12);
694
+ extents[k] = ae;
695
+ if (ae > maxExt) {
696
+ maxExt = ae;
697
+ }
698
+
699
+ if (ae < minExt) {
700
+ minExt = ae;
701
+ }
702
+ }
703
+
704
+ if (maxExt / minExt > DUAL_Y_RATIO_THRESHOLD) {
705
+ const threshold = maxExt / Math.sqrt(DUAL_Y_RATIO_THRESHOLD);
706
+ for (let k = 0; k < M; k++) {
707
+ const onRight = extents[k] < threshold;
708
+ if (onRight) {
709
+ for (const s of series) {
710
+ if (s.aggIdx === k) {
711
+ s.axis = 1;
712
+ }
713
+ }
714
+ }
715
+ }
716
+
717
+ // Propagate axis assignment into bar storage.
718
+ for (let i = 0; i < bars.count; i++) {
719
+ bars.axis[i] = series[bars.seriesId[i]].axis;
720
+ }
721
+
722
+ hasRightAxis = series.some((s) => s.axis === 1);
723
+ }
724
+ }
725
+
726
+ // Per-column `alt_axis` override — always wins over the auto
727
+ // split. Runs unconditionally so it works even when
728
+ // `autoAltYAxis` is off or there's only a single aggregate.
729
+ let forcedRight = false;
730
+ for (let k = 0; k < M; k++) {
731
+ if (resolveAltAxis(aggregates[k], columnsConfig)) {
732
+ for (const s of series) {
733
+ if (s.aggIdx === k) {
734
+ s.axis = 1;
735
+ forcedRight = true;
736
+ }
737
+ }
738
+ }
739
+ }
740
+
741
+ if (forcedRight) {
742
+ for (let i = 0; i < bars.count; i++) {
743
+ bars.axis[i] = series[bars.seriesId[i]].axis;
744
+ }
745
+
746
+ hasRightAxis = true;
747
+ }
748
+
749
+ // Axis domains: stack records contribute y0/y1; non-stacking
750
+ // samples contribute raw values. When `includeZero` is true the
751
+ // domain starts anchored at zero so bar / area glyphs always have
752
+ // their baseline in view; when false the domain opens to
753
+ // `[Infinity, -Infinity]` and closes around the data extent.
754
+ const leftExtent = includeZero
755
+ ? { min: 0, max: 0 }
756
+ : { min: Infinity, max: -Infinity };
757
+ const rightExtent = includeZero
758
+ ? { min: 0, max: 0 }
759
+ : { min: Infinity, max: -Infinity };
760
+ for (let i = 0; i < bars.count; i++) {
761
+ const ext = bars.axis[i] === 0 ? leftExtent : rightExtent;
762
+ const y0 = bars.y0[i];
763
+ const y1 = bars.y1[i];
764
+ if (y0 < ext.min) {
765
+ ext.min = y0;
766
+ }
767
+
768
+ if (y1 < ext.min) {
769
+ ext.min = y1;
770
+ }
771
+
772
+ if (y0 > ext.max) {
773
+ ext.max = y0;
774
+ }
775
+
776
+ if (y1 > ext.max) {
777
+ ext.max = y1;
778
+ }
779
+ }
780
+
781
+ for (let seriesId = 0; seriesId < S; seriesId++) {
782
+ const s = series[seriesId];
783
+ if (s.stack && (s.chartType === "bar" || s.chartType === "area")) {
784
+ continue; // already counted via bars
785
+ }
786
+
787
+ const ext = s.axis === 0 ? leftExtent : rightExtent;
788
+ for (let catI = 0; catI < N; catI++) {
789
+ const sampleIdx = catI * S + seriesId;
790
+ if (!((sampleValid[sampleIdx >> 3] >> (sampleIdx & 7)) & 1)) {
791
+ continue;
792
+ }
793
+
794
+ const v = samples[sampleIdx];
795
+ if (v < ext.min) {
796
+ ext.min = v;
797
+ }
798
+
799
+ if (v > ext.max) {
800
+ ext.max = v;
801
+ }
802
+ }
803
+ }
804
+
805
+ // Empty-data fallback: an untouched extent still sits at its
806
+ // sentinel state. `includeZero=true` initializes to `{0, 0}`;
807
+ // `includeZero=false` initializes to `{Infinity, -Infinity}`.
808
+ // Either way, collapse to `{0, 1}` so axis rendering has a finite
809
+ // domain to work with.
810
+ const leftEmpty =
811
+ !isFinite(leftExtent.min) ||
812
+ !isFinite(leftExtent.max) ||
813
+ (leftExtent.min === 0 && leftExtent.max === 0);
814
+ if (leftEmpty) {
815
+ leftExtent.min = 0;
816
+ leftExtent.max = 1;
817
+ }
818
+
819
+ const rightEmpty =
820
+ !isFinite(rightExtent.min) ||
821
+ !isFinite(rightExtent.max) ||
822
+ (rightExtent.min === 0 && rightExtent.max === 0);
823
+ const rightDomain: { min: number; max: number } | null = hasRightAxis
824
+ ? rightEmpty
825
+ ? { min: 0, max: 1 }
826
+ : rightExtent
827
+ : null;
828
+
829
+ return {
830
+ aggregates,
831
+ splitPrefixes,
832
+ rowPaths,
833
+ numCategories,
834
+ rowOffset,
835
+ axisMode,
836
+ numericCategoryDomain,
837
+ categoryPositions,
838
+ series,
839
+ bars,
840
+ posStack,
841
+ negStack,
842
+ samples,
843
+ sampleValid,
844
+ leftDomain: leftExtent,
845
+ rightDomain,
846
+ hasRightAxis,
847
+ };
848
+ }