@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,887 @@
|
|
|
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 { Context2D } from "../canvas-types";
|
|
14
|
+
import type { WebGLContextManager } from "../../webgl/context-manager";
|
|
15
|
+
import type { SunburstChart } from "./sunburst";
|
|
16
|
+
import { NULL_NODE } from "../common/node-store";
|
|
17
|
+
import { resolvePalette, type Vec3 } from "../../theme/palette";
|
|
18
|
+
import { type GradientStop } from "../../theme/gradient";
|
|
19
|
+
import { renderLegend, renderCategoricalLegend } from "../../axis/legend";
|
|
20
|
+
import { PlotLayout } from "../../layout/plot-layout";
|
|
21
|
+
import { leafColor, leafRGBA, luminance } from "../common/leaf-color";
|
|
22
|
+
import arcVert from "../../shaders/sunburst-arc.vert.glsl";
|
|
23
|
+
import arcFrag from "../../shaders/sunburst-arc.frag.glsl";
|
|
24
|
+
import { getInstancing } from "../../webgl/instanced-attrs";
|
|
25
|
+
import {
|
|
26
|
+
partitionSunburst,
|
|
27
|
+
collectVisibleArcs,
|
|
28
|
+
collectVisibleArcsAppend,
|
|
29
|
+
INNER_RING_PX,
|
|
30
|
+
} from "./sunburst-layout";
|
|
31
|
+
import { buildFacetGrid } from "../../layout/facet-grid";
|
|
32
|
+
import { renderCategoricalLegendAt } from "../../axis/legend";
|
|
33
|
+
import { withChromeCache } from "../common/chrome-cache";
|
|
34
|
+
import {
|
|
35
|
+
renderBreadcrumbs as renderTreeBreadcrumbs,
|
|
36
|
+
renderTreeTooltip,
|
|
37
|
+
} from "../common/tree-chrome";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Triangle-strip template resolution. `N_STEPS` angular samples × 2
|
|
41
|
+
* radial sides = `2 * (N_STEPS + 1)` strip vertices. 32 samples is
|
|
42
|
+
* smooth to the eye at typical viewport sizes; bump to 64 if faceting
|
|
43
|
+
* becomes visible on full-circle arcs.
|
|
44
|
+
*/
|
|
45
|
+
const N_STEPS = 32;
|
|
46
|
+
const BREADCRUMB_H = 28;
|
|
47
|
+
const LEGEND_W = 90;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the `(centerX, centerY)` of the facet that owns `nodeId`.
|
|
51
|
+
* Walks the ancestor chain and matches against each facet's
|
|
52
|
+
* `drillRoot`; returns `chart._centerX/_centerY` in non-faceted mode
|
|
53
|
+
* or as a defensive fallback. Used by every chrome path that needs to
|
|
54
|
+
* place geometry around an arc — labels, hover highlight, hover
|
|
55
|
+
* tooltip, pinned tooltip — so all four agree on which facet owns the
|
|
56
|
+
* node. The chart-wide `_centerX/_centerY` fields are
|
|
57
|
+
* `layoutFacetedSunburst`'s legacy first-facet publication and are
|
|
58
|
+
* not safe for these calls.
|
|
59
|
+
*/
|
|
60
|
+
export function facetCenterForNode(
|
|
61
|
+
chart: SunburstChart,
|
|
62
|
+
nodeId: number,
|
|
63
|
+
): { centerX: number; centerY: number } {
|
|
64
|
+
if (chart._facets.length === 0) {
|
|
65
|
+
return { centerX: chart._centerX, centerY: chart._centerY };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const store = chart._nodeStore;
|
|
69
|
+
for (const facet of chart._facets) {
|
|
70
|
+
let p = nodeId;
|
|
71
|
+
while (p !== NULL_NODE) {
|
|
72
|
+
if (p === facet.drillRoot) {
|
|
73
|
+
return { centerX: facet.centerX, centerY: facet.centerY };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
p = store.parent[p];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { centerX: chart._centerX, centerY: chart._centerY };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Full-frame render: layout → WebGL arcs → chrome overlay.
|
|
85
|
+
*/
|
|
86
|
+
export function renderSunburstFrame(
|
|
87
|
+
chart: SunburstChart,
|
|
88
|
+
glManager: WebGLContextManager,
|
|
89
|
+
): void {
|
|
90
|
+
if (chart._currentRootId === NULL_NODE) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const gl = glManager.gl;
|
|
95
|
+
const cssWidth = glManager.cssWidth;
|
|
96
|
+
const cssHeight = glManager.cssHeight;
|
|
97
|
+
if (cssWidth <= 0 || cssHeight <= 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hasSplits =
|
|
102
|
+
chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid";
|
|
103
|
+
const hasLegend =
|
|
104
|
+
chart._colorMode === "series"
|
|
105
|
+
? chart._uniqueColorLabels.size > 1
|
|
106
|
+
: chart._colorMode === "numeric" &&
|
|
107
|
+
chart._colorMin < chart._colorMax;
|
|
108
|
+
const breadcrumbH =
|
|
109
|
+
!hasSplits && chart._breadcrumbIds.length > 1 ? BREADCRUMB_H : 0;
|
|
110
|
+
const legendW = hasLegend ? LEGEND_W : 0;
|
|
111
|
+
|
|
112
|
+
if (hasSplits) {
|
|
113
|
+
layoutFacetedSunburst(chart, cssWidth, cssHeight, legendW);
|
|
114
|
+
} else {
|
|
115
|
+
chart._facetGrid = null;
|
|
116
|
+
chart._facets = [];
|
|
117
|
+
const plotW = cssWidth - legendW;
|
|
118
|
+
const plotH = cssHeight - breadcrumbH;
|
|
119
|
+
chart._centerX = plotW / 2;
|
|
120
|
+
chart._centerY = breadcrumbH + plotH / 2;
|
|
121
|
+
chart._maxRadius = Math.max(0, Math.min(plotW, plotH) / 2 - 4);
|
|
122
|
+
|
|
123
|
+
partitionSunburst(
|
|
124
|
+
chart._nodeStore,
|
|
125
|
+
chart._currentRootId,
|
|
126
|
+
chart._maxRadius,
|
|
127
|
+
);
|
|
128
|
+
collectVisibleArcs(chart, chart._currentRootId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ensureProgram(chart, glManager);
|
|
132
|
+
|
|
133
|
+
const theme = chart._resolveTheme();
|
|
134
|
+
const stops = theme.gradientStops;
|
|
135
|
+
const palette = resolvePalette(
|
|
136
|
+
theme.seriesPalette,
|
|
137
|
+
stops,
|
|
138
|
+
Math.max(1, chart._uniqueColorLabels.size),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (chart._gridlineCanvas) {
|
|
142
|
+
const gCtx = chart._gridlineCanvas.getContext("2d") as Context2D | null;
|
|
143
|
+
if (gCtx) {
|
|
144
|
+
gCtx.clearRect(
|
|
145
|
+
0,
|
|
146
|
+
0,
|
|
147
|
+
chart._gridlineCanvas.width,
|
|
148
|
+
chart._gridlineCanvas.height,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dpr = glManager.dpr;
|
|
154
|
+
chart._chromeCacheDirty = true;
|
|
155
|
+
uploadArcInstances(chart, gl, stops, palette, theme.areaOpacity, dpr);
|
|
156
|
+
gl.clearColor(0, 0, 0, 0);
|
|
157
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
158
|
+
gl.enable(gl.BLEND);
|
|
159
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
160
|
+
gl.useProgram(chart._program!);
|
|
161
|
+
|
|
162
|
+
const loc = chart._locations!;
|
|
163
|
+
gl.uniform2f(loc.u_resolution, gl.canvas.width, gl.canvas.height);
|
|
164
|
+
gl.uniform1f(loc.u_border_px, theme.sunburstGapPx * dpr);
|
|
165
|
+
|
|
166
|
+
if (chart._facets.length > 0) {
|
|
167
|
+
// Faceted: one dispatch per facet with the matching `u_center`
|
|
168
|
+
// and instance range. Instance attribs are rebound per facet so
|
|
169
|
+
// instance 0 of each dispatch is the facet's first arc.
|
|
170
|
+
for (const facet of chart._facets) {
|
|
171
|
+
if (facet.instanceCount === 0) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
gl.uniform2f(
|
|
176
|
+
loc.u_center,
|
|
177
|
+
facet.centerX * dpr,
|
|
178
|
+
facet.centerY * dpr,
|
|
179
|
+
);
|
|
180
|
+
drawArcs(
|
|
181
|
+
chart,
|
|
182
|
+
gl,
|
|
183
|
+
glManager,
|
|
184
|
+
facet.instanceStart,
|
|
185
|
+
facet.instanceCount,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
gl.uniform2f(loc.u_center, chart._centerX * dpr, chart._centerY * dpr);
|
|
190
|
+
drawArcs(chart, gl, glManager, 0, chart._instanceCount);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
renderSunburstChromeOverlay(chart);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Allocate the facet grid and compute per-facet (center, radius, drill
|
|
198
|
+
* root) triples. Also runs `partitionSunburst` + `collectVisibleArcs`
|
|
199
|
+
* per facet so the combined visible list is in `_visibleNodeIds` with
|
|
200
|
+
* facets in cell order (instance uploads walk this list).
|
|
201
|
+
*/
|
|
202
|
+
function layoutFacetedSunburst(
|
|
203
|
+
chart: SunburstChart,
|
|
204
|
+
cssWidth: number,
|
|
205
|
+
cssHeight: number,
|
|
206
|
+
legendW: number,
|
|
207
|
+
): void {
|
|
208
|
+
const store = chart._nodeStore;
|
|
209
|
+
const facetIds: number[] = [];
|
|
210
|
+
const labels: string[] = [];
|
|
211
|
+
for (
|
|
212
|
+
let c = store.firstChild[chart._rootId];
|
|
213
|
+
c !== NULL_NODE;
|
|
214
|
+
c = store.nextSibling[c]
|
|
215
|
+
) {
|
|
216
|
+
if (store.value[c] <= 0) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
facetIds.push(c);
|
|
221
|
+
labels.push(store.name[c]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const gridWidth = Math.max(1, cssWidth - legendW);
|
|
225
|
+
const grid = buildFacetGrid(labels, {
|
|
226
|
+
cssWidth: gridWidth,
|
|
227
|
+
cssHeight,
|
|
228
|
+
hasLegend: false,
|
|
229
|
+
|
|
230
|
+
// Sunburst has no X/Y axes — no per-cell gutter reservation.
|
|
231
|
+
xAxis: "none",
|
|
232
|
+
yAxis: "none",
|
|
233
|
+
gap: chart._facetConfig.facet_padding,
|
|
234
|
+
});
|
|
235
|
+
chart._facetGrid = grid;
|
|
236
|
+
|
|
237
|
+
const facets: SunburstChart["_facets"] = [];
|
|
238
|
+
let outIdx = 0;
|
|
239
|
+
for (let i = 0; i < facetIds.length; i++) {
|
|
240
|
+
const facetId = facetIds[i];
|
|
241
|
+
const cell = grid.cells[i];
|
|
242
|
+
if (!cell) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const label = store.name[facetId];
|
|
247
|
+
const drillRoot = chart._facetDrillRoots.get(label) ?? facetId;
|
|
248
|
+
const plot = cell.layout.plotRect;
|
|
249
|
+
const centerX = plot.x + plot.width / 2;
|
|
250
|
+
const centerY = plot.y + plot.height / 2;
|
|
251
|
+
const maxRadius = Math.max(
|
|
252
|
+
0,
|
|
253
|
+
Math.min(plot.width, plot.height) / 2 - 4,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
partitionSunburst(store, drillRoot, maxRadius);
|
|
257
|
+
const nextIdx = collectVisibleArcsAppend(chart, drillRoot, outIdx);
|
|
258
|
+
const instanceStart = outIdx;
|
|
259
|
+
const instanceCount = nextIdx - outIdx;
|
|
260
|
+
|
|
261
|
+
facets.push({
|
|
262
|
+
label,
|
|
263
|
+
centerX,
|
|
264
|
+
centerY,
|
|
265
|
+
maxRadius,
|
|
266
|
+
drillRoot,
|
|
267
|
+
instanceStart,
|
|
268
|
+
instanceCount,
|
|
269
|
+
nodeStart: instanceStart,
|
|
270
|
+
nodeCount: instanceCount,
|
|
271
|
+
});
|
|
272
|
+
outIdx = nextIdx;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
chart._visibleNodeCount = outIdx;
|
|
276
|
+
chart._facets = facets;
|
|
277
|
+
|
|
278
|
+
// Publish the first facet's center/radius to the legacy fields so
|
|
279
|
+
// chrome code paths that still read them (e.g. non-faceted label
|
|
280
|
+
// placement) pick sensible values.
|
|
281
|
+
if (facets.length > 0) {
|
|
282
|
+
chart._centerX = facets[0].centerX;
|
|
283
|
+
chart._centerY = facets[0].centerY;
|
|
284
|
+
chart._maxRadius = facets[0].maxRadius;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function ensureProgram(
|
|
289
|
+
chart: SunburstChart,
|
|
290
|
+
glManager: WebGLContextManager,
|
|
291
|
+
): void {
|
|
292
|
+
if (chart._program) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const gl = glManager.gl;
|
|
297
|
+
const prog = glManager.shaders.getOrCreate(
|
|
298
|
+
"sunburst-arc",
|
|
299
|
+
arcVert,
|
|
300
|
+
arcFrag,
|
|
301
|
+
);
|
|
302
|
+
chart._program = prog;
|
|
303
|
+
chart._locations = {
|
|
304
|
+
u_center: gl.getUniformLocation(prog, "u_center"),
|
|
305
|
+
u_resolution: gl.getUniformLocation(prog, "u_resolution"),
|
|
306
|
+
u_border_px: gl.getUniformLocation(prog, "u_border_px"),
|
|
307
|
+
a_strip_t: gl.getAttribLocation(prog, "a_strip_t"),
|
|
308
|
+
a_side: gl.getAttribLocation(prog, "a_side"),
|
|
309
|
+
a_angles: gl.getAttribLocation(prog, "a_angles"),
|
|
310
|
+
a_radii: gl.getAttribLocation(prog, "a_radii"),
|
|
311
|
+
a_color: gl.getAttribLocation(prog, "a_color"),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Build the static triangle-strip template once. Layout:
|
|
315
|
+
// pairs of (strip_t, side) for each of the 2*(N_STEPS+1) vertices.
|
|
316
|
+
// even vertex = inner (side=0), odd vertex = outer (side=1).
|
|
317
|
+
const template = new Float32Array((N_STEPS + 1) * 2 * 2);
|
|
318
|
+
for (let i = 0; i <= N_STEPS; i++) {
|
|
319
|
+
const t = i / N_STEPS;
|
|
320
|
+
const o = i * 4;
|
|
321
|
+
template[o + 0] = t;
|
|
322
|
+
template[o + 1] = 0; // inner
|
|
323
|
+
template[o + 2] = t;
|
|
324
|
+
template[o + 3] = 1; // outer
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
chart._stripBuffer = gl.createBuffer()!;
|
|
328
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, chart._stripBuffer);
|
|
329
|
+
gl.bufferData(gl.ARRAY_BUFFER, template, gl.STATIC_DRAW);
|
|
330
|
+
|
|
331
|
+
chart._instanceBuffer = gl.createBuffer()!;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function uploadArcInstances(
|
|
335
|
+
chart: SunburstChart,
|
|
336
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
337
|
+
stops: GradientStop[],
|
|
338
|
+
palette: Vec3[],
|
|
339
|
+
negativeAlpha: number,
|
|
340
|
+
dpr: number,
|
|
341
|
+
): void {
|
|
342
|
+
const store = chart._nodeStore;
|
|
343
|
+
const ids = chart._visibleNodeIds!;
|
|
344
|
+
const faceted = chart._facets.length > 0;
|
|
345
|
+
|
|
346
|
+
// Walk each facet's pre-upload visible range (instanceStart and
|
|
347
|
+
// instanceCount as set by `layoutFacetedSunburst`), skip the facet's
|
|
348
|
+
// drill root + any zero-width arcs, and emit one contiguous run per
|
|
349
|
+
// facet. Update `(instanceStart, instanceCount)` to the post-skip
|
|
350
|
+
// values so draw dispatch can offset into the shared buffer.
|
|
351
|
+
//
|
|
352
|
+
// 8 floats per instance: [a0, a1, r0, r1, r, g, b, a]. Alpha = 1
|
|
353
|
+
// for positive-size arcs, `negativeAlpha` for arcs whose raw size
|
|
354
|
+
// column value was negative (keeps the arc visible but dimmer).
|
|
355
|
+
const totalCap = faceted
|
|
356
|
+
? chart._facets.reduce((a, f) => a + f.instanceCount, 0)
|
|
357
|
+
: chart._visibleNodeCount;
|
|
358
|
+
const data = new Float32Array(totalCap * 8);
|
|
359
|
+
let instance = 0;
|
|
360
|
+
|
|
361
|
+
const emitRange = (start: number, end: number, drillRoot: number) => {
|
|
362
|
+
const rangeStart = instance;
|
|
363
|
+
for (let i = start; i < end; i++) {
|
|
364
|
+
const id = ids[i];
|
|
365
|
+
if (id === drillRoot) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const a0 = store.a0[id];
|
|
370
|
+
const a1 = store.a1[id];
|
|
371
|
+
const r0 = store.r0[id];
|
|
372
|
+
const r1 = store.r1[id];
|
|
373
|
+
if (a1 <= a0 || r1 <= r0) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const color = leafRGBA(chart, id, stops, palette, negativeAlpha);
|
|
378
|
+
const o = instance * 8;
|
|
379
|
+
data[o + 0] = a0;
|
|
380
|
+
data[o + 1] = a1;
|
|
381
|
+
data[o + 2] = r0 * dpr;
|
|
382
|
+
data[o + 3] = r1 * dpr;
|
|
383
|
+
data[o + 4] = color[0];
|
|
384
|
+
data[o + 5] = color[1];
|
|
385
|
+
data[o + 6] = color[2];
|
|
386
|
+
data[o + 7] = color[3];
|
|
387
|
+
instance++;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { rangeStart, rangeCount: instance - rangeStart };
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (faceted) {
|
|
394
|
+
for (const facet of chart._facets) {
|
|
395
|
+
const preStart = facet.instanceStart;
|
|
396
|
+
const preEnd = preStart + facet.instanceCount;
|
|
397
|
+
const { rangeStart, rangeCount } = emitRange(
|
|
398
|
+
preStart,
|
|
399
|
+
preEnd,
|
|
400
|
+
facet.drillRoot,
|
|
401
|
+
);
|
|
402
|
+
facet.instanceStart = rangeStart;
|
|
403
|
+
facet.instanceCount = rangeCount;
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
emitRange(0, chart._visibleNodeCount, chart._currentRootId);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
chart._instanceCount = instance;
|
|
410
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, chart._instanceBuffer);
|
|
411
|
+
gl.bufferData(
|
|
412
|
+
gl.ARRAY_BUFFER,
|
|
413
|
+
data.subarray(0, instance * 8),
|
|
414
|
+
gl.DYNAMIC_DRAW,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Dispatch one instanced draw over `[instanceStart, instanceStart+count)`
|
|
420
|
+
* of the shared arc instance buffer. In single-plot mode the range is
|
|
421
|
+
* the whole buffer; in faceted mode the caller dispatches once per
|
|
422
|
+
* facet with the matching `u_center` uniform.
|
|
423
|
+
*
|
|
424
|
+
* Instance attribute pointers are rebound with a byte offset per call
|
|
425
|
+
* so instance 0 of the draw is the facet's first arc.
|
|
426
|
+
*/
|
|
427
|
+
function drawArcs(
|
|
428
|
+
chart: SunburstChart,
|
|
429
|
+
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
|
430
|
+
glManager: WebGLContextManager,
|
|
431
|
+
instanceStart: number,
|
|
432
|
+
instanceCount: number,
|
|
433
|
+
): void {
|
|
434
|
+
if (instanceCount === 0) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const loc = chart._locations!;
|
|
439
|
+
|
|
440
|
+
// Static strip: per-vertex (strip_t, side).
|
|
441
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, chart._stripBuffer!);
|
|
442
|
+
const stripStride = 2 * Float32Array.BYTES_PER_ELEMENT;
|
|
443
|
+
gl.enableVertexAttribArray(loc.a_strip_t);
|
|
444
|
+
gl.vertexAttribPointer(loc.a_strip_t, 1, gl.FLOAT, false, stripStride, 0);
|
|
445
|
+
gl.enableVertexAttribArray(loc.a_side);
|
|
446
|
+
gl.vertexAttribPointer(
|
|
447
|
+
loc.a_side,
|
|
448
|
+
1,
|
|
449
|
+
gl.FLOAT,
|
|
450
|
+
false,
|
|
451
|
+
stripStride,
|
|
452
|
+
Float32Array.BYTES_PER_ELEMENT,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const instancing = getInstancing(glManager);
|
|
456
|
+
const { setDivisor } = instancing;
|
|
457
|
+
setDivisor(loc.a_strip_t, 0);
|
|
458
|
+
setDivisor(loc.a_side, 0);
|
|
459
|
+
|
|
460
|
+
// Per-instance interleaved buffer (rebind with byte offset so
|
|
461
|
+
// instance 0 of the draw is slot `instanceStart`). Layout:
|
|
462
|
+
// [0..1] a_angles (a0, a1)
|
|
463
|
+
// [2..3] a_radii (r0, r1)
|
|
464
|
+
// [4..7] a_color (r, g, b, a)
|
|
465
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, chart._instanceBuffer!);
|
|
466
|
+
const instStride = 8 * Float32Array.BYTES_PER_ELEMENT;
|
|
467
|
+
const f = Float32Array.BYTES_PER_ELEMENT;
|
|
468
|
+
const base = instanceStart * instStride;
|
|
469
|
+
gl.enableVertexAttribArray(loc.a_angles);
|
|
470
|
+
gl.vertexAttribPointer(loc.a_angles, 2, gl.FLOAT, false, instStride, base);
|
|
471
|
+
setDivisor(loc.a_angles, 1);
|
|
472
|
+
gl.enableVertexAttribArray(loc.a_radii);
|
|
473
|
+
gl.vertexAttribPointer(
|
|
474
|
+
loc.a_radii,
|
|
475
|
+
2,
|
|
476
|
+
gl.FLOAT,
|
|
477
|
+
false,
|
|
478
|
+
instStride,
|
|
479
|
+
base + 2 * f,
|
|
480
|
+
);
|
|
481
|
+
setDivisor(loc.a_radii, 1);
|
|
482
|
+
gl.enableVertexAttribArray(loc.a_color);
|
|
483
|
+
gl.vertexAttribPointer(
|
|
484
|
+
loc.a_color,
|
|
485
|
+
4,
|
|
486
|
+
gl.FLOAT,
|
|
487
|
+
false,
|
|
488
|
+
instStride,
|
|
489
|
+
base + 4 * f,
|
|
490
|
+
);
|
|
491
|
+
setDivisor(loc.a_color, 1);
|
|
492
|
+
|
|
493
|
+
instancing.drawArraysInstanced(
|
|
494
|
+
gl.TRIANGLE_STRIP,
|
|
495
|
+
0,
|
|
496
|
+
2 * (N_STEPS + 1),
|
|
497
|
+
instanceCount,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
setDivisor(loc.a_angles, 0);
|
|
501
|
+
setDivisor(loc.a_radii, 0);
|
|
502
|
+
setDivisor(loc.a_color, 0);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Chrome overlay (Canvas2D)
|
|
506
|
+
|
|
507
|
+
export function renderSunburstChromeOverlay(chart: SunburstChart): void {
|
|
508
|
+
if (!chart._chromeCanvas || chart._currentRootId === NULL_NODE) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const glManager = chart._glManager;
|
|
513
|
+
if (!glManager) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const { dpr, cssWidth, cssHeight } = glManager;
|
|
518
|
+
|
|
519
|
+
withChromeCache(
|
|
520
|
+
chart,
|
|
521
|
+
chart._chromeCanvas,
|
|
522
|
+
dpr,
|
|
523
|
+
cssWidth,
|
|
524
|
+
cssHeight,
|
|
525
|
+
(ctx) => drawStaticChrome(chart, ctx, dpr, cssWidth, cssHeight),
|
|
526
|
+
chart._hoveredNodeId !== NULL_NODE
|
|
527
|
+
? (ctx) => {
|
|
528
|
+
renderHoverHighlight(ctx, chart, chart._hoveredNodeId);
|
|
529
|
+
renderSunburstTooltip(
|
|
530
|
+
chart,
|
|
531
|
+
ctx,
|
|
532
|
+
chart._hoveredNodeId,
|
|
533
|
+
cssWidth,
|
|
534
|
+
cssHeight,
|
|
535
|
+
chart._resolveTheme().fontFamily,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
: null,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function drawStaticChrome(
|
|
543
|
+
chart: SunburstChart,
|
|
544
|
+
ctx: Context2D,
|
|
545
|
+
dpr: number,
|
|
546
|
+
cssWidth: number,
|
|
547
|
+
cssHeight: number,
|
|
548
|
+
): void {
|
|
549
|
+
const canvas = chart._chromeCanvas!;
|
|
550
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
551
|
+
ctx.save();
|
|
552
|
+
ctx.scale(dpr, dpr);
|
|
553
|
+
|
|
554
|
+
const theme = chart._resolveTheme();
|
|
555
|
+
const { fontFamily, labelColor: textColor, tooltipBg } = theme;
|
|
556
|
+
const stops = theme.gradientStops;
|
|
557
|
+
const palette = resolvePalette(
|
|
558
|
+
theme.seriesPalette,
|
|
559
|
+
stops,
|
|
560
|
+
Math.max(1, chart._uniqueColorLabels.size),
|
|
561
|
+
);
|
|
562
|
+
const store = chart._nodeStore;
|
|
563
|
+
const ids = chart._visibleNodeIds!;
|
|
564
|
+
const n = chart._visibleNodeCount;
|
|
565
|
+
const faceted = chart._facets.length > 0;
|
|
566
|
+
|
|
567
|
+
// Arc labels — skip each facet's own drill root (its label is the
|
|
568
|
+
// center text / facet title, handled below). In faceted mode, walk
|
|
569
|
+
// each facet's `nodeStart`/`nodeCount` range over `_visibleNodeIds`
|
|
570
|
+
// and rotate around that facet's `(centerX, centerY)`. Without the
|
|
571
|
+
// per-facet center, every label translates around the chart's
|
|
572
|
+
// `_centerX/_centerY`, which `layoutFacetedSunburst` publishes
|
|
573
|
+
// from facet 0 — the symptom is "all labels pile onto facet 0."
|
|
574
|
+
if (faceted) {
|
|
575
|
+
for (const facet of chart._facets) {
|
|
576
|
+
const end = facet.nodeStart + facet.nodeCount;
|
|
577
|
+
for (let i = facet.nodeStart; i < end; i++) {
|
|
578
|
+
const id = ids[i];
|
|
579
|
+
if (id === facet.drillRoot) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
renderArcLabel(
|
|
584
|
+
chart,
|
|
585
|
+
ctx,
|
|
586
|
+
id,
|
|
587
|
+
fontFamily,
|
|
588
|
+
stops,
|
|
589
|
+
palette,
|
|
590
|
+
facet.centerX,
|
|
591
|
+
facet.centerY,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
for (let i = 0; i < n; i++) {
|
|
597
|
+
const id = ids[i];
|
|
598
|
+
if (id === chart._currentRootId) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
renderArcLabel(
|
|
603
|
+
chart,
|
|
604
|
+
ctx,
|
|
605
|
+
id,
|
|
606
|
+
fontFamily,
|
|
607
|
+
stops,
|
|
608
|
+
palette,
|
|
609
|
+
chart._centerX,
|
|
610
|
+
chart._centerY,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Inner drill-up circle(s). One per facet in faceted mode so each
|
|
616
|
+
// facet has its own center hit target.
|
|
617
|
+
const innerDiscR = Math.max(0, INNER_RING_PX - theme.sunburstGapPx * 0.5);
|
|
618
|
+
ctx.fillStyle = tooltipBg;
|
|
619
|
+
ctx.textAlign = "center";
|
|
620
|
+
ctx.textBaseline = "middle";
|
|
621
|
+
|
|
622
|
+
if (faceted) {
|
|
623
|
+
for (const facet of chart._facets) {
|
|
624
|
+
ctx.beginPath();
|
|
625
|
+
ctx.fillStyle = tooltipBg;
|
|
626
|
+
ctx.arc(facet.centerX, facet.centerY, innerDiscR, 0, 2 * Math.PI);
|
|
627
|
+
ctx.fill();
|
|
628
|
+
ctx.fillStyle = textColor;
|
|
629
|
+
ctx.font = `11px ${fontFamily}`;
|
|
630
|
+
ctx.fillText(
|
|
631
|
+
store.name[facet.drillRoot],
|
|
632
|
+
facet.centerX,
|
|
633
|
+
facet.centerY,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Facet title band above the arcs.
|
|
637
|
+
if (chart._facetGrid) {
|
|
638
|
+
const cell = chart._facetGrid.cells.find(
|
|
639
|
+
(c) => c.label === facet.label,
|
|
640
|
+
);
|
|
641
|
+
if (cell?.titleRect) {
|
|
642
|
+
ctx.fillStyle = textColor;
|
|
643
|
+
ctx.font = `11px ${fontFamily}`;
|
|
644
|
+
ctx.textBaseline = "middle";
|
|
645
|
+
ctx.fillText(
|
|
646
|
+
facet.label,
|
|
647
|
+
cell.titleRect.x + cell.titleRect.width / 2,
|
|
648
|
+
cell.titleRect.y + cell.titleRect.height / 2,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
ctx.beginPath();
|
|
655
|
+
ctx.arc(chart._centerX, chart._centerY, innerDiscR, 0, 2 * Math.PI);
|
|
656
|
+
ctx.fill();
|
|
657
|
+
ctx.fillStyle = textColor;
|
|
658
|
+
ctx.font = `11px ${fontFamily}`;
|
|
659
|
+
ctx.fillText(
|
|
660
|
+
store.name[chart._currentRootId],
|
|
661
|
+
chart._centerX,
|
|
662
|
+
chart._centerY,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Breadcrumbs (non-facet only — per-facet drill is tracked through
|
|
667
|
+
// the per-facet drill root's label, not a global breadcrumb trail).
|
|
668
|
+
if (!faceted && chart._breadcrumbIds.length > 1) {
|
|
669
|
+
renderTreeBreadcrumbs(chart, ctx, cssWidth, fontFamily, textColor);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Legend. In faceted mode use the grid's explicit rect; otherwise
|
|
673
|
+
// derive from a synthetic single-plot layout.
|
|
674
|
+
if (faceted && chart._facetGrid?.legendRect) {
|
|
675
|
+
if (
|
|
676
|
+
chart._colorMode === "series" &&
|
|
677
|
+
chart._uniqueColorLabels.size > 1
|
|
678
|
+
) {
|
|
679
|
+
renderCategoricalLegendAt(
|
|
680
|
+
canvas,
|
|
681
|
+
chart._facetGrid.legendRect,
|
|
682
|
+
chart._uniqueColorLabels,
|
|
683
|
+
palette,
|
|
684
|
+
theme,
|
|
685
|
+
);
|
|
686
|
+
} else if (
|
|
687
|
+
chart._colorMode === "numeric" &&
|
|
688
|
+
chart._colorMin < chart._colorMax
|
|
689
|
+
) {
|
|
690
|
+
const legendLayout = new PlotLayout(cssWidth, cssHeight, {
|
|
691
|
+
hasXLabel: false,
|
|
692
|
+
hasYLabel: false,
|
|
693
|
+
hasLegend: true,
|
|
694
|
+
});
|
|
695
|
+
renderLegend(
|
|
696
|
+
canvas,
|
|
697
|
+
legendLayout,
|
|
698
|
+
{
|
|
699
|
+
min: chart._colorMin,
|
|
700
|
+
max: chart._colorMax,
|
|
701
|
+
label: chart._colorName,
|
|
702
|
+
},
|
|
703
|
+
stops,
|
|
704
|
+
theme,
|
|
705
|
+
chart.getColumnFormatter(chart._colorName, "value"),
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
} else if (
|
|
709
|
+
chart._colorMode === "series" &&
|
|
710
|
+
chart._uniqueColorLabels.size > 1
|
|
711
|
+
) {
|
|
712
|
+
const legendLayout = new PlotLayout(cssWidth, cssHeight, {
|
|
713
|
+
hasXLabel: false,
|
|
714
|
+
hasYLabel: false,
|
|
715
|
+
hasLegend: true,
|
|
716
|
+
});
|
|
717
|
+
renderCategoricalLegend(
|
|
718
|
+
canvas,
|
|
719
|
+
legendLayout,
|
|
720
|
+
chart._uniqueColorLabels,
|
|
721
|
+
palette,
|
|
722
|
+
theme,
|
|
723
|
+
);
|
|
724
|
+
} else if (
|
|
725
|
+
chart._colorMode === "numeric" &&
|
|
726
|
+
chart._colorMin < chart._colorMax
|
|
727
|
+
) {
|
|
728
|
+
const legendLayout = new PlotLayout(cssWidth, cssHeight, {
|
|
729
|
+
hasXLabel: false,
|
|
730
|
+
hasYLabel: false,
|
|
731
|
+
hasLegend: true,
|
|
732
|
+
});
|
|
733
|
+
renderLegend(
|
|
734
|
+
canvas,
|
|
735
|
+
legendLayout,
|
|
736
|
+
{
|
|
737
|
+
min: chart._colorMin,
|
|
738
|
+
max: chart._colorMax,
|
|
739
|
+
label: chart._colorName,
|
|
740
|
+
},
|
|
741
|
+
stops,
|
|
742
|
+
theme,
|
|
743
|
+
chart.getColumnFormatter(chart._colorName, "value"),
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
ctx.restore();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Label placement: rotate the label *radial* to the arc at its midpoint
|
|
752
|
+
* (text runs along the radius, from near the center outward). In
|
|
753
|
+
* `"upright"` mode, arcs on the left half get an extra 180° flip so
|
|
754
|
+
* text reads left-to-right in both halves; `"radial"` mode skips the
|
|
755
|
+
* flip — simpler, but labels on the left read right-to-left.
|
|
756
|
+
*
|
|
757
|
+
* Sizing:
|
|
758
|
+
* - Text length fits in the ring width (radial direction).
|
|
759
|
+
* - Font size fits in the arc length at mid-radius (tangential
|
|
760
|
+
* direction).
|
|
761
|
+
*/
|
|
762
|
+
function renderArcLabel(
|
|
763
|
+
chart: SunburstChart,
|
|
764
|
+
ctx: Context2D,
|
|
765
|
+
nodeId: number,
|
|
766
|
+
fontFamily: string,
|
|
767
|
+
stops: GradientStop[],
|
|
768
|
+
palette: Vec3[],
|
|
769
|
+
centerX: number,
|
|
770
|
+
centerY: number,
|
|
771
|
+
): void {
|
|
772
|
+
const store = chart._nodeStore;
|
|
773
|
+
const a0 = store.a0[nodeId];
|
|
774
|
+
const a1 = store.a1[nodeId];
|
|
775
|
+
const r0 = store.r0[nodeId];
|
|
776
|
+
const r1 = store.r1[nodeId];
|
|
777
|
+
const ringWidth = r1 - r0;
|
|
778
|
+
const midR = (r0 + r1) / 2;
|
|
779
|
+
const arcSpan = a1 - a0;
|
|
780
|
+
const arcLen = arcSpan * midR;
|
|
781
|
+
|
|
782
|
+
// Radial labels need enough ring-width for text length and enough
|
|
783
|
+
// tangential space for font height.
|
|
784
|
+
if (ringWidth < 16 || arcLen < 8) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const fontSize = Math.min(11, Math.floor(arcLen * 0.7));
|
|
789
|
+
if (fontSize < 7) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
794
|
+
const name = store.name[nodeId];
|
|
795
|
+
const maxTextWidth = ringWidth - 4;
|
|
796
|
+
let text = name;
|
|
797
|
+
if (ctx.measureText(text).width > maxTextWidth) {
|
|
798
|
+
while (text.length > 1) {
|
|
799
|
+
text = text.slice(0, -1);
|
|
800
|
+
if (ctx.measureText(text + "…").width <= maxTextWidth) {
|
|
801
|
+
text += "…";
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (text.length < 2) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const midA = (a0 + a1) / 2;
|
|
812
|
+
|
|
813
|
+
ctx.save();
|
|
814
|
+
ctx.translate(centerX, centerY);
|
|
815
|
+
|
|
816
|
+
// Rotate so the local +x axis points outward along the radius
|
|
817
|
+
// through the arc's midpoint. Text then runs along that axis.
|
|
818
|
+
let rot = midA;
|
|
819
|
+
const onLeftHalf = midA > Math.PI / 2 && midA < (3 * Math.PI) / 2;
|
|
820
|
+
if (chart._labelRotation === "upright" && onLeftHalf) {
|
|
821
|
+
rot += Math.PI;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
ctx.rotate(rot);
|
|
825
|
+
|
|
826
|
+
// Pick label color by luminance of the arc's fill for contrast.
|
|
827
|
+
const fill = leafColor(chart, nodeId, stops, palette);
|
|
828
|
+
const lum = luminance(fill[0], fill[1], fill[2]);
|
|
829
|
+
ctx.fillStyle = lum > 0.5 ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
830
|
+
ctx.textAlign = "center";
|
|
831
|
+
ctx.textBaseline = "middle";
|
|
832
|
+
|
|
833
|
+
// Place the label at radial midpoint along the rotated x-axis.
|
|
834
|
+
// Flip the sign when upright-mirrored so the center stays at the
|
|
835
|
+
// correct radial position (the rotation brought +x through the
|
|
836
|
+
// origin, so midR is now on the "back" side in local coords).
|
|
837
|
+
const x = chart._labelRotation === "upright" && onLeftHalf ? -midR : midR;
|
|
838
|
+
ctx.fillText(text, x, 0);
|
|
839
|
+
ctx.restore();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function renderHoverHighlight(
|
|
843
|
+
ctx: Context2D,
|
|
844
|
+
chart: SunburstChart,
|
|
845
|
+
nodeId: number,
|
|
846
|
+
): void {
|
|
847
|
+
const store = chart._nodeStore;
|
|
848
|
+
const a0 = store.a0[nodeId];
|
|
849
|
+
const a1 = store.a1[nodeId];
|
|
850
|
+
const r0 = store.r0[nodeId];
|
|
851
|
+
const r1 = store.r1[nodeId];
|
|
852
|
+
const { centerX, centerY } = facetCenterForNode(chart, nodeId);
|
|
853
|
+
|
|
854
|
+
ctx.strokeStyle = "rgba(255,255,255,0.9)";
|
|
855
|
+
ctx.lineWidth = 2;
|
|
856
|
+
ctx.beginPath();
|
|
857
|
+
ctx.arc(centerX, centerY, r1, a0, a1);
|
|
858
|
+
ctx.arc(centerX, centerY, r0, a1, a0, true);
|
|
859
|
+
ctx.closePath();
|
|
860
|
+
ctx.stroke();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function renderSunburstTooltip(
|
|
864
|
+
chart: SunburstChart,
|
|
865
|
+
ctx: Context2D,
|
|
866
|
+
nodeId: number,
|
|
867
|
+
cssWidth: number,
|
|
868
|
+
cssHeight: number,
|
|
869
|
+
fontFamily: string,
|
|
870
|
+
): void {
|
|
871
|
+
const store = chart._nodeStore;
|
|
872
|
+
const midA = (store.a0[nodeId] + store.a1[nodeId]) / 2;
|
|
873
|
+
const midR = (store.r0[nodeId] + store.r1[nodeId]) / 2;
|
|
874
|
+
const { centerX, centerY } = facetCenterForNode(chart, nodeId);
|
|
875
|
+
const cx = centerX + Math.cos(midA) * midR;
|
|
876
|
+
const cy = centerY + Math.sin(midA) * midR;
|
|
877
|
+
renderTreeTooltip(
|
|
878
|
+
chart,
|
|
879
|
+
ctx,
|
|
880
|
+
nodeId,
|
|
881
|
+
cx,
|
|
882
|
+
cy,
|
|
883
|
+
cssWidth,
|
|
884
|
+
cssHeight,
|
|
885
|
+
fontFamily,
|
|
886
|
+
);
|
|
887
|
+
}
|