// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ /** * Generic "find the value-axis extent among records whose category * position falls inside a visible window" helper. Used by Y Bar, * X Bar, Candlestick, and any future categorical-axis chart that * wants an auto-fit value axis on zoom. * * Takes a pre-extracted numeric tuple per record via the `extract` * callback instead of reading fields directly, so the same code path * handles both Bar's `{ catIdx, y0, y1, axis, seriesId }` shape and * Candlestick's `{ xCenter, low, high }` shape. * * TODO(perf): linear scan. Callers that order records by category * index could pre-sort once and binary-search the slice to reduce * this to O(log N + K_visible). Deferred until profiling shows the * scan in the hot path. */ export interface VisibleExtent { min: number; max: number; hasFit: boolean; } export interface VisibleExtentRecord { /** * Position on the categorical axis — compared to the visible window. */ cat: number; /** * Low bound of the value-axis extent for this record. */ lo: number; /** * High bound of the value-axis extent for this record. */ hi: number; /** * True to skip this record (hidden series / wrong axis / etc.). */ skip: boolean; } /** * Walk `items`, filter by `visCatMin <= cat <= visCatMax` (and the * caller-supplied `skip` flag), and return min/max over `lo`/`hi`. * * Returns `hasFit: false` when the window matches no records, so * callers can fall back to the base domain. * * Zero-range guard: if every visible record shares a single value * (flat run), pad by `±|value|` so the axis doesn't collapse to a * single pixel. */ export function computeVisibleExtent( items: readonly T[], visCatMin: number, visCatMax: number, extract: (item: T, out: VisibleExtentRecord) => void, out: VisibleExtent, ): VisibleExtent { let min = Infinity; let max = -Infinity; // Reuse a single scratch record across the walk. `extract` mutates // it in place — zero allocations per iteration. const scratch: VisibleExtentRecord = { cat: 0, lo: 0, hi: 0, skip: false }; for (let i = 0; i < items.length; i++) { scratch.skip = false; extract(items[i], scratch); if (scratch.skip) { continue; } if (scratch.cat < visCatMin || scratch.cat > visCatMax) { continue; } if (scratch.lo < min) { min = scratch.lo; } if (scratch.hi > max) { max = scratch.hi; } } const hasFit = isFinite(min) && isFinite(max); if (hasFit && min === max) { const pad = Math.abs(min) || 1; min -= pad; max += pad; } out.min = min; out.max = max; out.hasFit = hasFit; return out; }