@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,815 @@
|
|
|
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 { HeatmapChart } from "./heatmap";
|
|
15
|
+
import { PlotLayout } from "../../layout/plot-layout";
|
|
16
|
+
import { drawFacetTitle } from "../../axis/facet-chrome";
|
|
17
|
+
import {
|
|
18
|
+
renderInPlotFrame,
|
|
19
|
+
clearAndSetupFrame,
|
|
20
|
+
withScissor,
|
|
21
|
+
} from "../../webgl/plot-frame";
|
|
22
|
+
import { getInstancing } from "../../webgl/instanced-attrs";
|
|
23
|
+
import { initCanvas } from "../../axis/canvas";
|
|
24
|
+
import { buildFacetGrid } from "../../layout/facet-grid";
|
|
25
|
+
import {
|
|
26
|
+
measureCategoricalAxisHeight,
|
|
27
|
+
renderCategoricalXTicks,
|
|
28
|
+
type CategoricalDomain,
|
|
29
|
+
} from "../../axis/categorical-axis";
|
|
30
|
+
import {
|
|
31
|
+
measureCategoricalAxisWidth,
|
|
32
|
+
renderCategoricalYTicks,
|
|
33
|
+
type CategoricalYAxisOptions,
|
|
34
|
+
} from "./heatmap-y-axis";
|
|
35
|
+
import {
|
|
36
|
+
drawNumericCategoryX,
|
|
37
|
+
drawNumericCategoryY,
|
|
38
|
+
} from "../../axis/bar-axis";
|
|
39
|
+
import { computeNiceTicks } from "../../layout/ticks";
|
|
40
|
+
|
|
41
|
+
// The heatmap's Y-axis column names end with the (single, externally
|
|
42
|
+
// enforced) aggregate name. That leaf column is a redundant constant and
|
|
43
|
+
// doesn't belong on the axis — promote the deepest split prefix to the
|
|
44
|
+
// leaf position instead.
|
|
45
|
+
const HEATMAP_Y_AXIS_OPTS: CategoricalYAxisOptions = {
|
|
46
|
+
skipLeafLevel: true,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
import { renderLegend, renderLegendAt } from "../../axis/legend";
|
|
50
|
+
import heatmapVert from "../../shaders/heatmap.vert.glsl";
|
|
51
|
+
import heatmapFrag from "../../shaders/heatmap.frag.glsl";
|
|
52
|
+
import { colorValueToT } from "../../theme/gradient";
|
|
53
|
+
import {
|
|
54
|
+
bindGradientTexture,
|
|
55
|
+
ensureGradientTexture,
|
|
56
|
+
} from "../../webgl/gradient-texture";
|
|
57
|
+
import { renderHeatmapTooltip } from "./heatmap-interact";
|
|
58
|
+
|
|
59
|
+
export interface HeatmapLocations {
|
|
60
|
+
u_projection: WebGLUniformLocation | null;
|
|
61
|
+
u_cell_inset: WebGLUniformLocation | null;
|
|
62
|
+
u_cell_size: WebGLUniformLocation | null;
|
|
63
|
+
u_gradient_lut: WebGLUniformLocation | null;
|
|
64
|
+
a_corner: number;
|
|
65
|
+
a_cell: number;
|
|
66
|
+
a_color_t: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Full-frame render: WebGL cells → chrome overlay.
|
|
71
|
+
*/
|
|
72
|
+
export function renderHeatmapFrame(
|
|
73
|
+
chart: HeatmapChart,
|
|
74
|
+
glManager: WebGLContextManager,
|
|
75
|
+
): void {
|
|
76
|
+
const gl = glManager.gl;
|
|
77
|
+
const dpr = glManager.dpr;
|
|
78
|
+
const cssWidth = gl.canvas.width / dpr;
|
|
79
|
+
const cssHeight = gl.canvas.height / dpr;
|
|
80
|
+
if (cssWidth <= 0 || cssHeight <= 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (chart._facets.length > 0) {
|
|
85
|
+
renderFacetedHeatmap(chart, glManager, cssWidth, cssHeight);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (chart._numX === 0 || chart._numY === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const theme = chart._resolveTheme();
|
|
94
|
+
|
|
95
|
+
const xDomain: CategoricalDomain = {
|
|
96
|
+
levels: chart._xLevels,
|
|
97
|
+
numRows: chart._numX,
|
|
98
|
+
levelLabels: chart._groupBy.slice(),
|
|
99
|
+
};
|
|
100
|
+
const yDomain: CategoricalDomain = {
|
|
101
|
+
levels: chart._yLevels,
|
|
102
|
+
numRows: chart._numY,
|
|
103
|
+
levelLabels: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const xNumeric = chart._xAxisMode.mode === "numeric";
|
|
107
|
+
const yNumeric = chart._yAxisMode.mode === "numeric";
|
|
108
|
+
|
|
109
|
+
// Measure both hierarchical axes *before* building the layout so the
|
|
110
|
+
// plot rect accounts for their footprints. Numeric axes get fixed
|
|
111
|
+
// gutters matching bar's branch (24px bottom, 55px left).
|
|
112
|
+
const estLeft = yNumeric
|
|
113
|
+
? 55
|
|
114
|
+
: measureCategoricalAxisWidth(yDomain, HEATMAP_Y_AXIS_OPTS);
|
|
115
|
+
const bottomExtra = xNumeric
|
|
116
|
+
? 24
|
|
117
|
+
: measureCategoricalAxisHeight(
|
|
118
|
+
xDomain,
|
|
119
|
+
Math.max(1, cssWidth - estLeft - 110),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const layout = new PlotLayout(cssWidth, cssHeight, {
|
|
123
|
+
hasXLabel: chart._groupBy.length > 0,
|
|
124
|
+
hasYLabel: false,
|
|
125
|
+
hasLegend: true,
|
|
126
|
+
bottomExtra,
|
|
127
|
+
leftExtra: estLeft,
|
|
128
|
+
});
|
|
129
|
+
chart._lastLayout = layout;
|
|
130
|
+
if (chart._zoomController) {
|
|
131
|
+
chart._zoomController.updateLayout(layout);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Domain depends on axis mode. Category mode: cell grid
|
|
135
|
+
// `[-0.5, N-0.5]` so cells sit at integer coordinates. Numeric mode:
|
|
136
|
+
// pre-padded `numericDomain` already includes a half-band on each
|
|
137
|
+
// edge so cells stay flush with the axis.
|
|
138
|
+
const xDomainMin = xNumeric ? chart._xNumericDomain!.min : -0.5;
|
|
139
|
+
const xDomainMax = xNumeric
|
|
140
|
+
? chart._xNumericDomain!.max
|
|
141
|
+
: chart._numX - 0.5;
|
|
142
|
+
const yDomainMin = yNumeric ? chart._yNumericDomain!.min : -0.5;
|
|
143
|
+
const yDomainMax = yNumeric
|
|
144
|
+
? chart._yNumericDomain!.max
|
|
145
|
+
: chart._numY - 0.5;
|
|
146
|
+
if (chart._zoomController) {
|
|
147
|
+
chart._zoomController.setBaseDomain(
|
|
148
|
+
xDomainMin,
|
|
149
|
+
xDomainMax,
|
|
150
|
+
yDomainMin,
|
|
151
|
+
yDomainMax,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const vis = chart._zoomController
|
|
156
|
+
? chart._zoomController.getVisibleDomain()
|
|
157
|
+
: {
|
|
158
|
+
xMin: xDomainMin,
|
|
159
|
+
xMax: xDomainMax,
|
|
160
|
+
yMin: yDomainMin,
|
|
161
|
+
yMax: yDomainMax,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Heatmap cell rects span the exact domain edge-to-edge, so any
|
|
165
|
+
// cosmetic padding leaves a visible sliver between the outermost
|
|
166
|
+
// cells and the axis chrome. Force `padRatio: 0` for flush edges.
|
|
167
|
+
const projection = layout.buildProjectionMatrix(
|
|
168
|
+
vis.xMin,
|
|
169
|
+
vis.xMax,
|
|
170
|
+
vis.yMin,
|
|
171
|
+
vis.yMax,
|
|
172
|
+
undefined,
|
|
173
|
+
undefined,
|
|
174
|
+
0,
|
|
175
|
+
chart._xOrigin,
|
|
176
|
+
chart._yOrigin,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Cell gap is specified in CSS pixels but the shader needs data-space
|
|
180
|
+
// insets. Convert using the plot's data-per-pixel scale; clamp to
|
|
181
|
+
// half a band so the gap can't eat the entire cell.
|
|
182
|
+
const plot = layout.plotRect;
|
|
183
|
+
const pxPerDataX = plot.width / (vis.xMax - vis.xMin);
|
|
184
|
+
const pxPerDataY = plot.height / (vis.yMax - vis.yMin);
|
|
185
|
+
const halfGap = theme.heatmapGapPx * 0.5;
|
|
186
|
+
const cellSizeX = xNumeric ? chart._xNumericDomain!.bandWidth : 1;
|
|
187
|
+
const cellSizeY = yNumeric ? chart._yNumericDomain!.bandWidth : 1;
|
|
188
|
+
const insetX = Math.min(
|
|
189
|
+
cellSizeX * 0.5,
|
|
190
|
+
pxPerDataX > 0 ? halfGap / pxPerDataX : 0,
|
|
191
|
+
);
|
|
192
|
+
const insetY = Math.min(
|
|
193
|
+
cellSizeY * 0.5,
|
|
194
|
+
pxPerDataY > 0 ? halfGap / pxPerDataY : 0,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Gridline canvas isn't used by heatmap — clear it so stale content
|
|
198
|
+
// from a previous plugin doesn't bleed through.
|
|
199
|
+
if (chart._gridlineCanvas) {
|
|
200
|
+
const _gctx = initCanvas(chart._gridlineCanvas, layout, glManager.dpr);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ensureProgram(chart, glManager);
|
|
204
|
+
uploadInstanceBuffers(chart, glManager);
|
|
205
|
+
|
|
206
|
+
chart._gradientCache = ensureGradientTexture(
|
|
207
|
+
glManager,
|
|
208
|
+
chart._gradientCache,
|
|
209
|
+
theme.gradientStops,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
renderInPlotFrame(gl, layout, glManager.dpr, () => {
|
|
213
|
+
gl.useProgram(chart._program!);
|
|
214
|
+
const loc = chart._locations!;
|
|
215
|
+
gl.uniformMatrix4fv(loc.u_projection, false, projection);
|
|
216
|
+
gl.uniform2f(loc.u_cell_inset, insetX, insetY);
|
|
217
|
+
gl.uniform2f(loc.u_cell_size, cellSizeX, cellSizeY);
|
|
218
|
+
bindGradientTexture(
|
|
219
|
+
glManager,
|
|
220
|
+
chart._gradientCache!.texture,
|
|
221
|
+
loc.u_gradient_lut,
|
|
222
|
+
0,
|
|
223
|
+
);
|
|
224
|
+
drawCellsInstanced(chart, gl, glManager, 0, chart._uploadedCells);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
renderHeatmapChromeOverlay(chart);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function ensureProgram(
|
|
231
|
+
chart: HeatmapChart,
|
|
232
|
+
glManager: WebGLContextManager,
|
|
233
|
+
): void {
|
|
234
|
+
if (chart._program) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const gl = glManager.gl;
|
|
239
|
+
const program = glManager.shaders.getOrCreate(
|
|
240
|
+
"heatmap",
|
|
241
|
+
heatmapVert,
|
|
242
|
+
heatmapFrag,
|
|
243
|
+
);
|
|
244
|
+
chart._program = program;
|
|
245
|
+
chart._locations = {
|
|
246
|
+
u_projection: gl.getUniformLocation(program, "u_projection"),
|
|
247
|
+
u_cell_inset: gl.getUniformLocation(program, "u_cell_inset"),
|
|
248
|
+
u_cell_size: gl.getUniformLocation(program, "u_cell_size"),
|
|
249
|
+
u_gradient_lut: gl.getUniformLocation(program, "u_gradient_lut"),
|
|
250
|
+
a_corner: gl.getAttribLocation(program, "a_corner"),
|
|
251
|
+
a_cell: gl.getAttribLocation(program, "a_cell"),
|
|
252
|
+
a_color_t: gl.getAttribLocation(program, "a_color_t"),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const cornerBuffer = gl.createBuffer()!;
|
|
256
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
|
|
257
|
+
|
|
258
|
+
// Triangle strip: (0,0) (1,0) (0,1) (1,1)
|
|
259
|
+
gl.bufferData(
|
|
260
|
+
gl.ARRAY_BUFFER,
|
|
261
|
+
new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
|
|
262
|
+
gl.STATIC_DRAW,
|
|
263
|
+
);
|
|
264
|
+
chart._cornerBuffer = cornerBuffer;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function uploadInstanceBuffers(
|
|
268
|
+
chart: HeatmapChart,
|
|
269
|
+
glManager: WebGLContextManager,
|
|
270
|
+
): void {
|
|
271
|
+
const n = chart._cells.length;
|
|
272
|
+
chart._uploadedCells = n;
|
|
273
|
+
if (n === 0) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cellXY = new Float32Array(n * 2);
|
|
278
|
+
const colorT = new Float32Array(n);
|
|
279
|
+
|
|
280
|
+
// Sign-aware `t`: 0 always lands at 0.5. See `theme/gradient.ts`.
|
|
281
|
+
// Numeric-axis mode pre-multiplies the integer index into the real
|
|
282
|
+
// data position so the shader can apply `u_cell_size` (band width)
|
|
283
|
+
// uniformly without per-instance attrs. Datetime axes need an
|
|
284
|
+
// origin rebase before f32 narrowing — see {@link HeatmapFacet}
|
|
285
|
+
// and `HeatmapChart._xOrigin/_yOrigin`. Origins are 0 for category
|
|
286
|
+
// mode, where the integer index is already small.
|
|
287
|
+
if (chart._facets.length > 0) {
|
|
288
|
+
let i = 0;
|
|
289
|
+
for (const facet of chart._facets) {
|
|
290
|
+
const xPos = facet.pipeline.xPositions;
|
|
291
|
+
const yPos = facet.pipeline.yPositions;
|
|
292
|
+
const xO = facet.xOrigin;
|
|
293
|
+
const yO = facet.yOrigin;
|
|
294
|
+
for (const c of facet.pipeline.cells) {
|
|
295
|
+
cellXY[i * 2] = xPos ? xPos[c.xIdx] - xO : c.xIdx;
|
|
296
|
+
cellXY[i * 2 + 1] = yPos ? yPos[c.yIdx] - yO : c.yIdx;
|
|
297
|
+
colorT[i] = colorValueToT(
|
|
298
|
+
c.value,
|
|
299
|
+
chart._colorMin,
|
|
300
|
+
chart._colorMax,
|
|
301
|
+
);
|
|
302
|
+
i++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
const xPos = chart._xPositions;
|
|
307
|
+
const yPos = chart._yPositions;
|
|
308
|
+
const xO = chart._xOrigin;
|
|
309
|
+
const yO = chart._yOrigin;
|
|
310
|
+
for (let i = 0; i < n; i++) {
|
|
311
|
+
const c = chart._cells[i];
|
|
312
|
+
cellXY[i * 2] = xPos ? xPos[c.xIdx] - xO : c.xIdx;
|
|
313
|
+
cellXY[i * 2 + 1] = yPos ? yPos[c.yIdx] - yO : c.yIdx;
|
|
314
|
+
colorT[i] = colorValueToT(
|
|
315
|
+
c.value,
|
|
316
|
+
chart._colorMin,
|
|
317
|
+
chart._colorMax,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
glManager.bufferPool.ensureCapacity(n);
|
|
323
|
+
glManager.bufferPool.upload("heatmap_cell", cellXY, 0, 2);
|
|
324
|
+
glManager.bufferPool.upload("heatmap_t", colorT, 0, 1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function drawCellsInstanced(
|
|
328
|
+
chart: HeatmapChart,
|
|
329
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
330
|
+
glManager: WebGLContextManager,
|
|
331
|
+
instanceStart: number,
|
|
332
|
+
instanceCount: number,
|
|
333
|
+
): void {
|
|
334
|
+
if (instanceCount === 0) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const loc = chart._locations!;
|
|
339
|
+
const instancing = getInstancing(glManager);
|
|
340
|
+
const { setDivisor } = instancing;
|
|
341
|
+
const f = Float32Array.BYTES_PER_ELEMENT;
|
|
342
|
+
|
|
343
|
+
// Per-vertex corner buffer.
|
|
344
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, chart._cornerBuffer!);
|
|
345
|
+
gl.enableVertexAttribArray(loc.a_corner);
|
|
346
|
+
gl.vertexAttribPointer(loc.a_corner, 2, gl.FLOAT, false, 0, 0);
|
|
347
|
+
setDivisor(loc.a_corner, 0);
|
|
348
|
+
|
|
349
|
+
// Per-instance cell position. Byte offset into the packed buffer
|
|
350
|
+
// advances instance 0 of this draw to slot `instanceStart`.
|
|
351
|
+
//
|
|
352
|
+
// Render-path uses `peek` (not `getOrCreate`); if the buffers
|
|
353
|
+
// haven't been uploaded yet, skip the draw rather than render
|
|
354
|
+
// against a recreated zero buffer.
|
|
355
|
+
const cellBuf = glManager.bufferPool.peek("heatmap_cell");
|
|
356
|
+
const tBuf = glManager.bufferPool.peek("heatmap_t");
|
|
357
|
+
if (!cellBuf || !tBuf) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, cellBuf.buffer);
|
|
362
|
+
gl.enableVertexAttribArray(loc.a_cell);
|
|
363
|
+
gl.vertexAttribPointer(
|
|
364
|
+
loc.a_cell,
|
|
365
|
+
2,
|
|
366
|
+
gl.FLOAT,
|
|
367
|
+
false,
|
|
368
|
+
0,
|
|
369
|
+
instanceStart * 2 * f,
|
|
370
|
+
);
|
|
371
|
+
setDivisor(loc.a_cell, 1);
|
|
372
|
+
|
|
373
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, tBuf.buffer);
|
|
374
|
+
gl.enableVertexAttribArray(loc.a_color_t);
|
|
375
|
+
gl.vertexAttribPointer(
|
|
376
|
+
loc.a_color_t,
|
|
377
|
+
1,
|
|
378
|
+
gl.FLOAT,
|
|
379
|
+
false,
|
|
380
|
+
0,
|
|
381
|
+
instanceStart * f,
|
|
382
|
+
);
|
|
383
|
+
setDivisor(loc.a_color_t, 1);
|
|
384
|
+
|
|
385
|
+
instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount);
|
|
386
|
+
|
|
387
|
+
setDivisor(loc.a_cell, 0);
|
|
388
|
+
setDivisor(loc.a_color_t, 0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Chrome overlay: X axis + Y axis + color legend + (optional) tooltip.
|
|
393
|
+
*/
|
|
394
|
+
export function renderHeatmapChromeOverlay(chart: HeatmapChart): void {
|
|
395
|
+
if (!chart._chromeCanvas) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (chart._facets.length > 0) {
|
|
400
|
+
renderFacetedHeatmapChromeOverlay(chart);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!chart._lastLayout) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const layout = chart._lastLayout;
|
|
409
|
+
const theme = chart._resolveTheme();
|
|
410
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
411
|
+
|
|
412
|
+
const ctx = initCanvas(chart._chromeCanvas, layout, dpr);
|
|
413
|
+
if (!ctx) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// L-shaped axis line, same as bar chart chrome.
|
|
418
|
+
ctx.strokeStyle = theme.gridlineColor;
|
|
419
|
+
ctx.lineWidth = 1;
|
|
420
|
+
ctx.beginPath();
|
|
421
|
+
ctx.moveTo(layout.plotRect.x, layout.plotRect.y);
|
|
422
|
+
ctx.lineTo(layout.plotRect.x, layout.plotRect.y + layout.plotRect.height);
|
|
423
|
+
ctx.lineTo(
|
|
424
|
+
layout.plotRect.x + layout.plotRect.width,
|
|
425
|
+
layout.plotRect.y + layout.plotRect.height,
|
|
426
|
+
);
|
|
427
|
+
ctx.stroke();
|
|
428
|
+
|
|
429
|
+
const xDomain: CategoricalDomain = {
|
|
430
|
+
levels: chart._xLevels,
|
|
431
|
+
numRows: chart._numX,
|
|
432
|
+
levelLabels: chart._groupBy.slice(),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const yDomain: CategoricalDomain = {
|
|
436
|
+
levels: chart._yLevels,
|
|
437
|
+
numRows: chart._numY,
|
|
438
|
+
levelLabels: [],
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Heatmap X axis is the first group_by level; Y axis is the
|
|
442
|
+
// second (when present) or the first split_by level.
|
|
443
|
+
const xColumn = chart._groupBy[0];
|
|
444
|
+
const yColumn = chart._groupBy[1] ?? chart._splitBy[0];
|
|
445
|
+
|
|
446
|
+
if (chart._xAxisMode.mode === "numeric" && chart._xNumericDomain) {
|
|
447
|
+
const ticks = computeNiceTicks(layout.paddedXMin, layout.paddedXMax, 6);
|
|
448
|
+
drawNumericCategoryX(
|
|
449
|
+
ctx,
|
|
450
|
+
layout,
|
|
451
|
+
chart._xNumericDomain,
|
|
452
|
+
ticks,
|
|
453
|
+
theme,
|
|
454
|
+
chart.getColumnFormatter(xColumn, "tick"),
|
|
455
|
+
);
|
|
456
|
+
} else {
|
|
457
|
+
renderCategoricalXTicks(ctx, layout, xDomain, theme);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (chart._yAxisMode.mode === "numeric" && chart._yNumericDomain) {
|
|
461
|
+
const ticks = computeNiceTicks(layout.paddedYMin, layout.paddedYMax, 6);
|
|
462
|
+
drawNumericCategoryY(
|
|
463
|
+
ctx,
|
|
464
|
+
layout,
|
|
465
|
+
chart._yNumericDomain,
|
|
466
|
+
ticks,
|
|
467
|
+
theme,
|
|
468
|
+
chart.getColumnFormatter(yColumn, "tick"),
|
|
469
|
+
);
|
|
470
|
+
} else {
|
|
471
|
+
renderCategoricalYTicks(
|
|
472
|
+
ctx,
|
|
473
|
+
layout,
|
|
474
|
+
yDomain,
|
|
475
|
+
theme,
|
|
476
|
+
HEATMAP_Y_AXIS_OPTS,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Color legend on the right. The aggregate column name is in
|
|
481
|
+
// `_columnSlots[0]` (heatmap's only data column slot is "Color").
|
|
482
|
+
renderLegend(
|
|
483
|
+
chart._chromeCanvas,
|
|
484
|
+
layout,
|
|
485
|
+
{
|
|
486
|
+
min: chart._colorMin,
|
|
487
|
+
max: chart._colorMax,
|
|
488
|
+
label: chart._aggName,
|
|
489
|
+
},
|
|
490
|
+
theme.gradientStops,
|
|
491
|
+
theme,
|
|
492
|
+
chart.getColumnFormatter(chart._columnSlots[0], "value"),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
if (chart._hoveredCell) {
|
|
496
|
+
renderHeatmapTooltip(chart);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Multi-facet WebGL render. Packs all facets' cells into one instance
|
|
501
|
+
* buffer and dispatches once per facet with a rebound pointer offset,
|
|
502
|
+
* matching projection, and scissor to the facet's plot rect. */
|
|
503
|
+
function renderFacetedHeatmap(
|
|
504
|
+
chart: HeatmapChart,
|
|
505
|
+
glManager: WebGLContextManager,
|
|
506
|
+
cssWidth: number,
|
|
507
|
+
cssHeight: number,
|
|
508
|
+
): void {
|
|
509
|
+
const gl = glManager.gl;
|
|
510
|
+
const theme = chart._resolveTheme();
|
|
511
|
+
|
|
512
|
+
// Derive the effective shared-axis flags for this frame. Stamps
|
|
513
|
+
// `_lastEffectiveSharedX/Y` on the chart so
|
|
514
|
+
// `renderFacetedHeatmapChromeOverlay` reads the same values without
|
|
515
|
+
// re-deriving (and without us having to mutate `_facetConfig`).
|
|
516
|
+
const { effectiveSharedX, effectiveSharedY } =
|
|
517
|
+
chart.computeEffectiveFacetFlags();
|
|
518
|
+
|
|
519
|
+
const grid = buildFacetGrid(
|
|
520
|
+
chart._facets.map((f) => f.label),
|
|
521
|
+
{
|
|
522
|
+
cssWidth,
|
|
523
|
+
cssHeight,
|
|
524
|
+
xAxis: effectiveSharedX ? "outer" : "cell",
|
|
525
|
+
yAxis: effectiveSharedY ? "outer" : "cell",
|
|
526
|
+
hasLegend: true,
|
|
527
|
+
hasXLabel: chart._groupBy.length > 0,
|
|
528
|
+
hasYLabel: false,
|
|
529
|
+
gap: 8,
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
chart._facetGrid = grid;
|
|
533
|
+
|
|
534
|
+
for (let i = 0; i < chart._facets.length; i++) {
|
|
535
|
+
const cell = grid.cells[i];
|
|
536
|
+
if (cell) {
|
|
537
|
+
chart._facets[i].layout = cell.layout;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Wire every active zoom controller's layout pointer so wheel/pan
|
|
542
|
+
// hit-tests compute correct data deltas.
|
|
543
|
+
chart.syncFacetZoomLayouts(grid.cells);
|
|
544
|
+
|
|
545
|
+
ensureProgram(chart, glManager);
|
|
546
|
+
uploadInstanceBuffers(chart, glManager);
|
|
547
|
+
chart._gradientCache = ensureGradientTexture(
|
|
548
|
+
glManager,
|
|
549
|
+
chart._gradientCache,
|
|
550
|
+
theme.gradientStops,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
gl.useProgram(chart._program!);
|
|
554
|
+
const loc = chart._locations!;
|
|
555
|
+
bindGradientTexture(
|
|
556
|
+
glManager,
|
|
557
|
+
chart._gradientCache!.texture,
|
|
558
|
+
loc.u_gradient_lut,
|
|
559
|
+
0,
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// One clear for the whole frame; per-facet scissor keeps each
|
|
563
|
+
// facet's draw confined to its plot rect without wiping its
|
|
564
|
+
// neighbours.
|
|
565
|
+
clearAndSetupFrame(gl);
|
|
566
|
+
|
|
567
|
+
for (let i = 0; i < chart._facets.length; i++) {
|
|
568
|
+
const facet = chart._facets[i];
|
|
569
|
+
if (facet.instanceCount === 0) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const { numX, numY } = facet.pipeline;
|
|
574
|
+
if (numX === 0 || numY === 0) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const layout = facet.layout;
|
|
579
|
+
const xNumeric = facet.pipeline.xAxisMode.mode === "numeric";
|
|
580
|
+
const yNumeric = facet.pipeline.yAxisMode.mode === "numeric";
|
|
581
|
+
const xDomainMin = xNumeric ? facet.pipeline.xNumericDomain!.min : -0.5;
|
|
582
|
+
const xDomainMax = xNumeric
|
|
583
|
+
? facet.pipeline.xNumericDomain!.max
|
|
584
|
+
: numX - 0.5;
|
|
585
|
+
const yDomainMin = yNumeric ? facet.pipeline.yNumericDomain!.min : -0.5;
|
|
586
|
+
const yDomainMax = yNumeric
|
|
587
|
+
? facet.pipeline.yNumericDomain!.max
|
|
588
|
+
: numY - 0.5;
|
|
589
|
+
|
|
590
|
+
// Anchor the controller's base domain to this facet's data
|
|
591
|
+
// extent so wheel/pan transforms compose against a meaningful
|
|
592
|
+
// identity. In shared mode every facet writes the same base
|
|
593
|
+
// (heatmap facets share group_by → identical X domain, and
|
|
594
|
+
// matching Y shapes from `partitionColumnsPerFacet` → identical
|
|
595
|
+
// Y domain), so last-write-wins is a no-op. In independent
|
|
596
|
+
// mode each facet's own controller gets its own base.
|
|
597
|
+
const zc = chart.getZoomControllerForFacet(i);
|
|
598
|
+
if (zc) {
|
|
599
|
+
zc.setBaseDomain(xDomainMin, xDomainMax, yDomainMin, yDomainMax);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const vis = zc
|
|
603
|
+
? zc.getVisibleDomain()
|
|
604
|
+
: {
|
|
605
|
+
xMin: xDomainMin,
|
|
606
|
+
xMax: xDomainMax,
|
|
607
|
+
yMin: yDomainMin,
|
|
608
|
+
yMax: yDomainMax,
|
|
609
|
+
};
|
|
610
|
+
const projection = layout.buildProjectionMatrix(
|
|
611
|
+
vis.xMin,
|
|
612
|
+
vis.xMax,
|
|
613
|
+
vis.yMin,
|
|
614
|
+
vis.yMax,
|
|
615
|
+
undefined,
|
|
616
|
+
undefined,
|
|
617
|
+
0,
|
|
618
|
+
facet.xOrigin,
|
|
619
|
+
facet.yOrigin,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const plot = layout.plotRect;
|
|
623
|
+
const pxPerDataX = plot.width / (vis.xMax - vis.xMin);
|
|
624
|
+
const pxPerDataY = plot.height / (vis.yMax - vis.yMin);
|
|
625
|
+
const halfGap = theme.heatmapGapPx * 0.5;
|
|
626
|
+
const cellSizeX = xNumeric
|
|
627
|
+
? facet.pipeline.xNumericDomain!.bandWidth
|
|
628
|
+
: 1;
|
|
629
|
+
const cellSizeY = yNumeric
|
|
630
|
+
? facet.pipeline.yNumericDomain!.bandWidth
|
|
631
|
+
: 1;
|
|
632
|
+
const insetX = Math.min(
|
|
633
|
+
cellSizeX * 0.5,
|
|
634
|
+
pxPerDataX > 0 ? halfGap / pxPerDataX : 0,
|
|
635
|
+
);
|
|
636
|
+
const insetY = Math.min(
|
|
637
|
+
cellSizeY * 0.5,
|
|
638
|
+
pxPerDataY > 0 ? halfGap / pxPerDataY : 0,
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
withScissor(gl, layout, glManager.dpr, () => {
|
|
642
|
+
gl.uniformMatrix4fv(loc.u_projection, false, projection);
|
|
643
|
+
gl.uniform2f(loc.u_cell_inset, insetX, insetY);
|
|
644
|
+
gl.uniform2f(loc.u_cell_size, cellSizeX, cellSizeY);
|
|
645
|
+
drawCellsInstanced(
|
|
646
|
+
chart,
|
|
647
|
+
gl,
|
|
648
|
+
glManager,
|
|
649
|
+
facet.instanceStart,
|
|
650
|
+
facet.instanceCount,
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
renderHeatmapChromeOverlay(chart);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Multi-facet chrome: per-facet X/Y axis + title, one shared legend.
|
|
660
|
+
*/
|
|
661
|
+
function renderFacetedHeatmapChromeOverlay(chart: HeatmapChart): void {
|
|
662
|
+
if (!chart._chromeCanvas || !chart._facetGrid) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const theme = chart._resolveTheme();
|
|
667
|
+
|
|
668
|
+
// `initCanvas` wants a `PlotLayout` to sync DPR-aware sizing. The
|
|
669
|
+
// first facet's layout is canvas-sized (cssWidth/cssHeight match
|
|
670
|
+
// the element), so either facet works for the DPR handshake.
|
|
671
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
672
|
+
const ctx = initCanvas(chart._chromeCanvas, chart._facets[0].layout, dpr);
|
|
673
|
+
if (!ctx) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Shared-axis suppression: when shared-X is active the X tick
|
|
678
|
+
// labels paint just below `cell.layout.plotRect` — which, because
|
|
679
|
+
// `buildFacetGrid` was called with `xAxis: "outer"`, falls into
|
|
680
|
+
// the reserved `outerXAxisRect` band rather than per-cell padding.
|
|
681
|
+
// Painting from a bottom-edge cell's layout is enough; non-edge
|
|
682
|
+
// rows would paint the same labels at the wrong y coordinate, so
|
|
683
|
+
// we skip them. Symmetric for Y. The cleanest way to express
|
|
684
|
+
// "shared = only edge cells render axes" is to gate the per-cell
|
|
685
|
+
// call on `!sharedX || isBottomEdge` (and analogously for Y).
|
|
686
|
+
const sharedX = chart._lastEffectiveSharedX;
|
|
687
|
+
const sharedY = chart._lastEffectiveSharedY;
|
|
688
|
+
const grid = chart._facetGrid;
|
|
689
|
+
for (let i = 0; i < chart._facets.length; i++) {
|
|
690
|
+
const facet = chart._facets[i];
|
|
691
|
+
const cell = grid.cells[i];
|
|
692
|
+
const layout = facet.layout;
|
|
693
|
+
const plot = layout.plotRect;
|
|
694
|
+
|
|
695
|
+
ctx.strokeStyle = theme.gridlineColor;
|
|
696
|
+
ctx.lineWidth = 1;
|
|
697
|
+
ctx.beginPath();
|
|
698
|
+
ctx.moveTo(plot.x, plot.y);
|
|
699
|
+
ctx.lineTo(plot.x, plot.y + plot.height);
|
|
700
|
+
ctx.lineTo(plot.x + plot.width, plot.y + plot.height);
|
|
701
|
+
ctx.stroke();
|
|
702
|
+
|
|
703
|
+
const xDomain: CategoricalDomain = {
|
|
704
|
+
levels: facet.pipeline.xLevels,
|
|
705
|
+
numRows: facet.pipeline.numX,
|
|
706
|
+
levelLabels: chart._groupBy.slice(),
|
|
707
|
+
};
|
|
708
|
+
const yDomain: CategoricalDomain = {
|
|
709
|
+
levels: facet.pipeline.yLevels,
|
|
710
|
+
numRows: facet.pipeline.numY,
|
|
711
|
+
levelLabels: [],
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const xColumn = chart._groupBy[0];
|
|
715
|
+
const yColumn = chart._groupBy[1] ?? chart._splitBy[0];
|
|
716
|
+
|
|
717
|
+
if (!sharedX || cell.isBottomEdge) {
|
|
718
|
+
if (
|
|
719
|
+
facet.pipeline.xAxisMode.mode === "numeric" &&
|
|
720
|
+
facet.pipeline.xNumericDomain
|
|
721
|
+
) {
|
|
722
|
+
const ticks = computeNiceTicks(
|
|
723
|
+
layout.paddedXMin,
|
|
724
|
+
layout.paddedXMax,
|
|
725
|
+
6,
|
|
726
|
+
);
|
|
727
|
+
drawNumericCategoryX(
|
|
728
|
+
ctx,
|
|
729
|
+
layout,
|
|
730
|
+
facet.pipeline.xNumericDomain,
|
|
731
|
+
ticks,
|
|
732
|
+
theme,
|
|
733
|
+
chart.getColumnFormatter(xColumn, "tick"),
|
|
734
|
+
);
|
|
735
|
+
} else {
|
|
736
|
+
renderCategoricalXTicks(ctx, layout, xDomain, theme);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (!sharedY || cell.isLeftEdge) {
|
|
741
|
+
if (
|
|
742
|
+
facet.pipeline.yAxisMode.mode === "numeric" &&
|
|
743
|
+
facet.pipeline.yNumericDomain
|
|
744
|
+
) {
|
|
745
|
+
const ticks = computeNiceTicks(
|
|
746
|
+
layout.paddedYMin,
|
|
747
|
+
layout.paddedYMax,
|
|
748
|
+
6,
|
|
749
|
+
);
|
|
750
|
+
drawNumericCategoryY(
|
|
751
|
+
ctx,
|
|
752
|
+
layout,
|
|
753
|
+
facet.pipeline.yNumericDomain,
|
|
754
|
+
ticks,
|
|
755
|
+
theme,
|
|
756
|
+
chart.getColumnFormatter(yColumn, "tick"),
|
|
757
|
+
);
|
|
758
|
+
} else {
|
|
759
|
+
renderCategoricalYTicks(
|
|
760
|
+
ctx,
|
|
761
|
+
layout,
|
|
762
|
+
yDomain,
|
|
763
|
+
theme,
|
|
764
|
+
HEATMAP_Y_AXIS_OPTS,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Per-facet titles sit in the grid cell's titleRect — one strip per
|
|
771
|
+
// facet, above the plot rect. The grid's cells and the chart's
|
|
772
|
+
// facets are parallel arrays by construction.
|
|
773
|
+
for (let i = 0; i < grid.cells.length; i++) {
|
|
774
|
+
const cell = grid.cells[i];
|
|
775
|
+
const facet = chart._facets[i];
|
|
776
|
+
if (!facet || !cell.titleRect) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
drawFacetTitle(
|
|
781
|
+
chart._chromeCanvas,
|
|
782
|
+
facet.label,
|
|
783
|
+
cell.titleRect,
|
|
784
|
+
theme,
|
|
785
|
+
dpr,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Shared colorbar at `grid.legendRect`. No meaningful single label —
|
|
790
|
+
// the facet titles already name each column, and a combined label
|
|
791
|
+
// would be ambiguous when columns differ.
|
|
792
|
+
if (grid.legendRect) {
|
|
793
|
+
renderLegendAt(
|
|
794
|
+
chart._chromeCanvas,
|
|
795
|
+
{
|
|
796
|
+
x: grid.legendRect.x,
|
|
797
|
+
y: grid.legendRect.y + 20,
|
|
798
|
+
width: grid.legendRect.width,
|
|
799
|
+
height: Math.max(1, grid.legendRect.height - 20),
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
min: chart._colorMin,
|
|
803
|
+
max: chart._colorMax,
|
|
804
|
+
label: "",
|
|
805
|
+
},
|
|
806
|
+
theme.gradientStops,
|
|
807
|
+
theme,
|
|
808
|
+
chart.getColumnFormatter(chart._columnSlots[0], "value"),
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (chart._hoveredCell) {
|
|
813
|
+
renderHeatmapTooltip(chart);
|
|
814
|
+
}
|
|
815
|
+
}
|