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.

Outline
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: DataSourceDataModel → 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:

  1. Create or update a viewmodel with your data
  2. Feed it to the renderer via grid.data = viewModel
  3. 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 loaded data[][] arrays
  • totalRows - total rows in the full dataset (used for scrollbar sizing). Relevant for paginated flat tables; for pivots, totalRows === numRows
  • offsetTop - 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): BaseSliceResult

The 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:

  1. Calls getSlice(x0, y0, x1, y1) - which subclasses override to add row facets and row metadata
  2. Fires a debounced viewportDataChange callback (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:

Name
Revenue
Trend
Alice
8150
~~~
Bob
5650
~~~
Carol
1730
~~~

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:

Q1
Q2
Revenue
Cost
Revenue
Cost
8150
3200
5650
2100
7200
2800
6100
2400

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:

Name
Alice
Bob
Region
Revenue
Revenue
▼ USA
8150
5650
▼ Germany
3200
2100
▶ France

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 in fr, %, or px units

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

dataarray

Type

any[][]

schema?array

DataSchema definitions for the columns in data.

Type

DataSchema[] | undefined

metaStateobject

Namespaced key-value store for arbitrary state that persists across updateData calls. See MetaState.

Type

MetaState

metadataobject

Metadata for value cells, facets, and headers. Accessor methods are initialized once and survive merges via spread.

Type

ViewModelMetadata

mergeMetadatafunction

Type

(incoming: Partial<ViewModelMetadata>) => void

updateBasefunction

Replaces 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].

updatePaginationfunction

Updates 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.

initfunction

Resolves 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).

normalizeOptionsfunction

Type

(options: GridDataViewModelOptions | undefined) => { vTrackDefs: ResolvedVTrackDef[]; colFacetDefs: FacetDef[]; rowFacetDefs: FacetDef[]; }

normalizeFacetDefsfunction

Type

(input: Partial<FacetDef>[] | undefined, count: number) => FacetDef[]

vTrackDefsarray

Resolved per-column rendering configuration. One entry per column in data.

Type

ResolvedVTrackDef[]

facetDefsobject

Resolved facet rendering configuration for row and column facet levels.

Type

{ row: FacetDef[]; col: FacetDef[]; axis: "row" | "col"; }

hasStaticStrategyunion

true if any column or facet uses "static" sizing strategy. When true, all columns/facets use static sizing.

Type

boolean

setColSizefunction

Overrides 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.

numRowFacetLevelsnumber

Number of row facet levels. Subclasses implement this as this class does not handle row facets.

Type

number

numColFacetLevelsnumber

Number of column facet levels (header rows above data).

Type

number

numRowsnumber

Number of rows in the loaded data[][] arrays.

Type

number

numColsnumber

Number of columns in the loaded data[][] arrays.

Type

number

totalRowsnumber

Total rows in the full dataset. Used by the renderer for scrollbar sizing. Defaults to numRows when not set.

Type

number

offsetTopnumber

Number 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

getColFacetValuefunction

Returns 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.

columnFacetsFacetData

All column facet levels. columnFacets[level][colIndex] is the facet value.

Type

FacetData

rowFacetsFacetData

All row facet levels. Subclasses implement this as this class does not handle row facets.

Type

FacetData

sliceBasefunction

Slices 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].

getSlicefunction

Returns 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).

getViewportDatafunction

Called 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.

viewportobject

The most recent viewport bounds passed to getViewportData.

Type

DataViewport

registerfunction

Registers 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.

On this page