@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,1263 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type { WebGLContextManager } from "../../../webgl/context-manager";
|
|
14
|
+
import type { CartesianChart } from "../cartesian";
|
|
15
|
+
import type { Glyph } from "../glyph";
|
|
16
|
+
import { bindGradientTexture } from "../../../webgl/gradient-texture";
|
|
17
|
+
import { getInstancing } from "../../../webgl/instanced-attrs";
|
|
18
|
+
import { buildPointRowTooltipLines } from "../tooltip-lines";
|
|
19
|
+
import splatVert from "../../../shaders/density-splat.vert.glsl";
|
|
20
|
+
import splatFrag from "../../../shaders/density-splat.frag.glsl";
|
|
21
|
+
import extremeFrag from "../../../shaders/density-extreme.frag.glsl";
|
|
22
|
+
import mrtVert from "../../../shaders/density-mrt.vert.glsl";
|
|
23
|
+
import mrtFrag from "../../../shaders/density-mrt.frag.glsl";
|
|
24
|
+
import resolveVert from "../../../shaders/density-resolve.vert.glsl";
|
|
25
|
+
import resolveFrag from "../../../shaders/density-resolve.frag.glsl";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Integer mode identifiers shared with the resolve shader's
|
|
29
|
+
* `u_color_mode` branch ladder. Keep these in sync with the
|
|
30
|
+
* comparisons in `density-resolve.frag.glsl`.
|
|
31
|
+
*/
|
|
32
|
+
const MODE_DENSITY = 0;
|
|
33
|
+
const MODE_MEAN = 1;
|
|
34
|
+
const MODE_EXTREME = 2;
|
|
35
|
+
const MODE_SIGNED = 3;
|
|
36
|
+
|
|
37
|
+
type ColorMode = "mean" | "density" | "extreme" | "signed";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subset of `OES_draw_buffers_indexed` we touch. The official type
|
|
41
|
+
* isn't in `lib.dom.d.ts`; everything we use is `iOES`-suffixed.
|
|
42
|
+
*/
|
|
43
|
+
interface IndexedBlendExt {
|
|
44
|
+
blendEquationiOES(buf: number, mode: number): void;
|
|
45
|
+
blendFunciOES(buf: number, src: number, dst: number): void;
|
|
46
|
+
enableiOES(target: number, index: number): void;
|
|
47
|
+
disableiOES(target: number, index: number): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SplatProgramCache {
|
|
51
|
+
program: WebGLProgram;
|
|
52
|
+
u_projection: WebGLUniformLocation | null;
|
|
53
|
+
u_radius_ndc: WebGLUniformLocation | null;
|
|
54
|
+
u_intensity: WebGLUniformLocation | null;
|
|
55
|
+
u_color_range: WebGLUniformLocation | null;
|
|
56
|
+
a_corner: number;
|
|
57
|
+
a_position: number;
|
|
58
|
+
a_color_value: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DensityCache {
|
|
62
|
+
splat: SplatProgramCache;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Single-target splat program writing `(w, w·t, 0, 0)` into the
|
|
66
|
+
* extreme FBO with MAX blend. Lazily compiled on first
|
|
67
|
+
* `extreme`-mode render (when MRT is unavailable).
|
|
68
|
+
*/
|
|
69
|
+
extremeSplat: SplatProgramCache | null;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Two-target MRT splat program for the `extreme` path on hardware
|
|
73
|
+
* that advertises `OES_draw_buffers_indexed`. `gl_FragData[0]`
|
|
74
|
+
* routes to the heat FBO (ADD blend), `gl_FragData[1]` to the
|
|
75
|
+
* extreme FBO (MAX blend). Lazily compiled on first
|
|
76
|
+
* `extreme`-mode render after the indexed-blend extension is
|
|
77
|
+
* confirmed.
|
|
78
|
+
*/
|
|
79
|
+
mrtSplat: SplatProgramCache | null;
|
|
80
|
+
|
|
81
|
+
resolve: {
|
|
82
|
+
program: WebGLProgram;
|
|
83
|
+
u_heat: WebGLUniformLocation | null;
|
|
84
|
+
u_extreme: WebGLUniformLocation | null;
|
|
85
|
+
u_gradient_lut: WebGLUniformLocation | null;
|
|
86
|
+
u_heat_max: WebGLUniformLocation | null;
|
|
87
|
+
u_color_mode: WebGLUniformLocation | null;
|
|
88
|
+
a_corner: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
quadCornerBuffer: WebGLBuffer;
|
|
92
|
+
tripleCornerBuffer: WebGLBuffer;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Heat (density / weighted-color) framebuffer + texture. R = Σw,
|
|
96
|
+
* G = Σ(w·t). Always allocated.
|
|
97
|
+
*/
|
|
98
|
+
heatTexture: WebGLTexture;
|
|
99
|
+
heatFramebuffer: WebGLFramebuffer;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extreme (signed-max deviation) framebuffer + texture. R holds the
|
|
103
|
+
* MAX of positive deviation, G holds the MAX of negative deviation
|
|
104
|
+
* magnitude. Lazily allocated the first time `extreme` mode runs;
|
|
105
|
+
* `null` otherwise so the common case doesn't pay for a 4MB
|
|
106
|
+
* float texture it never reads.
|
|
107
|
+
*/
|
|
108
|
+
extremeTexture: WebGLTexture | null;
|
|
109
|
+
extremeFramebuffer: WebGLFramebuffer | null;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* MRT framebuffer with both `heatTexture` and `extremeTexture`
|
|
113
|
+
* attached. Used only on the indexed-blend fast path; `null`
|
|
114
|
+
* otherwise. Lazily allocated alongside `extremeTexture`.
|
|
115
|
+
*/
|
|
116
|
+
mrtFramebuffer: WebGLFramebuffer | null;
|
|
117
|
+
|
|
118
|
+
heatWidth: number;
|
|
119
|
+
heatHeight: number;
|
|
120
|
+
heatType: number;
|
|
121
|
+
heatInternalFormat: number;
|
|
122
|
+
heatFormat: number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `true` when the heat FBO uses a true float (or half-float)
|
|
126
|
+
* accumulation format. `signed` mode requires this; on the
|
|
127
|
+
* `UNSIGNED_BYTE` fallback the signed-sum math is meaningless
|
|
128
|
+
* (R and G saturate to 1 independently, so `G - 0.5·R` collapses
|
|
129
|
+
* to a constant 0.5) and the glyph silently degrades to `mean`.
|
|
130
|
+
*/
|
|
131
|
+
floatFbo: boolean;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Cached probe result for `OES_draw_buffers_indexed`. `null` until
|
|
135
|
+
* the first `extreme`-mode draw, then either the extension object
|
|
136
|
+
* (MRT path) or `false` (two-pass fallback).
|
|
137
|
+
*/
|
|
138
|
+
indexedBlend: IndexedBlendExt | null | false;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `true` after `console.warn` has fired once for a `signed`-mode
|
|
142
|
+
* downgrade on this glyph. Suppresses repeat noise across the
|
|
143
|
+
* 60Hz render loop.
|
|
144
|
+
*/
|
|
145
|
+
signedDowngradeWarned: boolean;
|
|
146
|
+
|
|
147
|
+
robustBounds: {
|
|
148
|
+
lo: number;
|
|
149
|
+
hi: number;
|
|
150
|
+
dataCount: number;
|
|
151
|
+
colorName: string;
|
|
152
|
+
colorIsString: boolean;
|
|
153
|
+
} | null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Density-field glyph. Each cartesian row is rasterized as an additive
|
|
158
|
+
* radial splat into an RGBA float FBO; a fullscreen pass resolves the
|
|
159
|
+
* accumulated density (and optional color-weighted average) through the
|
|
160
|
+
* chart's gradient LUT and composites the result inside the plot rect.
|
|
161
|
+
*
|
|
162
|
+
* The user-facing `gradient_color_mode` plugin field selects between:
|
|
163
|
+
*
|
|
164
|
+
* - `density` — alpha and hue from density alone.
|
|
165
|
+
* - `mean` — density-weighted average of color-t (default).
|
|
166
|
+
* - `extreme` — sign-aware MAX of per-point color deviation. Requires
|
|
167
|
+
* a second accumulation target; uses `OES_draw_buffers_indexed`
|
|
168
|
+
* MRT in one pass when available, otherwise falls back to two
|
|
169
|
+
* sequential splat passes.
|
|
170
|
+
* - `signed` — net positive vs. negative accumulation via the
|
|
171
|
+
* `G - 0.5·R` identity. Requires a float-capable framebuffer; on
|
|
172
|
+
* `UNSIGNED_BYTE` fallback the glyph silently degrades to `mean`
|
|
173
|
+
* with a one-line console warning.
|
|
174
|
+
*/
|
|
175
|
+
export class DensityGlyph implements Glyph {
|
|
176
|
+
readonly name = "density" as const;
|
|
177
|
+
private _cache: DensityCache | null = null;
|
|
178
|
+
|
|
179
|
+
ensureProgram(chart: CartesianChart, glManager: WebGLContextManager): void {
|
|
180
|
+
if (this._cache) {
|
|
181
|
+
this.ensureHeatTarget(chart, glManager);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const gl = glManager.gl;
|
|
186
|
+
const splatProgram = glManager.shaders.getOrCreate(
|
|
187
|
+
"density-splat",
|
|
188
|
+
splatVert,
|
|
189
|
+
splatFrag,
|
|
190
|
+
);
|
|
191
|
+
const resolveProgram = glManager.shaders.getOrCreate(
|
|
192
|
+
"density-resolve",
|
|
193
|
+
resolveVert,
|
|
194
|
+
resolveFrag,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const quadCornerBuffer = gl.createBuffer()!;
|
|
198
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadCornerBuffer);
|
|
199
|
+
gl.bufferData(
|
|
200
|
+
gl.ARRAY_BUFFER,
|
|
201
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
|
202
|
+
gl.STATIC_DRAW,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const tripleCornerBuffer = gl.createBuffer()!;
|
|
206
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, tripleCornerBuffer);
|
|
207
|
+
gl.bufferData(
|
|
208
|
+
gl.ARRAY_BUFFER,
|
|
209
|
+
new Float32Array([-1, -1, 3, -1, -1, 3]),
|
|
210
|
+
gl.STATIC_DRAW,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const { internalFormat, format, type, isFloat } =
|
|
214
|
+
pickHeatFormat(glManager);
|
|
215
|
+
|
|
216
|
+
const heatTexture = createAccumTexture(gl);
|
|
217
|
+
const heatFramebuffer = gl.createFramebuffer()!;
|
|
218
|
+
|
|
219
|
+
this._cache = {
|
|
220
|
+
splat: extractSplatLocations(gl, splatProgram),
|
|
221
|
+
extremeSplat: null,
|
|
222
|
+
mrtSplat: null,
|
|
223
|
+
resolve: {
|
|
224
|
+
program: resolveProgram,
|
|
225
|
+
u_heat: gl.getUniformLocation(resolveProgram, "u_heat"),
|
|
226
|
+
u_extreme: gl.getUniformLocation(resolveProgram, "u_extreme"),
|
|
227
|
+
u_gradient_lut: gl.getUniformLocation(
|
|
228
|
+
resolveProgram,
|
|
229
|
+
"u_gradient_lut",
|
|
230
|
+
),
|
|
231
|
+
u_heat_max: gl.getUniformLocation(resolveProgram, "u_heat_max"),
|
|
232
|
+
u_color_mode: gl.getUniformLocation(
|
|
233
|
+
resolveProgram,
|
|
234
|
+
"u_color_mode",
|
|
235
|
+
),
|
|
236
|
+
a_corner: gl.getAttribLocation(resolveProgram, "a_corner"),
|
|
237
|
+
},
|
|
238
|
+
quadCornerBuffer,
|
|
239
|
+
tripleCornerBuffer,
|
|
240
|
+
heatTexture,
|
|
241
|
+
heatFramebuffer,
|
|
242
|
+
extremeTexture: null,
|
|
243
|
+
extremeFramebuffer: null,
|
|
244
|
+
mrtFramebuffer: null,
|
|
245
|
+
heatWidth: 0,
|
|
246
|
+
heatHeight: 0,
|
|
247
|
+
heatType: type,
|
|
248
|
+
heatInternalFormat: internalFormat,
|
|
249
|
+
heatFormat: format,
|
|
250
|
+
floatFbo: isFloat,
|
|
251
|
+
indexedBlend: null,
|
|
252
|
+
signedDowngradeWarned: false,
|
|
253
|
+
robustBounds: null,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
this.ensureHeatTarget(chart, glManager);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
draw(
|
|
260
|
+
chart: CartesianChart,
|
|
261
|
+
glManager: WebGLContextManager,
|
|
262
|
+
projection: Float32Array,
|
|
263
|
+
): void {
|
|
264
|
+
const cache = this._cache;
|
|
265
|
+
if (!cache || !ensurePointBuffers(glManager)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const numSeries = Math.max(1, chart._splitGroups.length);
|
|
270
|
+
const cap = chart._seriesCapacity;
|
|
271
|
+
let total = 0;
|
|
272
|
+
for (let s = 0; s < numSeries; s++) {
|
|
273
|
+
total += chart._seriesUploadedCounts[s] ?? 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (total === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.runSplatAndResolve(chart, glManager, cache, projection, (cb) => {
|
|
281
|
+
for (let s = 0; s < numSeries; s++) {
|
|
282
|
+
const count = chart._seriesUploadedCounts[s] ?? 0;
|
|
283
|
+
if (count <= 0) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
cb(s * cap, count);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
drawSeries(
|
|
293
|
+
chart: CartesianChart,
|
|
294
|
+
glManager: WebGLContextManager,
|
|
295
|
+
projection: Float32Array,
|
|
296
|
+
seriesIdx: number,
|
|
297
|
+
): void {
|
|
298
|
+
const cache = this._cache;
|
|
299
|
+
if (!cache || !ensurePointBuffers(glManager)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const count = chart._seriesUploadedCounts[seriesIdx] ?? 0;
|
|
304
|
+
if (count <= 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const cap = chart._seriesCapacity;
|
|
309
|
+
this.runSplatAndResolve(chart, glManager, cache, projection, (cb) =>
|
|
310
|
+
cb(seriesIdx * cap, count),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
buildTooltipLines(
|
|
315
|
+
chart: CartesianChart,
|
|
316
|
+
flatIdx: number,
|
|
317
|
+
): Promise<string[]> {
|
|
318
|
+
return buildPointRowTooltipLines(chart, flatIdx);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
tooltipOptions() {
|
|
322
|
+
return { crosshair: true, highlightRadius: 0 };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
destroy(chart: CartesianChart): void {
|
|
326
|
+
const cache = this._cache;
|
|
327
|
+
if (!cache || !chart._glManager) {
|
|
328
|
+
this._cache = null;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const gl = chart._glManager.gl;
|
|
333
|
+
gl.deleteBuffer(cache.quadCornerBuffer);
|
|
334
|
+
gl.deleteBuffer(cache.tripleCornerBuffer);
|
|
335
|
+
gl.deleteTexture(cache.heatTexture);
|
|
336
|
+
gl.deleteFramebuffer(cache.heatFramebuffer);
|
|
337
|
+
if (cache.extremeTexture) {
|
|
338
|
+
gl.deleteTexture(cache.extremeTexture);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (cache.extremeFramebuffer) {
|
|
342
|
+
gl.deleteFramebuffer(cache.extremeFramebuffer);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (cache.mrtFramebuffer) {
|
|
346
|
+
gl.deleteFramebuffer(cache.mrtFramebuffer);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this._cache = null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resize the heat (and, when allocated, extreme + MRT) targets to
|
|
354
|
+
* the current canvas bitmap size. The canvas backing store changes
|
|
355
|
+
* on DPR or layout updates, so we compare cached dimensions and
|
|
356
|
+
* re-allocate when stale.
|
|
357
|
+
*/
|
|
358
|
+
private ensureHeatTarget(
|
|
359
|
+
_chart: CartesianChart,
|
|
360
|
+
glManager: WebGLContextManager,
|
|
361
|
+
): void {
|
|
362
|
+
const cache = this._cache;
|
|
363
|
+
if (!cache) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const gl = glManager.gl;
|
|
368
|
+
const w = gl.canvas.width;
|
|
369
|
+
const h = gl.canvas.height;
|
|
370
|
+
if (w === cache.heatWidth && h === cache.heatHeight) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (w <= 0 || h <= 0) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.allocAccumTexture(gl, cache, cache.heatTexture, w, h);
|
|
379
|
+
|
|
380
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, cache.heatFramebuffer);
|
|
381
|
+
gl.framebufferTexture2D(
|
|
382
|
+
gl.FRAMEBUFFER,
|
|
383
|
+
gl.COLOR_ATTACHMENT0,
|
|
384
|
+
gl.TEXTURE_2D,
|
|
385
|
+
cache.heatTexture,
|
|
386
|
+
0,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (cache.extremeTexture) {
|
|
390
|
+
this.allocAccumTexture(gl, cache, cache.extremeTexture, w, h);
|
|
391
|
+
if (cache.extremeFramebuffer) {
|
|
392
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, cache.extremeFramebuffer);
|
|
393
|
+
gl.framebufferTexture2D(
|
|
394
|
+
gl.FRAMEBUFFER,
|
|
395
|
+
gl.COLOR_ATTACHMENT0,
|
|
396
|
+
gl.TEXTURE_2D,
|
|
397
|
+
cache.extremeTexture,
|
|
398
|
+
0,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (cache.mrtFramebuffer) {
|
|
403
|
+
// MRT FBO only exists when indexed-blend was probed
|
|
404
|
+
// successfully, which is gated on WebGL2.
|
|
405
|
+
const gl2 = gl as WebGL2RenderingContext;
|
|
406
|
+
gl2.bindFramebuffer(gl2.FRAMEBUFFER, cache.mrtFramebuffer);
|
|
407
|
+
gl2.framebufferTexture2D(
|
|
408
|
+
gl2.FRAMEBUFFER,
|
|
409
|
+
gl2.COLOR_ATTACHMENT0,
|
|
410
|
+
gl2.TEXTURE_2D,
|
|
411
|
+
cache.heatTexture,
|
|
412
|
+
0,
|
|
413
|
+
);
|
|
414
|
+
gl2.framebufferTexture2D(
|
|
415
|
+
gl2.FRAMEBUFFER,
|
|
416
|
+
gl2.COLOR_ATTACHMENT1,
|
|
417
|
+
gl2.TEXTURE_2D,
|
|
418
|
+
cache.extremeTexture,
|
|
419
|
+
0,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
425
|
+
|
|
426
|
+
cache.heatWidth = w;
|
|
427
|
+
cache.heatHeight = h;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Re-allocate the storage for one accumulation texture using the
|
|
432
|
+
* cached format triple. Called both at first draw and on every
|
|
433
|
+
* canvas-size change.
|
|
434
|
+
*/
|
|
435
|
+
private allocAccumTexture(
|
|
436
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
437
|
+
cache: DensityCache,
|
|
438
|
+
tex: WebGLTexture,
|
|
439
|
+
w: number,
|
|
440
|
+
h: number,
|
|
441
|
+
): void {
|
|
442
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
443
|
+
gl.texImage2D(
|
|
444
|
+
gl.TEXTURE_2D,
|
|
445
|
+
0,
|
|
446
|
+
cache.heatInternalFormat,
|
|
447
|
+
w,
|
|
448
|
+
h,
|
|
449
|
+
0,
|
|
450
|
+
cache.heatFormat,
|
|
451
|
+
cache.heatType,
|
|
452
|
+
null,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Lazily allocate the extreme-mode accumulation texture + its
|
|
458
|
+
* framebuffers. Sized to match the heat target. Also probes
|
|
459
|
+
* `OES_draw_buffers_indexed` once per cache; if available, builds
|
|
460
|
+
* the MRT framebuffer with both textures attached.
|
|
461
|
+
*/
|
|
462
|
+
private ensureExtremeTarget(
|
|
463
|
+
glManager: WebGLContextManager,
|
|
464
|
+
cache: DensityCache,
|
|
465
|
+
): void {
|
|
466
|
+
const gl = glManager.gl;
|
|
467
|
+
if (cache.extremeTexture) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const tex = createAccumTexture(gl);
|
|
472
|
+
cache.extremeTexture = tex;
|
|
473
|
+
cache.extremeFramebuffer = gl.createFramebuffer()!;
|
|
474
|
+
|
|
475
|
+
this.allocAccumTexture(
|
|
476
|
+
gl,
|
|
477
|
+
cache,
|
|
478
|
+
tex,
|
|
479
|
+
cache.heatWidth,
|
|
480
|
+
cache.heatHeight,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, cache.extremeFramebuffer);
|
|
484
|
+
gl.framebufferTexture2D(
|
|
485
|
+
gl.FRAMEBUFFER,
|
|
486
|
+
gl.COLOR_ATTACHMENT0,
|
|
487
|
+
gl.TEXTURE_2D,
|
|
488
|
+
tex,
|
|
489
|
+
0,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
if (cache.indexedBlend === null) {
|
|
493
|
+
// First chance to probe: only attempt MRT on WebGL2 where
|
|
494
|
+
// `gl.drawBuffers` is in core. On WebGL1 we'd also need
|
|
495
|
+
// `WEBGL_draw_buffers` for the JS function, but the
|
|
496
|
+
// indexed-blend extension itself doesn't ship there.
|
|
497
|
+
const ext = glManager.isWebGL2
|
|
498
|
+
? (gl.getExtension(
|
|
499
|
+
"OES_draw_buffers_indexed",
|
|
500
|
+
) as IndexedBlendExt | null)
|
|
501
|
+
: null;
|
|
502
|
+
cache.indexedBlend = ext ?? false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (cache.indexedBlend) {
|
|
506
|
+
// Indexed blend is gated on `isWebGL2`, so `gl` is a
|
|
507
|
+
// WebGL2 context here — cast for `COLOR_ATTACHMENT1`,
|
|
508
|
+
// which isn't on the WebGL1 type.
|
|
509
|
+
const gl2 = gl as WebGL2RenderingContext;
|
|
510
|
+
cache.mrtFramebuffer = gl2.createFramebuffer()!;
|
|
511
|
+
gl2.bindFramebuffer(gl2.FRAMEBUFFER, cache.mrtFramebuffer);
|
|
512
|
+
gl2.framebufferTexture2D(
|
|
513
|
+
gl2.FRAMEBUFFER,
|
|
514
|
+
gl2.COLOR_ATTACHMENT0,
|
|
515
|
+
gl2.TEXTURE_2D,
|
|
516
|
+
cache.heatTexture,
|
|
517
|
+
0,
|
|
518
|
+
);
|
|
519
|
+
gl2.framebufferTexture2D(
|
|
520
|
+
gl2.FRAMEBUFFER,
|
|
521
|
+
gl2.COLOR_ATTACHMENT1,
|
|
522
|
+
gl2.TEXTURE_2D,
|
|
523
|
+
tex,
|
|
524
|
+
0,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Compile (and cache) the single-target extreme splat program — the
|
|
533
|
+
* fallback two-pass path's second pass. Reuses the splat vertex
|
|
534
|
+
* shader so `v_color_t` semantics match the heat pass.
|
|
535
|
+
*/
|
|
536
|
+
private ensureExtremeSplatProgram(
|
|
537
|
+
glManager: WebGLContextManager,
|
|
538
|
+
cache: DensityCache,
|
|
539
|
+
): SplatProgramCache {
|
|
540
|
+
if (cache.extremeSplat) {
|
|
541
|
+
return cache.extremeSplat;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const program = glManager.shaders.getOrCreate(
|
|
545
|
+
"density-extreme",
|
|
546
|
+
splatVert,
|
|
547
|
+
extremeFrag,
|
|
548
|
+
);
|
|
549
|
+
cache.extremeSplat = extractSplatLocations(glManager.gl, program);
|
|
550
|
+
return cache.extremeSplat;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Compile (and cache) the MRT splat program. Only safe to call
|
|
555
|
+
* after `cache.indexedBlend` resolves truthy — the program's
|
|
556
|
+
* `#extension GL_EXT_draw_buffers : require` would fail to
|
|
557
|
+
* compile on contexts without multi-render-target support.
|
|
558
|
+
*/
|
|
559
|
+
private ensureMrtSplatProgram(
|
|
560
|
+
glManager: WebGLContextManager,
|
|
561
|
+
cache: DensityCache,
|
|
562
|
+
): SplatProgramCache {
|
|
563
|
+
if (cache.mrtSplat) {
|
|
564
|
+
return cache.mrtSplat;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// The MRT frag is GLSL ES 3.00 (`layout(location=N) out vec4`);
|
|
568
|
+
// the legacy GLSL 100 splat vert can't link against it because
|
|
569
|
+
// a program's shaders must share a version. Use the paired
|
|
570
|
+
// `density-mrt.vert.glsl` instead — same math, 300 ES dialect.
|
|
571
|
+
const program = glManager.shaders.getOrCreate(
|
|
572
|
+
"density-mrt",
|
|
573
|
+
mrtVert,
|
|
574
|
+
mrtFrag,
|
|
575
|
+
);
|
|
576
|
+
cache.mrtSplat = extractSplatLocations(glManager.gl, program);
|
|
577
|
+
return cache.mrtSplat;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Resolve the active mode for this frame. Folds in the silent
|
|
582
|
+
* downgrades to `mean` with a one-shot console warning:
|
|
583
|
+
*
|
|
584
|
+
* - `signed` requires a float-capable framebuffer
|
|
585
|
+
* (`EXT_color_buffer_float` on WebGL2 in practice).
|
|
586
|
+
* - `extreme` requires `gl.MAX` blend and a second color
|
|
587
|
+
* attachment, both of which are WebGL2-only here. On WebGL1
|
|
588
|
+
* we could probe `EXT_blend_minmax` + `WEBGL_draw_buffers`
|
|
589
|
+
* but degrading is simpler and the context manager prefers
|
|
590
|
+
* WebGL2 already.
|
|
591
|
+
*/
|
|
592
|
+
private activeMode(
|
|
593
|
+
glManager: WebGLContextManager,
|
|
594
|
+
chart: CartesianChart,
|
|
595
|
+
cache: DensityCache,
|
|
596
|
+
): ColorMode {
|
|
597
|
+
const requested = chart._pluginConfig.gradient_color_mode;
|
|
598
|
+
if (requested === "signed" && !cache.floatFbo) {
|
|
599
|
+
this.warnDowngradeOnce(
|
|
600
|
+
cache,
|
|
601
|
+
"signed mode requires a float framebuffer (EXT_color_buffer_float); falling back to mean.",
|
|
602
|
+
);
|
|
603
|
+
return "mean";
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (requested === "extreme" && !glManager.isWebGL2) {
|
|
607
|
+
this.warnDowngradeOnce(
|
|
608
|
+
cache,
|
|
609
|
+
"extreme mode requires WebGL2 (for MAX blend and a second color attachment); falling back to mean.",
|
|
610
|
+
);
|
|
611
|
+
return "mean";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return requested;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private warnDowngradeOnce(cache: DensityCache, message: string): void {
|
|
618
|
+
if (cache.signedDowngradeWarned) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
cache.signedDowngradeWarned = true;
|
|
623
|
+
console.warn(`Density: ${message}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Shared splat → resolve pipeline. `dispatchSplats(cb)` iterates
|
|
628
|
+
* the series ranges the caller wants drawn, invoking
|
|
629
|
+
* `cb(slotOffset, count)` per range — `drawSeries` passes a single
|
|
630
|
+
* range, `draw` iterates every series. Internally branches on the
|
|
631
|
+
* active color mode: density/mean/signed share the single-target
|
|
632
|
+
* heat-only pass, `extreme` runs either an MRT single-pass or two
|
|
633
|
+
* sequential passes depending on extension support.
|
|
634
|
+
*/
|
|
635
|
+
private runSplatAndResolve(
|
|
636
|
+
chart: CartesianChart,
|
|
637
|
+
glManager: WebGLContextManager,
|
|
638
|
+
cache: DensityCache,
|
|
639
|
+
projection: Float32Array,
|
|
640
|
+
dispatchSplats: (
|
|
641
|
+
cb: (slotOffset: number, count: number) => void,
|
|
642
|
+
) => void,
|
|
643
|
+
): void {
|
|
644
|
+
this.ensureHeatTarget(chart, glManager);
|
|
645
|
+
if (cache.heatWidth === 0 || cache.heatHeight === 0) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!chart._gradientCache) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const mode = this.activeMode(glManager, chart, cache);
|
|
654
|
+
|
|
655
|
+
// Resolve the color range we want the splat shader to use for
|
|
656
|
+
// its per-point `t` mapping. Robust bounds apply to modes that
|
|
657
|
+
// consume `t` directly (`mean`, `extreme`); `signed` actively
|
|
658
|
+
// benefits from raw extents so outlier influence accumulates;
|
|
659
|
+
// `density` ignores color entirely.
|
|
660
|
+
const hasColor =
|
|
661
|
+
chart._colorMin < chart._colorMax &&
|
|
662
|
+
(!!chart._colorName || chart._splitGroups.length > 1);
|
|
663
|
+
let cmin = 0.0;
|
|
664
|
+
let cmax = 0.0;
|
|
665
|
+
if (mode !== "density" && hasColor) {
|
|
666
|
+
cmin = chart._colorMin;
|
|
667
|
+
cmax = chart._colorMax;
|
|
668
|
+
const useRobust =
|
|
669
|
+
!chart._colorIsString &&
|
|
670
|
+
(mode === "mean" || mode === "extreme");
|
|
671
|
+
if (useRobust) {
|
|
672
|
+
const robust = ensureRobustBounds(chart, cache);
|
|
673
|
+
if (robust) {
|
|
674
|
+
cmin = robust.lo;
|
|
675
|
+
cmax = robust.hi;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (mode === "extreme") {
|
|
681
|
+
this.ensureExtremeTarget(glManager, cache);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (mode === "extreme" && cache.indexedBlend) {
|
|
685
|
+
this.runMrtExtremePass(
|
|
686
|
+
glManager,
|
|
687
|
+
cache,
|
|
688
|
+
projection,
|
|
689
|
+
chart._pluginConfig.gradient_intensity,
|
|
690
|
+
glManager.dpr * chart._pluginConfig.gradient_radius_px,
|
|
691
|
+
cmin,
|
|
692
|
+
cmax,
|
|
693
|
+
dispatchSplats,
|
|
694
|
+
);
|
|
695
|
+
} else {
|
|
696
|
+
this.runHeatPass(
|
|
697
|
+
glManager,
|
|
698
|
+
cache,
|
|
699
|
+
projection,
|
|
700
|
+
chart._pluginConfig.gradient_intensity,
|
|
701
|
+
glManager.dpr * chart._pluginConfig.gradient_radius_px,
|
|
702
|
+
cmin,
|
|
703
|
+
cmax,
|
|
704
|
+
dispatchSplats,
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
if (mode === "extreme") {
|
|
708
|
+
this.runExtremePass(
|
|
709
|
+
glManager,
|
|
710
|
+
cache,
|
|
711
|
+
projection,
|
|
712
|
+
chart._pluginConfig.gradient_intensity,
|
|
713
|
+
glManager.dpr * chart._pluginConfig.gradient_radius_px,
|
|
714
|
+
cmin,
|
|
715
|
+
cmax,
|
|
716
|
+
dispatchSplats,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
this.runResolvePass(glManager, cache, chart, mode);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Single-target accumulation into the heat FBO. ADD blend; writes
|
|
726
|
+
* `(w, w·t, 0, 0)`. Used by every mode except `extreme` on the
|
|
727
|
+
* MRT path (which does this work and the extreme pass in one go).
|
|
728
|
+
*/
|
|
729
|
+
private runHeatPass(
|
|
730
|
+
glManager: WebGLContextManager,
|
|
731
|
+
cache: DensityCache,
|
|
732
|
+
projection: Float32Array,
|
|
733
|
+
intensity: number,
|
|
734
|
+
radiusPx: number,
|
|
735
|
+
cmin: number,
|
|
736
|
+
cmax: number,
|
|
737
|
+
dispatchSplats: (
|
|
738
|
+
cb: (slotOffset: number, count: number) => void,
|
|
739
|
+
) => void,
|
|
740
|
+
): void {
|
|
741
|
+
const gl = glManager.gl;
|
|
742
|
+
const wasScissor = !!gl.getParameter(gl.SCISSOR_TEST);
|
|
743
|
+
|
|
744
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, cache.heatFramebuffer);
|
|
745
|
+
gl.viewport(0, 0, cache.heatWidth, cache.heatHeight);
|
|
746
|
+
this.clearTarget(gl, wasScissor);
|
|
747
|
+
|
|
748
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
749
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
750
|
+
|
|
751
|
+
this.bindSplatProgram(
|
|
752
|
+
gl,
|
|
753
|
+
cache.splat,
|
|
754
|
+
projection,
|
|
755
|
+
intensity,
|
|
756
|
+
radiusPx,
|
|
757
|
+
cache.heatWidth,
|
|
758
|
+
cache.heatHeight,
|
|
759
|
+
cmin,
|
|
760
|
+
cmax,
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
this.bindAndDispatchInstanced(
|
|
764
|
+
glManager,
|
|
765
|
+
cache,
|
|
766
|
+
cache.splat,
|
|
767
|
+
dispatchSplats,
|
|
768
|
+
);
|
|
769
|
+
this.unbindSplatInstancing(glManager, cache.splat);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Second pass of the two-pass extreme path. MAX blend; writes
|
|
774
|
+
* sign-split deviation magnitudes into the extreme FBO. Skipped
|
|
775
|
+
* entirely on the MRT fast path.
|
|
776
|
+
*/
|
|
777
|
+
private runExtremePass(
|
|
778
|
+
glManager: WebGLContextManager,
|
|
779
|
+
cache: DensityCache,
|
|
780
|
+
projection: Float32Array,
|
|
781
|
+
intensity: number,
|
|
782
|
+
radiusPx: number,
|
|
783
|
+
cmin: number,
|
|
784
|
+
cmax: number,
|
|
785
|
+
dispatchSplats: (
|
|
786
|
+
cb: (slotOffset: number, count: number) => void,
|
|
787
|
+
) => void,
|
|
788
|
+
): void {
|
|
789
|
+
const gl = glManager.gl;
|
|
790
|
+
const wasScissor = !!gl.getParameter(gl.SCISSOR_TEST);
|
|
791
|
+
const program = this.ensureExtremeSplatProgram(glManager, cache);
|
|
792
|
+
|
|
793
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, cache.extremeFramebuffer!);
|
|
794
|
+
gl.viewport(0, 0, cache.heatWidth, cache.heatHeight);
|
|
795
|
+
this.clearTarget(gl, wasScissor);
|
|
796
|
+
|
|
797
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
798
|
+
// `MAX` is WebGL2-only on the type; `activeMode` gates the
|
|
799
|
+
// extreme path on WebGL2 so the cast is safe at runtime.
|
|
800
|
+
const gl2 = gl as WebGL2RenderingContext;
|
|
801
|
+
gl2.blendEquation(gl2.MAX);
|
|
802
|
+
|
|
803
|
+
this.bindSplatProgram(
|
|
804
|
+
gl,
|
|
805
|
+
program,
|
|
806
|
+
projection,
|
|
807
|
+
intensity,
|
|
808
|
+
radiusPx,
|
|
809
|
+
cache.heatWidth,
|
|
810
|
+
cache.heatHeight,
|
|
811
|
+
cmin,
|
|
812
|
+
cmax,
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
this.bindAndDispatchInstanced(
|
|
816
|
+
glManager,
|
|
817
|
+
cache,
|
|
818
|
+
program,
|
|
819
|
+
dispatchSplats,
|
|
820
|
+
);
|
|
821
|
+
this.unbindSplatInstancing(glManager, program);
|
|
822
|
+
|
|
823
|
+
// Restore default ADD equation for downstream callers.
|
|
824
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* MRT fast path: one splat draw writes density (ADD) and extreme
|
|
829
|
+
* (MAX) in the same invocation by routing `gl_FragData[0]` /
|
|
830
|
+
* `gl_FragData[1]` to attachments 0 and 1 with per-attachment
|
|
831
|
+
* blend equations.
|
|
832
|
+
*/
|
|
833
|
+
private runMrtExtremePass(
|
|
834
|
+
glManager: WebGLContextManager,
|
|
835
|
+
cache: DensityCache,
|
|
836
|
+
projection: Float32Array,
|
|
837
|
+
intensity: number,
|
|
838
|
+
radiusPx: number,
|
|
839
|
+
cmin: number,
|
|
840
|
+
cmax: number,
|
|
841
|
+
dispatchSplats: (
|
|
842
|
+
cb: (slotOffset: number, count: number) => void,
|
|
843
|
+
) => void,
|
|
844
|
+
): void {
|
|
845
|
+
const gl = glManager.gl as WebGL2RenderingContext;
|
|
846
|
+
const ext = cache.indexedBlend as IndexedBlendExt;
|
|
847
|
+
const wasScissor = !!gl.getParameter(gl.SCISSOR_TEST);
|
|
848
|
+
const program = this.ensureMrtSplatProgram(glManager, cache);
|
|
849
|
+
|
|
850
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, cache.mrtFramebuffer!);
|
|
851
|
+
gl.viewport(0, 0, cache.heatWidth, cache.heatHeight);
|
|
852
|
+
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
|
|
853
|
+
this.clearTarget(gl, wasScissor);
|
|
854
|
+
|
|
855
|
+
// Per-attachment blend: ADD for density, MAX for extreme.
|
|
856
|
+
ext.enableiOES(gl.BLEND, 0);
|
|
857
|
+
ext.enableiOES(gl.BLEND, 1);
|
|
858
|
+
ext.blendEquationiOES(0, gl.FUNC_ADD);
|
|
859
|
+
ext.blendFunciOES(0, gl.ONE, gl.ONE);
|
|
860
|
+
ext.blendEquationiOES(1, gl.MAX);
|
|
861
|
+
ext.blendFunciOES(1, gl.ONE, gl.ONE);
|
|
862
|
+
|
|
863
|
+
this.bindSplatProgram(
|
|
864
|
+
gl,
|
|
865
|
+
program,
|
|
866
|
+
projection,
|
|
867
|
+
intensity,
|
|
868
|
+
radiusPx,
|
|
869
|
+
cache.heatWidth,
|
|
870
|
+
cache.heatHeight,
|
|
871
|
+
cmin,
|
|
872
|
+
cmax,
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
this.bindAndDispatchInstanced(
|
|
876
|
+
glManager,
|
|
877
|
+
cache,
|
|
878
|
+
program,
|
|
879
|
+
dispatchSplats,
|
|
880
|
+
);
|
|
881
|
+
this.unbindSplatInstancing(glManager, program);
|
|
882
|
+
|
|
883
|
+
// Restore the global default for both attachments — subsequent
|
|
884
|
+
// single-target draws (resolve, other charts) rely on it. The
|
|
885
|
+
// indexed extension leaks state across attachments otherwise.
|
|
886
|
+
ext.blendEquationiOES(0, gl.FUNC_ADD);
|
|
887
|
+
ext.blendEquationiOES(1, gl.FUNC_ADD);
|
|
888
|
+
ext.blendFunciOES(0, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
889
|
+
ext.blendFunciOES(1, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
890
|
+
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Clear the currently bound framebuffer's color attachment(s) to
|
|
895
|
+
* fully transparent, bypassing scissor so leftovers from a prior
|
|
896
|
+
* facet's region don't bleed into this pass's full sample range.
|
|
897
|
+
* Restores the scissor state on exit.
|
|
898
|
+
*/
|
|
899
|
+
private clearTarget(
|
|
900
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
901
|
+
wasScissor: boolean,
|
|
902
|
+
): void {
|
|
903
|
+
if (wasScissor) {
|
|
904
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
gl.clearColor(0, 0, 0, 0);
|
|
908
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
909
|
+
|
|
910
|
+
if (wasScissor) {
|
|
911
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Upload the per-frame splat-program uniforms (projection, splat
|
|
917
|
+
* radius, intensity, color range). Shared by the heat-only pass,
|
|
918
|
+
* the extreme single-target pass, and the MRT pass since each
|
|
919
|
+
* program exposes the same uniform layout.
|
|
920
|
+
*/
|
|
921
|
+
private bindSplatProgram(
|
|
922
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
923
|
+
cache: SplatProgramCache,
|
|
924
|
+
projection: Float32Array,
|
|
925
|
+
intensity: number,
|
|
926
|
+
radiusPx: number,
|
|
927
|
+
targetWidth: number,
|
|
928
|
+
targetHeight: number,
|
|
929
|
+
cmin: number,
|
|
930
|
+
cmax: number,
|
|
931
|
+
): void {
|
|
932
|
+
gl.useProgram(cache.program);
|
|
933
|
+
gl.uniformMatrix4fv(cache.u_projection, false, projection);
|
|
934
|
+
gl.uniform1f(cache.u_intensity, intensity);
|
|
935
|
+
|
|
936
|
+
const radiusNdcX = (2 * radiusPx) / Math.max(1, targetWidth);
|
|
937
|
+
const radiusNdcY = (2 * radiusPx) / Math.max(1, targetHeight);
|
|
938
|
+
gl.uniform2f(cache.u_radius_ndc, radiusNdcX, radiusNdcY);
|
|
939
|
+
gl.uniform2f(cache.u_color_range, cmin, cmax);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Bind the static unit-quad corner buffer (divisor 0) and per-
|
|
944
|
+
* instance position + color attributes (divisor 1), then iterate
|
|
945
|
+
* the caller's series ranges issuing one instanced draw each.
|
|
946
|
+
*/
|
|
947
|
+
private bindAndDispatchInstanced(
|
|
948
|
+
glManager: WebGLContextManager,
|
|
949
|
+
cache: DensityCache,
|
|
950
|
+
program: SplatProgramCache,
|
|
951
|
+
dispatchSplats: (
|
|
952
|
+
cb: (slotOffset: number, count: number) => void,
|
|
953
|
+
) => void,
|
|
954
|
+
): void {
|
|
955
|
+
const gl = glManager.gl;
|
|
956
|
+
|
|
957
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, cache.quadCornerBuffer);
|
|
958
|
+
gl.enableVertexAttribArray(program.a_corner);
|
|
959
|
+
gl.vertexAttribPointer(program.a_corner, 2, gl.FLOAT, false, 0, 0);
|
|
960
|
+
|
|
961
|
+
const instancing = getInstancing(glManager);
|
|
962
|
+
instancing.setDivisor(program.a_corner, 0);
|
|
963
|
+
instancing.setDivisor(program.a_position, 1);
|
|
964
|
+
instancing.setDivisor(program.a_color_value, 1);
|
|
965
|
+
|
|
966
|
+
const posBuf = glManager.bufferPool.peek("a_position")!;
|
|
967
|
+
const colorBuf = glManager.bufferPool.peek("a_color_value")!;
|
|
968
|
+
|
|
969
|
+
dispatchSplats((slotOffset, count) => {
|
|
970
|
+
const posStride = 2 * Float32Array.BYTES_PER_ELEMENT;
|
|
971
|
+
const scalarStride = Float32Array.BYTES_PER_ELEMENT;
|
|
972
|
+
|
|
973
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf.buffer);
|
|
974
|
+
gl.enableVertexAttribArray(program.a_position);
|
|
975
|
+
gl.vertexAttribPointer(
|
|
976
|
+
program.a_position,
|
|
977
|
+
2,
|
|
978
|
+
gl.FLOAT,
|
|
979
|
+
false,
|
|
980
|
+
posStride,
|
|
981
|
+
slotOffset * posStride,
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf.buffer);
|
|
985
|
+
gl.enableVertexAttribArray(program.a_color_value);
|
|
986
|
+
gl.vertexAttribPointer(
|
|
987
|
+
program.a_color_value,
|
|
988
|
+
1,
|
|
989
|
+
gl.FLOAT,
|
|
990
|
+
false,
|
|
991
|
+
scalarStride,
|
|
992
|
+
slotOffset * scalarStride,
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Reset the per-instance divisors so subsequent draws (in this or
|
|
1001
|
+
* another chart) don't inherit the instanced bindings.
|
|
1002
|
+
*/
|
|
1003
|
+
private unbindSplatInstancing(
|
|
1004
|
+
glManager: WebGLContextManager,
|
|
1005
|
+
program: SplatProgramCache,
|
|
1006
|
+
): void {
|
|
1007
|
+
const instancing = getInstancing(glManager);
|
|
1008
|
+
instancing.setDivisor(program.a_position, 0);
|
|
1009
|
+
instancing.setDivisor(program.a_color_value, 0);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Resolve pass on the canvas FBO. Standard alpha composite. Reads
|
|
1014
|
+
* the heat FBO (always) and, in `extreme` mode, the extreme FBO.
|
|
1015
|
+
* Uploads the mode int that the resolve frag branches on.
|
|
1016
|
+
*/
|
|
1017
|
+
private runResolvePass(
|
|
1018
|
+
glManager: WebGLContextManager,
|
|
1019
|
+
cache: DensityCache,
|
|
1020
|
+
chart: CartesianChart,
|
|
1021
|
+
mode: ColorMode,
|
|
1022
|
+
): void {
|
|
1023
|
+
const gl = glManager.gl;
|
|
1024
|
+
|
|
1025
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1026
|
+
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
1027
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
1028
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1029
|
+
|
|
1030
|
+
const resolve = cache.resolve;
|
|
1031
|
+
gl.useProgram(resolve.program);
|
|
1032
|
+
gl.uniform1f(resolve.u_heat_max, chart._pluginConfig.gradient_heat_max);
|
|
1033
|
+
gl.uniform1i(resolve.u_color_mode, modeToInt(mode));
|
|
1034
|
+
|
|
1035
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1036
|
+
gl.bindTexture(gl.TEXTURE_2D, cache.heatTexture);
|
|
1037
|
+
gl.uniform1i(resolve.u_heat, 0);
|
|
1038
|
+
|
|
1039
|
+
// The shader unconditionally samples `u_extreme` in the extreme
|
|
1040
|
+
// branch. Bind whatever we have (the heat texture as a no-op
|
|
1041
|
+
// bind in non-extreme modes) so the unit stays defined and
|
|
1042
|
+
// texture-completeness checks pass.
|
|
1043
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1044
|
+
gl.bindTexture(
|
|
1045
|
+
gl.TEXTURE_2D,
|
|
1046
|
+
cache.extremeTexture ?? cache.heatTexture,
|
|
1047
|
+
);
|
|
1048
|
+
gl.uniform1i(resolve.u_extreme, 1);
|
|
1049
|
+
|
|
1050
|
+
bindGradientTexture(
|
|
1051
|
+
glManager,
|
|
1052
|
+
chart._gradientCache!.texture,
|
|
1053
|
+
resolve.u_gradient_lut,
|
|
1054
|
+
2,
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, cache.tripleCornerBuffer);
|
|
1058
|
+
gl.enableVertexAttribArray(resolve.a_corner);
|
|
1059
|
+
gl.vertexAttribPointer(resolve.a_corner, 2, gl.FLOAT, false, 0, 0);
|
|
1060
|
+
|
|
1061
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function modeToInt(mode: ColorMode): number {
|
|
1066
|
+
switch (mode) {
|
|
1067
|
+
case "density":
|
|
1068
|
+
return MODE_DENSITY;
|
|
1069
|
+
case "extreme":
|
|
1070
|
+
return MODE_EXTREME;
|
|
1071
|
+
case "signed":
|
|
1072
|
+
return MODE_SIGNED;
|
|
1073
|
+
case "mean":
|
|
1074
|
+
default:
|
|
1075
|
+
return MODE_MEAN;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function createAccumTexture(
|
|
1080
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
1081
|
+
): WebGLTexture {
|
|
1082
|
+
const tex = gl.createTexture()!;
|
|
1083
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1084
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1085
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1086
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1087
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1088
|
+
return tex;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function extractSplatLocations(
|
|
1092
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
1093
|
+
program: WebGLProgram,
|
|
1094
|
+
): SplatProgramCache {
|
|
1095
|
+
return {
|
|
1096
|
+
program,
|
|
1097
|
+
u_projection: gl.getUniformLocation(program, "u_projection"),
|
|
1098
|
+
u_radius_ndc: gl.getUniformLocation(program, "u_radius_ndc"),
|
|
1099
|
+
u_intensity: gl.getUniformLocation(program, "u_intensity"),
|
|
1100
|
+
u_color_range: gl.getUniformLocation(program, "u_color_range"),
|
|
1101
|
+
a_corner: gl.getAttribLocation(program, "a_corner"),
|
|
1102
|
+
a_position: gl.getAttribLocation(program, "a_position"),
|
|
1103
|
+
a_color_value: gl.getAttribLocation(program, "a_color_value"),
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Resolve the highest-precision float color buffer the running GL
|
|
1109
|
+
* context will accept. WebGL2 + `EXT_color_buffer_float` gives
|
|
1110
|
+
* RGBA16F; otherwise fall back to RGBA8. The fallback compresses
|
|
1111
|
+
* density into [0, 1] and saturates earlier; `signed` mode degrades
|
|
1112
|
+
* to `mean` on this path because its `G - 0.5·R` math depends on
|
|
1113
|
+
* unclamped accumulation.
|
|
1114
|
+
*/
|
|
1115
|
+
function pickHeatFormat(glManager: WebGLContextManager): {
|
|
1116
|
+
internalFormat: number;
|
|
1117
|
+
format: number;
|
|
1118
|
+
type: number;
|
|
1119
|
+
isFloat: boolean;
|
|
1120
|
+
} {
|
|
1121
|
+
const gl = glManager.gl;
|
|
1122
|
+
if (glManager.isWebGL2) {
|
|
1123
|
+
const gl2 = gl as WebGL2RenderingContext;
|
|
1124
|
+
if (gl2.getExtension("EXT_color_buffer_float")) {
|
|
1125
|
+
return {
|
|
1126
|
+
internalFormat: gl2.RGBA16F,
|
|
1127
|
+
format: gl2.RGBA,
|
|
1128
|
+
type: gl2.HALF_FLOAT,
|
|
1129
|
+
isFloat: true,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
internalFormat: gl.RGBA,
|
|
1136
|
+
format: gl.RGBA,
|
|
1137
|
+
type: gl.UNSIGNED_BYTE,
|
|
1138
|
+
isFloat: false,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Verify the shared cartesian position + color attribute buffers exist.
|
|
1144
|
+
* The cartesian build pipeline uploads them on each chunk; render-path
|
|
1145
|
+
* callers must use `peek` (never `getOrCreate`) so a pan/zoom render
|
|
1146
|
+
* landing between an `ensureBufferCapacity` and its `uploadChunk`
|
|
1147
|
+
* doesn't recreate the buffer with zeros.
|
|
1148
|
+
*/
|
|
1149
|
+
function ensurePointBuffers(glManager: WebGLContextManager): boolean {
|
|
1150
|
+
const pos = glManager.bufferPool.peek("a_position");
|
|
1151
|
+
const color = glManager.bufferPool.peek("a_color_value");
|
|
1152
|
+
return !!pos && !!color;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Cap for the strided sample used to compute the 5th/95th percentile
|
|
1157
|
+
* color-column bounds. A larger sample tightens the quantile estimate
|
|
1158
|
+
* but costs O(n log n) sort time. At 50k the sort runs ~10ms once per
|
|
1159
|
+
* data refresh; subsequent renders hit the cache.
|
|
1160
|
+
*/
|
|
1161
|
+
const ROBUST_SAMPLE_MAX = 50_000;
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Resolve the robust (5th/95th percentile) bounds for the color column,
|
|
1165
|
+
* reading from the cache when `(dataCount, colorName, colorIsString)`
|
|
1166
|
+
* hasn't changed since the last compute. Returns `null` when robust
|
|
1167
|
+
* clipping doesn't apply — no color column, categorical column (exact
|
|
1168
|
+
* palette indices), or a degenerate sample.
|
|
1169
|
+
*/
|
|
1170
|
+
function ensureRobustBounds(
|
|
1171
|
+
chart: CartesianChart,
|
|
1172
|
+
cache: DensityCache,
|
|
1173
|
+
): { lo: number; hi: number } | null {
|
|
1174
|
+
if (!chart._colorName || chart._colorIsString) {
|
|
1175
|
+
cache.robustBounds = null;
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const cur = cache.robustBounds;
|
|
1180
|
+
if (
|
|
1181
|
+
cur &&
|
|
1182
|
+
cur.dataCount === chart._dataCount &&
|
|
1183
|
+
cur.colorName === chart._colorName &&
|
|
1184
|
+
cur.colorIsString === chart._colorIsString
|
|
1185
|
+
) {
|
|
1186
|
+
return { lo: cur.lo, hi: cur.hi };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const computed = computeRobustBounds(chart);
|
|
1190
|
+
if (!computed) {
|
|
1191
|
+
cache.robustBounds = null;
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
cache.robustBounds = {
|
|
1196
|
+
lo: computed.lo,
|
|
1197
|
+
hi: computed.hi,
|
|
1198
|
+
dataCount: chart._dataCount,
|
|
1199
|
+
colorName: chart._colorName,
|
|
1200
|
+
colorIsString: chart._colorIsString,
|
|
1201
|
+
};
|
|
1202
|
+
return computed;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Sample `chart._colorData` along its slotted per-series ranges, sort
|
|
1207
|
+
* the strided sample, and return the 5th/95th percentile values. The
|
|
1208
|
+
* sample skips unused tail slots (per-series `_seriesUploadedCounts`
|
|
1209
|
+
* cap) so split mode doesn't pollute the distribution with default
|
|
1210
|
+
* `0.5` placeholders.
|
|
1211
|
+
*
|
|
1212
|
+
* Falls back to raw `_colorMin`/`_colorMax` when the quantile sample
|
|
1213
|
+
* collapses to a single value — otherwise a zero-width range would
|
|
1214
|
+
* trip the splat shader's `cmax <= cmin` branch and paint every
|
|
1215
|
+
* point at t=0.5.
|
|
1216
|
+
*/
|
|
1217
|
+
function computeRobustBounds(
|
|
1218
|
+
chart: CartesianChart,
|
|
1219
|
+
): { lo: number; hi: number } | null {
|
|
1220
|
+
if (!chart._colorData || chart._dataCount < 2) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const cap = chart._seriesCapacity;
|
|
1225
|
+
const numSeries = Math.max(1, chart._splitGroups.length);
|
|
1226
|
+
const stride = Math.max(1, Math.ceil(chart._dataCount / ROBUST_SAMPLE_MAX));
|
|
1227
|
+
|
|
1228
|
+
const samples: number[] = [];
|
|
1229
|
+
const data = chart._colorData;
|
|
1230
|
+
for (let s = 0; s < numSeries; s++) {
|
|
1231
|
+
const count = chart._seriesUploadedCounts[s] ?? 0;
|
|
1232
|
+
const base = s * cap;
|
|
1233
|
+
for (let j = 0; j < count; j += stride) {
|
|
1234
|
+
const v = data[base + j];
|
|
1235
|
+
if (Number.isFinite(v)) {
|
|
1236
|
+
samples.push(v);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (samples.length < 2) {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
samples.sort((a, b) => a - b);
|
|
1246
|
+
const loIdx = Math.floor(samples.length * 0.05);
|
|
1247
|
+
const hiIdx = Math.min(
|
|
1248
|
+
samples.length - 1,
|
|
1249
|
+
Math.ceil(samples.length * 0.95),
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
const lo = samples[loIdx];
|
|
1253
|
+
const hi = samples[hiIdx];
|
|
1254
|
+
if (!(hi > lo)) {
|
|
1255
|
+
if (chart._colorMax > chart._colorMin) {
|
|
1256
|
+
return { lo: chart._colorMin, hi: chart._colorMax };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return { lo, hi };
|
|
1263
|
+
}
|