Skip to content

Data Model

The CellStore is a sparse storage backed by a Map<string, CellData>. Keys are "row:col" strings (e.g., "5:3"). Only cells that contain data are stored — empty cells return undefined.

// O(1) get and set
const cell = cellStore.get(5, 3); // CellData | undefined
cellStore.set(5, 3, { value: 42 }); // Stores cell at row 5, col 3
cellStore.delete(5, 3); // Removes cell data
cellStore.has(5, 3); // Check if cell exists
cellStore.clear(); // Clear all cells
cellStore.setMetadata(5, 3, { status: 'changed' });
cellStore.clearMetadata(5, 3);
// Load a chunk of data from an array
cellStore.bulkLoadChunk(data, columnKeys, startRow);
// Generate rows with a factory function
cellStore.bulkGenerate(startRow, count, columnKeys, (index) => ({
id: index + 1,
name: `Row ${index + 1}`,
}));
PropertyTypeDescription
sizenumberNumber of cells stored
versionnumberMutation counter (increments on every write)

This sparse representation is efficient for large datasets where most cells may be empty (e.g., 100K rows × 40 columns, but only populated cells use memory).

Each stored cell is represented by a CellData object:

interface CellData {
value: CellValue; // Raw value: string | number | boolean | Date | null
displayValue?: string; // Formatted text shown in the cell
formula?: string; // Formula string (e.g., "=SUM(A1:A10)")
style?: CellStyleRef; // Reference to a shared style in the StylePool
type?: CellType; // Overrides column-level type
metadata?: CellMetadata; // Status indicators, links, comments
}

The value field stores one of:

TypeExampleNotes
string"Hello"Plain text
number42, 3.14Numeric values
booleantrue, falseRendered as checkbox
nullnullEmpty cell

The displayValue is the formatted string used for canvas rendering. For example, a number 1234.5 might have a displayValue of "1,234.50" depending on formatting. If displayValue is not set, value is converted to string for rendering.

The DataView layer provides logical-to-physical row mapping. It sits between the rendering/interaction layer and the CellStore.

Logical index (what the user sees)
┌────▼────┐
│ DataView │ ← Remaps indices when sort/filter is active
└────┬────┘
Physical index (actual position in CellStore)

When no sort or filter is active, DataView is a passthrough — logical index equals physical index. When sorting or filtering, DataView maintains a mapping array that translates visible row indices to the actual data positions.

// Convert between logical and physical
const physicalRow = dataView.getPhysicalRow(logicalRow);
const logicalRow = dataView.getLogicalRow(physicalRow);
// Get the number of visible rows (may be less than total when filtered)
const visibleCount = dataView.visibleRowCount;
// Update total row count
dataView.setTotalRowCount(newCount);

Row heights and column widths are stored as Float64Array cumulative position arrays. This enables two key operations:

  • O(1) cell rectangle lookup by index — Position of row n is cumulative[n], height is cumulative[n+1] - cumulative[n].
  • O(log n) index lookup by pixel coordinate — Binary search on the cumulative array to find which row/column a pixel position falls in.
// Cumulative positions example for 4 rows with height 30px each:
// [0, 30, 60, 90, 120]
//
// Row 2 starts at position 60, ends at 90, height = 30

The LayoutEngine combines RowStore and ColStore to compute cell rectangles:

interface CellRect {
x: number; // Left edge in pixels
y: number; // Top edge in pixels
width: number; // Cell width in pixels
height: number; // Cell height in pixels
}
// O(1) - direct array lookup
const rect = layoutEngine.getCellRect(row, col);
// O(log n) - binary search on cumulative array
const { row, col } = layoutEngine.getCellAt(pixelX, pixelY);

The getCellAt method is used by EventTranslator for hit-testing — converting mouse click coordinates into cell addresses.

The StylePool deduplicates cell styles. When multiple cells share the same style (font, color, alignment, etc.), they reference the same style object in memory instead of each holding a copy.

// Two cells with identical styles share one object
const styleId = stylePool.intern({ fontWeight: 'bold', color: '#333' });
cellA.style = stylePool.get(styleId);
cellB.style = stylePool.get(styleId); // Same reference as cellA.style

This reduces memory usage significantly in tables where many cells share common formatting.