// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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 { TreemapChart } from "./treemap"; import { NodeStore, NULL_NODE } from "../common/node-store"; export type { BreadcrumbRegion } from "../common/tree-chrome"; export const PADDING_OUTER = 1; export const PADDING_LABEL = 14; export const PADDING_INNER = 1; // Re-export shared streaming pipeline so treemap-layout stays the one // import site consumers use. export { resetTreeState as resetTreemapState, processTreeChunk as processTreemapChunk, finalizeTree as finalizeTreemap, rebuildBreadcrumbs, } from "../common/tree-data"; /** * Minimum visible rect area (in CSS pixels squared). Subtrees that * project to less than this are not subdivided during `squarify` and * are skipped by `collectVisible`. */ const MIN_VISIBLE_AREA = 4; // 2×2 px // Squarify layout /** * Order-preserving treemap layout. Walks the linked-list child graph * and writes `x0/y0/x1/y1` in place into the node store. */ export function squarify( store: NodeStore, id: number, x0: number, y0: number, x1: number, y1: number, baseDepth: number, scratch: Int32Array, showBranchHeader: boolean, ): void { store.x0[id] = Math.round(x0); store.y0[id] = Math.round(y0); store.x1[id] = Math.round(x1); store.y1[id] = Math.round(y1); if (store.firstChild[id] === NULL_NODE) { return; } const area = (x1 - x0) * (y1 - y0); if (area < MIN_VISIBLE_AREA) { return; } const relDepth = store.depth[id] - baseDepth; const showHeader = showBranchHeader && relDepth === 1 && store.firstChild[id] !== NULL_NODE; const padTop = showHeader ? PADDING_LABEL : PADDING_INNER; const padOuter = showHeader ? PADDING_OUTER : PADDING_INNER; const ix0 = store.x0[id] + padOuter; const iy0 = store.y0[id] + padTop; const ix1 = store.x1[id] - padOuter; const iy1 = store.y1[id] - padOuter; if (ix1 <= ix0 || iy1 <= iy0) { return; } let activeCount = 0; for ( let c = store.firstChild[id]; c !== NULL_NODE; c = store.nextSibling[c] ) { if (store.value[c] > 0) { scratch[activeCount++] = c; } } if (activeCount === 0) { return; } layoutOrdered( store, scratch, 0, activeCount, ix0, iy0, ix1, iy1, baseDepth, scratch.subarray(activeCount), showBranchHeader, ); } function layoutOrdered( store: NodeStore, nodes: Int32Array, lo: number, hi: number, x0: number, y0: number, x1: number, y1: number, baseDepth: number, childScratch: Int32Array, showBranchHeader: boolean, ): void { const n = hi - lo; if (n === 0) { return; } if (n === 1) { squarify( store, nodes[lo], x0, y0, x1, y1, baseDepth, childScratch, showBranchHeader, ); return; } let totalValue = 0; for (let i = lo; i < hi; i++) { totalValue += store.value[nodes[i]]; } const halfValue = totalValue / 2; let cumulative = 0; let splitIdx = lo + 1; let bestDiff = Infinity; for (let i = lo; i < hi - 1; i++) { cumulative += store.value[nodes[i]]; const diff = Math.abs(cumulative - halfValue); if (diff < bestDiff) { bestDiff = diff; splitIdx = i + 1; } } let leftValue = 0; for (let i = lo; i < splitIdx; i++) { leftValue += store.value[nodes[i]]; } const fraction = leftValue / totalValue; const rw = x1 - x0; const rh = y1 - y0; if (rw >= rh) { const splitX = Math.round(x0 + rw * fraction); layoutOrdered( store, nodes, lo, splitIdx, x0, y0, splitX, y1, baseDepth, childScratch, showBranchHeader, ); layoutOrdered( store, nodes, splitIdx, hi, splitX, y0, x1, y1, baseDepth, childScratch, showBranchHeader, ); } else { const splitY = Math.round(y0 + rh * fraction); layoutOrdered( store, nodes, lo, splitIdx, x0, y0, x1, splitY, baseDepth, childScratch, showBranchHeader, ); layoutOrdered( store, nodes, splitIdx, hi, x0, splitY, x1, y1, baseDepth, childScratch, showBranchHeader, ); } } // Collect visible /** * Walk from `startId` depth-first, emitting every descendant whose rect * area is above `MIN_VISIBLE_AREA`. O(visible), not O(total). * * Faceted render paths call {@link collectVisibleAppend} once per facet * and do the final `_visibleNodeCount` bookkeeping themselves; this * single-facet entry point wraps that for non-split trees. */ export function collectVisible( chart: TreemapChart, startId: number, maxDepth: number, baseDepth: number, ): void { chart._visibleNodeCount = collectVisibleAppend( chart, startId, maxDepth, baseDepth, 0, ); } /** * Append the visible-node IDs below `startId` into `_visibleNodeIds` * starting at `startOffset`. Returns the new length. Used by faceted * treemap rendering to concatenate per-facet visibility without doing * a second pass. */ export function collectVisibleAppend( chart: TreemapChart, startId: number, maxDepth: number, baseDepth: number, startOffset: number, ): number { const store = chart._nodeStore; const x0 = store.x0; const y0 = store.y0; const x1 = store.x1; const y1 = store.y1; const depth = store.depth; 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; } if (depth[id] >= baseDepth) { out[outIdx++] = id; } if (depth[id] - baseDepth >= maxDepth) { continue; } const w = x1[id] - x0[id]; const h = y1[id] - y0[id]; if (w * h < MIN_VISIBLE_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; }