// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import type { SunburstChart } from "./sunburst"; import { NULL_NODE, type NodeStore } from "../common/node-store"; /** * Minimum arc area (in pixels²) below which a subtree stops being * subdivided. Keeps the visible count bounded independent of tree * size — the core mechanism that lets sunburst scale to 2M nodes. */ const MIN_VISIBLE_ARC_AREA = 4; /** * Inner radius: reserved for the current-root drill-up target. */ const INNER_RING_PX = 30; /** * Compute the max visible tree depth beneath `currentRootId`. * Needed for ring-width calculation. */ function maxDepthBelow(store: NodeStore, currentRootId: number): number { // Iterative DFS, no recursion. let maxDepth = 0; const baseDepth = store.depth[currentRootId]; let stack = new Int32Array(128); stack[0] = currentRootId; let sp = 1; while (sp > 0) { sp--; const id = stack[sp]; const d = store.depth[id] - baseDepth; if (d > maxDepth) { maxDepth = d; } for ( let c = store.firstChild[id]; c !== NULL_NODE; c = store.nextSibling[c] ) { if (sp >= stack.length) { const bigger = new Int32Array(stack.length * 2); bigger.set(stack); stack = bigger; } stack[sp++] = c; } } return maxDepth; } /** * Recursive polar partition writing `(a0, a1, r0, r1)` into the store. * The root node's own ring is reserved (inner radius = 0, outer = * `INNER_RING_PX`) for the drill-up click target; descendants start at * `INNER_RING_PX` and extend out. */ export function partitionSunburst( store: NodeStore, currentRootId: number, maxRadius: number, ): void { const baseDepth = store.depth[currentRootId]; const maxD = Math.max(1, maxDepthBelow(store, currentRootId)); const usableRadius = Math.max(0, maxRadius - INNER_RING_PX); const ringWidth = usableRadius / maxD; // Root spans full circle; its ring is the inner drill-up circle. store.a0[currentRootId] = 0; store.a1[currentRootId] = 2 * Math.PI; store.r0[currentRootId] = 0; store.r1[currentRootId] = INNER_RING_PX; // Recurse into children with stack (same pattern as treemap's // squarify scratch). partitionChildren( store, currentRootId, 0, 2 * Math.PI, INNER_RING_PX, ringWidth, baseDepth, ); } function partitionChildren( store: NodeStore, parentId: number, a0: number, a1: number, parentR1: number, ringWidth: number, baseDepth: number, ): void { const totalValue = store.value[parentId]; if (totalValue <= 0) { return; } const span = a1 - a0; let cursor = a0; for ( let c = store.firstChild[parentId]; c !== NULL_NODE; c = store.nextSibling[c] ) { const v = store.value[c]; if (v <= 0) { continue; } const frac = v / totalValue; const childA0 = cursor; const childA1 = cursor + span * frac; cursor = childA1; const childR0 = parentR1; const childR1 = parentR1 + ringWidth; store.a0[c] = childA0; store.a1[c] = childA1; store.r0[c] = childR0; store.r1[c] = childR1; // LOD: stop subdividing if this arc's projected pixel area // falls below the threshold. Descendants keep stale coords but // `collectVisibleArcs` will skip them. const arcSpan = childA1 - childA0; const midR = (childR0 + childR1) / 2; const arcLen = arcSpan * midR; // pixel length along arc const area = arcLen * ringWidth; if (area < MIN_VISIBLE_ARC_AREA) { continue; } if (store.firstChild[c] !== NULL_NODE) { partitionChildren( store, c, childA0, childA1, childR1, ringWidth, baseDepth, ); } } } /** * Walk from `startId` depth-first, emitting every descendant whose * arc area exceeds `MIN_VISIBLE_ARC_AREA`. * * The single-facet entry point; faceted rendering uses * {@link collectVisibleArcsAppend} to concatenate across facets. */ export function collectVisibleArcs( chart: SunburstChart, startId: number, ): void { chart._visibleNodeCount = collectVisibleArcsAppend(chart, startId, 0); } /** * Append visible arcs under `startId` to `_visibleNodeIds` starting at * `startOffset`, returning the new length. */ export function collectVisibleArcsAppend( chart: SunburstChart, startId: number, startOffset: number, ): number { const store = chart._nodeStore; const a0 = store.a0; const a1 = store.a1; const r0 = store.r0; const r1 = store.r1; const firstChild = store.firstChild; const nextSibling = store.nextSibling; const value = store.value; if (!chart._visibleNodeIds || chart._visibleNodeIds.length < store.count) { chart._visibleNodeIds = new Int32Array(store.count); } const out = chart._visibleNodeIds; let outIdx = startOffset; let stack = new Int32Array(128); stack[0] = startId; let sp = 1; while (sp > 0) { sp--; const id = stack[sp]; if (value[id] <= 0) { continue; } out[outIdx++] = id; const arcSpan = a1[id] - a0[id]; const midR = (r0[id] + r1[id]) / 2; const arcLen = arcSpan * midR; const ringWidth = r1[id] - r0[id]; if (arcLen * ringWidth < MIN_VISIBLE_ARC_AREA) { continue; } for (let c = firstChild[id]; c !== NULL_NODE; c = nextSibling[c]) { if (sp >= stack.length) { const bigger = new Int32Array(stack.length * 2); bigger.set(stack); stack = bigger; } stack[sp++] = c; } } return outIdx; } export { INNER_RING_PX, MIN_VISIBLE_ARC_AREA };