@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,346 @@
|
|
|
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 { PlotLayout, type PlotRect } from "./plot-layout";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tri-state axis mode.
|
|
17
|
+
*
|
|
18
|
+
* - `"outer"` — one shared axis band reserved at the grid edge;
|
|
19
|
+
* `outerXAxisRect` / `outerYAxisRect` populated, per-cell gutter
|
|
20
|
+
* collapsed to 0 on that side. Caller paints the shared axis
|
|
21
|
+
* once per frame using the grid's outer rect.
|
|
22
|
+
* - `"cell"` — every cell reserves its own gutter on that side;
|
|
23
|
+
* caller paints one axis per cell. Outer rect is undefined.
|
|
24
|
+
* - `"none"` — no gutter anywhere on that side: neither an outer
|
|
25
|
+
* band nor a per-cell reservation. Intended for chart types with
|
|
26
|
+
* no numeric axis at all (treemap, sunburst). When BOTH axes are
|
|
27
|
+
* `"none"` cells are also made flush on the right so adjacent
|
|
28
|
+
* plot rects share a boundary.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to `"cell"` when undefined.
|
|
31
|
+
*/
|
|
32
|
+
export type AxisMode = "outer" | "cell" | "none";
|
|
33
|
+
|
|
34
|
+
export interface FacetGridOptions {
|
|
35
|
+
cssWidth: number;
|
|
36
|
+
cssHeight: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* See {@link AxisMode}. Default `"cell"`.
|
|
40
|
+
*/
|
|
41
|
+
xAxis?: AxisMode;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* See {@link AxisMode}. Default `"cell"`.
|
|
45
|
+
*/
|
|
46
|
+
yAxis?: AxisMode;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reserve a right gutter for a single shared legend.
|
|
50
|
+
*/
|
|
51
|
+
hasLegend?: boolean;
|
|
52
|
+
|
|
53
|
+
/** Axis-label allowance (consumed only when the corresponding axis
|
|
54
|
+
* mode produces a gutter — outer band or per-cell). */
|
|
55
|
+
hasXLabel?: boolean;
|
|
56
|
+
hasYLabel?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-facet title strip height (px). 0 disables.
|
|
60
|
+
*/
|
|
61
|
+
titleBand?: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pixel gap between adjacent cells. Carved out of the grid
|
|
65
|
+
* interior before cell sizing; outer edges of the leftmost /
|
|
66
|
+
* rightmost columns and top / bottom rows are unaffected. Default
|
|
67
|
+
* 0 (flush cells).
|
|
68
|
+
*/
|
|
69
|
+
gap?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface FacetCell {
|
|
73
|
+
index: number;
|
|
74
|
+
label: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sub-plot layout. Every cell in a grid has *identical*
|
|
78
|
+
* `plotRect.width` and `plotRect.height` — cell internal margins
|
|
79
|
+
* do not vary by edge position. Shared-axis gutters live in
|
|
80
|
+
* `FacetGrid.outerXAxisRect` / `outerYAxisRect` instead, painted
|
|
81
|
+
* once per frame by the caller.
|
|
82
|
+
*/
|
|
83
|
+
layout: PlotLayout;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Title strip above the facet's plot rect, if `titleBand > 0`.
|
|
87
|
+
*/
|
|
88
|
+
titleRect?: PlotRect;
|
|
89
|
+
isLeftEdge: boolean;
|
|
90
|
+
isBottomEdge: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface FacetGrid {
|
|
94
|
+
cells: FacetCell[];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Right-gutter rect for the shared legend.
|
|
98
|
+
*/
|
|
99
|
+
legendRect?: PlotRect;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Outer band reserved for the shared X axis (ticks + label). Only
|
|
103
|
+
* set when `xAxis === "outer"`. Spans the grid interior's
|
|
104
|
+
* horizontal extent and sits immediately below the bottom row of
|
|
105
|
+
* cells.
|
|
106
|
+
*/
|
|
107
|
+
outerXAxisRect?: PlotRect;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Outer band reserved for the shared Y axis (ticks + label). Only
|
|
111
|
+
* set when `yAxis === "outer"`. Spans the grid interior's
|
|
112
|
+
* vertical extent and sits immediately left of the leftmost
|
|
113
|
+
* column of cells.
|
|
114
|
+
*/
|
|
115
|
+
outerYAxisRect?: PlotRect;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Collect the bottom-row cells' `PlotLayout`s — i.e. the cells that
|
|
120
|
+
* sit on the grid's bottom edge. Shared-X axis renderers paint X
|
|
121
|
+
* ticks aligned to each of these. Empty when the grid has zero cells.
|
|
122
|
+
*/
|
|
123
|
+
export function bottomRowLayouts(grid: FacetGrid): PlotLayout[] {
|
|
124
|
+
return grid.cells.filter((c) => c.isBottomEdge).map((c) => c.layout);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Collect the left-column cells' `PlotLayout`s — symmetric to
|
|
129
|
+
* {@link bottomRowLayouts} for the shared-Y axis path.
|
|
130
|
+
*/
|
|
131
|
+
export function leftColumnLayouts(grid: FacetGrid): PlotLayout[] {
|
|
132
|
+
return grid.cells.filter((c) => c.isLeftEdge).map((c) => c.layout);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Per-cell internal gutter defaults mirror `PlotLayout`'s constants so
|
|
136
|
+
// that a cell with `leftExtra: undefined` reserves the same space the
|
|
137
|
+
// outer band would reserve when the axis is shared. Keep these in sync
|
|
138
|
+
// with `plot-layout.ts`.
|
|
139
|
+
const CELL_LEFT_GUTTER = 55;
|
|
140
|
+
const CELL_BOTTOM_GUTTER = 24;
|
|
141
|
+
const AXIS_LABEL_W = 16;
|
|
142
|
+
const AXIS_LABEL_H = 18;
|
|
143
|
+
|
|
144
|
+
const TITLE_BAND_DEFAULT = 18;
|
|
145
|
+
const LEGEND_GUTTER = 96;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Pick `(cols, rows)` so that each resulting cell's aspect ratio is as
|
|
149
|
+
* close to 1 as possible given the grid interior. Sweeps `cols ∈ [1,
|
|
150
|
+
* count]` with `rows = ceil(count / cols)` and minimizes
|
|
151
|
+
* `max(cellW/cellH, cellH/cellW)`. Ties break toward fewer total cells
|
|
152
|
+
* (less unused grid area).
|
|
153
|
+
*/
|
|
154
|
+
function pickGridShape(
|
|
155
|
+
count: number,
|
|
156
|
+
gridW: number,
|
|
157
|
+
gridH: number,
|
|
158
|
+
gap: number,
|
|
159
|
+
): { cols: number; rows: number } {
|
|
160
|
+
if (count <= 1) {
|
|
161
|
+
return { cols: 1, rows: 1 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let bestCols = 1;
|
|
165
|
+
let bestRows = count;
|
|
166
|
+
let bestCost = Infinity;
|
|
167
|
+
let bestTotal = count;
|
|
168
|
+
for (let cols = 1; cols <= count; cols++) {
|
|
169
|
+
const rows = Math.ceil(count / cols);
|
|
170
|
+
const cellW = Math.max(1, (gridW - (cols - 1) * gap) / cols);
|
|
171
|
+
const cellH = Math.max(1, (gridH - (rows - 1) * gap) / rows);
|
|
172
|
+
const aspect = cellW / cellH;
|
|
173
|
+
const cost = Math.max(aspect, 1 / aspect);
|
|
174
|
+
const total = cols * rows;
|
|
175
|
+
if (cost < bestCost || (cost === bestCost && total < bestTotal)) {
|
|
176
|
+
bestCols = cols;
|
|
177
|
+
bestRows = rows;
|
|
178
|
+
bestCost = cost;
|
|
179
|
+
bestTotal = total;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { cols: bestCols, rows: bestRows };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Arrange `labels.length` sub-plots in a row-major grid sized to fit
|
|
188
|
+
* `(cssWidth, cssHeight)`.
|
|
189
|
+
*
|
|
190
|
+
* Grid shape is chosen to minimize cell aspect distance from square
|
|
191
|
+
* given the container's grid interior: `cols ∈ [1, count]`,
|
|
192
|
+
* `rows = ceil(count / cols)`, tie-broken toward fewer total cells.
|
|
193
|
+
*
|
|
194
|
+
* **Invariant:** every `cells[i].layout.plotRect` has the same
|
|
195
|
+
* `width` and `height`. Shared-axis gutters are carved out of the
|
|
196
|
+
* outer canvas BEFORE cell sizing, so a cell's edge position never
|
|
197
|
+
* affects its internal margins. This lets per-facet draws reuse the
|
|
198
|
+
* same projection scale and lets shared ticks line up with the
|
|
199
|
+
* interior cell boundaries exactly.
|
|
200
|
+
*
|
|
201
|
+
* Axis modes — see {@link AxisMode}:
|
|
202
|
+
* - `"outer"` → outer band rect is populated; per-cell gutter is 0.
|
|
203
|
+
* - `"cell"` → outer band is undefined; each cell owns its own gutter.
|
|
204
|
+
* - `"none"` → no gutter anywhere on that side; used by axis-less
|
|
205
|
+
* chart types.
|
|
206
|
+
*
|
|
207
|
+
* Because all cells are identical in size, callers can sample *any*
|
|
208
|
+
* cell's layout (e.g. `cells[0].layout`) for tick / scale
|
|
209
|
+
* computations.
|
|
210
|
+
*/
|
|
211
|
+
export function buildFacetGrid(
|
|
212
|
+
labels: string[],
|
|
213
|
+
opts: FacetGridOptions,
|
|
214
|
+
): FacetGrid {
|
|
215
|
+
const count = labels.length;
|
|
216
|
+
const { cssWidth, cssHeight } = opts;
|
|
217
|
+
|
|
218
|
+
if (count <= 0 || cssWidth <= 0 || cssHeight <= 0) {
|
|
219
|
+
return { cells: [] };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const titleBand = opts.titleBand ?? TITLE_BAND_DEFAULT;
|
|
223
|
+
const legendW = opts.hasLegend ? LEGEND_GUTTER : 0;
|
|
224
|
+
|
|
225
|
+
const xMode: AxisMode = opts.xAxis ?? "cell";
|
|
226
|
+
const yMode: AxisMode = opts.yAxis ?? "cell";
|
|
227
|
+
|
|
228
|
+
// Axis-less chart types (trees) benefit from fully-flush cells —
|
|
229
|
+
// no per-cell breathing on the right either, so adjacent plot
|
|
230
|
+
// rects share a boundary instead of leaving a 16 px seam.
|
|
231
|
+
const cellsFlush = xMode === "none" && yMode === "none";
|
|
232
|
+
|
|
233
|
+
// Outer margins: shared-axis gutters + legend gutter live OUTSIDE
|
|
234
|
+
// the per-cell rects.
|
|
235
|
+
const outerLeft =
|
|
236
|
+
yMode === "outer"
|
|
237
|
+
? CELL_LEFT_GUTTER + (opts.hasYLabel ? AXIS_LABEL_W : 0)
|
|
238
|
+
: 0;
|
|
239
|
+
const outerBottom =
|
|
240
|
+
xMode === "outer"
|
|
241
|
+
? CELL_BOTTOM_GUTTER + (opts.hasXLabel ? AXIS_LABEL_H : 0)
|
|
242
|
+
: 0;
|
|
243
|
+
const outerTop = 0;
|
|
244
|
+
const outerRight = legendW;
|
|
245
|
+
|
|
246
|
+
const gridX = outerLeft;
|
|
247
|
+
const gridY = outerTop;
|
|
248
|
+
const gridW = Math.max(1, cssWidth - outerLeft - outerRight);
|
|
249
|
+
const gridH = Math.max(1, cssHeight - outerTop - outerBottom);
|
|
250
|
+
|
|
251
|
+
// Carve the total inter-cell gap out of the grid interior before
|
|
252
|
+
// sizing cells so every cell remains identical in size (the
|
|
253
|
+
// grid-uniform invariant). Gaps only sit BETWEEN neighbors — not
|
|
254
|
+
// against the outer edges.
|
|
255
|
+
const gap = Math.max(0, opts.gap ?? 0);
|
|
256
|
+
const { cols, rows } = pickGridShape(count, gridW, gridH, gap);
|
|
257
|
+
const totalGapX = Math.max(0, cols - 1) * gap;
|
|
258
|
+
const totalGapY = Math.max(0, rows - 1) * gap;
|
|
259
|
+
const cellW = Math.max(1, (gridW - totalGapX) / cols);
|
|
260
|
+
const cellH = Math.max(1, (gridH - totalGapY) / rows);
|
|
261
|
+
|
|
262
|
+
const cells: FacetCell[] = [];
|
|
263
|
+
for (let i = 0; i < count; i++) {
|
|
264
|
+
const row = Math.floor(i / cols);
|
|
265
|
+
const col = i - row * cols;
|
|
266
|
+
const isBottomEdge = row === rows - 1 || i + cols >= count;
|
|
267
|
+
const isLeftEdge = col === 0;
|
|
268
|
+
|
|
269
|
+
const cellX = gridX + col * (cellW + gap);
|
|
270
|
+
const cellY = gridY + row * (cellH + gap);
|
|
271
|
+
|
|
272
|
+
// Carve a title strip from the top of each cell. The remaining
|
|
273
|
+
// rect becomes the per-cell `PlotLayout`.
|
|
274
|
+
const plotTop = cellY + titleBand;
|
|
275
|
+
const plotLeft = cellX;
|
|
276
|
+
const plotWidth = cellW;
|
|
277
|
+
const plotHeight = Math.max(1, cellH - titleBand);
|
|
278
|
+
|
|
279
|
+
// Per-cell gutters:
|
|
280
|
+
// - "cell" → keep `PlotLayout` default (undefined).
|
|
281
|
+
// - "outer" / "none" → collapse to 0 (no internal gutter).
|
|
282
|
+
// Per-cell labels only paint when the axis is per-cell.
|
|
283
|
+
const layout = new PlotLayout(cssWidth, cssHeight, {
|
|
284
|
+
hasXLabel: xMode === "cell" && opts.hasXLabel === true,
|
|
285
|
+
hasYLabel: yMode === "cell" && opts.hasYLabel === true,
|
|
286
|
+
hasLegend: false,
|
|
287
|
+
leftExtra: yMode === "cell" ? undefined : 0,
|
|
288
|
+
bottomExtra: xMode === "cell" ? undefined : 0,
|
|
289
|
+
rightExtra: cellsFlush ? 0 : undefined,
|
|
290
|
+
originX: plotLeft,
|
|
291
|
+
originY: plotTop,
|
|
292
|
+
cellWidth: plotWidth,
|
|
293
|
+
cellHeight: plotHeight,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const titleRect: PlotRect | undefined =
|
|
297
|
+
titleBand > 0
|
|
298
|
+
? {
|
|
299
|
+
x: plotLeft,
|
|
300
|
+
y: cellY,
|
|
301
|
+
width: plotWidth,
|
|
302
|
+
height: titleBand,
|
|
303
|
+
}
|
|
304
|
+
: undefined;
|
|
305
|
+
|
|
306
|
+
cells.push({
|
|
307
|
+
index: i,
|
|
308
|
+
label: labels[i],
|
|
309
|
+
layout,
|
|
310
|
+
titleRect,
|
|
311
|
+
isLeftEdge,
|
|
312
|
+
isBottomEdge,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const legendRect: PlotRect | undefined = opts.hasLegend
|
|
317
|
+
? {
|
|
318
|
+
x: gridX + gridW,
|
|
319
|
+
y: outerTop,
|
|
320
|
+
width: legendW,
|
|
321
|
+
height: gridH,
|
|
322
|
+
}
|
|
323
|
+
: undefined;
|
|
324
|
+
|
|
325
|
+
const outerXAxisRect: PlotRect | undefined =
|
|
326
|
+
xMode === "outer"
|
|
327
|
+
? {
|
|
328
|
+
x: gridX,
|
|
329
|
+
y: gridY + gridH,
|
|
330
|
+
width: gridW,
|
|
331
|
+
height: outerBottom,
|
|
332
|
+
}
|
|
333
|
+
: undefined;
|
|
334
|
+
|
|
335
|
+
const outerYAxisRect: PlotRect | undefined =
|
|
336
|
+
yMode === "outer"
|
|
337
|
+
? {
|
|
338
|
+
x: 0,
|
|
339
|
+
y: gridY,
|
|
340
|
+
width: outerLeft,
|
|
341
|
+
height: gridH,
|
|
342
|
+
}
|
|
343
|
+
: undefined;
|
|
344
|
+
|
|
345
|
+
return { cells, legendRect, outerXAxisRect, outerYAxisRect };
|
|
346
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
export interface PlotMargins {
|
|
14
|
+
top: number;
|
|
15
|
+
right: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
left: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PlotRect {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PlotLayoutOptions {
|
|
28
|
+
hasXLabel: boolean;
|
|
29
|
+
hasYLabel: boolean;
|
|
30
|
+
hasLegend: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Additional CSS-pixel height reserved at the bottom of the plot for a
|
|
34
|
+
* hierarchical / rotated categorical X axis. Overrides the default 24px
|
|
35
|
+
* tick band. The axis-label allowance from `hasXLabel` is preserved.
|
|
36
|
+
*/
|
|
37
|
+
bottomExtra?: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Total CSS-pixel width reserved at the left of the plot for a
|
|
41
|
+
* hierarchical categorical Y axis. Overrides the default `55 +
|
|
42
|
+
* hasYLabel*16` left gutter. The axis-label allowance from `hasYLabel`
|
|
43
|
+
* is preserved.
|
|
44
|
+
*/
|
|
45
|
+
leftExtra?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Total CSS-pixel width reserved at the right of the plot. Overrides
|
|
49
|
+
* the default (`80` when `hasLegend`, else `16`). Faceted cells
|
|
50
|
+
* without axes (treemap / sunburst in grid mode) pass `0` to make
|
|
51
|
+
* adjacent cell plot rects flush; axis-bearing charts leave it
|
|
52
|
+
* unset to keep the default breathing-room margin.
|
|
53
|
+
*/
|
|
54
|
+
rightExtra?: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Absolute canvas-coordinate offset for this layout's plot origin.
|
|
58
|
+
* When set, `cssWidth` / `cssHeight` describe the *outer* canvas, and
|
|
59
|
+
* `originX` / `originY` name the top-left corner of the cell this
|
|
60
|
+
* layout represents. The cell's own width/height come from
|
|
61
|
+
* `cellWidth` / `cellHeight`. `margins` are computed relative to the
|
|
62
|
+
* cell then shifted into canvas-absolute space so projection
|
|
63
|
+
* matrices, scissor, and `dataToPixel` all operate in full-canvas
|
|
64
|
+
* coordinates without branching per-facet.
|
|
65
|
+
*
|
|
66
|
+
* When any of these fields is unset, the layout is single-plot: the
|
|
67
|
+
* cell occupies the whole canvas and `originX` / `originY` default
|
|
68
|
+
* to 0.
|
|
69
|
+
*/
|
|
70
|
+
originX?: number;
|
|
71
|
+
originY?: number;
|
|
72
|
+
cellWidth?: number;
|
|
73
|
+
cellHeight?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Coordinates margins and coordinate transforms between WebGL and Canvas2D.
|
|
78
|
+
* All measurements are in CSS pixels (not physical/DPR-scaled pixels).
|
|
79
|
+
*/
|
|
80
|
+
export class PlotLayout {
|
|
81
|
+
readonly margins: PlotMargins;
|
|
82
|
+
readonly plotRect: PlotRect;
|
|
83
|
+
readonly cssWidth: number;
|
|
84
|
+
readonly cssHeight: number;
|
|
85
|
+
|
|
86
|
+
// Padded domain set by buildProjectionMatrix, used by dataToPixel
|
|
87
|
+
// and pixelToData for tooltip hit-testing.
|
|
88
|
+
paddedXMin = 0;
|
|
89
|
+
paddedXMax = 1;
|
|
90
|
+
paddedYMin = 0;
|
|
91
|
+
paddedYMax = 1;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
cssWidth: number,
|
|
95
|
+
cssHeight: number,
|
|
96
|
+
options: PlotLayoutOptions,
|
|
97
|
+
) {
|
|
98
|
+
this.cssWidth = cssWidth;
|
|
99
|
+
this.cssHeight = cssHeight;
|
|
100
|
+
|
|
101
|
+
const baseLeft = options.leftExtra ?? 55;
|
|
102
|
+
const left = baseLeft + (options.hasYLabel ? 16 : 0);
|
|
103
|
+
const baseBottom = options.bottomExtra ?? 24;
|
|
104
|
+
const bottom = baseBottom + (options.hasXLabel ? 18 : 0);
|
|
105
|
+
const top = 0;
|
|
106
|
+
const right = options.rightExtra ?? (options.hasLegend ? 80 : 16);
|
|
107
|
+
|
|
108
|
+
// Facet cells: the sub-plot lives at `(originX, originY)` within a
|
|
109
|
+
// larger canvas of size `(cssWidth, cssHeight)`. Its own bounds are
|
|
110
|
+
// `cellWidth × cellHeight`. The gutters above are then interpreted
|
|
111
|
+
// inside that cell, and `margins` / `plotRect` are shifted into
|
|
112
|
+
// canvas-absolute coordinates. Single-plot layouts leave these
|
|
113
|
+
// unset, in which case `originX / originY = 0` and the cell
|
|
114
|
+
// occupies the whole canvas — identical to pre-facet semantics.
|
|
115
|
+
const originX = options.originX ?? 0;
|
|
116
|
+
const originY = options.originY ?? 0;
|
|
117
|
+
const cellW = options.cellWidth ?? cssWidth;
|
|
118
|
+
const cellH = options.cellHeight ?? cssHeight;
|
|
119
|
+
|
|
120
|
+
const marginLeftAbs = originX + left;
|
|
121
|
+
const marginTopAbs = originY + top;
|
|
122
|
+
const plotW = Math.max(1, cellW - left - right);
|
|
123
|
+
const plotH = Math.max(1, cellH - top - bottom);
|
|
124
|
+
const marginRightAbs = cssWidth - (marginLeftAbs + plotW);
|
|
125
|
+
const marginBottomAbs = cssHeight - (marginTopAbs + plotH);
|
|
126
|
+
|
|
127
|
+
this.margins = {
|
|
128
|
+
top: marginTopAbs,
|
|
129
|
+
right: marginRightAbs,
|
|
130
|
+
bottom: marginBottomAbs,
|
|
131
|
+
left: marginLeftAbs,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
this.plotRect = {
|
|
135
|
+
x: marginLeftAbs,
|
|
136
|
+
y: marginTopAbs,
|
|
137
|
+
width: plotW,
|
|
138
|
+
height: plotH,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build an orthographic projection matrix that maps data coordinates
|
|
144
|
+
* [xMin..xMax, yMin..yMax] to the plot area sub-region of clip space [-1, 1].
|
|
145
|
+
*
|
|
146
|
+
* The matrix bakes margin offsets into the transform so that gl.viewport
|
|
147
|
+
* remains full-canvas and no scissor/sub-viewport is needed.
|
|
148
|
+
*
|
|
149
|
+
* `clamp`, when set, names the axis that carries the *value* (as
|
|
150
|
+
* opposed to categorical / positional) data. Today it only affects
|
|
151
|
+
* `requireZero`; both axes always receive symmetric `padRatio` padding.
|
|
152
|
+
*
|
|
153
|
+
* `requireZero`, when true, guarantees that the unpadded value `0`
|
|
154
|
+
* falls inside the clamped axis's final domain. For all-positive
|
|
155
|
+
* data the axis minimum is pinned at `0` (the baseline sits on the
|
|
156
|
+
* axis line); for all-negative data the maximum is pinned at `0`;
|
|
157
|
+
* for data that already straddles zero, nothing changes. Pairs with
|
|
158
|
+
* `clamp`, and is a no-op when `clamp` is unset.
|
|
159
|
+
*
|
|
160
|
+
* `padRatio` controls the symmetric cosmetic pad on both axes
|
|
161
|
+
* (default 2%). Charts that want plot edges flush with the axes
|
|
162
|
+
* (e.g. heatmap, whose cell rects already span the exact domain)
|
|
163
|
+
* pass `0`.
|
|
164
|
+
*/
|
|
165
|
+
buildProjectionMatrix(
|
|
166
|
+
xMin: number,
|
|
167
|
+
xMax: number,
|
|
168
|
+
yMin: number,
|
|
169
|
+
yMax: number,
|
|
170
|
+
clamp?: "x" | "y",
|
|
171
|
+
requireZero?: boolean,
|
|
172
|
+
padRatio: number = 0.02,
|
|
173
|
+
xOrigin: number = 0,
|
|
174
|
+
yOrigin: number = 0,
|
|
175
|
+
): Float32Array {
|
|
176
|
+
// Symmetric cosmetic padding on both axes (default 2%).
|
|
177
|
+
let xRange = xMax - xMin;
|
|
178
|
+
let yRange = yMax - yMin;
|
|
179
|
+
if (xRange === 0) {
|
|
180
|
+
xRange = 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (yRange === 0) {
|
|
184
|
+
yRange = 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const xPad = xRange * padRatio;
|
|
188
|
+
const yPad = yRange * padRatio;
|
|
189
|
+
|
|
190
|
+
// Evaluate the zero-snap condition against the *pre-pad*
|
|
191
|
+
// values so that an exact-zero boundary (e.g. bar pipelines
|
|
192
|
+
// that snap `valMin` to 0) still qualifies — otherwise the
|
|
193
|
+
// padding step would tip the boundary slightly negative and
|
|
194
|
+
// the snap branch below would miss. Inclusive comparison is
|
|
195
|
+
// deliberate.
|
|
196
|
+
const snapYMin = requireZero && clamp === "y" && yMin >= 0;
|
|
197
|
+
const snapYMax = requireZero && clamp === "y" && yMax <= 0;
|
|
198
|
+
const snapXMin = requireZero && clamp === "x" && xMin >= 0;
|
|
199
|
+
const snapXMax = requireZero && clamp === "x" && xMax <= 0;
|
|
200
|
+
|
|
201
|
+
xMin -= xPad;
|
|
202
|
+
xMax += xPad;
|
|
203
|
+
yMin -= yPad;
|
|
204
|
+
yMax += yPad;
|
|
205
|
+
|
|
206
|
+
// Pin the snapped boundary to exactly zero and give the
|
|
207
|
+
// opposite boundary a second pad for visual headroom above the
|
|
208
|
+
// tallest bar. No-op when data straddles zero (neither flag
|
|
209
|
+
// set) so no boundary collapses onto an in-range value.
|
|
210
|
+
if (snapYMin) {
|
|
211
|
+
yMin = 0;
|
|
212
|
+
yMax += yPad;
|
|
213
|
+
} else if (snapYMax) {
|
|
214
|
+
yMax = 0;
|
|
215
|
+
yMin -= yPad;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (snapXMin) {
|
|
219
|
+
xMin = 0;
|
|
220
|
+
xMax += xPad;
|
|
221
|
+
} else if (snapXMax) {
|
|
222
|
+
xMax = 0;
|
|
223
|
+
xMin -= xPad;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Store padded domain for dataToPixel
|
|
227
|
+
this.paddedXMin = xMin;
|
|
228
|
+
this.paddedXMax = xMax;
|
|
229
|
+
this.paddedYMin = yMin;
|
|
230
|
+
this.paddedYMax = yMax;
|
|
231
|
+
|
|
232
|
+
// Clip-space bounds for the plot area
|
|
233
|
+
const clipLeft = (2 * this.margins.left) / this.cssWidth - 1;
|
|
234
|
+
const clipRight = 1 - (2 * this.margins.right) / this.cssWidth;
|
|
235
|
+
const clipBottom = (2 * this.margins.bottom) / this.cssHeight - 1;
|
|
236
|
+
const clipTop = 1 - (2 * this.margins.top) / this.cssHeight;
|
|
237
|
+
|
|
238
|
+
// Scale and translate: rebased data [(min-origin), (max-origin)]
|
|
239
|
+
// → clip [clipMin, clipMax]. Callers that ship rebased values to
|
|
240
|
+
// the GPU pass the origin they used; for ordinary numeric data
|
|
241
|
+
// both origins default to 0 and the math collapses to the
|
|
242
|
+
// legacy `tx = clipLeft - sx*xMin` form. With a datetime axis
|
|
243
|
+
// the origin lifts ~1.7e12 out of `tx` before the f32
|
|
244
|
+
// narrowing in the matrix below, keeping shader cancellation
|
|
245
|
+
// precision-safe down to sub-millisecond granularity.
|
|
246
|
+
const sx = (clipRight - clipLeft) / (xMax - xMin);
|
|
247
|
+
const sy = (clipTop - clipBottom) / (yMax - yMin);
|
|
248
|
+
const tx = clipLeft - sx * (xMin - xOrigin);
|
|
249
|
+
const ty = clipBottom - sy * (yMin - yOrigin);
|
|
250
|
+
|
|
251
|
+
// Column-major 4x4 matrix
|
|
252
|
+
// prettier-ignore
|
|
253
|
+
return new Float32Array([
|
|
254
|
+
sx, 0, 0, 0,
|
|
255
|
+
0, sy, 0, 0,
|
|
256
|
+
0, 0, -1, 0,
|
|
257
|
+
tx, ty, 0, 1,
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Convert data coordinates to CSS pixel coordinates on the overlay canvas.
|
|
263
|
+
* Uses the padded domain from the last `buildProjectionMatrix` call so
|
|
264
|
+
* that pixel positions align exactly with the WebGL projection.
|
|
265
|
+
*/
|
|
266
|
+
dataToPixel(dataX: number, dataY: number): { px: number; py: number } {
|
|
267
|
+
const { x, y, width, height } = this.plotRect;
|
|
268
|
+
const tx =
|
|
269
|
+
(dataX - this.paddedXMin) / (this.paddedXMax - this.paddedXMin);
|
|
270
|
+
const ty =
|
|
271
|
+
(dataY - this.paddedYMin) / (this.paddedYMax - this.paddedYMin);
|
|
272
|
+
return {
|
|
273
|
+
px: x + tx * width,
|
|
274
|
+
py: y + (1 - ty) * height, // Y is flipped (CSS Y goes down)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|