// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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 { ColumnDataMap } from "../../data/view-reader"; import type { WebGLContextManager } from "../../webgl/context-manager"; import { TreeChartBase, firstNonMetadataColumn } from "../common/tree-chart"; import { NULL_NODE } from "../common/node-store"; import { type BreadcrumbRegion, processTreemapChunk, finalizeTreemap, resetTreemapState, } from "./treemap-layout"; import { renderTreemapFrame } from "./treemap-render"; import { handleTreemapHover, handleTreemapClick, handleTreemapDblClick, dismissTreemapPinnedTooltip, } from "./treemap-interact"; export interface TreemapLocations { u_resolution: WebGLUniformLocation | null; a_position: number; a_color: number; } /** * Treemap chart. Shares tree storage + streaming-pipeline + color-mode * state with `TreeChartBase`; adds rectangular layout + WebGL quad * rendering + drill / tooltip interactions. */ export class TreemapChart extends TreeChartBase { _program: WebGLProgram | null = null; _locations: TreemapLocations | null = null; _positionBuffer: WebGLBuffer | null = null; _colorBuffer: WebGLBuffer | null = null; _vertexCount = 0; // Interaction _hoveredNodeId: number = NULL_NODE; _pinnedNodeId: number = NULL_NODE; _breadcrumbRegions: BreadcrumbRegion[] = []; _chromeCache: ImageBitmap | null = null; _chromeCacheDirty = true; /** * Monotonic generation counter bumped every time the static chrome * content changes (a new `drawStaticChrome` call). The async * `createImageBitmap` callback captures the current gen at kickoff * and only installs the resulting bitmap if its gen is still the * most-recent one. Without this, out-of-order bitmap resolutions * can store a stale bitmap in `_chromeCache` — any subsequent * hover-only overlay call then blits that stale snapshot over the * fresh chart, producing "leftover labels / cells" artefacts. */ _chromeCacheGen = 0; // Faceted state /** * Per-facet drill roots in split_by mode. Key is the facet label * (the top-level child of `_rootId`); value is the currently drilled * node inside that facet's subtree. Missing keys mean the facet * shows its full subtree. */ _facetDrillRoots: Map = new Map(); _facetGrid: import("../../layout/facet-grid").FacetGrid | null = null; /** When `false`, branch nodes at relDepth=1 render as a centered * overlay (same style as relDepth=2) and no top-of-rect label * reservation is made in `squarify`. Default `true` preserves the * legacy title-bar look. */ _showBranchHeader = false; /** * Parallel to `_visibleNodeIds`. Each entry stores the depth of the * drill root that owns the corresponding visible node, so render * paths can compute `relDepth` uniformly without knowing whether * faceting is active. Populated in `renderTreemapFrame` during * layout. */ _visibleBaseDepths: Int32Array | null = null; /** * Parallel to `_visibleNodeIds`. The drill-root node id that owns * each visible node (= `_currentRootId` in non-facet mode, per- * facet drill root in facet mode). Used by hit-testing and chrome * to skip the drill-root itself without a separate equality check. */ _visibleRootIds: Int32Array | null = null; protected override tooltipCallbacks() { return { onHover: (mx: number, my: number) => handleTreemapHover(this, mx, my), onLeave: () => { if ( this._hoveredNodeId !== NULL_NODE && this._pinnedNodeId === NULL_NODE ) { this._hoveredNodeId = NULL_NODE; if (this._glManager) { renderTreemapFrame(this, this._glManager); } } }, onClickPre: (mx: number, my: number) => { handleTreemapClick(this, mx, my); return true; // treemap owns all click logic }, onDblClick: (mx: number, my: number) => handleTreemapDblClick(this, mx, my), }; } async uploadAndRender( glManager: WebGLContextManager, columns: ColumnDataMap, startRow: number, _endRow: number, ): Promise { this._glManager = glManager; if (startRow === 0) { const slots = this._columnSlots; this._sizeName = slots[0] || firstNonMetadataColumn(columns) || ""; this._colorName = slots[1] || ""; if (!this._colorName) { this._colorMode = "empty"; } else { const t = this._columnTypes[this._colorName]; const isNumeric = t === "float" || t === "integer" || t === "date" || t === "datetime"; this._colorMode = isNumeric ? "numeric" : "series"; } // Clear per-draw state that's tied to the OLD tree. Node // IDs from the previous render don't map to anything in // the fresh tree; leaving them around lets stale drill // roots, hovered/pinned IDs, breadcrumb regions, the // cached chrome bitmap, or an old WebGL vertex count bleed // into the new render as ghost rects / labels / hit // targets. See tree-data.ts's `resetTreeState` for the // shared fields; everything below is treemap-specific. this._hoveredNodeId = NULL_NODE; this._pinnedNodeId = NULL_NODE; this._breadcrumbRegions = []; this._facetDrillRoots.clear(); this._facetGrid = null; this._visibleBaseDepths = null; this._visibleRootIds = null; // Invalidate the GPU buffer contents so any render that // fires before `generateAndUploadTreemap` has refilled the // buffers draws zero triangles instead of the previous // tree's geometry. this._vertexCount = 0; // Drop any in-flight hover tooltip promise — bumping the // controller's serials makes stale resolutions no-ops // rather than painting old lines on the new chart. this._lazyTooltip.clearHover(); this._lazyTooltip.invalidatePin(); dismissTreemapPinnedTooltip(this); this._chromeCache?.close(); this._chromeCache = null; this._chromeCacheDirty = true; this._chromeCacheGen++; resetTreemapState(this); } processTreemapChunk(this, columns); finalizeTreemap(this); if (this._rootId !== NULL_NODE) { await this.requestRender(glManager); } } _fullRender(glManager: WebGLContextManager): void { if (this._rootId === NULL_NODE) { return; } this._glManager = glManager; renderTreemapFrame(this, glManager); } protected destroyInternal(): void { dismissTreemapPinnedTooltip(this); this._chromeCache?.close(); this._chromeCache = null; const gl = this._glManager?.gl; if (gl) { if (this._positionBuffer) { gl.deleteBuffer(this._positionBuffer); } if (this._colorBuffer) { gl.deleteBuffer(this._colorBuffer); } } this._positionBuffer = null; this._colorBuffer = null; this._program = null; this._locations = null; this._rootId = NULL_NODE; this._currentRootId = NULL_NODE; this._breadcrumbIds = []; this._childLookup.clear(); this._visibleNodeIds = null; this._visibleNodeCount = 0; this._breadcrumbRegions = []; this._facetDrillRoots.clear(); this._facetGrid = null; this._visibleBaseDepths = null; this._visibleRootIds = null; } }