@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,468 @@
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 { PlotLayout } from "../layout/plot-layout";
14
+
15
+ export interface ZoomState {
16
+ scaleX: number;
17
+ scaleY: number;
18
+
19
+ // Translate as fraction of base domain range (0 = centered, ±0.5 = edge)
20
+ normTranslateX: number;
21
+ normTranslateY: number;
22
+ }
23
+
24
+ /**
25
+ * Runtime config for `ZoomController`. Not part of `ZoomState` — wired
26
+ * by the owning chart at construction, not serialized with zoom level.
27
+ *
28
+ * `lockAxis` pins one axis at `scale=1, translate=0` and suppresses
29
+ * wheel / pan updates on that axis, leaving the other axis fully
30
+ * zoomable. Categorical axes are the typical candidates.
31
+ *
32
+ * `lockAspect` keeps `dataPerPixel` equal on both axes by padding the
33
+ * narrower axis of the rendered domain to match the plot rect's aspect
34
+ * ratio. Required by map plugins (Mercator preserves local angle, so
35
+ * map glyphs distort under independent X/Y zoom), and useful for any
36
+ * cartesian view where stretching points along one axis would lie
37
+ * about the data.
38
+ */
39
+ export interface ZoomConfig {
40
+ lockAxis?: "x" | "y" | null;
41
+ lockAspect?: boolean;
42
+ }
43
+
44
+ export const MAX_ZOOM = 100_000;
45
+ export const MIN_ZOOM = 1;
46
+
47
+ /**
48
+ * Pad the narrower axis of `domain` so its aspect ratio matches
49
+ * `plotRect`. Preserves the center of each axis and the longer axis's
50
+ * extent — only the shorter axis grows. Returns the input unmodified
51
+ * if either dimension is non-positive.
52
+ *
53
+ * Used by `lockAspect` mode to keep `dataPerPixel` equal on both axes,
54
+ * which is what map plugins (Mercator) and any "square pixel" view
55
+ * require.
56
+ */
57
+ function applyAspectLock(
58
+ domain: { xMin: number; xMax: number; yMin: number; yMax: number },
59
+ plotRect: { width: number; height: number },
60
+ ): { xMin: number; xMax: number; yMin: number; yMax: number } {
61
+ if (plotRect.width <= 0 || plotRect.height <= 0) {
62
+ return domain;
63
+ }
64
+
65
+ const xRange = domain.xMax - domain.xMin;
66
+ const yRange = domain.yMax - domain.yMin;
67
+ if (xRange <= 0 || yRange <= 0) {
68
+ return domain;
69
+ }
70
+
71
+ const plotAspect = plotRect.width / plotRect.height;
72
+ const dataAspect = xRange / yRange;
73
+
74
+ if (dataAspect < plotAspect) {
75
+ const cx = (domain.xMin + domain.xMax) / 2;
76
+ const newX = (yRange * plotAspect) / 2;
77
+ return {
78
+ xMin: cx - newX,
79
+ xMax: cx + newX,
80
+ yMin: domain.yMin,
81
+ yMax: domain.yMax,
82
+ };
83
+ } else {
84
+ const cy = (domain.yMin + domain.yMax) / 2;
85
+ const newY = xRange / plotAspect / 2;
86
+ return {
87
+ xMin: domain.xMin,
88
+ xMax: domain.xMax,
89
+ yMin: cy - newY,
90
+ yMax: cy + newY,
91
+ };
92
+ }
93
+ }
94
+
95
+ export class ZoomController {
96
+ private _scaleX = 1;
97
+ private _scaleY = 1;
98
+
99
+ // Normalized translate: fraction of base domain range
100
+ private _normTX = 0;
101
+ private _normTY = 0;
102
+
103
+ private _baseXMin = 0;
104
+ private _baseXMax = 1;
105
+ private _baseYMin = 0;
106
+ private _baseYMax = 1;
107
+
108
+ private _lockAxis: "x" | "y" | null = null;
109
+ private _lockAspect = false;
110
+
111
+ private _element: HTMLElement | null = null;
112
+ private _layout: PlotLayout | null = null;
113
+ private _onUpdate: (() => void) | null = null;
114
+
115
+ private _pointerDown = false;
116
+ private _lastPointerX = 0;
117
+ private _lastPointerY = 0;
118
+
119
+ private _onWheel: ((e: WheelEvent) => void) | null = null;
120
+ private _onPointerDown: ((e: PointerEvent) => void) | null = null;
121
+ private _onPointerMove: ((e: PointerEvent) => void) | null = null;
122
+ private _onPointerUp: ((e: PointerEvent) => void) | null = null;
123
+
124
+ // Per-controller mutators used by `ZoomRouter` to apply wheel/pan
125
+ // events without going through `attach`. Live below under "Router
126
+ // helpers" for the facet-aware zoom path.
127
+ get lockedAxis(): "x" | "y" | null {
128
+ return this._lockAxis;
129
+ }
130
+ get scaleX(): number {
131
+ return this._scaleX;
132
+ }
133
+ get scaleY(): number {
134
+ return this._scaleY;
135
+ }
136
+ set scaleX(v: number) {
137
+ this._scaleX = v;
138
+ }
139
+ set scaleY(v: number) {
140
+ this._scaleY = v;
141
+ }
142
+ get normTranslateX(): number {
143
+ return this._normTX;
144
+ }
145
+ get normTranslateY(): number {
146
+ return this._normTY;
147
+ }
148
+ set normTranslateX(v: number) {
149
+ this._normTX = v;
150
+ }
151
+ set normTranslateY(v: number) {
152
+ this._normTY = v;
153
+ }
154
+ get baseXRange(): number {
155
+ return this._baseXMax - this._baseXMin;
156
+ }
157
+ get baseYRange(): number {
158
+ return this._baseYMax - this._baseYMin;
159
+ }
160
+
161
+ /**
162
+ * Update the base (full-data) domain that this controller's
163
+ * normalized translate is interpreted against, while preserving
164
+ * the *absolute* center of any user-applied pan.
165
+ *
166
+ * `_normTX` / `_normTY` are stored as fractions of the base
167
+ * range, not absolute coordinates. A naive "swap base, keep
168
+ * normTranslate" update reinterprets the same fraction against a
169
+ * new range, so when an external `draw()` updates the extent
170
+ * (via `processCartesianChunk` → `setZoomBaseDomain`) the user's
171
+ * pan-offset visible center jumps to a different absolute
172
+ * position. With concurrent pan events feeding in offsets that
173
+ * were computed against the old base, the jump can project the
174
+ * visible center past the data entirely, leaving `_fullRender`
175
+ * to draw zero glyphs onto a freshly-cleared canvas — a blank
176
+ * bitmap reaches the host as a flicker.
177
+ *
178
+ * When the user is in default state (no pan, no zoom — fresh
179
+ * controller, or just-reset), no rebase is needed; just swap the
180
+ * base and let the chart auto-fit to the new data. Otherwise
181
+ * recompute `_normTX` / `_normTY` so the visible center stays at
182
+ * the same absolute (data-coordinate) position before and after
183
+ * the swap.
184
+ */
185
+ setBaseDomain(
186
+ xMin: number,
187
+ xMax: number,
188
+ yMin: number,
189
+ yMax: number,
190
+ ): void {
191
+ if (this.isDefault()) {
192
+ this._baseXMin = xMin;
193
+ this._baseXMax = xMax;
194
+ this._baseYMin = yMin;
195
+ this._baseYMax = yMax;
196
+ return;
197
+ }
198
+
199
+ const oldRangeX = this._baseXMax - this._baseXMin;
200
+ const oldRangeY = this._baseYMax - this._baseYMin;
201
+ const oldCx =
202
+ (this._baseXMin + this._baseXMax) / 2 + this._normTX * oldRangeX;
203
+ const oldCy =
204
+ (this._baseYMin + this._baseYMax) / 2 + this._normTY * oldRangeY;
205
+
206
+ this._baseXMin = xMin;
207
+ this._baseXMax = xMax;
208
+ this._baseYMin = yMin;
209
+ this._baseYMax = yMax;
210
+
211
+ const newRangeX = xMax - xMin;
212
+ const newRangeY = yMax - yMin;
213
+ this._normTX =
214
+ newRangeX > 0 ? (oldCx - (xMin + xMax) / 2) / newRangeX : 0;
215
+ this._normTY =
216
+ newRangeY > 0 ? (oldCy - (yMin + yMax) / 2) / newRangeY : 0;
217
+ }
218
+
219
+ /**
220
+ * Apply config. Called once by the chart during `setZoomController`.
221
+ * Locking an axis snaps its `scale`/`translate` to identity so any
222
+ * pre-existing state on that axis is cleared; subsequent wheel /
223
+ * pan events leave the locked axis alone.
224
+ */
225
+ configure(config: ZoomConfig): void {
226
+ this._lockAxis = config.lockAxis ?? null;
227
+ this._lockAspect = config.lockAspect ?? false;
228
+ if (this._lockAxis === "x") {
229
+ this._scaleX = 1;
230
+ this._normTX = 0;
231
+ } else if (this._lockAxis === "y") {
232
+ this._scaleY = 1;
233
+ this._normTY = 0;
234
+ }
235
+ }
236
+
237
+ isDefault(): boolean {
238
+ return (
239
+ this._scaleX === 1 &&
240
+ this._scaleY === 1 &&
241
+ this._normTX === 0 &&
242
+ this._normTY === 0
243
+ );
244
+ }
245
+
246
+ getVisibleDomain(): {
247
+ xMin: number;
248
+ xMax: number;
249
+ yMin: number;
250
+ yMax: number;
251
+ } {
252
+ const bxRange = this._baseXMax - this._baseXMin;
253
+ const byRange = this._baseYMax - this._baseYMin;
254
+ const vxRange = bxRange / this._scaleX;
255
+ const vyRange = byRange / this._scaleY;
256
+
257
+ // Center = base midpoint + normalized translate * base range
258
+ const cx =
259
+ (this._baseXMin + this._baseXMax) / 2 + this._normTX * bxRange;
260
+ const cy =
261
+ (this._baseYMin + this._baseYMax) / 2 + this._normTY * byRange;
262
+
263
+ const domain = {
264
+ xMin: cx - vxRange / 2,
265
+ xMax: cx + vxRange / 2,
266
+ yMin: cy - vyRange / 2,
267
+ yMax: cy + vyRange / 2,
268
+ };
269
+
270
+ if (this._lockAspect && this._layout) {
271
+ return applyAspectLock(domain, this._layout.plotRect);
272
+ }
273
+
274
+ return domain;
275
+ }
276
+
277
+ attach(
278
+ element: HTMLElement,
279
+ layout: PlotLayout,
280
+ onUpdate: () => void,
281
+ ): void {
282
+ this.detach();
283
+ this._element = element;
284
+ this._layout = layout;
285
+ this._onUpdate = onUpdate;
286
+
287
+ this._onWheel = (e: WheelEvent) => {
288
+ e.preventDefault();
289
+ const rect = element.getBoundingClientRect();
290
+ const mouseX = e.clientX - rect.left;
291
+ const mouseY = e.clientY - rect.top;
292
+ const plot = this._layout!.plotRect;
293
+
294
+ if (
295
+ mouseX < plot.x ||
296
+ mouseX > plot.x + plot.width ||
297
+ mouseY < plot.y ||
298
+ mouseY > plot.y + plot.height
299
+ ) {
300
+ return;
301
+ }
302
+
303
+ // Data coordinate under cursor before zoom
304
+ const domain = this.getVisibleDomain();
305
+ const dataX =
306
+ domain.xMin +
307
+ ((mouseX - plot.x) / plot.width) * (domain.xMax - domain.xMin);
308
+ const dataY =
309
+ domain.yMax -
310
+ ((mouseY - plot.y) / plot.height) * (domain.yMax - domain.yMin);
311
+
312
+ // Zoom factor — skip the locked axis so its scale stays
313
+ // pinned at 1.
314
+ const factor = Math.pow(1.1, -e.deltaY / 100);
315
+ if (this._lockAxis !== "x") {
316
+ this._scaleX = Math.max(
317
+ MIN_ZOOM,
318
+ Math.min(MAX_ZOOM, this._scaleX * factor),
319
+ );
320
+ }
321
+
322
+ if (this._lockAxis !== "y") {
323
+ this._scaleY = Math.max(
324
+ MIN_ZOOM,
325
+ Math.min(MAX_ZOOM, this._scaleY * factor),
326
+ );
327
+ }
328
+
329
+ // Adjust translate so the data point under cursor stays put
330
+ const newDomain = this.getVisibleDomain();
331
+ const newDataX =
332
+ newDomain.xMin +
333
+ ((mouseX - plot.x) / plot.width) *
334
+ (newDomain.xMax - newDomain.xMin);
335
+ const newDataY =
336
+ newDomain.yMax -
337
+ ((mouseY - plot.y) / plot.height) *
338
+ (newDomain.yMax - newDomain.yMin);
339
+
340
+ const bxRange = this._baseXMax - this._baseXMin;
341
+ const byRange = this._baseYMax - this._baseYMin;
342
+ if (this._lockAxis !== "x" && bxRange > 0) {
343
+ this._normTX += (dataX - newDataX) / bxRange;
344
+ }
345
+
346
+ if (this._lockAxis !== "y" && byRange > 0) {
347
+ this._normTY += (dataY - newDataY) / byRange;
348
+ }
349
+
350
+ this._onUpdate!();
351
+ };
352
+
353
+ this._onPointerDown = (e: PointerEvent) => {
354
+ const rect = element.getBoundingClientRect();
355
+ const mouseX = e.clientX - rect.left;
356
+ const mouseY = e.clientY - rect.top;
357
+ const plot = this._layout!.plotRect;
358
+
359
+ if (
360
+ mouseX >= plot.x &&
361
+ mouseX <= plot.x + plot.width &&
362
+ mouseY >= plot.y &&
363
+ mouseY <= plot.y + plot.height
364
+ ) {
365
+ this._pointerDown = true;
366
+ this._lastPointerX = e.clientX;
367
+ this._lastPointerY = e.clientY;
368
+ element.setPointerCapture(e.pointerId);
369
+ }
370
+ };
371
+
372
+ this._onPointerMove = (e: PointerEvent) => {
373
+ if (!this._pointerDown) {
374
+ return;
375
+ }
376
+
377
+ const dx = e.clientX - this._lastPointerX;
378
+ const dy = e.clientY - this._lastPointerY;
379
+ this._lastPointerX = e.clientX;
380
+ this._lastPointerY = e.clientY;
381
+
382
+ const domain = this.getVisibleDomain();
383
+ const plot = this._layout!.plotRect;
384
+ const dataPerPixelX = (domain.xMax - domain.xMin) / plot.width;
385
+ const dataPerPixelY = (domain.yMax - domain.yMin) / plot.height;
386
+
387
+ const bxRange = this._baseXMax - this._baseXMin;
388
+ const byRange = this._baseYMax - this._baseYMin;
389
+ if (this._lockAxis !== "x" && bxRange > 0) {
390
+ this._normTX -= (dx * dataPerPixelX) / bxRange;
391
+ }
392
+
393
+ if (this._lockAxis !== "y" && byRange > 0) {
394
+ this._normTY += (dy * dataPerPixelY) / byRange;
395
+ }
396
+
397
+ this._onUpdate!();
398
+ };
399
+
400
+ this._onPointerUp = () => {
401
+ this._pointerDown = false;
402
+ };
403
+
404
+ element.addEventListener("wheel", this._onWheel, { passive: false });
405
+ element.addEventListener("pointerdown", this._onPointerDown);
406
+ element.addEventListener("pointermove", this._onPointerMove);
407
+ element.addEventListener("pointerup", this._onPointerUp);
408
+ }
409
+
410
+ updateLayout(layout: PlotLayout): void {
411
+ this._layout = layout;
412
+ }
413
+
414
+ detach(): void {
415
+ if (this._element) {
416
+ if (this._onWheel) {
417
+ this._element.removeEventListener("wheel", this._onWheel);
418
+ }
419
+
420
+ if (this._onPointerDown) {
421
+ this._element.removeEventListener(
422
+ "pointerdown",
423
+ this._onPointerDown,
424
+ );
425
+ }
426
+
427
+ if (this._onPointerMove) {
428
+ this._element.removeEventListener(
429
+ "pointermove",
430
+ this._onPointerMove,
431
+ );
432
+ }
433
+
434
+ if (this._onPointerUp) {
435
+ this._element.removeEventListener(
436
+ "pointerup",
437
+ this._onPointerUp,
438
+ );
439
+ }
440
+ }
441
+
442
+ this._element = null;
443
+ this._onUpdate = null;
444
+ }
445
+
446
+ reset(): void {
447
+ this._scaleX = 1;
448
+ this._scaleY = 1;
449
+ this._normTX = 0;
450
+ this._normTY = 0;
451
+ }
452
+
453
+ serialize(): ZoomState {
454
+ return {
455
+ scaleX: this._scaleX,
456
+ scaleY: this._scaleY,
457
+ normTranslateX: this._normTX,
458
+ normTranslateY: this._normTY,
459
+ };
460
+ }
461
+
462
+ restore(state: ZoomState): void {
463
+ this._scaleX = state.scaleX;
464
+ this._scaleY = state.scaleY;
465
+ this._normTX = state.normTranslateX;
466
+ this._normTY = state.normTranslateY;
467
+ }
468
+ }
@@ -0,0 +1,230 @@
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 { PlotLayout } from "../layout/plot-layout";
14
+ import { MAX_ZOOM, MIN_ZOOM, type ZoomController } from "./zoom-controller";
15
+
16
+ /**
17
+ * Resolver that maps a cursor position to `{ controller, layout }` —
18
+ * returns `null` when the cursor is not inside any facet. In
19
+ * independent-zoom mode the facet under the cursor owns its events; in
20
+ * shared-zoom mode the resolver always returns the same controller for
21
+ * every plot rect in the grid.
22
+ */
23
+ export interface ZoomTarget {
24
+ controller: ZoomController;
25
+ layout: PlotLayout;
26
+ }
27
+ export type ZoomTargetResolver = (mx: number, my: number) => ZoomTarget | null;
28
+
29
+ /**
30
+ * One set of wheel / pointer listeners on the GL canvas that dispatches
31
+ * zoom + pan events to a {@link ZoomController} resolved from the
32
+ * cursor position. Replaces `ZoomController.attach` so multiple
33
+ * controllers (one per facet) can coexist on a single canvas.
34
+ */
35
+ export class ZoomRouter {
36
+ private _element: HTMLElement | null = null;
37
+ private _resolve: ZoomTargetResolver | null = null;
38
+ private _onUpdate: (() => void) | null = null;
39
+
40
+ private _pointerDown = false;
41
+ private _pointerTarget: ZoomTarget | null = null;
42
+ private _lastPointerX = 0;
43
+ private _lastPointerY = 0;
44
+
45
+ private _onWheel: ((e: WheelEvent) => void) | null = null;
46
+ private _onPointerDown: ((e: PointerEvent) => void) | null = null;
47
+ private _onPointerMove: ((e: PointerEvent) => void) | null = null;
48
+ private _onPointerUp: ((e: PointerEvent) => void) | null = null;
49
+
50
+ attach(
51
+ element: HTMLElement,
52
+ resolve: ZoomTargetResolver,
53
+ onUpdate: () => void,
54
+ ): void {
55
+ this.detach();
56
+ this._element = element;
57
+ this._resolve = resolve;
58
+ this._onUpdate = onUpdate;
59
+
60
+ this._onWheel = (e: WheelEvent) => {
61
+ const rect = element.getBoundingClientRect();
62
+ const mouseX = e.clientX - rect.left;
63
+ const mouseY = e.clientY - rect.top;
64
+ const target = resolve(mouseX, mouseY);
65
+ if (!target) {
66
+ return;
67
+ }
68
+
69
+ e.preventDefault();
70
+ applyWheel(target, mouseX, mouseY, e.deltaY);
71
+ onUpdate();
72
+ };
73
+
74
+ this._onPointerDown = (e: PointerEvent) => {
75
+ const rect = element.getBoundingClientRect();
76
+ const mouseX = e.clientX - rect.left;
77
+ const mouseY = e.clientY - rect.top;
78
+ const target = resolve(mouseX, mouseY);
79
+ if (!target) {
80
+ return;
81
+ }
82
+
83
+ this._pointerDown = true;
84
+ this._pointerTarget = target;
85
+ this._lastPointerX = e.clientX;
86
+ this._lastPointerY = e.clientY;
87
+ element.setPointerCapture(e.pointerId);
88
+ };
89
+
90
+ this._onPointerMove = (e: PointerEvent) => {
91
+ if (!this._pointerDown || !this._pointerTarget) {
92
+ return;
93
+ }
94
+
95
+ const dx = e.clientX - this._lastPointerX;
96
+ const dy = e.clientY - this._lastPointerY;
97
+ this._lastPointerX = e.clientX;
98
+ this._lastPointerY = e.clientY;
99
+ applyPan(this._pointerTarget, dx, dy);
100
+ onUpdate();
101
+ };
102
+
103
+ this._onPointerUp = () => {
104
+ this._pointerDown = false;
105
+ this._pointerTarget = null;
106
+ };
107
+
108
+ element.addEventListener("wheel", this._onWheel, { passive: false });
109
+ element.addEventListener("pointerdown", this._onPointerDown);
110
+ element.addEventListener("pointermove", this._onPointerMove);
111
+ element.addEventListener("pointerup", this._onPointerUp);
112
+ }
113
+
114
+ detach(): void {
115
+ if (this._element) {
116
+ if (this._onWheel) {
117
+ this._element.removeEventListener("wheel", this._onWheel);
118
+ }
119
+
120
+ if (this._onPointerDown) {
121
+ this._element.removeEventListener(
122
+ "pointerdown",
123
+ this._onPointerDown,
124
+ );
125
+ }
126
+
127
+ if (this._onPointerMove) {
128
+ this._element.removeEventListener(
129
+ "pointermove",
130
+ this._onPointerMove,
131
+ );
132
+ }
133
+
134
+ if (this._onPointerUp) {
135
+ this._element.removeEventListener(
136
+ "pointerup",
137
+ this._onPointerUp,
138
+ );
139
+ }
140
+ }
141
+
142
+ this._element = null;
143
+ this._resolve = null;
144
+ this._onUpdate = null;
145
+ this._pointerDown = false;
146
+ this._pointerTarget = null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Apply a wheel-zoom delta to the resolved target. Exported for
152
+ * re-use inside the worker, where the renderer dispatches interaction
153
+ * events forwarded from the host's `RawEventForwarder` instead of
154
+ * receiving DOM events directly.
155
+ */
156
+ export function applyWheel(
157
+ target: ZoomTarget,
158
+ mouseX: number,
159
+ mouseY: number,
160
+ deltaY: number,
161
+ ): void {
162
+ const { controller, layout } = target;
163
+ const plot = layout.plotRect;
164
+
165
+ const domain = controller.getVisibleDomain();
166
+ const dataX =
167
+ domain.xMin +
168
+ ((mouseX - plot.x) / plot.width) * (domain.xMax - domain.xMin);
169
+ const dataY =
170
+ domain.yMax -
171
+ ((mouseY - plot.y) / plot.height) * (domain.yMax - domain.yMin);
172
+
173
+ const factor = Math.pow(1.1, -deltaY / 100);
174
+ const locked = controller.lockedAxis;
175
+ if (locked !== "x") {
176
+ controller.scaleX = Math.max(
177
+ MIN_ZOOM,
178
+ Math.min(MAX_ZOOM, controller.scaleX * factor),
179
+ );
180
+ }
181
+
182
+ if (locked !== "y") {
183
+ controller.scaleY = Math.max(
184
+ MIN_ZOOM,
185
+ Math.min(MAX_ZOOM, controller.scaleY * factor),
186
+ );
187
+ }
188
+
189
+ const newDomain = controller.getVisibleDomain();
190
+ const newDataX =
191
+ newDomain.xMin +
192
+ ((mouseX - plot.x) / plot.width) * (newDomain.xMax - newDomain.xMin);
193
+ const newDataY =
194
+ newDomain.yMax -
195
+ ((mouseY - plot.y) / plot.height) * (newDomain.yMax - newDomain.yMin);
196
+
197
+ const bxRange = controller.baseXRange;
198
+ const byRange = controller.baseYRange;
199
+ if (locked !== "x" && bxRange > 0) {
200
+ controller.normTranslateX += (dataX - newDataX) / bxRange;
201
+ }
202
+
203
+ if (locked !== "y" && byRange > 0) {
204
+ controller.normTranslateY += (dataY - newDataY) / byRange;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Apply a drag-pan delta to the resolved target. Exported for the
210
+ * same reason as {@link applyWheel}: worker-mode interaction
211
+ * dispatch reuses the math without owning DOM listeners.
212
+ */
213
+ export function applyPan(target: ZoomTarget, dx: number, dy: number): void {
214
+ const { controller, layout } = target;
215
+ const domain = controller.getVisibleDomain();
216
+ const plot = layout.plotRect;
217
+ const dataPerPixelX = (domain.xMax - domain.xMin) / plot.width;
218
+ const dataPerPixelY = (domain.yMax - domain.yMin) / plot.height;
219
+
220
+ const locked = controller.lockedAxis;
221
+ const bxRange = controller.baseXRange;
222
+ const byRange = controller.baseYRange;
223
+ if (locked !== "x" && bxRange > 0) {
224
+ controller.normTranslateX -= (dx * dataPerPixelX) / bxRange;
225
+ }
226
+
227
+ if (locked !== "y" && byRange > 0) {
228
+ controller.normTranslateY += (dy * dataPerPixelY) / byRange;
229
+ }
230
+ }