// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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 { SpatialGrid } from "./spatial-grid"; export interface HitBounds { xMin: number; xMax: number; yMin: number; yMax: number; } /** * Wraps SpatialGrid with dirty-flag bookkeeping + the cell-size heuristic * (~√n cells), so chart code no longer repeats the insertion loop scaffold. * Callers drive the iteration through `rebuild` and the `visit` callback, * which lets scatter (flat array) and line (series-indexed array) share * the same code path. */ export class SpatialHitTester { private _grid: SpatialGrid | null = null; private _dirty = true; markDirty(): void { this._dirty = true; } get isDirty(): boolean { return this._dirty; } /** * Rebuild the grid. `forEachPoint` is called once with an insert * function; the caller drives iteration. Cleared when `pointCount` * is zero. */ rebuild( bounds: HitBounds, pointCount: number, forEachPoint: ( insert: (idx: number, x: number, y: number) => void, ) => void, ): void { if (pointCount === 0) { this._grid = null; this._dirty = false; return; } const xRange = bounds.xMax - bounds.xMin || 1; const yRange = bounds.yMax - bounds.yMin || 1; const avgRange = (xRange + yRange) / 2; const cellSize = avgRange / Math.max(1, Math.sqrt(pointCount)); const grid = new SpatialGrid( bounds.xMin, bounds.xMax, bounds.yMin, bounds.yMax, cellSize, ); forEachPoint((idx, x, y) => grid.insert(idx, x, y)); this._grid = grid; this._dirty = false; } /** * Query the nearest point within `radiusPx` of (dataX, dataY). */ query( dataX: number, dataY: number, radiusPx: number, pxPerDataX: number, pxPerDataY: number, xData: Float32Array | null, yData: Float32Array | null, ): number { if (!this._grid || !xData || !yData) { return -1; } return this._grid.query( dataX, dataY, radiusPx, pxPerDataX, pxPerDataY, xData, yData, ); } clear(): void { this._grid = null; this._dirty = true; } }