GridDataViewModel
The source of data for the layout (renderer). The datamodel produces viewmodel data; the renderer consumes it. Holds column-major data arrays and facet metadata for virtualized rendering.
GridDataViewModel is the abstract base class that connects data models to the renderer. It holds column-major data arrays, column facets, and rendering configuration. The renderer never touches the datamodel directly - it only calls getViewportData() on the viewmodel to fetch data for the current viewport.
abstract class GridDataViewModel {
protected data: any[][];
schema: DataSchema[];
readonly metaState: MetaState;
metadata: ViewModelMetadata;
constructor(data: any[][], columnFacets: FacetData);
protected mergeMetadata(incoming: Partial<ViewModelMetadata>): void;
protected updateBase(data: any[][], columnFacets: FacetData): void;
protected updatePagination(totalRows: number | undefined, offsetTop: number | undefined): void;
protected init(options: GridDataViewModelOptions | undefined): void;
setColSize(colIndex: number, colSize: ResolvedVTrackDef["colSize"]): void;
getColFacetValue(level: number, colIndex: number): string | null | undefined;
protected sliceBase(x0: number, y0: number, x1: number, y1: number): BaseSliceResult;
abstract getSlice(x0: number, y0: number, x1: number, y1: number): BaseSliceResult;
getViewportData(x0: number, y0: number, x1: number, y1: number): BaseSliceResult;
register<K>(name: K, callback: ViewModelCallbacks[K]): () => void;
}
interface DataSchema extends Schema {
name: string;
displayName?: string;
}
interface Schema {
type: "measure" | "dimension";
subtype?: SchemaSubtype;
datetimeFormat?: string;
aggregateFn?: AggregateFn;
cardinality?: "low" | "high";
}
type SchemaSubtype = "temporal" | "nominal" | "integer" | "decimal";
type AggregateFn = "sum" | "avg" | "count" | "min" | "max";
interface ViewModelMetadata {
columnFacets?: ColumnFacetMetadata[];
rowFacets?: RowFacetMetadata[];
headers?: HeaderMetadata[];
valueColumns?: ValueColumnMetadata[];
valueRows?: ValueRowMetadata[];
valueCells?: ValueCellMetadata[];
getValueCellMeta(colIndex: number, rowIndex: number): MetadataValue | undefined;
getValueColumnMeta(colIndex: number): MetadataValue | undefined;
getValueRowMeta(rowIndex: number): MetadataValue | undefined;
getColumnFacetMeta(level: number, index: number): MetadataValue | undefined;
getRowFacetMeta(level: number, index: number): MetadataValue | undefined;
getHeaderMeta(axis: "row" | "column", level: number): MetadataValue | undefined;
}
interface ColumnFacetMetadata {
level: number;
index: number;
meta: MetadataValue;
}
interface RowFacetMetadata {
level: number;
index: number;
meta: MetadataValue;
}
interface HeaderMetadata {
axis: "row" | "column";
level: number;
meta: MetadataValue;
}
interface ValueColumnMetadata {
colIndex: number;
meta: MetadataValue;
}
interface ValueRowMetadata {
rowIndex: number;
meta: MetadataValue;
}
interface ValueCellMetadata {
colIndex: number;
rowIndex: number;
meta: MetadataValue;
}
interface GridDataViewModelOptions {
vTrackDefs?: VTrackDef[];
facetDefs?: {
row: Partial<FacetDef>[];
col: Partial<FacetDef>[];
axis: "row" | "col";
};
}
interface VTrackDef {
renderer?: CellRenderer<T>;
cellHeight?: number;
sampleData?: T;
colSize?: ColAutoSizeConfig;
valueFormatter?: ValueFormatter<T>;
}
type ColAutoSizeConfig = | IColAutoSizeStrategyMaxCell
| IColAutoSizeStrategyClampedWidth
| IColAutoSizeStrategyStatic;
interface IColAutoSizeStrategyMaxCell extends IColAutoSize {
strategy: "max-cell";
}
interface IColAutoSize {
strategy: "max-cell" | "clamped-width" | "static";
excludeColumnFacets?: boolean;
}
interface IColAutoSizeStrategyClampedWidth extends IColAutoSize {
strategy: "clamped-width";
maxWidthInPx?: number;
minWidthInPx?: number;
}
interface IColAutoSizeStrategyStatic extends IColAutoSize {
strategy: "static";
width: number;
unit: "%" | "fr" | "px";
}
interface FacetDef {
text: string;
facetField?: string | null;
headerRenderer: FacetHeaderRenderer;
trackRenderer: FacetCellRenderer;
meta?: FacetMeta;
pseudo?: boolean;
colSize?: ColAutoSizeConfig;
groupSchema?: DataSchema[];
valueFormatter?: ValueFormatter;
}
interface FacetMeta {
projectionState: ProjectionState;
projectedValues: Set<string>;
}
enum ProjectionState {
PROJECTED,
NOT_PROJECTED,
SOME_PROJECTED,
PROJECTION_NOT_CONFIGURED,
}
interface ResolvedVTrackDef extends VTrackDef {
renderer: CellRenderer<any>;
isCustom: boolean;
colSize: ColAutoSizeConfig;
valueFormatter?: ValueFormatter;
}
interface BaseSliceResult {
numRows: number;
numCols: number;
sliceNumRows: number;
sliceNumCols: number;
columnFacets?: (string | null)[][];
data?: any[][];
}
interface DataViewport {
x0: number;
y0: number;
x1: number;
y1: number;
}
interface ViewModelCallbacks {
viewportDataChange: (viewport: DataViewport) => void;
}Role in the pipeline
The grid pipeline is: DataSource → DataModel → ViewModel → Renderer.
The ViewModel is the data the renderer (layout) needs to draw a grid. The DataModel produces *Params objects (PivotDataViewModelParams or FlattenedDataViewModelParams) that are fed into the ViewModel constructor. However, you can skip the DataModel entirely and push data directly into a ViewModel - useful when you already have the full data and want to handle transformations yourself.
The viewmodel-to-renderer path is designed to be fast (under ~2ms) - it performs memory reads and array slicing with no computation. Resetting or updating the viewmodel is the recommended way to push new data into the grid.
Unidirectional data flow
Data flows one way: ViewModel → Renderer. The renderer never mutates the viewmodel's data.
The cycle works like this:
- Create or update a viewmodel with your data
- Feed it to the renderer via
grid.data = viewModel - On interaction or data update, recreate/update the viewmodel and set it on the renderer again
// Initial render
const viewModel = new PivotDataViewModel(params);
grid.data = viewModel;
// On data update - update viewmodel and reset on renderer
const newParams = await dataModel.getViewModelData(config);
viewModel.updateData(newParams);
grid.data = viewModel;This unidirectional cycle is cheap to perform because viewmodel updates are fast memory operations - swapping array references and re-resolving options.
Data layout
Data is stored in column-major format: data[col][row]. Each inner array holds all values for one column across all rows.
// 3 rows, 2 columns (name, age):
const data = [
["Alice", "Bob", "Carol"], // column 0: name
[30, 25, 28], // column 1: age
];
// Column facets - header labels for the columns.
// Each inner array is one facet level across all columns.
const columnFacets: FacetData = [
["name", "age"], // level 0
];Column facets can have multiple levels for nested column headers. Each level is an array of labels; the layout renders them as stacked header rows:
// 4 data columns with 2 levels of column headers:
const columnFacets: FacetData = [
["Q1", "Q1", "Q2", "Q2"], // level 0 (top)
["revenue", "cost", "revenue", "cost"], // level 1 (bottom)
];Key dimensions:
numRows/numCols- dimensions of the loadeddata[][]arraystotalRows- total rows in the full dataset (used for scrollbar sizing). Relevant for paginated flat tables; for pivots,totalRows === numRowsoffsetTop- number of rows before the loaded block (used for scroll positioning). Relevant for paginated flat tables; for pivots,offsetTop === 0
See FlattenedDataViewModel for the full pagination explanation.
getViewportData
getViewportData(x0: number, y0: number, x1: number, y1: number): BaseSliceResultThe renderer calls getViewportData on every render cycle with the visible cell range (plus overscan).
It returns a BaseSliceResult - a window into the viewmodel's data for the requested range. The data in the slice stays column-major (same as the viewmodel), but columnFacets are transposed - the viewmodel stores facets as colFacets[level][colIndex], but the slice returns them as columnFacets[colIndex][level] (grouped per column).
Example: given a viewmodel with 3 columns (Name, Revenue, Trend) and 5 rows, requesting the slice (1, 0, 3, 2) (columns 1-2, rows 0-1):
const slice = viewModel.getViewportData(1, 0, 3, 2);
slice.numRows; // 5 (full viewmodel)
slice.numCols; // 3 (full viewmodel)
slice.sliceNumRows; // 2 (rows 0-1)
slice.sliceNumCols; // 2 (columns 1-2)
slice.data;
// [
// [8150, 5650], // column 1 (Revenue), rows 0-1
// [~~~, ~~~], // column 2 (Trend), rows 0-1
// ]
slice.columnFacets;
// [
// ["Revenue"], // column 1: facet values across levels
// ["Trend"], // column 2: facet values across levels
// ]Internally it:
- Calls
getSlice(x0, y0, x1, y1)- which subclasses override to add row facets and row metadata - Fires a debounced
viewportDataChangecallback (50ms debounce) when the viewport bounds change
Subscribe to this callback to perform lazy operations on data that is currently in view - for example, fetching expanded row children only when the user scrolls them into the viewport.
const unsubscribe = viewModel.register("viewportDataChange", (viewport) => {
// viewport: { x0, y0, x1, y1 }
});
// Later: unsubscribe()VTrackDefs and FacetDefs
A grid is composed of vertical tracks (columns of data cells) and facet tracks (header rows/columns). GridDataViewModelOptions lets you configure how each of these is rendered.
interface GridDataViewModelOptions {
vTrackDefs?: VTrackDef[];
facetDefs?: {
row: Partial<FacetDef>[];
col: Partial<FacetDef>[];
axis: "row" | "col";
};
}Vertical tracks (VTrackDef)
Each column of data cells is a vertical track. A VTrackDef configures one vertical track - its cell renderer, cell height, and column sizing strategy. The vTrackDefs array is indexed by column position: vTrackDefs[0] configures column 0, vTrackDefs[1] configures column 1, and so on.
Consider a simple table with 3 columns - Name, Revenue, and Trend:
The blue-highlighted column is one vertical track, configured by vTrackDefs[1]. Each column in the grid is a vertical track.
Name and Revenue use the default textRenderer; Trend uses a custom sparkline renderer with a fixed cell height:
interface VTrackDef<T = any> {
renderer?: CellRenderer<T>;
cellHeight?: number;
sampleData?: T;
colSize?: ColAutoSizeConfig;
}const options: GridDataViewModelOptions = {
vTrackDefs: [
undefined, // column 0 (Name): default textRenderer
undefined, // column 1 (Revenue): default textRenderer
{ // column 2 (Trend): custom sparkline
renderer: sparklineRenderer,
cellHeight: 40,
colSize: { strategy: "static", width: 2, unit: "fr" },
},
],
};Columns without a VTrackDef (or with no renderer set) get the default textRenderer.
Facet tracks (FacetDef)
When columns are nested (grouped), the header area has multiple levels. Each level is populated by a field's values and rendered as a horizontal facet track. A FacetDef configures one facet level - its track renderer (draws the facet cell), header renderer (draws the level label), display text, and optional metadata.
Consider a table with columns grouped by Quarter, each containing Revenue and Cost:
Yellow rows are facet tracks (column facets) - the top row (Q1, Q2) is facet level 0, the second row (Revenue, Cost) is facet level 1. The blue column is one vertical track (vTrackDefs[1]).
The header has two facet levels - level 0 is the Quarter field, level 1 is the measure names. Each facet level is a horizontal track configured by a FacetDef:
interface FacetDef {
text: string;
facetField?: string | null;
headerRenderer: FacetHeaderRenderer;
trackRenderer: FacetCellRenderer;
meta?: FacetMeta;
pseudo?: boolean;
colSize?: ColAutoSizeConfig; // row facets only
groupSchema?: DataSchema[];
}The same concept applies to row facets. When rows are grouped (e.g. by region), the row facet column on the left is a facet track:
Yellow cells are facet tracks - the top row (Alice, Bob) is a column facet track, and the left column (USA, Germany, France) is a row facet track. The blue column is a vertical track (vTrackDefs[0]). The gray "Name" and "Region" cells are facet headers (rendered by headerRenderer).
A FacetDef has two renderers:
trackRenderer- draws each facet cell (e.g. the "Alice", "Revenue" cells in the yellow facet track above). This is the per-cell renderer for the facet track.headerRenderer- draws the level label (e.g. the gray "Name" and "Region" cells above). This is called once per level.
const options: GridDataViewModelOptions = {
facetDefs: {
row: [
{
text: "Region",
trackRenderer: myGroupFacetRenderer,
headerRenderer: defaultFacetHeaderRenderer,
meta: { projectionState: "expanded", projectedValues: new Set(["Europe"]) },
},
],
col: [
{ text: "Quarter", trackRenderer: defaultFacetRenderer, headerRenderer: defaultFacetHeaderRenderer },
{ text: "Measure", trackRenderer: defaultFacetRenderer, headerRenderer: defaultFacetHeaderRenderer },
],
axis: "col",
},
};ColAutoSizeConfig - column sizing strategies
ColAutoSizeConfig supports three strategies:
"max-cell"(default) - auto-sizes the column to the widest cell content"clamped-width"- content-sized, clamped by optional min/max pixel bounds"static"- fixed width infr,%, orpxunits
All strategies accept excludeColumnFacets?: boolean - when true, column facet cells are excluded from auto-size measurement.
// Auto-size to content (default)
{ strategy: "max-cell" }
// Content-sized, clamped between min/max bounds
{ strategy: "clamped-width", minWidthInPx: 100, maxWidthInPx: 400 }
// Fixed 2fr width
{ strategy: "static", width: 2, unit: "fr" }When any column or facet uses "static" strategy, the viewmodel propagates static strategy to all columns and facets. Mixed strategies are not supported - it is all-or-nothing.
MetaState
MetaState is a namespaced key-value store attached to each viewmodel instance. You can store any data here and use it as a general-purpose grid store - it survives viewmodel data updates (the data arrays get swapped, but metaState persists).
Only one viewmodel instance lives for the lifetime of a grid (or layout). When updateData is called, the data arrays are swapped but the metaState persists - so the renderer does not lose its transient state across data refreshes.
// Store state
viewModel.metaState.set("scroll", "x", 150);
viewModel.metaState.set("scroll", "y", 300);
// Read state
const scrollState = viewModel.metaState.get("scroll");
// { x: 150, y: 300 }
// Clear a single key
viewModel.metaState.clear("scroll", "x");
// Clear entire namespace
viewModel.metaState.clear("scroll");API Reference
Stores column-major data arrays (data[col][row]), column facets, and rendering configuration (VTrackDefs, FacetDefs). Subclasses must implement numRowFacetLevels, rowFacets, and getSlice() to provide row-axis data. The renderer calls getViewportData() each render cycle to get a slice of the visible data.
Prop
Type
dataarrayType
any[][]
schema?arraymetaStateobjectNamespaced key-value store for arbitrary state that persists across updateData calls. See MetaState.
Type
MetaState
metadataobjectMetadata for value cells, facets, and headers. Accessor methods are initialized once and survive merges via spread.
Type
ViewModelMetadata
mergeMetadatafunctionType
(incoming: Partial<ViewModelMetadata>) => void
updateBasefunctionReplaces the internal data arrays and column facets. Called by subclass updateData methods.
Type
(data: any[][], columnFacets: FacetData) => void
Parameters
data -
Column-major data arrays. data[col][row].
columnFacets -
Column header values. columnFacets[level][colIndex].
updatePaginationfunctionUpdates pagination state. Called by subclass updateData methods.
Type
(totalRows: number | undefined, offsetTop: number | undefined) => void
Parameters
totalRows -
Total rows in the full dataset, or undefined to default to numRows.
offsetTop -
Number of rows before the loaded contiguous block, or undefined to default to 0.
initfunctionResolves GridDataViewModelOptions into vTrackDefs and facetDefs. Propagates static sizing strategy to all columns/facets if any column or facet uses it. Called by subclass constructors and updateData methods.
Type
(options: GridDataViewModelOptions | undefined) => void
Parameters
options -
Rendering options, or undefined to use defaults (textRenderer, max-cell sizing).
normalizeOptionsfunctionType
(options: GridDataViewModelOptions | undefined) => { vTrackDefs: ResolvedVTrackDef[]; colFacetDefs: FacetDef[]; rowFacetDefs: FacetDef[]; }
normalizeFacetDefsfunctionType
(input: Partial<FacetDef>[] | undefined, count: number) => FacetDef[]
vTrackDefsarrayResolved per-column rendering configuration. One entry per column in data.
Type
ResolvedVTrackDef[]
facetDefsobjectResolved facet rendering configuration for row and column facet levels.
Type
{ row: FacetDef[]; col: FacetDef[]; axis: "row" | "col"; }
hasStaticStrategyuniontrue if any column or facet uses "static" sizing strategy. When true, all columns/facets use static sizing.
Type
boolean
setColSizefunctionOverrides the column sizing strategy for a single column.
Type
(colIndex: number, colSize: ResolvedVTrackDef["colSize"]) => void
Parameters
colIndex -
The column index to update.
colSize -
The new ColAutoSizeConfig to apply.
numRowFacetLevelsnumberNumber of row facet levels. Subclasses implement this as this class does not handle row facets.
Type
number
numColFacetLevelsnumberNumber of column facet levels (header rows above data).
Type
number
numRowsnumberNumber of rows in the loaded data[][] arrays.
Type
number
numColsnumberNumber of columns in the loaded data[][] arrays.
Type
number
totalRowsnumberTotal rows in the full dataset. Used by the renderer for scrollbar sizing. Defaults to numRows when not set.
Type
number
offsetTopnumberNumber of rows before the contiguous data block loaded in the viewmodel. The viewmodel can only hold one contiguous block of rows; offsetTop tells the renderer where that block sits within the full dataset. Defaults to 0.
Type
number
getColFacetValuefunctionReturns the column facet value at the given level and column index.
Type
(level: number, colIndex: number) => string | null | undefined
Parameters
level -
The facet level (0 = topmost header row).
colIndex -
The column index.
Returns
The facet value, null if merged with the previous column, or undefined if out of bounds.
columnFacetsFacetDataAll column facet levels. columnFacets[level][colIndex] is the facet value.
Type
FacetData
rowFacetsFacetDataAll row facet levels. Subclasses implement this as this class does not handle row facets.
Type
FacetData
sliceBasefunctionSlices the base data and column facets for the given viewport range. Subclass getSlice implementations call this and extend the result with row-specific data.
Type
(x0: number, y0: number, x1: number, y1: number) => BaseSliceResult
Parameters
x0 -
Start column index (inclusive).
y0 -
Start row index (inclusive).
x1 -
End column index (exclusive).
y1 -
End row index (exclusive).
Returns
A BaseSliceResult with data in column-major format and columnFacets transposed to [colIndex][level].
getSlicefunctionReturns a slice of data for the given range. Subclasses override to add row facets and row metadata.
Type
(x0: number, y0: number, x1: number, y1: number) => BaseSliceResult
Parameters
x0 -
Start column index (inclusive).
y0 -
Start row index (inclusive).
x1 -
End column index (exclusive).
y1 -
End row index (exclusive).
Returns
A subclass-specific slice result (PivotSliceResult or FlatSliceResult).
getViewportDatafunctionCalled by the renderer each render cycle. Returns a BaseSliceResult for the visible range and fires a debounced viewportDataChange callback when the viewport changes.
Type
(x0: number, y0: number, x1: number, y1: number) => BaseSliceResult
Parameters
x0 -
Start column index (inclusive).
y0 -
Start row index (inclusive).
x1 -
End column index (exclusive).
y1 -
End row index (exclusive).
Returns
The slice result from getSlice.
viewportobjectThe most recent viewport bounds passed to getViewportData.
Type
DataViewport
registerfunctionRegisters a callback for viewmodel events (e.g. "viewportDataChange").
Type
<K extends keyof ViewModelCallbacks>(name: K, callback: ViewModelCallbacks[K]) => () => void
Parameters
name -
The event name.
callback -
The callback function.
Returns
An unsubscribe function that removes the callback.