FlattenedDataViewModel

ViewModel for flat tables with row grouping, packed row metadata, expand/collapse, and pagination.

FlattenedDataViewModel extends GridDataViewModel. It adds a single-level row facet for group labels and packed per-row metadata for hierarchical grouping with expand/collapse. Constructed from FlattenedDataViewModelParams.

What it adds and why

Unlike pivot grids (where all data is fetched at once), flat tables can have millions of rows loaded via pagination, with hierarchical grouping where rows have different depths and expand/collapse states. The viewmodel needs to track this per-row metadata efficiently because it sits in the render hot path - every scroll frame reads it.

On top of the base class, FlattenedDataViewModel provides:

  • Row facet - a single-level array of group labels
  • Row metadata - packed Uint8Array encoding depth, isLeaf, and isExpanded per row
  • Expand/collapse methods - flip expand state in the metadata
  • getSelectPath - reconstructs the full group path for a row index

Row facet and row metadata

Unlike PivotDataViewModel which has multi-level row facets (one level per dimension), FlattenedDataViewModel has a single-level row facet. All grouping depth information is encoded in the row metadata instead.

The row facet array (rowFacet) has one entry per row. Group rows always have a string label (the group field's value at that depth). When the deepest group-by level is the leaf level, there are no null entries - every row is a group row with a label. When there are data rows below the deepest group level (individual rows that aren't group headers), those data rows have null in the rowFacet.

The row metadata array (rowMeta) is a packed Uint8Array where each byte encodes the row's depth, isLeaf, and isExpanded state.

Case 1: Deepest group level is the leaf level (no null entries)

A table grouped by ["continent", "country"] where Europe is expanded:

rowFacet: ["Europe", "Germany", "UK", "North America"]

rowMeta: [
  createRowMeta(0, false, true),   // Europe        depth=0, group, expanded
  createRowMeta(1, true,  false),  // Germany       depth=1, leaf
  createRowMeta(1, true,  false),  // UK            depth=1, leaf
  createRowMeta(0, false, false),  // North America depth=0, group, collapsed
]
Group
Revenue
Cost
▼ Europe
7380
4585
Germany
4030
2565
UK
3350
2020
▶ North America
9820
6160

Every row has a string label in rowFacet. Germany and UK are leaf rows (isLeaf: true) at the deepest group level - no null entries needed.

Case 2: Data rows below the deepest group level (null entries)

A table grouped by ["continent"] where Europe is expanded to show individual data rows:

rowFacet: ["Europe", "Germany", null, null, "UK", null, "North America"]

rowMeta: [
  createRowMeta(0, false, true),   // Europe        depth=0, group, expanded
  createRowMeta(1, false, false),  // Germany       depth=1, group, collapsed
  createRowMeta(2, true,  false),  // (data row)    depth=2, leaf
  createRowMeta(2, true,  false),  // (data row)    depth=2, leaf
  createRowMeta(1, false, false),  // UK            depth=1, group, collapsed
  createRowMeta(2, true,  false),  // (data row)    depth=2, leaf
  createRowMeta(0, false, false),  // North America depth=0, group, collapsed
]
Group
Product
Revenue
Cost
▼ Europe
7380
4585
▶ Germany
4030
2565
Laptop
1300
750
Phone
2730
1815
▶ UK
3350
2020
Laptop
1350
820
▶ North America
9820
6160

The data rows (Laptop, Phone) have null in rowFacet - they are individual records below the deepest group level. Their data appears in the data columns (Product, Revenue, Cost) instead of the group column.

Packed Uint8Array

Each row's metadata is packed into a single byte instead of an object. This avoids creating millions of small objects on every update, which would cause GC pressure and slow down rendering.

Bit layout:

  • Bits 7-4: depth (up to 15 nesting levels)
  • Bit 3: isLeaf (1 = leaf/data row, 0 = group row)
  • Bit 2: isExpanded (1 = expanded, 0 = collapsed)
import { createRowMeta } from "grid";

createRowMeta(0, false, true);   // depth=0, group, expanded  → 0b0000_0100 = 4
createRowMeta(1, true, false);   // depth=1, leaf              → 0b0001_1000 = 24
createRowMeta(2, false, false);  // depth=2, group, collapsed  → 0b0010_0000 = 32

getSlice unpacks these bytes into FlatRowMeta objects for the visible slice only - the slice is small (only the visible rows), so unpacking is cheap.

expand / collapse / toggleExpand

expand(rowIndex: number): void
collapse(rowIndex: number): void
toggleExpand(rowIndex: number): void

These flip the expand bit in the metadata byte at the given row index. They are viewmodel-level state changes only - they update what the renderer sees (the ▼/▶ icon direction), but they do not add or remove rows. The datamodel handles the actual data fetching separately.

For example, calling toggleExpand(6) on the EU row (collapsed, isExpanded=false) flips it to isExpanded=true. The renderer immediately shows ▼ instead of ▶. But the children rows are not yet in the viewmodel - those arrive when the datamodel fetches them.

