@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,256 @@
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 { CandlestickChart } from "./candlestick";
14
+ import { renderCandlestickChromeOverlay } from "./candlestick-render";
15
+
16
+ /**
17
+ * Pixels of horizontal slack around the wick centerline so narrow
18
+ * bodies with tall wicks stay clickable.
19
+ */
20
+ const WICK_TOLERANCE_PX = 3;
21
+
22
+ /**
23
+ * Find the leftmost candle index whose `xCenter` is `>= target`. Bars
24
+ * are appended in (split, cat) order; within a split `xCenter` is
25
+ * monotonically increasing, but across splits it interleaves at the
26
+ * same catIdx. The hit-test still only needs the first candidate at or
27
+ * after `target` — subsequent split records share the same catIdx and
28
+ * are visited until xCenter exceeds `target + halfWidth`, so a plain
29
+ * binary search on `xCenter` ordered as written suffices when
30
+ * splits=1; for multi-split we fall back to a linear scan from the
31
+ * lower bound found.
32
+ */
33
+ function lowerBoundXCenter(
34
+ xC: Float64Array,
35
+ count: number,
36
+ target: number,
37
+ ): number {
38
+ let lo = 0;
39
+ let hi = count;
40
+ while (lo < hi) {
41
+ const mid = (lo + hi) >>> 1;
42
+ if (xC[mid] < target) {
43
+ lo = mid + 1;
44
+ } else {
45
+ hi = mid;
46
+ }
47
+ }
48
+
49
+ return lo;
50
+ }
51
+
52
+ export function handleCandlestickHover(
53
+ chart: CandlestickChart,
54
+ mx: number,
55
+ my: number,
56
+ ): void {
57
+ if (chart._pinnedIdx !== -1) {
58
+ return;
59
+ }
60
+
61
+ const layout = chart._lastLayout;
62
+ if (!layout) {
63
+ return;
64
+ }
65
+
66
+ const candles = chart._candles;
67
+ if (candles.count === 0) {
68
+ if (chart._hoveredIdx !== -1) {
69
+ chart._hoveredIdx = -1;
70
+ renderCandlestickChromeOverlay(chart);
71
+ }
72
+
73
+ return;
74
+ }
75
+
76
+ // Convert mouse → data once; from then on hit-tests are in data
77
+ // space, eliminating ~5 `dataToPixel` calls per candidate that the
78
+ // legacy implementation performed.
79
+ const plot = layout.plotRect;
80
+ const padXMin = layout.paddedXMin;
81
+ const padXMax = layout.paddedXMax;
82
+ const padYMin = layout.paddedYMin;
83
+ const padYMax = layout.paddedYMax;
84
+ if (
85
+ mx < plot.x ||
86
+ mx > plot.x + plot.width ||
87
+ my < plot.y ||
88
+ my > plot.y + plot.height
89
+ ) {
90
+ if (chart._hoveredIdx !== -1) {
91
+ chart._hoveredIdx = -1;
92
+ renderCandlestickChromeOverlay(chart);
93
+ }
94
+
95
+ return;
96
+ }
97
+
98
+ const dataX = padXMin + ((mx - plot.x) / plot.width) * (padXMax - padXMin);
99
+ const dataY = padYMax - ((my - plot.y) / plot.height) * (padYMax - padYMin);
100
+ const pxPerDataX = plot.width / (padXMax - padXMin);
101
+ const wickToleranceData = WICK_TOLERANCE_PX / pxPerDataX;
102
+
103
+ const xC = candles.xCenter;
104
+ const hw = candles.halfWidth;
105
+ const open = candles.open;
106
+ const close = candles.close;
107
+ const high = candles.high;
108
+ const low = candles.low;
109
+
110
+ // Estimate a generous halfWidth bound so the binary-search visible
111
+ // slice covers any candle whose body could overlap `dataX`. The
112
+ // halfWidth is uniform per build; conservatively read from the
113
+ // first record (or fall back to a small constant).
114
+ const maxHalfWidth = candles.count > 0 ? hw[0] : 0;
115
+ const tol = Math.max(maxHalfWidth, wickToleranceData);
116
+
117
+ // Binary-search to a small slice [lo, hi) covering candidates whose
118
+ // xCenter falls within ±tol of dataX. Candles outside this window
119
+ // can't possibly be hit; the linear scan that follows is bounded by
120
+ // (split count × overlap), not the full candle count.
121
+ const lo = lowerBoundXCenter(xC, candles.count, dataX - tol);
122
+ const hi = lowerBoundXCenter(xC, candles.count, dataX + tol + 1e-12);
123
+
124
+ // Walk the slice in reverse so the most-recently-added (frontmost)
125
+ // candle wins ties — matches legacy behavior.
126
+ let hit = -1;
127
+ for (let i = hi - 1; i >= lo; i--) {
128
+ const xc = xC[i];
129
+ const halfW = hw[i];
130
+ const xWithinBody = dataX >= xc - halfW && dataX <= xc + halfW;
131
+ const xWithinWick = Math.abs(dataX - xc) <= wickToleranceData;
132
+ if (!xWithinBody && !xWithinWick) {
133
+ continue;
134
+ }
135
+
136
+ const o = open[i];
137
+ const c = close[i];
138
+ const bodyLow = o < c ? o : c;
139
+ const bodyHigh = o < c ? c : o;
140
+ const insideBody = xWithinBody && dataY >= bodyLow && dataY <= bodyHigh;
141
+ const insideWick = dataY >= low[i] && dataY <= high[i];
142
+ if (insideBody || insideWick) {
143
+ hit = i;
144
+ break;
145
+ }
146
+ }
147
+
148
+ if (hit !== chart._hoveredIdx) {
149
+ chart._hoveredIdx = hit;
150
+ renderCandlestickChromeOverlay(chart);
151
+ }
152
+ }
153
+
154
+ export function showCandlestickPinnedTooltip(
155
+ chart: CandlestickChart,
156
+ idx: number,
157
+ ): void {
158
+ chart._tooltip.dismiss();
159
+ chart._pinnedIdx = idx;
160
+
161
+ const candles = chart._candles;
162
+ if (idx < 0 || idx >= candles.count || !chart._lastLayout) {
163
+ return;
164
+ }
165
+
166
+ const lines = buildCandlestickTooltipLines(chart, idx);
167
+ if (lines.length === 0) {
168
+ return;
169
+ }
170
+
171
+ const xCenter = candles.xCenter[idx];
172
+ const yMid = (candles.high[idx] + candles.low[idx]) / 2;
173
+ const pos = chart._lastLayout.dataToPixel(xCenter, yMid);
174
+
175
+ // CSS bounds come from the chart's own layout, which is populated
176
+ // by the render path regardless of where the chart runs.
177
+ const cssWidth = chart._lastLayout.cssWidth;
178
+ const cssHeight = chart._lastLayout.cssHeight;
179
+
180
+ chart._tooltip.pin(lines, pos, { cssWidth, cssHeight });
181
+
182
+ // Pinning hides the inline hover tooltip but does not change the
183
+ // WebGL pass — only the chrome overlay needs to redraw.
184
+ chart._hoveredIdx = -1;
185
+ renderCandlestickChromeOverlay(chart);
186
+ }
187
+
188
+ export function dismissCandlestickPinnedTooltip(chart: CandlestickChart): void {
189
+ chart._tooltip.dismiss();
190
+ chart._pinnedIdx = -1;
191
+ }
192
+
193
+ /**
194
+ * Build tooltip lines for candle at index `idx` in the columnar
195
+ * storage. Indexed access avoids materializing a `CandleRecord` POJO
196
+ * on the hot tooltip path.
197
+ */
198
+ export function buildCandlestickTooltipLines(
199
+ chart: CandlestickChart,
200
+ idx: number,
201
+ ): string[] {
202
+ const lines: string[] = [];
203
+ const candles = chart._candles;
204
+ if (idx < 0 || idx >= candles.count) {
205
+ return lines;
206
+ }
207
+
208
+ const catIdx = candles.catIdx[idx];
209
+ const splitIdx = candles.splitIdx[idx];
210
+ const open = candles.open[idx];
211
+ const close = candles.close[idx];
212
+ const high = candles.high[idx];
213
+ const low = candles.low[idx];
214
+
215
+ if (
216
+ chart._categoryAxisMode === "numeric" &&
217
+ chart._numericCategoryDomain &&
218
+ chart._categoryPositions
219
+ ) {
220
+ const v = chart._categoryPositions[catIdx];
221
+ const xColumn = chart._groupBy[0];
222
+ lines.push(chart.getColumnFormatter(xColumn, "value")(v));
223
+ } else if (chart._rowPaths.length > 0) {
224
+ const parts: string[] = [];
225
+ for (const rp of chart._rowPaths) {
226
+ const s = rp.labels[catIdx] ?? "";
227
+ if (s) {
228
+ parts.push(s);
229
+ }
230
+ }
231
+
232
+ if (parts.length > 0) {
233
+ lines.push(parts.join(" › "));
234
+ }
235
+ } else {
236
+ lines.push(`Row ${catIdx + chart._rowOffset}`);
237
+ }
238
+
239
+ if (splitIdx >= 0 && chart._splitPrefixes.length > 1) {
240
+ const prefix = chart._splitPrefixes[splitIdx];
241
+ if (prefix) {
242
+ lines.push(prefix);
243
+ }
244
+ }
245
+
246
+ const openFmt = chart.getColumnFormatter(chart._columnSlots[0], "value");
247
+ const closeFmt = chart.getColumnFormatter(chart._columnSlots[1], "value");
248
+ const highFmt = chart.getColumnFormatter(chart._columnSlots[2], "value");
249
+ const lowFmt = chart.getColumnFormatter(chart._columnSlots[3], "value");
250
+ lines.push(`Open: ${openFmt(open)}`);
251
+ lines.push(`Close: ${closeFmt(close)}`);
252
+ lines.push(`High: ${highFmt(high)}`);
253
+ lines.push(`Low: ${lowFmt(low)}`);
254
+
255
+ return lines;
256
+ }
@@ -0,0 +1,387 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import type { WebGLContextManager } from "../../webgl/context-manager";
14
+ import type { CandlestickChart, CandlestickAutoFitCache } from "./candlestick";
15
+ import { PlotLayout } from "../../layout/plot-layout";
16
+ import { sampleGradient } from "../../theme/gradient";
17
+ import { renderInPlotFrame } from "../../webgl/plot-frame";
18
+ import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
19
+ import { computeNiceTicks } from "../../layout/ticks";
20
+ import { type AxisDomain } from "../../axis/numeric-axis";
21
+ import {
22
+ renderBarAxesChrome,
23
+ renderBarGridlines,
24
+ type BarCategoryAxis,
25
+ } from "../../axis/bar-axis";
26
+ import {
27
+ measureCategoricalAxisHeight,
28
+ type CategoricalDomain,
29
+ } from "../../axis/categorical-axis";
30
+ import { buildCandlestickTooltipLines } from "./candlestick-interact";
31
+ import {
32
+ computeVisibleExtent,
33
+ type VisibleExtent,
34
+ } from "../common/visible-extent";
35
+
36
+ /**
37
+ * Resolve up/down body colors from `theme.gradientStops`. Cached on the
38
+ * chart via `_upDownColorKey` (reference identity of the stops array)
39
+ * — only `restyle()` (which clears the theme cache via
40
+ * `invalidateTheme`) or a data load with a fresh theme triggers
41
+ * resampling. Legacy code re-sampled every frame.
42
+ */
43
+ export function ensureUpDownColors(chart: CandlestickChart): void {
44
+ const theme = chart._resolveTheme();
45
+ const stops = theme.gradientStops;
46
+ if (chart._upDownColorKey === stops) {
47
+ return;
48
+ }
49
+
50
+ const upSample = sampleGradient(stops, 1.0);
51
+ const downSample = sampleGradient(stops, 0.0);
52
+ chart._upColor = [upSample[0], upSample[1], upSample[2]];
53
+ chart._downColor = [downSample[0], downSample[1], downSample[2]];
54
+ chart._upDownColorKey = stops;
55
+ }
56
+
57
+ /**
58
+ * Drop persistent body / wick / OHLC vertex buffers. Subsequent draws
59
+ * no-op until the next {@link rebuildGlyphBuffers} call.
60
+ */
61
+ export function invalidateGlyphBuffers(chart: CandlestickChart): void {
62
+ chart._glyphs.bodyWick.invalidateBuffers(chart);
63
+ chart._glyphs.ohlc.invalidateBuffers(chart);
64
+ }
65
+
66
+ /**
67
+ * Rebuild the persistent body / wick / OHLC vertex buffers. Reads
68
+ * `_candles` (columnar) plus the cached `_upColor` / `_downColor` to
69
+ * populate the GPU buffers exactly once per data load. Subsequent pan/
70
+ * zoom redraws bind + dispatch with no uploads.
71
+ */
72
+ export function rebuildGlyphBuffers(
73
+ chart: CandlestickChart,
74
+ glManager: WebGLContextManager,
75
+ ): void {
76
+ chart._glyphs.bodyWick.rebuildBuffers(chart, glManager);
77
+ chart._glyphs.ohlc.rebuildBuffers(chart, glManager);
78
+ }
79
+
80
+ export function renderCandlestickFrame(
81
+ chart: CandlestickChart,
82
+ glManager: WebGLContextManager,
83
+ ): void {
84
+ const gl = glManager.gl;
85
+ const dpr = glManager.dpr;
86
+ const cssWidth = gl.canvas.width / dpr;
87
+ const cssHeight = gl.canvas.height / dpr;
88
+ if (cssWidth <= 0 || cssHeight <= 0) {
89
+ return;
90
+ }
91
+
92
+ if (chart._numCategories === 0) {
93
+ return;
94
+ }
95
+
96
+ const theme = chart._resolveTheme();
97
+
98
+ // Up/down colors sampled at the extremes of the theme gradient.
99
+ // Cached on the chart — `ensureUpDownColors` is a no-op when the
100
+ // gradient-stops reference matches the previous call. `restyle()`
101
+ // clears the cache via `invalidateTheme`, and the data-load path
102
+ // refreshes it before rebuilding glyph buffers.
103
+ ensureUpDownColors(chart);
104
+
105
+ const numericCat = chart._categoryAxisMode === "numeric";
106
+ const xDomainMin = numericCat ? chart._numericCategoryDomain!.min : -0.5;
107
+ const xDomainMax = numericCat
108
+ ? chart._numericCategoryDomain!.max
109
+ : chart._numCategories - 0.5;
110
+ if (chart._zoomController) {
111
+ chart._zoomController.setBaseDomain(
112
+ xDomainMin,
113
+ xDomainMax,
114
+ chart._yDomain.min,
115
+ chart._yDomain.max,
116
+ );
117
+ }
118
+
119
+ const vis = chart._zoomController
120
+ ? chart._zoomController.getVisibleDomain()
121
+ : {
122
+ xMin: xDomainMin,
123
+ xMax: xDomainMax,
124
+ yMin: chart._yDomain.min,
125
+ yMax: chart._yDomain.max,
126
+ };
127
+
128
+ // Auto-fit the price axis to the visible X window. Skipped at
129
+ // default zoom (the refit equals `_yDomain` there and would only
130
+ // churn baselines).
131
+ if (
132
+ chart._autoFitValue &&
133
+ chart._zoomController &&
134
+ !chart._zoomController.isDefault()
135
+ ) {
136
+ const fit = computeVisibleCandleExtent(chart, vis.xMin, vis.xMax);
137
+ if (fit.hasFit) {
138
+ vis.yMin = fit.min;
139
+ vis.yMax = fit.max;
140
+ }
141
+ }
142
+
143
+ const hasXLabel = chart._groupBy.length > 0;
144
+
145
+ const provisionalDomain: CategoricalDomain = {
146
+ levels: chart._rowPaths,
147
+ numRows: chart._numCategories,
148
+ levelLabels: chart._groupBy.slice(),
149
+ };
150
+
151
+ let layout: PlotLayout;
152
+ if (numericCat) {
153
+ layout = new PlotLayout(cssWidth, cssHeight, {
154
+ hasXLabel,
155
+ hasYLabel: true,
156
+ hasLegend: false,
157
+ bottomExtra: 24,
158
+ });
159
+ } else {
160
+ const estLeft = 55 + 16;
161
+ const estRight = 16;
162
+ const estPlotWidth = Math.max(1, cssWidth - estLeft - estRight);
163
+ const bottomExtra = measureCategoricalAxisHeight(
164
+ provisionalDomain,
165
+ estPlotWidth,
166
+ );
167
+ layout = new PlotLayout(cssWidth, cssHeight, {
168
+ hasXLabel,
169
+ hasYLabel: true,
170
+ hasLegend: false,
171
+ bottomExtra,
172
+ });
173
+ }
174
+
175
+ chart._lastLayout = layout;
176
+ if (chart._zoomController) {
177
+ chart._zoomController.updateLayout(layout);
178
+ }
179
+
180
+ const projection = layout.buildProjectionMatrix(
181
+ vis.xMin,
182
+ vis.xMax,
183
+ vis.yMin,
184
+ vis.yMax,
185
+ "y",
186
+ undefined,
187
+ undefined,
188
+ chart._categoryOrigin,
189
+ 0,
190
+ );
191
+
192
+ const yTicks = computeNiceTicks(vis.yMin, vis.yMax, 6);
193
+ const yLabel = chart._columnSlots[0] || "";
194
+
195
+ const xDomain: CategoricalDomain = provisionalDomain;
196
+ const yDomain: AxisDomain = {
197
+ min: vis.yMin,
198
+ max: vis.yMax,
199
+ label: yLabel,
200
+ };
201
+
202
+ if (chart._gridlineCanvas) {
203
+ renderBarGridlines(
204
+ chart._gridlineCanvas,
205
+ layout,
206
+ yTicks,
207
+ theme,
208
+ glManager.dpr,
209
+ );
210
+ }
211
+
212
+ renderInPlotFrame(gl, layout, glManager.dpr, () => {
213
+ if (chart._defaultChartType === "ohlc") {
214
+ chart._glyphs.ohlc.draw(chart, gl, glManager, projection);
215
+ } else {
216
+ chart._glyphs.bodyWick.draw(chart, gl, glManager, projection);
217
+ }
218
+ });
219
+
220
+ chart._lastXDomain = xDomain;
221
+ chart._lastYDomain = yDomain;
222
+ chart._lastYTicks = yTicks;
223
+ chart._lastCatTicks = numericCat
224
+ ? computeNiceTicks(vis.xMin, vis.xMax, 6)
225
+ : null;
226
+ renderCandlestickChromeOverlay(chart);
227
+ }
228
+
229
+ export function renderCandlestickChromeOverlay(chart: CandlestickChart): void {
230
+ if (
231
+ !chart._chromeCanvas ||
232
+ !chart._lastLayout ||
233
+ !chart._lastYDomain ||
234
+ !chart._lastYTicks
235
+ ) {
236
+ return;
237
+ }
238
+
239
+ const theme = chart._resolveTheme();
240
+ let catAxis: BarCategoryAxis;
241
+ if (
242
+ chart._categoryAxisMode === "numeric" &&
243
+ chart._numericCategoryDomain &&
244
+ chart._lastCatTicks
245
+ ) {
246
+ catAxis = {
247
+ mode: "numeric",
248
+ domain: {
249
+ min: chart._numericCategoryDomain.min,
250
+ max: chart._numericCategoryDomain.max,
251
+ isDate: chart._numericCategoryDomain.isDate,
252
+ label: chart._numericCategoryDomain.label,
253
+ },
254
+ ticks: chart._lastCatTicks,
255
+ };
256
+ } else if (chart._lastXDomain) {
257
+ catAxis = { mode: "category", domain: chart._lastXDomain };
258
+ } else {
259
+ return;
260
+ }
261
+
262
+ // OHLC value axis: all four price columns share the value axis;
263
+ // pick the first available (Open is always present, the rest can
264
+ // be null per `candlestick-build.ts`).
265
+ const valueColumn =
266
+ chart._columnSlots[0] ??
267
+ chart._columnSlots[1] ??
268
+ chart._columnSlots[2] ??
269
+ chart._columnSlots[3];
270
+ const xColumn = chart._groupBy[0];
271
+ renderBarAxesChrome(
272
+ chart._chromeCanvas,
273
+ catAxis,
274
+ chart._lastYDomain,
275
+ chart._lastYTicks,
276
+ chart._lastLayout,
277
+ theme,
278
+ chart._glManager?.dpr ?? 1,
279
+ undefined,
280
+ undefined,
281
+ false,
282
+ {
283
+ value: chart.getColumnFormatter(valueColumn, "tick"),
284
+ category: chart.getColumnFormatter(xColumn, "tick"),
285
+ },
286
+ );
287
+
288
+ if (chart._hoveredIdx >= 0 && chart._hoveredIdx < chart._candles.count) {
289
+ renderCandlestickTooltip(chart);
290
+ }
291
+ }
292
+
293
+ function renderCandlestickTooltip(chart: CandlestickChart): void {
294
+ if (!chart._chromeCanvas || !chart._lastLayout) {
295
+ return;
296
+ }
297
+
298
+ const i = chart._hoveredIdx;
299
+ const candles = chart._candles;
300
+ if (i < 0 || i >= candles.count) {
301
+ return;
302
+ }
303
+
304
+ const layout = chart._lastLayout;
305
+ const xCenter = candles.xCenter[i];
306
+ const yMid = (candles.high[i] + candles.low[i]) / 2;
307
+ const pos = layout.dataToPixel(xCenter, yMid);
308
+ const lines = buildCandlestickTooltipLines(chart, i);
309
+ const theme = chart._resolveTheme();
310
+ renderCanvasTooltip(
311
+ chart._chromeCanvas,
312
+ pos,
313
+ lines,
314
+ layout,
315
+ theme,
316
+ chart._glManager?.dpr ?? 1,
317
+ {
318
+ crosshair: false,
319
+ highlightRadius: 0,
320
+ },
321
+ );
322
+ }
323
+
324
+ /**
325
+ * Price extent over candles whose `xCenter` falls inside
326
+ * `[visXMin, visXMax]`. Uses `low`/`high` (not `open`/`close`) so the
327
+ * wick stays inside the plot at any zoom. Cached on
328
+ * `chart._autoFitCache`; hover-only redraws hit the cache.
329
+ *
330
+ * Cache lifetime: reset on data upload ([candlestick.ts]
331
+ * `uploadAndRender`).
332
+ */
333
+ function computeVisibleCandleExtent(
334
+ chart: CandlestickChart,
335
+ visXMin: number,
336
+ visXMax: number,
337
+ ): VisibleExtent {
338
+ const cache = chart._autoFitCache;
339
+ if (cache && cache.xMin === visXMin && cache.xMax === visXMax) {
340
+ return cache;
341
+ }
342
+
343
+ const next = cache ?? newCandlestickAutoFitCache();
344
+ next.xMin = visXMin;
345
+ next.xMax = visXMax;
346
+
347
+ // Walk the columnar storage directly; the legacy form built a
348
+ // closure adapter per call, defeating monomorphism in
349
+ // `computeVisibleExtent`.
350
+ const candles = chart._candles;
351
+ let lo = Infinity;
352
+ let hi = -Infinity;
353
+ let hasFit = false;
354
+ const xC = candles.xCenter;
355
+ const lows = candles.low;
356
+ const highs = candles.high;
357
+ for (let j = 0; j < candles.count; j++) {
358
+ const cx = xC[j];
359
+ if (cx < visXMin || cx > visXMax) {
360
+ continue;
361
+ }
362
+
363
+ if (lows[j] < lo) {
364
+ lo = lows[j];
365
+ }
366
+
367
+ if (highs[j] > hi) {
368
+ hi = highs[j];
369
+ }
370
+
371
+ hasFit = true;
372
+ }
373
+
374
+ next.min = hasFit ? lo : 0;
375
+ next.max = hasFit ? hi : 1;
376
+ next.hasFit = hasFit;
377
+ chart._autoFitCache = next;
378
+
379
+ // Reference suppression — `computeVisibleExtent` retained for the
380
+ // shared common helper but no longer used in this fast path.
381
+ void computeVisibleExtent;
382
+ return next;
383
+ }
384
+
385
+ function newCandlestickAutoFitCache(): CandlestickAutoFitCache {
386
+ return { xMin: 0, xMax: 0, min: 0, max: 1, hasFit: false };
387
+ }