import type { ColumnDataMap } from "../../data/view-reader"; import type { CategoricalDomain, CategoricalLevel } from "../../axis/categorical-axis"; import { type AxisMode, type NumericCategoryDomain } from "../common/category-axis-resolver"; import { type ChartType, type ColumnChartConfig, type InterpolateMode } from "./series-type"; export interface SeriesInfo { seriesId: number; aggIdx: number; splitIdx: number; aggName: string; splitKey: string; label: string; color: [number, number, number]; axis: 0 | 1; chartType: ChartType; stack: boolean; /** * First / last category index this series contributes data to, in * the post-Pass-2 sample grid. For line+any mode and area+solid: * every cell in `[start, end]` has a value (real or synthesized). * For area+skip: `[start, end]` is the real-data extent; interior * cells with `sampleValid=0` are gaps. `start = -1` (with `end = -1`) * means the series has no real samples — downstream skips it. */ start: number; end: number; /** * Resolved interpolation mode for this aggregate. The build * pipeline reads it to decide whether Pass 2 runs for area * (and which fills to apply); the line glyph reads it at draw * time to set `u_interp_alpha`. Always one of the three modes; * never the legacy boolean form. */ interpolateMode: InterpolateMode; } /** * Logical bar/area record. Synthesized on demand from {@link BarColumns} * via {@link readBarRecord} for tooltip / hover paths. The pipeline never * materializes these — see `BarColumns` for the columnar storage that * replaces the legacy `SeriesChartRecord[]`. */ export interface SeriesChartRecord { catIdx: number; aggIdx: number; splitIdx: number; seriesId: number; xCenter: number; halfWidth: number; y0: number; y1: number; value: number; axis: 0 | 1; /** * `"bar"` quads or `"area"` strip segments both stack via this record. */ chartType: "bar" | "area"; } export declare const BAR_TYPE_BAR = 0; export declare const BAR_TYPE_AREA = 1; /** * Columnar storage for the bar/area record set. Replaces the legacy * `SeriesChartRecord[]` to avoid per-record POJO allocation at scale — * with N×M×P potentially in the millions, the array-of-objects layout * was the dominant build-time GC pressure. * * Records are appended in `(catIdx, aggIdx, splitIdx)` lexicographic * order — the outer category loop guarantees `catIdx` is monotonically * non-decreasing, which the renderer / hit-test use for binary-search * narrowing. * * `count` is the active record count; the underlying typed arrays may * be over-allocated for capacity reuse across builds. */ /** * Compact columnar storage for the bar/area record set. * * Three fields the prior schema carried have been dropped because * they're cheaply derivable at hover time: * - `aggIdx` ← `seriesId / splitCount` (integer division) * - `splitIdx` ← `seriesId % splitCount` * - `value` ← `samples[catIdx * S + seriesId]` * * Per-cell write count drops from 11 to 8 (~27% fewer typed-array * stores) and per-record memory drops from 58 B to 42 B (~28% lower * footprint at scale). `chartType` is kept (1 B / record) — it's * read in tight loops in the render and hit-test paths and a string * dispatch via `_series[]` would be slower than the byte compare. */ export interface BarColumns { count: number; catIdx: Int32Array; seriesId: Int32Array; /** * 0 = left axis, 1 = right axis. */ axis: Uint8Array; /** * {@link BAR_TYPE_BAR} | {@link BAR_TYPE_AREA}. */ chartType: Uint8Array; xCenter: Float64Array; halfWidth: Float64Array; y0: Float64Array; y1: Float64Array; } export declare function emptyBarColumns(): BarColumns; /** * Reuse `prev`'s typed arrays when capacity is sufficient, else allocate * fresh. Resets `count` to 0 either way; pipeline writes from index 0. */ export declare function ensureBarColumnsCapacity(prev: BarColumns | null, capacity: number): BarColumns; /** * Synthesize a {@link SeriesChartRecord} POJO for record `i`. Used by * tooltip / hover paths that hand out a single record reference; not * called in any frame-rate hot loop. * * `splitCount` is `splitPrefixes.length` from the build result (= `P` * in pipeline notation). `samples` + `numSeries` recover the raw * value from the unstacked sample grid; `samples[catIdx * S + sid]` * always carries the same value the pipeline saw when emitting the * record (both writes share the same `v` source). */ export declare function readBarRecord(cols: BarColumns, i: number, splitCount: number, samples: Float32Array, numSeries: number): SeriesChartRecord; /** * Reusable Float64 scratch — chart owns one for `posStack` and one for * `negStack`. Pipeline zero-fills the active prefix on entry. */ export declare function ensureFloat64Scratch(prev: Float64Array | null, capacity: number): Float64Array; export interface SeriesPipelineInput { columns: ColumnDataMap; numRows: number; columnSlots: (string | null)[]; groupBy: string[]; splitBy: string[]; /** * Source-column types for `group_by` columns (table.schema() merged * with view.expression_schema()). Used to (a) stringify non-string * row-path levels and (b) decide between category and numeric axis * mode for single-level group_bys. */ groupByTypes: Record; columnsConfig: Record | undefined; /** * Plugin-scoped default glyph when a column has no explicit entry. */ defaultChartType?: ChartType; /** * Plugin-config knobs consumed by the build pipeline. Pulled from * the chart impl's `_pluginConfig` (sourced from the host's * `plugin_config_schema` / `restore({ plugin_config })`): * * - `autoAltYAxis` — auto-split aggregates onto a secondary Y * axis when their magnitude ratio exceeds * `DUAL_Y_RATIO_THRESHOLD`. Replaces the `AUTO_ALT_Y_AXIS` * compile-time toggle. * - `bandInnerFrac` / `barInnerPad` — band-slot geometry forwarded * to `computeSlotGeometry`. Replace the `BAND_INNER_FRAC` / * `BAR_INNER_PAD` constants. */ autoAltYAxis: boolean; bandInnerFrac: number; barInnerPad: number; /** * Anchor value-axis extents to zero. When `true` (bar / area * default), `leftDomain` / `rightDomain` are guaranteed to enclose * `0` so bars and areas render against their natural baseline. * When `false` (line / scatter default), the domain is the raw * `min`/`max` of the data — the axis tightens around the visible * variation. Maps directly to `PluginConfig.include_zero`. */ includeZero: boolean; /** * Reusable scratch — pipeline writes records into these in place * and zero-fills the stack ladder. Pass the previous build's * outputs to amortize allocation across data reloads. */ scratchBars?: BarColumns | null; scratchPosStack?: Float64Array | null; scratchNegStack?: Float64Array | null; } export type { NumericCategoryDomain }; export interface SeriesPipelineResult { aggregates: string[]; splitPrefixes: string[]; rowPaths: CategoricalLevel[]; numCategories: number; rowOffset: number; /** * Axis mode discriminator. `category` is the default (zero or * many group_by levels, or a single string/boolean level). `numeric` * fires for a single non-string non-boolean group_by — bars are * positioned by the underlying data value rather than logical * category index. */ axisMode: AxisMode; /** * Populated only when `axisMode.mode === "numeric"`. */ numericCategoryDomain: NumericCategoryDomain | null; /** * Per-category X coordinate in real data units. Populated only in * numeric axis mode — `null` in category mode where catIdx itself * is the position. Indexed by `catIdx` (0..numCategories-1). */ categoryPositions: Float64Array | null; series: SeriesInfo[]; /** * Columnar bar/area records, one per (catIdx, agg, split) for series * where `stack === true && chartType in ["bar", "area"]` (stacked) or * `chartType in ["bar", "area"]` with non-zero value (unstacked). */ bars: BarColumns; /** * Reusable scratch passthrough — these own the stack ladder typed * arrays so the next build can reuse capacity. */ posStack: Float64Array | null; negStack: Float64Array | null; /** * Unstacked sample grid: `samples[catIdx * S + seriesId]` is the raw * value for that cell. Only valid for non-stacking series (or for * stacking series when you need the raw, pre-stack value); the * corresponding bit in `sampleValid` indicates whether the cell carries * data. `S === series.length`. */ samples: Float32Array; sampleValid: Uint8Array; leftDomain: { min: number; max: number; }; rightDomain: { min: number; max: number; } | null; hasRightAxis: boolean; /** * Per-axis-side value mode discriminator. `"category"` fires when * every aggregate on that side is post-aggregation `string`-typed * (all-or-nothing rule). Bar y0/y1 then hold dictionary slot * indices and the chrome overlay paints a categorical axis on * that side. `null` for the alt side when there are no series * pinned to alt. */ leftValueAxisMode: "numeric" | "category"; rightValueAxisMode: "numeric" | "category" | null; /** * Single-level `CategoricalDomain` shared across every aggregate * on the corresponding side. Set only when that side's mode is * `"category"`; the chrome renderer in `series-render` materializes * the side's `BarCategoryAxis` from this. */ leftValueCategoryDomain: CategoricalDomain | null; rightValueCategoryDomain: CategoricalDomain | null; } /** * Pure pipeline: turn a raw `ColumnDataMap` into (a) columnar stacked * bar/area records and (b) an unstacked `samples` grid for line/scatter * glyphs plus non-stacking bar/area series. Holds row_path data as * zero-copy views (no materialization of category strings). * * Automatically splits aggregates across a secondary Y axis when their * extents differ by more than {@link DUAL_Y_RATIO_THRESHOLD}×. */ export declare function buildSeriesPipeline(input: SeriesPipelineInput): SeriesPipelineResult;