Typical flow:

  1. User clicks a group row (e.g. EU at row index 6)
  2. Renderer calls viewModel.toggleExpand(6) - icon flips to ▼ immediately
  3. Application calls dataModel.expand(["EU"]) - triggers data fetch
  4. DataModel returns new FlattenedDataViewModelParams with EU's children included
  5. viewModel.updateData(newParams) and grid.data = viewModel - renderer shows the children

getSelectPath

getSelectPath(rowIndex: number): string[]

Walks backward through row metadata to reconstruct the full group path from a row index. Returns an array of group values from the root to the target row.

// If rows are: USA (depth 0) → California (depth 1) → row at index 5
viewModel.getSelectPath(5);
// ["USA", "California"]

Used when the renderer needs to tell the datamodel which group was clicked (e.g. for expand/collapse or drill-down).

getSlice

getSlice returns a FlatSliceResult which extends BaseSliceResult with:

  • rowFacets: (string | null)[] - group label per row, or null for data rows
  • rowMeta: FlatRowMeta[] - unpacked metadata for the visible slice
const slice = viewModel.getSlice(0, 0, 3, 4);

slice.rowFacets;
// ["USA", null, null, "Canada"]
// Index 0: group row "USA"
// Index 1-2: data/leaf rows (null facet)
// Index 3: group row "Canada"

slice.rowMeta;
// [
//   { depth: 0, isLeaf: false, isExpanded: true },   // USA - expanded group
//   { depth: 1, isLeaf: true, isExpanded: false },   // leaf row under USA
//   { depth: 1, isLeaf: true, isExpanded: false },   // leaf row under USA
//   { depth: 0, isLeaf: false, isExpanded: false },  // Canada - collapsed group
// ]

updateData

updateData(params: FlattenedDataViewModelParams): void

Same pattern as PivotDataViewModel - swaps internal data arrays and pagination state without constructing a new instance. metaState is preserved.

Pagination: totalRows and offsetTop

The data source (in the server) may have 1,000,000 rows but only 10,000 are loaded in data[][]. The viewmodel uses two fields to position the loaded block within the full scroll space:

  • totalRows - total rows in the full dataset. The renderer uses this for scrollbar height (totalRows * rowHeight).
  • offsetTop - number of rows before the loaded block. The renderer adds offsetTop * rowHeight as top padding so the loaded rows sit at the correct scroll position.

Example with pageSize 10,000:

// Page 1 (first load) - user is at the top
{ totalRows: 1_000_000, offsetTop: 0, data: /* 10,000 rows */ }

// Page 5 - user scrolled down
{ totalRows: 1_000_000, offsetTop: 40_000, data: /* 10,000 rows */ }

// All data fits in one page - no pagination
{ totalRows: 500, offsetTop: 0, data: /* 500 rows */ }
// (totalRows === numRows and offsetTop === 0)

See StandardTableDataModel for how pages are managed and fetched.

API Reference.

Extends GridDataViewModel.

ViewModel for flat/standard tables. Adds a single-level row facet for group labels and packed per-row metadata (Uint8Array) for hierarchical grouping with expand/collapse. Supports pagination via totalRows and offsetTop.

Prop

Type

expandfunction

Sets the expand bit in the metadata byte. Viewmodel-level only - does not fetch children.

Type

(rowIndex: number) => void

Parameters

rowIndex -

The row index to expand.

collapsefunction

Clears the expand bit in the metadata byte. Viewmodel-level only - does not remove children.

Type

(rowIndex: number) => void

Parameters

rowIndex -

The row index to collapse.

toggleExpandfunction

Flips the expand bit in the metadata byte. Viewmodel-level only - does not fetch or remove children.

Type

(rowIndex: number) => void

Parameters

rowIndex -

The row index to toggle.

updateDatafunction

Swaps internal data arrays, row facet, row metadata, and pagination state without constructing a new instance. metaState is preserved.

Type

(params: FlattenedDataViewModelParams) => void

Parameters

getSelectPathfunction

Walks backward through row metadata to reconstruct the full group path from root to the given row. Used to identify which group was clicked (e.g. for expand/collapse).

Type

(rowIndex: number) => string[]

Parameters

rowIndex -

The row index to build the path for.

Returns

Array of group values from root to the target row, e.g. ["Europe", "Germany"].

numRowFacetLevelsnumber

Always 1 when rowFacet is provided, 0 otherwise. Unlike pivot viewmodels, flat tables have at most one row facet level - depth is encoded in the row metadata instead.

Type

number

rowFacetsFacetData

Row facet wrapped in level-major format: [rowFacet] (single level). Returns [] when no rowFacet was provided.

Type

FacetData

getSlicefunction

Returns a FlatSliceResult for the given range. Extends the base slice with single-level rowFacets and unpacked rowMeta (FlatRowMeta objects) for the visible rows.

Type

(x0: number, y0: number, x1: number, y1: number) => FlatSliceResult

Parameters

x0 -

Start column index (inclusive).

y0 -

Start row index (inclusive).

x1 -

End column index (exclusive).

y1 -

End row index (exclusive).

Returns

Slice data with rowFacets and rowMeta for the visible rows.

On this page