@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,948 @@
|
|
|
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 { Canvas2D } from "../canvas-types";
|
|
14
|
+
import { drawFacetTitle } from "../../axis/facet-chrome";
|
|
15
|
+
import type { WebGLContextManager } from "../../webgl/context-manager";
|
|
16
|
+
import type { CartesianChart } from "./cartesian";
|
|
17
|
+
import { PlotLayout } from "../../layout/plot-layout";
|
|
18
|
+
import {
|
|
19
|
+
buildFacetGrid,
|
|
20
|
+
bottomRowLayouts,
|
|
21
|
+
leftColumnLayouts,
|
|
22
|
+
type FacetGrid,
|
|
23
|
+
} from "../../layout/facet-grid";
|
|
24
|
+
import { type Theme } from "../../theme/theme";
|
|
25
|
+
import { resolvePalette } from "../../theme/palette";
|
|
26
|
+
import { paletteToStops } from "../../theme/gradient";
|
|
27
|
+
import {
|
|
28
|
+
renderInPlotFrame,
|
|
29
|
+
clearAndSetupFrame,
|
|
30
|
+
withScissor,
|
|
31
|
+
} from "../../webgl/plot-frame";
|
|
32
|
+
import { ensureGradientTexture } from "../../webgl/gradient-texture";
|
|
33
|
+
import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
|
|
34
|
+
import {
|
|
35
|
+
computeTicks,
|
|
36
|
+
renderGridlines,
|
|
37
|
+
renderAxesChrome,
|
|
38
|
+
renderCellXAxis,
|
|
39
|
+
renderCellYAxis,
|
|
40
|
+
renderOuterXAxis,
|
|
41
|
+
renderOuterYAxis,
|
|
42
|
+
type AxisDomain,
|
|
43
|
+
} from "../../axis/numeric-axis";
|
|
44
|
+
import { initCanvas, getScaledContext } from "../../axis/canvas";
|
|
45
|
+
import {
|
|
46
|
+
renderLegend,
|
|
47
|
+
renderLegendAt,
|
|
48
|
+
renderCategoricalLegend,
|
|
49
|
+
renderCategoricalLegendAt,
|
|
50
|
+
} from "../../axis/legend";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* NaN guard: `_xOrigin`/`_yOrigin` start as NaN before the first valid sample.
|
|
54
|
+
*/
|
|
55
|
+
function rebaseOrigin(o: number): number {
|
|
56
|
+
return isNaN(o) ? 0 : o;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Full-frame render: gridlines → glyph draw inside the plot-frame
|
|
61
|
+
* scissor → chrome overlay (axes + legend + tooltip).
|
|
62
|
+
*
|
|
63
|
+
* Branches on `_facetConfig.facet_mode`:
|
|
64
|
+
*
|
|
65
|
+
* - `"overlay"` (legacy): a single plot rect; all split series are
|
|
66
|
+
* drawn together, distinguished by color. This is the pre-facet
|
|
67
|
+
* behavior, preserved for manual opt-in via `plugin_config.facet_mode`.
|
|
68
|
+
* - `"grid"` (default): when splits are present, `_splitGroups` laid
|
|
69
|
+
* out as a grid of sub-plots by {@link buildFacetGrid}. When splits
|
|
70
|
+
* are absent, falls through to the single-plot path — identical to
|
|
71
|
+
* the `"overlay"` case with 0 splits, so the non-split render path
|
|
72
|
+
* is byte-for-byte unchanged from before this feature.
|
|
73
|
+
*/
|
|
74
|
+
export function renderCartesianFrame(
|
|
75
|
+
chart: CartesianChart,
|
|
76
|
+
glManager: WebGLContextManager,
|
|
77
|
+
): void {
|
|
78
|
+
const gl = glManager.gl;
|
|
79
|
+
const dpr = glManager.dpr;
|
|
80
|
+
const cssWidth = gl.canvas.width / dpr;
|
|
81
|
+
const cssHeight = gl.canvas.height / dpr;
|
|
82
|
+
if (cssWidth <= 0 || cssHeight <= 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const hasSplits = chart._splitGroups.length > 0;
|
|
87
|
+
const facetMode = chart._facetConfig.facet_mode;
|
|
88
|
+
const useGrid = hasSplits && facetMode === "grid";
|
|
89
|
+
|
|
90
|
+
chart.computeEffectiveFacetFlags();
|
|
91
|
+
|
|
92
|
+
// Legend appears only when the user wired a color column with a
|
|
93
|
+
// non-degenerate range. `split_by` alone no longer forces a
|
|
94
|
+
// legend — faceting is the axis of splitting, not coloring.
|
|
95
|
+
const hasColorCol =
|
|
96
|
+
chart._colorName !== "" && chart._colorMin < chart._colorMax;
|
|
97
|
+
|
|
98
|
+
// Overall domain = current viewport in shared-zoom mode, full data
|
|
99
|
+
// extents in independent-zoom mode (each facet consults its own
|
|
100
|
+
// controller inside `renderFacetedFrame`).
|
|
101
|
+
const independent =
|
|
102
|
+
useGrid && chart._facetConfig.zoom_mode === "independent";
|
|
103
|
+
let domain: { xMin: number; xMax: number; yMin: number; yMax: number };
|
|
104
|
+
if (chart._zoomController && !independent) {
|
|
105
|
+
domain = chart._zoomController.getVisibleDomain();
|
|
106
|
+
} else {
|
|
107
|
+
domain = {
|
|
108
|
+
xMin: chart._xMin,
|
|
109
|
+
xMax: chart._xMax,
|
|
110
|
+
yMin: chart._yMin,
|
|
111
|
+
yMax: chart._yMax,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isFinite(domain.xMin) || !isFinite(domain.yMin)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const theme = chart._resolveTheme();
|
|
120
|
+
const seriesPalette = theme.seriesPalette;
|
|
121
|
+
|
|
122
|
+
const xType = chart._columnTypes[chart._xLabel] || "";
|
|
123
|
+
const yType = chart._columnTypes[chart._yLabel] || "";
|
|
124
|
+
const xIsDate = xType === "date" || xType === "datetime";
|
|
125
|
+
const yIsDate = yType === "date" || yType === "datetime";
|
|
126
|
+
|
|
127
|
+
// Prepare the shared gradient LUT once (used by all facets).
|
|
128
|
+
//
|
|
129
|
+
// Three color sources map to three LUT types:
|
|
130
|
+
// - split_by or string color column → multi-entry series palette
|
|
131
|
+
// keyed by `_uniqueColorLabels.size`.
|
|
132
|
+
// - no color source at all → single-entry series palette
|
|
133
|
+
// (`palette[0]`). Points are stored with `a_color_value = 0.5`
|
|
134
|
+
// in the build; a 1-color LUT returns the same RGB for every
|
|
135
|
+
// sample so the default value is harmless.
|
|
136
|
+
// - numeric color column → continuous theme gradient.
|
|
137
|
+
// Categorical only when a string color column was wired —
|
|
138
|
+
// `split_by` alone no longer implies categorical coloring.
|
|
139
|
+
const isCategorical = chart._colorIsString;
|
|
140
|
+
const hasNoColorSource = !isCategorical && !chart._colorName;
|
|
141
|
+
let lutStops = theme.gradientStops;
|
|
142
|
+
if (isCategorical || hasNoColorSource) {
|
|
143
|
+
const labelCount = hasNoColorSource
|
|
144
|
+
? Math.max(1, chart._splitGroups.length)
|
|
145
|
+
: Math.max(1, chart._uniqueColorLabels.size);
|
|
146
|
+
|
|
147
|
+
// Cache key carries the `seriesPalette` reference (changes per
|
|
148
|
+
// theme — `_resolveTheme` returns a fresh `Theme` after
|
|
149
|
+
// `invalidateTheme()`) plus `labelCount`. Reference compare
|
|
150
|
+
// catches theme switches that the prior length-only key
|
|
151
|
+
// missed.
|
|
152
|
+
if (
|
|
153
|
+
chart._lastLutStops &&
|
|
154
|
+
chart._lastLutSeriesPalette === seriesPalette &&
|
|
155
|
+
chart._lastLutLabelCount === labelCount
|
|
156
|
+
) {
|
|
157
|
+
lutStops = chart._lastLutStops;
|
|
158
|
+
} else {
|
|
159
|
+
const palette = resolvePalette(
|
|
160
|
+
seriesPalette,
|
|
161
|
+
theme.gradientStops,
|
|
162
|
+
labelCount,
|
|
163
|
+
);
|
|
164
|
+
lutStops = paletteToStops(palette);
|
|
165
|
+
chart._lastLutStops = lutStops;
|
|
166
|
+
chart._lastLutSeriesPalette = seriesPalette;
|
|
167
|
+
chart._lastLutLabelCount = labelCount;
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
chart._lastLutStops = null;
|
|
171
|
+
chart._lastLutSeriesPalette = null;
|
|
172
|
+
chart._lastLutLabelCount = -1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
chart._gradientCache = ensureGradientTexture(
|
|
176
|
+
glManager,
|
|
177
|
+
chart._gradientCache,
|
|
178
|
+
lutStops,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (useGrid) {
|
|
182
|
+
renderFacetedFrame(chart, glManager, domain, theme, {
|
|
183
|
+
xIsDate,
|
|
184
|
+
yIsDate,
|
|
185
|
+
cssWidth,
|
|
186
|
+
cssHeight,
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
// Single-plot path (no splits, or `"overlay"` mode).
|
|
190
|
+
chart._facetGrid = null;
|
|
191
|
+
renderSinglePlotFrame(chart, glManager, domain, theme, {
|
|
192
|
+
xIsDate,
|
|
193
|
+
yIsDate,
|
|
194
|
+
cssWidth,
|
|
195
|
+
cssHeight,
|
|
196
|
+
hasColorCol,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
renderCartesianChromeOverlay(chart);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface RenderFrameCtx {
|
|
204
|
+
xIsDate: boolean;
|
|
205
|
+
yIsDate: boolean;
|
|
206
|
+
cssWidth: number;
|
|
207
|
+
cssHeight: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface SinglePlotCtx extends RenderFrameCtx {
|
|
211
|
+
hasColorCol: boolean;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildXDomain(
|
|
215
|
+
chart: CartesianChart,
|
|
216
|
+
min: number,
|
|
217
|
+
max: number,
|
|
218
|
+
isDate: boolean,
|
|
219
|
+
): AxisDomain {
|
|
220
|
+
return {
|
|
221
|
+
min,
|
|
222
|
+
max,
|
|
223
|
+
label:
|
|
224
|
+
chart._xLabel || (chart._xIsRowIndex ? "Row" : chart._xName || ""),
|
|
225
|
+
isDate,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildYDomain(
|
|
230
|
+
chart: CartesianChart,
|
|
231
|
+
min: number,
|
|
232
|
+
max: number,
|
|
233
|
+
isDate: boolean,
|
|
234
|
+
): AxisDomain {
|
|
235
|
+
return {
|
|
236
|
+
min,
|
|
237
|
+
max,
|
|
238
|
+
label: chart._yLabel || chart._yName,
|
|
239
|
+
isDate,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Original single-plot render path — all series drawn into one
|
|
245
|
+
* `PlotLayout` with one projection matrix. Used when splits are absent
|
|
246
|
+
* or when `facet_mode === "overlay"`.
|
|
247
|
+
*/
|
|
248
|
+
function renderSinglePlotFrame(
|
|
249
|
+
chart: CartesianChart,
|
|
250
|
+
glManager: WebGLContextManager,
|
|
251
|
+
domain: { xMin: number; xMax: number; yMin: number; yMax: number },
|
|
252
|
+
theme: Theme,
|
|
253
|
+
ctx: SinglePlotCtx,
|
|
254
|
+
): void {
|
|
255
|
+
const gl = glManager.gl;
|
|
256
|
+
const { cssWidth, cssHeight, xIsDate, yIsDate, hasColorCol } = ctx;
|
|
257
|
+
|
|
258
|
+
const layout = new PlotLayout(cssWidth, cssHeight, {
|
|
259
|
+
hasXLabel: !!chart._xLabel,
|
|
260
|
+
hasYLabel: !!chart._yLabel,
|
|
261
|
+
hasLegend: hasColorCol,
|
|
262
|
+
});
|
|
263
|
+
chart._lastLayout = layout;
|
|
264
|
+
if (chart._zoomController) {
|
|
265
|
+
chart._zoomController.updateLayout(layout);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const projection = layout.buildProjectionMatrix(
|
|
269
|
+
domain.xMin,
|
|
270
|
+
domain.xMax,
|
|
271
|
+
domain.yMin,
|
|
272
|
+
domain.yMax,
|
|
273
|
+
undefined,
|
|
274
|
+
undefined,
|
|
275
|
+
undefined,
|
|
276
|
+
rebaseOrigin(chart._xOrigin),
|
|
277
|
+
rebaseOrigin(chart._yOrigin),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const xDomain = buildXDomain(chart, domain.xMin, domain.xMax, xIsDate);
|
|
281
|
+
const yDomain = buildYDomain(chart, domain.yMin, domain.yMax, yIsDate);
|
|
282
|
+
const { xTicks, yTicks } = computeTicks(xDomain, yDomain, layout);
|
|
283
|
+
|
|
284
|
+
const isMap = chart._renderMode === "map";
|
|
285
|
+
|
|
286
|
+
if (chart._gridlineCanvas && !isMap) {
|
|
287
|
+
// One-shot destructive prep (resizes + clears + scales to DPR).
|
|
288
|
+
// `renderGridlines` itself is non-destructive.
|
|
289
|
+
const dpr = glManager.dpr;
|
|
290
|
+
initCanvas(chart._gridlineCanvas, layout, dpr);
|
|
291
|
+
renderGridlines(
|
|
292
|
+
chart._gridlineCanvas,
|
|
293
|
+
layout,
|
|
294
|
+
xTicks,
|
|
295
|
+
yTicks,
|
|
296
|
+
theme,
|
|
297
|
+
dpr,
|
|
298
|
+
);
|
|
299
|
+
} else if (chart._gridlineCanvas && isMap) {
|
|
300
|
+
// Map mode draws no cartesian gridlines, but the gridline
|
|
301
|
+
// canvas may carry stale ink from a prior cartesian chart
|
|
302
|
+
// type. Reset it to a clean transparent surface so the
|
|
303
|
+
// basemap (rendered into the GL canvas below) reads as the
|
|
304
|
+
// only background layer.
|
|
305
|
+
initCanvas(chart._gridlineCanvas, layout, glManager.dpr);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
renderInPlotFrame(gl, layout, glManager.dpr, () => {
|
|
309
|
+
if (isMap) {
|
|
310
|
+
chart.renderBackground(
|
|
311
|
+
glManager,
|
|
312
|
+
layout,
|
|
313
|
+
projection,
|
|
314
|
+
domain,
|
|
315
|
+
rebaseOrigin(chart._xOrigin),
|
|
316
|
+
rebaseOrigin(chart._yOrigin),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
chart.glyph.draw(chart, glManager, projection);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
chart._lastXDomain = xDomain;
|
|
324
|
+
chart._lastYDomain = yDomain;
|
|
325
|
+
chart._lastXTicks = xTicks;
|
|
326
|
+
chart._lastYTicks = yTicks;
|
|
327
|
+
chart._lastGradientStops = theme.gradientStops;
|
|
328
|
+
chart._lastHasColorCol = hasColorCol;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Faceted render path — one sub-plot per split, laid out in a grid.
|
|
333
|
+
* Each facet gets its own `PlotLayout` (with canvas-absolute margins),
|
|
334
|
+
* its own projection matrix, and one `drawSeries(s)` dispatch inside
|
|
335
|
+
* its scissor rect. Shader, buffers, gradient texture, and zoom
|
|
336
|
+
* controller state are all shared.
|
|
337
|
+
*
|
|
338
|
+
* Shared-zoom mode uses one global domain for every facet's projection
|
|
339
|
+
* (current default). Independent-zoom mode (Stage 6) will consult a
|
|
340
|
+
* per-facet `ZoomController`.
|
|
341
|
+
*/
|
|
342
|
+
function renderFacetedFrame(
|
|
343
|
+
chart: CartesianChart,
|
|
344
|
+
glManager: WebGLContextManager,
|
|
345
|
+
domain: { xMin: number; xMax: number; yMin: number; yMax: number },
|
|
346
|
+
theme: Theme,
|
|
347
|
+
ctx: RenderFrameCtx,
|
|
348
|
+
): void {
|
|
349
|
+
const gl = glManager.gl;
|
|
350
|
+
const { cssWidth, cssHeight, xIsDate, yIsDate } = ctx;
|
|
351
|
+
|
|
352
|
+
const labels = chart._splitGroups.map((g) => g.prefix);
|
|
353
|
+
|
|
354
|
+
// Legend: reserve space only when the user wired a color column.
|
|
355
|
+
// - string column: categorical swatches from `_uniqueColorLabels`.
|
|
356
|
+
// - numeric column: gradient bar from `_colorMin/_colorMax`.
|
|
357
|
+
// - no color column: no legend (facets alone don't warrant one).
|
|
358
|
+
const hasCategoricalLegend =
|
|
359
|
+
chart._colorIsString && chart._uniqueColorLabels.size > 1;
|
|
360
|
+
const hasGradientLegend =
|
|
361
|
+
!!chart._colorName &&
|
|
362
|
+
!chart._colorIsString &&
|
|
363
|
+
chart._colorMin < chart._colorMax;
|
|
364
|
+
const hasLegend = hasCategoricalLegend || hasGradientLegend;
|
|
365
|
+
|
|
366
|
+
// Use the frame-local effective flags (set in
|
|
367
|
+
// `renderCartesianFrame`) so independent-zoom mode falls through
|
|
368
|
+
// to per-cell axes without mutating the user's stored
|
|
369
|
+
// `_facetConfig.shared_x_axis` / `shared_y_axis`. Continuous
|
|
370
|
+
// charts always have both axes, so the false branch maps to
|
|
371
|
+
// per-cell mode (never to "none", which is reserved for tree
|
|
372
|
+
// charts).
|
|
373
|
+
const grid: FacetGrid = buildFacetGrid(labels, {
|
|
374
|
+
cssWidth,
|
|
375
|
+
cssHeight,
|
|
376
|
+
xAxis: chart._lastEffectiveSharedX ? "outer" : "cell",
|
|
377
|
+
yAxis: chart._lastEffectiveSharedY ? "outer" : "cell",
|
|
378
|
+
hasLegend,
|
|
379
|
+
hasXLabel: !!chart._xLabel,
|
|
380
|
+
hasYLabel: !!chart._yLabel,
|
|
381
|
+
gap: chart._facetConfig.facet_padding,
|
|
382
|
+
});
|
|
383
|
+
chart._facetGrid = grid;
|
|
384
|
+
|
|
385
|
+
// Grid invariant: every cell has the same plot rect dimensions.
|
|
386
|
+
// Downstream code (tick sampling, projection math) depends on
|
|
387
|
+
// this. The O(N) comparison runs at most once per frame and bails
|
|
388
|
+
// at the first mismatch — cheap enough to leave on unconditionally.
|
|
389
|
+
if (grid.cells.length > 1) {
|
|
390
|
+
const r0 = grid.cells[0].layout.plotRect;
|
|
391
|
+
for (let i = 1; i < grid.cells.length; i++) {
|
|
392
|
+
const r = grid.cells[i].layout.plotRect;
|
|
393
|
+
if (r.width !== r0.width || r.height !== r0.height) {
|
|
394
|
+
console.warn(
|
|
395
|
+
`facet-grid: cell ${i} size (${r.width}×${r.height}) ` +
|
|
396
|
+
`differs from cell 0 (${r0.width}×${r0.height})`,
|
|
397
|
+
);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// `_lastLayout` backs the hover hit-test in `continuous-interact.ts`.
|
|
404
|
+
// In faceted mode the hover routine resolves the facet under the
|
|
405
|
+
// cursor and consults that cell's layout directly; for legacy
|
|
406
|
+
// fallback (shouldn't fire), publish the first cell's layout.
|
|
407
|
+
chart._lastLayout = grid.cells[0]?.layout ?? null;
|
|
408
|
+
|
|
409
|
+
// Keep every controller's layout pointer fresh for wheel/pan math.
|
|
410
|
+
chart.syncFacetZoomLayouts(grid.cells);
|
|
411
|
+
const independent = chart._facetConfig.zoom_mode === "independent";
|
|
412
|
+
|
|
413
|
+
const xDomain = buildXDomain(chart, domain.xMin, domain.xMax, xIsDate);
|
|
414
|
+
const yDomain = buildYDomain(chart, domain.yMin, domain.yMax, yIsDate);
|
|
415
|
+
|
|
416
|
+
// Gridlines + per-facet axes use the first cell's layout for tick
|
|
417
|
+
// sampling (all cells have identical plotRect dimensions). Per-facet
|
|
418
|
+
// rendering then reuses the same tick arrays.
|
|
419
|
+
const sampleLayout = grid.cells[0]?.layout;
|
|
420
|
+
const { xTicks, yTicks } = sampleLayout
|
|
421
|
+
? computeTicks(xDomain, yDomain, sampleLayout)
|
|
422
|
+
: { xTicks: [], yTicks: [] };
|
|
423
|
+
|
|
424
|
+
// One-shot destructive prep for the gridline + WebGL canvases.
|
|
425
|
+
// Both phases below are per-facet; calling their destructive
|
|
426
|
+
// helpers (initCanvas / renderInPlotFrame) in the loop would wipe
|
|
427
|
+
// every previously-drawn facet, leaving only the last cell
|
|
428
|
+
// visible.
|
|
429
|
+
if (chart._gridlineCanvas && sampleLayout) {
|
|
430
|
+
initCanvas(chart._gridlineCanvas, sampleLayout, glManager.dpr);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
clearAndSetupFrame(gl);
|
|
434
|
+
|
|
435
|
+
for (let i = 0; i < grid.cells.length; i++) {
|
|
436
|
+
const cell = grid.cells[i];
|
|
437
|
+
const zc = chart.getZoomControllerForFacet(i);
|
|
438
|
+
const facetDomain = independent && zc ? zc.getVisibleDomain() : domain;
|
|
439
|
+
|
|
440
|
+
// `buildProjectionMatrix` must run before `renderGridlines`:
|
|
441
|
+
// it seeds the padded-domain fields on `cell.layout` that
|
|
442
|
+
// `dataToPixel` (used by gridline tick → pixel mapping) reads.
|
|
443
|
+
// Skipping this order leaves the layout on its default
|
|
444
|
+
// `[0, 1]` padded domain, and every tick pixel falls outside
|
|
445
|
+
// the cell's `plotRect`, so `drawGridlinesX/Y` filters them
|
|
446
|
+
// all out and the gridline canvas stays blank.
|
|
447
|
+
const projection = cell.layout.buildProjectionMatrix(
|
|
448
|
+
facetDomain.xMin,
|
|
449
|
+
facetDomain.xMax,
|
|
450
|
+
facetDomain.yMin,
|
|
451
|
+
facetDomain.yMax,
|
|
452
|
+
undefined,
|
|
453
|
+
undefined,
|
|
454
|
+
undefined,
|
|
455
|
+
rebaseOrigin(chart._xOrigin),
|
|
456
|
+
rebaseOrigin(chart._yOrigin),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// Per-facet gridlines: reuse shared ticks in shared-zoom mode,
|
|
460
|
+
// compute fresh ticks in independent mode (each facet has its
|
|
461
|
+
// own domain). Map mode skips gridlines entirely; the
|
|
462
|
+
// basemap layer is rendered into the GL canvas inside the
|
|
463
|
+
// facet's scissor below.
|
|
464
|
+
const isMap = chart._renderMode === "map";
|
|
465
|
+
if (chart._gridlineCanvas && !isMap) {
|
|
466
|
+
const localXTicks = independent
|
|
467
|
+
? computeTicks(
|
|
468
|
+
buildXDomain(
|
|
469
|
+
chart,
|
|
470
|
+
facetDomain.xMin,
|
|
471
|
+
facetDomain.xMax,
|
|
472
|
+
xIsDate,
|
|
473
|
+
),
|
|
474
|
+
buildYDomain(
|
|
475
|
+
chart,
|
|
476
|
+
facetDomain.yMin,
|
|
477
|
+
facetDomain.yMax,
|
|
478
|
+
yIsDate,
|
|
479
|
+
),
|
|
480
|
+
cell.layout,
|
|
481
|
+
).xTicks
|
|
482
|
+
: xTicks;
|
|
483
|
+
const localYTicks = independent
|
|
484
|
+
? computeTicks(
|
|
485
|
+
buildXDomain(
|
|
486
|
+
chart,
|
|
487
|
+
facetDomain.xMin,
|
|
488
|
+
facetDomain.xMax,
|
|
489
|
+
xIsDate,
|
|
490
|
+
),
|
|
491
|
+
buildYDomain(
|
|
492
|
+
chart,
|
|
493
|
+
facetDomain.yMin,
|
|
494
|
+
facetDomain.yMax,
|
|
495
|
+
yIsDate,
|
|
496
|
+
),
|
|
497
|
+
cell.layout,
|
|
498
|
+
).yTicks
|
|
499
|
+
: yTicks;
|
|
500
|
+
renderGridlines(
|
|
501
|
+
chart._gridlineCanvas,
|
|
502
|
+
cell.layout,
|
|
503
|
+
localXTicks,
|
|
504
|
+
localYTicks,
|
|
505
|
+
theme,
|
|
506
|
+
glManager.dpr,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
withScissor(gl, cell.layout, glManager.dpr, () => {
|
|
511
|
+
if (isMap) {
|
|
512
|
+
chart.renderBackground(
|
|
513
|
+
glManager,
|
|
514
|
+
cell.layout,
|
|
515
|
+
projection,
|
|
516
|
+
facetDomain,
|
|
517
|
+
rebaseOrigin(chart._xOrigin),
|
|
518
|
+
rebaseOrigin(chart._yOrigin),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
chart.glyph.drawSeries(chart, glManager, projection, i);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
chart._lastXDomain = xDomain;
|
|
527
|
+
chart._lastYDomain = yDomain;
|
|
528
|
+
chart._lastXTicks = xTicks;
|
|
529
|
+
chart._lastYTicks = yTicks;
|
|
530
|
+
chart._lastGradientStops = theme.gradientStops;
|
|
531
|
+
chart._lastHasColorCol = hasLegend;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Redraw the chrome canvas only. Used for lightweight hover updates.
|
|
536
|
+
*/
|
|
537
|
+
export function renderCartesianChromeOverlay(chart: CartesianChart): void {
|
|
538
|
+
if (
|
|
539
|
+
!chart._chromeCanvas ||
|
|
540
|
+
!chart._lastLayout ||
|
|
541
|
+
!chart._lastXDomain ||
|
|
542
|
+
!chart._lastYDomain ||
|
|
543
|
+
!chart._glManager
|
|
544
|
+
) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// One-shot destructive prep for the chrome canvas — resizes to
|
|
549
|
+
// CSS × DPR and scales the transform. Per-facet calls below read
|
|
550
|
+
// the already-prepared context via `getScaledContext` so the
|
|
551
|
+
// bitmap persists across the loop.
|
|
552
|
+
initCanvas(chart._chromeCanvas, chart._lastLayout, chart._glManager.dpr);
|
|
553
|
+
if (chart._facetGrid) {
|
|
554
|
+
renderFacetedChromeOverlay(chart);
|
|
555
|
+
} else {
|
|
556
|
+
renderSinglePlotChromeOverlay(chart);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function renderSinglePlotChromeOverlay(chart: CartesianChart): void {
|
|
561
|
+
const layout = chart._lastLayout!;
|
|
562
|
+
const theme = chart._resolveTheme();
|
|
563
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
564
|
+
const isMap = chart._renderMode === "map";
|
|
565
|
+
|
|
566
|
+
if (isMap) {
|
|
567
|
+
chart.renderMapChrome(chart._chromeCanvas!, layout, theme, dpr);
|
|
568
|
+
} else {
|
|
569
|
+
renderAxesChrome(
|
|
570
|
+
chart._chromeCanvas!,
|
|
571
|
+
chart._lastXDomain!,
|
|
572
|
+
chart._lastYDomain!,
|
|
573
|
+
layout,
|
|
574
|
+
chart._lastXTicks!,
|
|
575
|
+
chart._lastYTicks!,
|
|
576
|
+
theme,
|
|
577
|
+
dpr,
|
|
578
|
+
chart.getColumnFormatter(chart._xName, "tick"),
|
|
579
|
+
chart.getColumnFormatter(chart._yName, "tick"),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (chart._lastHasColorCol) {
|
|
584
|
+
const stops = chart._lastGradientStops ?? theme.gradientStops;
|
|
585
|
+
if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
|
|
586
|
+
const palette = resolvePalette(
|
|
587
|
+
theme.seriesPalette,
|
|
588
|
+
stops,
|
|
589
|
+
chart._uniqueColorLabels.size,
|
|
590
|
+
);
|
|
591
|
+
renderCategoricalLegend(
|
|
592
|
+
chart._chromeCanvas!,
|
|
593
|
+
layout,
|
|
594
|
+
chart._uniqueColorLabels,
|
|
595
|
+
palette,
|
|
596
|
+
theme,
|
|
597
|
+
);
|
|
598
|
+
} else if (chart._colorName) {
|
|
599
|
+
renderLegend(
|
|
600
|
+
chart._chromeCanvas!,
|
|
601
|
+
layout,
|
|
602
|
+
{
|
|
603
|
+
min: chart._colorMin,
|
|
604
|
+
max: chart._colorMax,
|
|
605
|
+
label: chart._colorName,
|
|
606
|
+
},
|
|
607
|
+
stops,
|
|
608
|
+
theme,
|
|
609
|
+
chart.getColumnFormatter(chart._colorName, "value"),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
renderScatterLabels(chart, chart._chromeCanvas!, layout, 0, 1);
|
|
615
|
+
|
|
616
|
+
if (chart._hoveredIndex >= 0 && chart._xData && chart._yData) {
|
|
617
|
+
renderTooltip(chart, chart._chromeCanvas!, layout);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function renderFacetedChromeOverlay(chart: CartesianChart): void {
|
|
622
|
+
const grid = chart._facetGrid!;
|
|
623
|
+
const canvas = chart._chromeCanvas!;
|
|
624
|
+
const theme = chart._resolveTheme();
|
|
625
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
626
|
+
const sharedXTicks = chart._lastXTicks!;
|
|
627
|
+
const sharedYTicks = chart._lastYTicks!;
|
|
628
|
+
const xDomain = chart._lastXDomain!;
|
|
629
|
+
const yDomain = chart._lastYDomain!;
|
|
630
|
+
const isMap = chart._renderMode === "map";
|
|
631
|
+
|
|
632
|
+
// Read the frame-local effective flags set by `renderCartesianFrame`
|
|
633
|
+
// — these already fold in the independent-zoom override (outer
|
|
634
|
+
// axes are incompatible with per-cell viewports), so `sharedX` /
|
|
635
|
+
// `sharedY` true here implies shared-zoom too.
|
|
636
|
+
const sharedX = chart._lastEffectiveSharedX;
|
|
637
|
+
const sharedY = chart._lastEffectiveSharedY;
|
|
638
|
+
const independent = chart._facetConfig.zoom_mode === "independent";
|
|
639
|
+
|
|
640
|
+
// Shared X axis: one outer band across the bottom of the grid,
|
|
641
|
+
// with ticks painted per-column (one pass per bottom-row cell).
|
|
642
|
+
// Shared Y axis: one outer band down the left, ticks per-row
|
|
643
|
+
// (one pass per leftmost-column cell). Map mode replaces both
|
|
644
|
+
// with `renderMapChrome` (attribution + scale bar), painted once
|
|
645
|
+
// over the whole facet grid.
|
|
646
|
+
if (isMap) {
|
|
647
|
+
chart.renderMapChrome(canvas, chart._lastLayout!, theme, dpr);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!isMap && sharedX && grid.outerXAxisRect) {
|
|
651
|
+
renderOuterXAxis(
|
|
652
|
+
canvas,
|
|
653
|
+
grid.outerXAxisRect,
|
|
654
|
+
xDomain,
|
|
655
|
+
sharedXTicks,
|
|
656
|
+
bottomRowLayouts(grid),
|
|
657
|
+
theme,
|
|
658
|
+
!!chart._xLabel,
|
|
659
|
+
dpr,
|
|
660
|
+
chart.getColumnFormatter(chart._xName, "tick"),
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!isMap && sharedY && grid.outerYAxisRect) {
|
|
665
|
+
renderOuterYAxis(
|
|
666
|
+
canvas,
|
|
667
|
+
grid.outerYAxisRect,
|
|
668
|
+
yDomain,
|
|
669
|
+
sharedYTicks,
|
|
670
|
+
leftColumnLayouts(grid),
|
|
671
|
+
theme,
|
|
672
|
+
!!chart._yLabel,
|
|
673
|
+
dpr,
|
|
674
|
+
chart.getColumnFormatter(chart._yName, "tick"),
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Per-facet axes for the non-shared sides + title strips.
|
|
679
|
+
// Map mode skips per-cell axis rendering (no cartesian axes
|
|
680
|
+
// belong on a map) but still paints facet titles and labels.
|
|
681
|
+
for (let i = 0; i < grid.cells.length; i++) {
|
|
682
|
+
const cell = grid.cells[i];
|
|
683
|
+
const zc = independent ? chart.getZoomControllerForFacet(i) : null;
|
|
684
|
+
const d = zc ? zc.getVisibleDomain() : null;
|
|
685
|
+
const localX = d ? { ...xDomain, min: d.xMin, max: d.xMax } : xDomain;
|
|
686
|
+
const localY = d ? { ...yDomain, min: d.yMin, max: d.yMax } : yDomain;
|
|
687
|
+
const ticks = independent
|
|
688
|
+
? computeTicks(localX, localY, cell.layout)
|
|
689
|
+
: { xTicks: sharedXTicks, yTicks: sharedYTicks };
|
|
690
|
+
|
|
691
|
+
if (!isMap && !sharedX) {
|
|
692
|
+
renderCellXAxis(
|
|
693
|
+
canvas,
|
|
694
|
+
localX,
|
|
695
|
+
cell.layout,
|
|
696
|
+
ticks.xTicks,
|
|
697
|
+
theme,
|
|
698
|
+
!!chart._xLabel,
|
|
699
|
+
dpr,
|
|
700
|
+
chart.getColumnFormatter(chart._xName, "tick"),
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!isMap && !sharedY) {
|
|
705
|
+
renderCellYAxis(
|
|
706
|
+
canvas,
|
|
707
|
+
localY,
|
|
708
|
+
cell.layout,
|
|
709
|
+
ticks.yTicks,
|
|
710
|
+
theme,
|
|
711
|
+
!!chart._yLabel,
|
|
712
|
+
dpr,
|
|
713
|
+
chart.getColumnFormatter(chart._yName, "tick"),
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (cell.titleRect) {
|
|
718
|
+
drawFacetTitle(canvas, cell.label, cell.titleRect, theme, dpr);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
renderScatterLabels(chart, canvas, cell.layout, i, i + 1);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Shared legend: categorical (string color) or gradient
|
|
725
|
+
// (numeric color). Position derives from `grid.legendRect`
|
|
726
|
+
// which `buildFacetGrid` populates when `hasLegend` was set.
|
|
727
|
+
if (chart._lastHasColorCol && grid.legendRect) {
|
|
728
|
+
const stops = chart._lastGradientStops ?? theme.gradientStops;
|
|
729
|
+
if (chart._colorIsString && chart._uniqueColorLabels.size > 0) {
|
|
730
|
+
const palette = resolvePalette(
|
|
731
|
+
theme.seriesPalette,
|
|
732
|
+
stops,
|
|
733
|
+
Math.max(1, chart._uniqueColorLabels.size),
|
|
734
|
+
);
|
|
735
|
+
renderCategoricalLegendAt(
|
|
736
|
+
canvas,
|
|
737
|
+
grid.legendRect,
|
|
738
|
+
chart._uniqueColorLabels,
|
|
739
|
+
palette,
|
|
740
|
+
theme,
|
|
741
|
+
);
|
|
742
|
+
} else if (chart._colorName) {
|
|
743
|
+
// Numeric gradient legend in the shared outer rect. The
|
|
744
|
+
// label sits above the bar, so inset the rect's top by
|
|
745
|
+
// the usual 20 px that `renderLegend` reserves.
|
|
746
|
+
renderLegendAt(
|
|
747
|
+
canvas,
|
|
748
|
+
{
|
|
749
|
+
x: grid.legendRect.x,
|
|
750
|
+
y: grid.legendRect.y + 20,
|
|
751
|
+
width: grid.legendRect.width,
|
|
752
|
+
height: grid.legendRect.height - 20,
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
min: chart._colorMin,
|
|
756
|
+
max: chart._colorMax,
|
|
757
|
+
label: chart._colorName,
|
|
758
|
+
},
|
|
759
|
+
stops,
|
|
760
|
+
theme,
|
|
761
|
+
chart.getColumnFormatter(chart._colorName, "value"),
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Coordinated hover / click indicators across facets. The tooltip
|
|
767
|
+
// lines are whatever the last resolved lazy fetch produced (or
|
|
768
|
+
// null while a fetch is still in flight); `renderCanvasTooltip`
|
|
769
|
+
// paints crosshair + ring regardless, but skips the text box
|
|
770
|
+
// until lines are available. See `handleCartesianHover`.
|
|
771
|
+
if (chart._hoveredIndex >= 0 && chart._xData && chart._yData) {
|
|
772
|
+
// `_xData`/`_yData` are rebased; `dataToPixel` expects absolute
|
|
773
|
+
// domain coords (matching `paddedXMin`/`paddedXMax`), so undo
|
|
774
|
+
// the rebase before mapping.
|
|
775
|
+
const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
|
|
776
|
+
const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
|
|
777
|
+
const dataX = chart._xData[chart._hoveredIndex] + xOrigin;
|
|
778
|
+
const dataY = chart._yData[chart._hoveredIndex] + yOrigin;
|
|
779
|
+
const sourceFacet = seriesFromIndex(chart, chart._hoveredIndex);
|
|
780
|
+
const opts = chart.glyph.tooltipOptions();
|
|
781
|
+
const tooltipLines = chart._lazyTooltip.lines ?? [];
|
|
782
|
+
|
|
783
|
+
for (let i = 0; i < grid.cells.length; i++) {
|
|
784
|
+
const cell = grid.cells[i];
|
|
785
|
+
const isSource = i === sourceFacet;
|
|
786
|
+
|
|
787
|
+
// Pixel position inside this facet for the source point's
|
|
788
|
+
// data coordinate — ghost indicator in non-source facets.
|
|
789
|
+
const pos = cell.layout.dataToPixel(dataX, dataY);
|
|
790
|
+
const plot = cell.layout.plotRect;
|
|
791
|
+
if (
|
|
792
|
+
pos.px < plot.x ||
|
|
793
|
+
pos.px > plot.x + plot.width ||
|
|
794
|
+
pos.py < plot.y ||
|
|
795
|
+
pos.py > plot.y + plot.height
|
|
796
|
+
) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const coordinated = chart._facetConfig.coordinated_tooltip;
|
|
801
|
+
const lines = isSource || coordinated ? tooltipLines : [];
|
|
802
|
+
renderCanvasTooltip(canvas, pos, lines, cell.layout, theme, dpr, {
|
|
803
|
+
crosshair: opts.crosshair,
|
|
804
|
+
highlightRadius: isSource ? opts.highlightRadius : 0,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Map a flat slotted index back to its series (facet) index.
|
|
812
|
+
*/
|
|
813
|
+
export function seriesFromIndex(
|
|
814
|
+
chart: CartesianChart,
|
|
815
|
+
flatIdx: number,
|
|
816
|
+
): number {
|
|
817
|
+
if (chart._seriesCapacity <= 0) {
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return Math.floor(flatIdx / chart._seriesCapacity);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Maximum scatter labels painted in a single chrome pass. Beyond this
|
|
826
|
+
* we sample with a fixed stride so the canvas pass stays bounded as
|
|
827
|
+
* the user zooms out. The chrome overlay redraws on hover, so an
|
|
828
|
+
* unbounded `fillText` loop would stutter on every mouse move.
|
|
829
|
+
*/
|
|
830
|
+
const MAX_SCATTER_LABELS = 5_000;
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Draw the scatter-label column (slot 4) as 2D text next to each
|
|
834
|
+
* visible point. Labels are anchored slightly to the right of the
|
|
835
|
+
* point and vertically centered on it, painted in the theme's
|
|
836
|
+
* `labelColor`. Caller scopes us to a series range so faceted mode
|
|
837
|
+
* draws only the cell's own labels.
|
|
838
|
+
*/
|
|
839
|
+
function renderScatterLabels(
|
|
840
|
+
chart: CartesianChart,
|
|
841
|
+
canvas: Canvas2D,
|
|
842
|
+
layout: PlotLayout,
|
|
843
|
+
seriesStart: number,
|
|
844
|
+
seriesEnd: number,
|
|
845
|
+
): void {
|
|
846
|
+
if (!chart._labels || !chart._xData || !chart._yData) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const dict = chart._labels.dictionary;
|
|
851
|
+
const labelData = chart._labels.data;
|
|
852
|
+
const xData = chart._xData;
|
|
853
|
+
const yData = chart._yData;
|
|
854
|
+
const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
|
|
855
|
+
const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
|
|
856
|
+
const cap = chart._seriesCapacity;
|
|
857
|
+
if (cap <= 0) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
let visibleCount = 0;
|
|
862
|
+
for (let s = seriesStart; s < seriesEnd; s++) {
|
|
863
|
+
visibleCount += chart._seriesUploadedCounts[s] ?? 0;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (visibleCount === 0) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
871
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
872
|
+
if (!ctx) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const theme = chart._resolveTheme();
|
|
877
|
+
const plot = layout.plotRect;
|
|
878
|
+
const stride = Math.max(1, Math.ceil(visibleCount / MAX_SCATTER_LABELS));
|
|
879
|
+
|
|
880
|
+
ctx.save();
|
|
881
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
882
|
+
ctx.fillStyle = theme.labelColor;
|
|
883
|
+
ctx.textAlign = "left";
|
|
884
|
+
ctx.textBaseline = "middle";
|
|
885
|
+
|
|
886
|
+
for (let s = seriesStart; s < seriesEnd; s++) {
|
|
887
|
+
const count = chart._seriesUploadedCounts[s] ?? 0;
|
|
888
|
+
const base = s * cap;
|
|
889
|
+
for (let j = 0; j < count; j += stride) {
|
|
890
|
+
const idx = base + j;
|
|
891
|
+
const dictIdx = labelData[idx];
|
|
892
|
+
if (dictIdx < 0) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const { px, py } = layout.dataToPixel(
|
|
897
|
+
xData[idx] + xOrigin,
|
|
898
|
+
yData[idx] + yOrigin,
|
|
899
|
+
);
|
|
900
|
+
if (
|
|
901
|
+
px < plot.x ||
|
|
902
|
+
px > plot.x + plot.width ||
|
|
903
|
+
py < plot.y ||
|
|
904
|
+
py > plot.y + plot.height
|
|
905
|
+
) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
ctx.fillText(dict[dictIdx], px + 8, py - 4);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
ctx.restore();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function renderTooltip(
|
|
917
|
+
chart: CartesianChart,
|
|
918
|
+
canvas: Canvas2D,
|
|
919
|
+
layout: PlotLayout,
|
|
920
|
+
): void {
|
|
921
|
+
const idx = chart._hoveredIndex;
|
|
922
|
+
if (idx < 0 || !chart._xData || !chart._yData) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const xOrigin = isNaN(chart._xOrigin) ? 0 : chart._xOrigin;
|
|
927
|
+
const yOrigin = isNaN(chart._yOrigin) ? 0 : chart._yOrigin;
|
|
928
|
+
const pos = layout.dataToPixel(
|
|
929
|
+
chart._xData[idx] + xOrigin,
|
|
930
|
+
chart._yData[idx] + yOrigin,
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
// Lines come from the async lazy tooltip fetch kicked off in
|
|
934
|
+
// `handleCartesianHover`. While a fetch is in flight this is
|
|
935
|
+
// `null`; the canvas tooltip helper still paints the crosshair /
|
|
936
|
+
// highlight ring but skips the text box.
|
|
937
|
+
const lines = chart._lazyTooltip.lines ?? [];
|
|
938
|
+
const theme = chart._resolveTheme();
|
|
939
|
+
renderCanvasTooltip(
|
|
940
|
+
canvas,
|
|
941
|
+
pos,
|
|
942
|
+
lines,
|
|
943
|
+
layout,
|
|
944
|
+
theme,
|
|
945
|
+
chart._glManager?.dpr ?? 1,
|
|
946
|
+
chart.glyph.tooltipOptions(),
|
|
947
|
+
);
|
|
948
|
+
}
|