import type { WaveformDisplayMode } from '../../types' export const GRID_MIN_COUNT = 1 export const GRID_MAX_COUNT = 10 export const X_AXIS_BAND = 16 export interface WaveformGridOptions { rowCount?: number columnCount?: number showPagination?: boolean } export interface NormalizedWaveformGridOptions { rowCount: number columnCount: number showPagination: boolean } export interface GridCellGeometry { slotIndex: number row: number column: number left: number top: number width: number /** Backward-compatible alias for the actual plot height. */ height: number plotHeight: number cellHeight: number xAxisBand: number } const normalizeCount = (value: unknown, fallback: number) => { const numeric = typeof value === 'number' ? value : Number(value) if (!Number.isFinite(numeric)) return fallback return Math.min(GRID_MAX_COUNT, Math.max(GRID_MIN_COUNT, Math.floor(numeric))) } export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedWaveformGridOptions { return { rowCount: normalizeCount(options?.rowCount, 2), columnCount: normalizeCount(options?.columnCount, 1), showPagination: options?.showPagination ?? true, } } export function getPageSize(options: NormalizedWaveformGridOptions): number { return options.rowCount * options.columnCount } export function getPageCount(seriesCount: number, options: NormalizedWaveformGridOptions): number { return Math.max(1, Math.ceil(Math.max(0, seriesCount) / getPageSize(options))) } export function paginateSeries( series: T[], page: number, options: NormalizedWaveformGridOptions, ): T[] { const pageCount = getPageCount(series.length, options) const safePage = Math.min(pageCount, Math.max(1, Math.floor(page))) const start = (safePage - 1) * getPageSize(options) return series.slice(start, start + getPageSize(options)) } export function getGridGap(displayMode: WaveformDisplayMode): number { return displayMode === 'compact' ? 0 : displayMode === 'separated' ? 16 : 14 } export function resolveGridCellGeometry( innerWidth: number, innerHeight: number, options: NormalizedWaveformGridOptions, displayMode: WaveformDisplayMode, slotHasSeries: boolean[] = [], horizontalGap?: number, ): GridCellGeometry[] { const defaultGap = getGridGap(displayMode) const columnGap = Number.isFinite(horizontalGap) ? Math.max(0, horizontalGap as number) : defaultGap const totalHorizontalGap = Math.max(0, options.columnCount - 1) * columnGap const axisRows = new Set() if (displayMode === 'independent') { for (let row = 0; row < options.rowCount; row += 1) axisRows.add(row) } else if (displayMode === 'compact') { // Compact tracks share one continuous plot stack. Reserve the X-axis band // only beneath the final grid row, including when that row is empty. axisRows.add(options.rowCount - 1) } else { for (let column = 0; column < options.columnCount; column += 1) { for (let slotIndex = getPageSize(options) - 1; slotIndex >= 0; slotIndex -= 1) { if (slotIndex % options.columnCount !== column) continue if (slotHasSeries[slotIndex]) { axisRows.add(Math.floor(slotIndex / options.columnCount)) break } } } } const totalVerticalGap = Math.max(0, options.rowCount - 1) * defaultGap const totalAxisBand = axisRows.size * X_AXIS_BAND const width = Math.max(1, (innerWidth - totalHorizontalGap) / options.columnCount) const plotHeight = Math.max(1, (innerHeight - totalVerticalGap - totalAxisBand) / options.rowCount) return Array.from({ length: getPageSize(options) }, (_, slotIndex) => { const row = Math.floor(slotIndex / options.columnCount) const column = slotIndex % options.columnCount const xAxisBand = axisRows.has(row) ? X_AXIS_BAND : 0 const cellHeight = plotHeight + xAxisBand const top = Array.from({ length: row }, (_, previousRow) => { const previousBand = axisRows.has(previousRow) ? X_AXIS_BAND : 0 return plotHeight + previousBand + defaultGap }).reduce((sum, value) => sum + value, 0) return { slotIndex, row, column, left: column * (width + columnGap), top, width, height: plotHeight, plotHeight, cellHeight, xAxisBand, } }) } export function getBottomRowCellIndexes( cells: Array, columnCount: number, ): Set { const visible = new Set() for (let column = 0; column < columnCount; column += 1) { for (let index = cells.length - 1; index >= 0; index -= 1) { const cell = cells[index] if (cell.column === column && cell.hasSeries) { visible.add(cell.slotIndex) break } } } return visible }