@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.
- package/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +3 -0
- package/dist/cdn/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/axis/axis-primitives.d.ts +24 -0
- package/dist/esm/axis/bar-axis.d.ts +51 -0
- package/dist/esm/axis/canvas.d.ts +24 -0
- package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
- package/dist/esm/axis/categorical-axis.d.ts +27 -0
- package/dist/esm/axis/facet-chrome.d.ts +13 -0
- package/dist/esm/axis/label-geometry.d.ts +41 -0
- package/dist/esm/axis/legend.d.ts +44 -0
- package/dist/esm/axis/numeric-axis.d.ts +20 -0
- package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
- package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
- package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
- package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
- package/dist/esm/charts/canvas-types.d.ts +15 -0
- package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
- package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
- package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
- package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
- package/dist/esm/charts/chart-base.d.ts +402 -0
- package/dist/esm/charts/chart.d.ts +338 -0
- package/dist/esm/charts/common/band-layout.d.ts +32 -0
- package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
- package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
- package/dist/esm/charts/common/leaf-color.d.ts +33 -0
- package/dist/esm/charts/common/node-store.d.ts +81 -0
- package/dist/esm/charts/common/tree-chart.d.ts +48 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
- package/dist/esm/charts/common/tree-data.d.ts +54 -0
- package/dist/esm/charts/common/visible-extent.d.ts +51 -0
- package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
- package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
- package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
- package/dist/esm/charts/map/map.d.ts +67 -0
- package/dist/esm/charts/registry.d.ts +14 -0
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
- package/dist/esm/charts/series/series-build.d.ts +228 -0
- package/dist/esm/charts/series/series-interact.d.ts +35 -0
- package/dist/esm/charts/series/series-render.d.ts +41 -0
- package/dist/esm/charts/series/series-type.d.ts +49 -0
- package/dist/esm/charts/series/series.d.ts +317 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
- package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
- package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
- package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
- package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
- package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
- package/dist/esm/charts/treemap/treemap.d.ts +74 -0
- package/dist/esm/config.d.ts +27 -0
- package/dist/esm/data/lazy-row.d.ts +32 -0
- package/dist/esm/data/split-groups.d.ts +20 -0
- package/dist/esm/data/view-reader.d.ts +35 -0
- package/dist/esm/event-detail.d.ts +28 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/interaction/hit-test.d.ts +30 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +46 -0
- package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
- package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
- package/dist/esm/interaction/spatial-grid.d.ts +15 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
- package/dist/esm/interaction/zoom-controller.d.ts +106 -0
- package/dist/esm/interaction/zoom-router.d.ts +48 -0
- package/dist/esm/layout/facet-grid.d.ts +126 -0
- package/dist/esm/layout/plot-layout.d.ts +104 -0
- package/dist/esm/layout/ticks.d.ts +17 -0
- package/dist/esm/map/mercator.d.ts +102 -0
- package/dist/esm/map/tile-cache.d.ts +38 -0
- package/dist/esm/map/tile-layer.d.ts +66 -0
- package/dist/esm/map/tile-loader.d.ts +52 -0
- package/dist/esm/map/tile-source.d.ts +66 -0
- package/dist/esm/perspective-viewer-charts.js +3 -0
- package/dist/esm/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/plugin/charts.d.ts +40 -0
- package/dist/esm/plugin/plugin.d.ts +95 -0
- package/dist/esm/render/scheduler.d.ts +41 -0
- package/dist/esm/theme/gradient.d.ts +48 -0
- package/dist/esm/theme/palette.d.ts +13 -0
- package/dist/esm/theme/theme-snapshot.d.ts +7 -0
- package/dist/esm/theme/theme.d.ts +53 -0
- package/dist/esm/transport/protocol.d.ts +430 -0
- package/dist/esm/transport/renderer-transport.d.ts +201 -0
- package/dist/esm/utils/css.d.ts +1 -0
- package/dist/esm/utils/font-snapshot.d.ts +50 -0
- package/dist/esm/webgl/buffer-pool.d.ts +62 -0
- package/dist/esm/webgl/context-manager.d.ts +184 -0
- package/dist/esm/webgl/gradient-texture.d.ts +17 -0
- package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
- package/dist/esm/webgl/plot-frame.d.ts +39 -0
- package/dist/esm/webgl/program-cache.d.ts +13 -0
- package/dist/esm/webgl/shader-manifest.d.ts +53 -0
- package/dist/esm/webgl/shader-registry.d.ts +22 -0
- package/dist/esm/worker/boot.d.ts +0 -0
- package/dist/esm/worker/dispatch.d.ts +9 -0
- package/dist/esm/worker/font-loader.d.ts +2 -0
- package/dist/esm/worker/renderer.worker.d.ts +115 -0
- package/dist/esm/worker/session-host.d.ts +26 -0
- package/package.json +47 -0
- package/src/css/perspective-viewer-charts.css +95 -0
- package/src/ts/axis/axis-primitives.ts +125 -0
- package/src/ts/axis/bar-axis.ts +345 -0
- package/src/ts/axis/canvas.ts +64 -0
- package/src/ts/axis/categorical-axis-core.ts +125 -0
- package/src/ts/axis/categorical-axis.ts +716 -0
- package/src/ts/axis/facet-chrome.ts +42 -0
- package/src/ts/axis/label-geometry.ts +188 -0
- package/src/ts/axis/legend.ts +218 -0
- package/src/ts/axis/numeric-axis.ts +353 -0
- package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
- package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
- package/src/ts/charts/candlestick/candlestick.ts +367 -0
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
- package/src/ts/charts/canvas-types.ts +30 -0
- package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
- package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
- package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
- package/src/ts/charts/cartesian/cartesian.ts +469 -0
- package/src/ts/charts/cartesian/glyph.ts +81 -0
- package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
- package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
- package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
- package/src/ts/charts/cartesian/label-interner.ts +56 -0
- package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
- package/src/ts/charts/chart-base.ts +840 -0
- package/src/ts/charts/chart.ts +427 -0
- package/src/ts/charts/common/band-layout.ts +63 -0
- package/src/ts/charts/common/categorical-y-chart.ts +81 -0
- package/src/ts/charts/common/category-axis-resolver.ts +314 -0
- package/src/ts/charts/common/chrome-cache.ts +79 -0
- package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
- package/src/ts/charts/common/leaf-color.ts +92 -0
- package/src/ts/charts/common/node-store.ts +235 -0
- package/src/ts/charts/common/tree-chart.ts +76 -0
- package/src/ts/charts/common/tree-chrome.ts +123 -0
- package/src/ts/charts/common/tree-data.ts +623 -0
- package/src/ts/charts/common/visible-extent.ts +112 -0
- package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
- package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
- package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
- package/src/ts/charts/heatmap/heatmap.ts +368 -0
- package/src/ts/charts/map/map.ts +201 -0
- package/src/ts/charts/registry.ts +65 -0
- package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
- package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
- package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
- package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
- package/src/ts/charts/series/series-build.ts +848 -0
- package/src/ts/charts/series/series-interact.ts +604 -0
- package/src/ts/charts/series/series-render.ts +1109 -0
- package/src/ts/charts/series/series-type.ts +99 -0
- package/src/ts/charts/series/series.ts +794 -0
- package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
- package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
- package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
- package/src/ts/charts/sunburst/sunburst.ts +248 -0
- package/src/ts/charts/treemap/treemap-interact.ts +445 -0
- package/src/ts/charts/treemap/treemap-layout.ts +328 -0
- package/src/ts/charts/treemap/treemap-render.ts +886 -0
- package/src/ts/charts/treemap/treemap.ts +247 -0
- package/src/ts/config.ts +41 -0
- package/src/ts/data/lazy-row.ts +140 -0
- package/src/ts/data/split-groups.ts +97 -0
- package/src/ts/data/view-reader.ts +107 -0
- package/src/ts/event-detail.ts +44 -0
- package/src/ts/index.ts +53 -0
- package/src/ts/interaction/hit-test.ts +106 -0
- package/src/ts/interaction/host-sink-dom.ts +85 -0
- package/src/ts/interaction/host-sink-message.ts +75 -0
- package/src/ts/interaction/lazy-tooltip.ts +102 -0
- package/src/ts/interaction/raw-event-forwarder.ts +175 -0
- package/src/ts/interaction/spatial-grid.ts +100 -0
- package/src/ts/interaction/tooltip-controller.ts +407 -0
- package/src/ts/interaction/zoom-controller.ts +468 -0
- package/src/ts/interaction/zoom-router.ts +230 -0
- package/src/ts/layout/facet-grid.ts +346 -0
- package/src/ts/layout/plot-layout.ts +277 -0
- package/src/ts/layout/ticks.ts +168 -0
- package/src/ts/map/mercator.ts +204 -0
- package/src/ts/map/tile-cache.ts +96 -0
- package/src/ts/map/tile-layer.ts +382 -0
- package/src/ts/map/tile-loader.ts +143 -0
- package/src/ts/map/tile-source.ts +156 -0
- package/src/ts/plugin/charts.ts +286 -0
- package/src/ts/plugin/plugin.ts +668 -0
- package/src/ts/render/scheduler.ts +339 -0
- package/src/ts/shaders/area.frag.glsl +20 -0
- package/src/ts/shaders/area.vert.glsl +19 -0
- package/src/ts/shaders/bar.frag.glsl +25 -0
- package/src/ts/shaders/bar.vert.glsl +60 -0
- package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
- package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
- package/src/ts/shaders/density-extreme.frag.glsl +30 -0
- package/src/ts/shaders/density-mrt.frag.glsl +44 -0
- package/src/ts/shaders/density-mrt.vert.glsl +48 -0
- package/src/ts/shaders/density-resolve.frag.glsl +89 -0
- package/src/ts/shaders/density-resolve.vert.glsl +23 -0
- package/src/ts/shaders/density-splat.frag.glsl +34 -0
- package/src/ts/shaders/density-splat.vert.glsl +52 -0
- package/src/ts/shaders/gridline.frag.glsl +18 -0
- package/src/ts/shaders/gridline.vert.glsl +18 -0
- package/src/ts/shaders/heatmap.frag.glsl +23 -0
- package/src/ts/shaders/heatmap.vert.glsl +42 -0
- package/src/ts/shaders/line-uniform.frag.glsl +26 -0
- package/src/ts/shaders/line-uniform.vert.glsl +54 -0
- package/src/ts/shaders/line.frag.glsl +28 -0
- package/src/ts/shaders/line.vert.glsl +87 -0
- package/src/ts/shaders/scatter.frag.glsl +39 -0
- package/src/ts/shaders/scatter.vert.glsl +67 -0
- package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
- package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
- package/src/ts/shaders/tile.frag.glsl +27 -0
- package/src/ts/shaders/tile.vert.glsl +35 -0
- package/src/ts/shaders/treemap.frag.glsl +19 -0
- package/src/ts/shaders/treemap.vert.glsl +25 -0
- package/src/ts/shaders/y-scatter.frag.glsl +30 -0
- package/src/ts/shaders/y-scatter.vert.glsl +31 -0
- package/src/ts/theme/gradient.ts +312 -0
- package/src/ts/theme/palette.ts +64 -0
- package/src/ts/theme/theme-snapshot.ts +66 -0
- package/src/ts/theme/theme.ts +166 -0
- package/src/ts/transport/protocol.ts +497 -0
- package/src/ts/transport/renderer-transport.ts +788 -0
- package/src/ts/utils/css.ts +36 -0
- package/src/ts/utils/font-snapshot.ts +159 -0
- package/src/ts/webgl/buffer-pool.ts +163 -0
- package/src/ts/webgl/context-manager.ts +414 -0
- package/src/ts/webgl/gradient-texture.ts +84 -0
- package/src/ts/webgl/instanced-attrs.ts +139 -0
- package/src/ts/webgl/plot-frame.ts +91 -0
- package/src/ts/webgl/program-cache.ts +46 -0
- package/src/ts/webgl/shader-manifest.ts +148 -0
- package/src/ts/webgl/shader-registry.ts +97 -0
- package/src/ts/worker/boot.ts +22 -0
- package/src/ts/worker/dispatch.ts +99 -0
- package/src/ts/worker/font-loader.ts +89 -0
- package/src/ts/worker/renderer.worker.ts +734 -0
- 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
|
+
}
|