import type { ColumnDataMap } from "../../data/view-reader"; import type { WebGLContextManager } from "../../webgl/context-manager"; import type { ZoomConfig } from "../../interaction/zoom-controller"; import { CategoricalYChart } from "../common/categorical-y-chart"; import { type PlotRect } from "../../layout/plot-layout"; import { type AxisDomain } from "../../axis/numeric-axis"; import type { CategoricalDomain } from "../../axis/categorical-axis"; import { type SeriesChartRecord, type NumericCategoryDomain, type SeriesInfo, type BarColumns } from "./series-build"; import { LineGlyph } from "./glyphs/draw-lines"; import { ScatterGlyph } from "./glyphs/draw-scatter"; import { AreaGlyph } from "./glyphs/draw-areas"; /** * Per-frame memo of the auto-fit value extent for a {@link SeriesChart}, * keyed on the visible categorical window. Two axis slots (`left*` / * `right*`) because dual-axis bar charts refit independently. */ export interface SeriesAutoFitCache { catMin: number; catMax: number; hidden: Set; leftMin: number; leftMax: number; hasLeft: boolean; rightMin: number; rightMax: number; hasRight: boolean; } export interface CachedLocations { u_proj_left: WebGLUniformLocation | null; u_proj_right: WebGLUniformLocation | null; u_hover_series: WebGLUniformLocation | null; u_horizontal: WebGLUniformLocation | null; a_corner: number; a_x_center: number; a_half_width: number; a_y0: number; a_y1: number; a_color: number; a_series_id: number; a_axis: number; } /** * Bar chart. Fields are package-internal (no `private`) so helper modules * in this folder can read/write them. * * Orientation: vertical (Y Bar) is the default — categorical X, numeric * Y. When `_isHorizontal` is true (X Bar) the roles swap: numeric X, * categorical Y reading top-to-bottom. The data pipeline + instance * attributes stay in *logical* coordinates (xCenter = category center, * y0/y1 = value extent); the swap happens in three places only: * 1. Projection matrix (`bar-render.ts`) — args reordered, Y flipped. * 2. Vertex shader — `u_horizontal` uniform transposes position. * 3. Chrome (`bar-axis.ts`) — categorical axis moves from bottom to * left; numeric axis from left to bottom. * Hit-testing reads the swapped pixel→data mapping via the projected * `PlotLayout`, so its logical comparisons don't need changes. */ export declare class SeriesChart extends CategoricalYChart { readonly _isHorizontal: boolean; constructor(orientation?: "vertical" | "horizontal"); /** * Lock the categorical axis — scrolling through category indices * isn't meaningful, and the layout code assumes all categories are * always present. The value axis stays freely zoomable. */ protected getZoomConfig(): ZoomConfig; _locations: CachedLocations | null; _aggregates: string[]; _splitPrefixes: string[]; _series: SeriesInfo[]; /** * Columnar bar/area record storage. Indexed by bar slot in * `[0, _bars.count)`. Replaces the legacy `SeriesChartRecord[]` to * avoid per-record POJO allocation on data load. */ _bars: BarColumns; /** * Pre-partitioned series indices by glyph type — populated at the end * of `uploadAndRender` and reused across frames. Eliminates per-glyph * `chart._series.filter(...)` allocations in the render loop. Each * holds the full list of that type (including hidden series); the * draw paths still skip hidden via `_hiddenSeries` lookup. */ _barSeries: SeriesInfo[]; _lineSeries: SeriesInfo[]; _scatterSeries: SeriesInfo[]; _areaSeries: SeriesInfo[]; /** * Cached primary / secondary axis labels — `_series.filter().map(). * dedupe().join()` per axis, recomputed only on series-set change. */ _primaryValueLabel: string; _altValueLabel: string; /** * Per-side value-axis mode. `"category"` fires when every * aggregate on that side is post-aggregation `string`-typed * (all-or-nothing rule, evaluated independently for primary and * alt). When set, `_bars[].y0`/`y1` carry dictionary slot indices * instead of numeric values, and the chrome overlay paints a * categorical axis on that side. * * Read by `series-render.ts` to construct the `BarCategoryAxis` * descriptor for the value-axis sides. */ _leftValueAxisMode: "numeric" | "category"; _rightValueAxisMode: "numeric" | "category" | null; _leftValueCategoryDomain: CategoricalDomain | null; _rightValueCategoryDomain: CategoricalDomain | null; /** * (seriesId * 1e9 + catIdx) → bar-record index in `_bars`. Built once * per pipeline run for area-strip lookups; rebuilt on hidden-toggle * is unnecessary because the index keys don't depend on hidden state. */ _areaBarIndex: Map | null; /** * Cached Y-color buffer state for `uploadBarColors` short-circuit. * `_lastUploadedColors` mirrors the bytes last shipped to the GPU; * `uploadBarColors` skips when the new buffer matches byte-for-byte. * Reset (set to `null`) on data load or palette change. */ _lastUploadedColors: Float32Array | null; /** * Cached palette + identity-keys for short-circuiting per-frame * resolution. Inputs (`seriesPalette` ref, `gradientStops` ref, * `series.length`) only change on data load or `restyle()`. */ _paletteCache: [number, number, number][] | null; _paletteCacheKey: { seriesPalette: [number, number, number][] | null; gradientStops: unknown; seriesLength: number; } | null; /** * Reusable scratch for the build pipeline — keeps the stack ladder * `Float64Array(N*M)` capacity hot across data reloads. The pipeline * resizes if the new build's footprint exceeds capacity. */ _posStackScratch: Float64Array | null; _negStackScratch: Float64Array | null; _leftDomain: { min: number; max: number; }; _rightDomain: { min: number; max: number; } | null; _hasRightAxis: boolean; /** * `domain_mode: "expand"` accumulators. Hold the running union of * every prior build's value-axis (and, in numeric-category mode, * category-axis) extent for as long as the option is active. * Cleared in `resetExpandedDomain` — wired from the worker's * `resetAllZooms` and from view-config mutations on the base * class. `null` whenever the option is `"fit"` or the accumulator * has just been cleared; the next build re-seeds. */ _expandedLeftDomain: { min: number; max: number; } | null; _expandedRightDomain: { min: number; max: number; } | null; _expandedCategoryDomain: { min: number; max: number; } | null; /** * Numeric category-axis state. Populated only when `group_by` has * exactly one level and that level is `date | datetime | integer | * float` (boolean → category). When set, `_bars[].xCenter` lives in * real data units (not logical category indices), and the * categorical-side axis renders as a numeric axis instead of the * stringified-category one. */ _categoryAxisMode: "category" | "numeric"; _numericCategoryDomain: NumericCategoryDomain | null; /** * Origin used to rebase category positions before f32 narrowing. * Datetime numeric category axes carry ~1.7e12-magnitude values * which f32 cannot represent below ~256ms; the GPU buffers store * `(xCenter - _categoryOrigin)` and the projection matrix is built * with the same origin so its `tx` term stays small. Leftover * absolute coords are still available via `_numericCategoryDomain` * for axis-tick formatting and `dataToPixel`. `0` in category mode * (where positions are small integer indices) and in non-datetime * numeric modes (integer / float categories also fit in f32). */ _categoryOrigin: number; /** * Cached numeric category-axis ticks for the last frame. */ _lastCatTicks: number[] | null; /** * Per-category X coordinate in real data units (numeric axis mode * only). `null` in category mode — line/scatter/area glyphs fall * back to using `catIdx` directly as the X coordinate. */ _categoryPositions: Float64Array | null; _hiddenSeries: Set; _hoveredBarIdx: number; _pinnedBarIdx: number; /** * Synthetic bar record for hover hits on line / scatter glyphs that * don't have a real `BarRecord` in `_bars`. At most one of * `_hoveredBarIdx` and `_hoveredSample` is populated per frame; see * {@link ./bar-interact.getHoveredBar}. */ _hoveredSample: SeriesChartRecord | null; _samples: Float32Array; _sampleValid: Uint8Array; /** * Typed glyph composition. Each glyph (line / scatter / area) owns * its program cache and persistent vertex buffers privately; the * chart routes draw / rebuild / invalidate via `_glyphs`. Bar * glyph state lives on the chart directly (shared bar program + * `_locations` + buffer pool), so it's a free function rather than * a class. */ readonly _glyphs: { readonly lines: LineGlyph; readonly scatter: ScatterGlyph; readonly areas: AreaGlyph; }; _lastAltYDomain: AxisDomain | null; _lastAltYTicks: number[] | null; _uploadedBars: number; /** * Bar-record indices uploaded to the instance buffers, in dispatch * order. `_uploadedBars` is the active prefix length; the trailing * capacity is reused across data reloads / legend toggles. */ _visibleBarIndices: Int32Array; _legendRects: { seriesId: number; rect: PlotRect; }[]; /** * Cached legend layout — recomputed only on series-set / palette / * hidden-set / theme change. Frame-rate redraws read from this * directly; otherwise `ctx.measureText` would run per series each * frame. `null` flags an invalidation; `_legendRects` is rebuilt * lazily on the next chrome pass. */ _legendCacheValid: boolean; /** * Per-frame memo of the auto-fit value extent keyed on the visible * categorical window. Two comparisons per hit → no walk. Reset to * null on any mutation that would change the outcome (data reload, * legend toggle). * * Two axis slots because dual-axis bar charts refit left and right * independently. * * TODO(perf): when the visible window shrinks from a large N, the * linear walk over `_bars` dominates for N > ~100K. `_bars` is * already ordered by `catIdx`, so a binary-search pair to find the * visible slice drops this to O(log N + K_visible). Deferred until * profiling shows the walk in the hot path — current scale caps * keep it below 1% of frame time. */ _autoFitCache: SeriesAutoFitCache | null; /** * Per-category extent buckets. Built once per data load (and * rebuilt when `_hiddenSeries` changes), then read per-frame by * `computeVisibleValueExtent` to compute the auto-fit window over * the visible cat range in O(visibleCats) instead of * O(`bars.count`). Capacity reused across builds via * length-checked grow. * * Memory: 4 × Float64 + 2 × Uint8 = 34 bytes per category. For * typical N (≤ 1000 cats) this is < 35 KB; for high-cardinality * N = 100k it's 3.4 MB. Acceptable trade for eliminating the * O(N×M×P) per-frame walk during pan/zoom animations. */ _catExtents: { leftMin: Float64Array; leftMax: Float64Array; rightMin: Float64Array; rightMax: Float64Array; hasLeft: Uint8Array; hasRight: Uint8Array; n: number; } | null; /** * Identity of the `_hiddenSeries` set baked into `_catExtents`. * Pointer-compares to detect legend-toggle invalidations. */ _catExtentsHidden: Set | null; protected tooltipCallbacks(): { onHover: (mx: number, my: number) => void; onLeave: () => void; onClickPre: (mx: number, my: number) => boolean; onPin: (mx: number, my: number) => void; onUnpin: () => void; }; /** * Resolve a clicked bar / point into a `PerspectiveClickDetail` * (via `buildClickDetail`) and emit both * `perspective-click` and `perspective-global-filter` to the host. * * `rowIdx` derivation: the series pipeline emits one record per * (catIdx, agg, split) tuple, and a pivoted view has one view row * per category — so `catIdx + _rowOffset` is the source-view row. * `_aggregates[aggIdx]` is the *base* column name (no split * prefix). Group-by values come from per-level `_rowPaths`, split-by * values are recovered by splitting `_splitPrefixes[splitIdx]` on * the `|` delimiter the engine uses for pivoted column names. */ private _emitSeriesClickSelect; uploadAndRender(glManager: WebGLContextManager, columns: ColumnDataMap, startRow: number, endRow: number): Promise; _fullRender(glManager: WebGLContextManager): void; resetExpandedDomain(): void; protected destroyInternal(): void; } /** * Resolve the per-series palette and stamp it onto `_series[i].color`. * Cached on `_paletteCache` keyed by reference identity of the theme * inputs + series count — only `restyle()` (which clears `_paletteCache` * via `invalidateTheme`) or a data load (which clears it explicitly) * forces re-resolution. * * Returns true when the cache changed (caller invalidates color upload). */ export declare function ensurePalette(chart: SeriesChart): boolean; /** * Horizontal bar chart — numeric X, categorical Y. */ export declare class XBarChart extends SeriesChart { constructor(); }