StandardTableDataModel

Produces flat rows with optional row grouping, expand/collapse, and paginated data fetching with cache management.

StandardTableDataModel is an abstract class that extends DataModel. It produces flat rows with optional hierarchical row grouping, expand/collapse, and paginated data fetching. It manages an internal page cache to limit how much data the browser holds in memory - since the server typically has far more data than the browser can load at once.

Outline
abstract class StandardTableDataModel extends DataModel<StandardDataFetchAndTransformIR, FlattenedDataViewModelParams> {
  config: StandardTableConfig;
  pages: PageNode[];
  topLevelRowCount: number;
  columnMetadata: StandardColumnMetadata[];
  constructor(schema: DataSchema[], config: Partial<StandardTableConfig>, metadataPlumber?: StandardMetadataPlumber);
  abstract getData(ir: StandardDataFetchAndTransformIR, metadataResolver?: StandardMetadataResolver<unknown>): Promise<GetRowsResponse>;
  abstract getRangeOfColumn(field: string): Promise<ColumnRangeValues>;
  setViewModelOptions(options: GridDataViewModelOptions): void;
  setConfig(config: Partial<StandardTableConfig>): void;
  getViewModelData(ir: StandardDataFetchAndTransformIR): Promise<FlattenedDataViewModelParams>;
  expand(groupPath: string[]): Promise<FlattenedDataViewModelParams>;
  collapse(groupPath: string[]): Promise<FlattenedDataViewModelParams>;
  protected buildResolverInput(ir: StandardDataFetchAndTransformIR): StandardMetadataResolverInput;
  getExpandedPaths(): string[][];
  computeTotalLogicalRows(): number;
}

interface StandardTableConfig {
  pageSize: number;
  maxNumPageBeforeEviction: number;
}

interface PageNode {
  data: any[][] | null;
  physicalStart: number;
  rowCount: number;
  expandedRows: Map<number, ExpandedGroup>;
  metadata?: PageMetadata;
}

interface ExpandedGroup {
  expanded: boolean;
  totalRowCount: number;
  pages: PageNode[];
}

interface PageMetadata {
  rows?: StandardPageRowMetadata[];
  cells?: StandardPageCellMetadata[];
}

interface StandardPageRowMetadata {
  rowIdx: number;
  meta: MetadataValue;
}

interface StandardPageCellMetadata {
  rowIdx: number;
  colIdx: number;
  meta: MetadataValue;
}

interface StandardColumnMetadata {
  colIdx: number;
  meta: MetadataValue;
}

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 StandardDataFetchAndTransformIR {
  startRow: number;
  endRow: number;
  groupPath: string[];
  groupBy: string[];
  project: string[];
  sort: SortEntry[];
  filter: ScalarFilter[];
  metadata?: unknown;
}

interface SortEntry {
  field: string;
  direction: SortDirection;
  by?: string;
}

type SortDirection = "asc" | "desc" | "noop";

interface ScalarFilter {
  type: "scalar";
  field: string;
  subtype?: "date";

  op: "eq" | "neq" | "in" | "not_in"
    | "gt" | "lt" | "gte" | "lte"
    | "between"
    | "contains" | "doesNotContain" | "startsWith" | "endsWith"
    | "before" | "after"
    | "empty" | "notEmpty";

  value: string | string[] | number | number[] | null;
}

interface StandardMetadataResolver {
  resolve(input: StandardMetadataResolverInput): T[];
}

interface StandardMetadataResolverInput {
  ir: StandardDataFetchAndTransformIR;
  schema: DataSchema[];
  dataSource?: DataSource<any>;
}

interface DataSource {
  execute(req: T): Promise<Record<string, any>[]>;
  addRef(): void;
  release(): Promise<void>;
}

interface GetRowsResponse {
  rowData: any[][];
  totalRowCount: number;
  metadata?: Record<string, any[]>;
}

type ColumnRangeValues = | { type: "categorical"; values: string[] }
  | { type: "range"; min: number; max: number }
  | { type: "temporal"; min: string; max: string };

interface FlattenedDataViewModelParams {
  data: any[][];
  columnFacets: FacetData;
  rowFacet?: (string | null)[];
  rowMeta?: Uint8Array;
  options?: GridDataViewModelOptions;
  schema?: DataSchema[];
  totalRows?: number;
  offsetTop?: number;
  metadata?: Partial<ViewModelMetadata>;
}

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;
}
  • TInput is fixed to StandardDataFetchAndTransformIR - a query describing which rows to fetch (range, grouping, projection, sort, filter)
  • TViewModelData is fixed to FlattenedDataViewModelParams - the flat row structure the renderer expects

How it works

StandardTableDataModel breaks large datasets into fixed-size pages. When the caller requests a row range via getViewModelData(), the model:

  1. Checks if the IR (sort, filter, groupBy, project) changed since the last call - if so, resets the page cache
  2. Bootstraps page slots by fetching the first page to learn the total row count
  3. Identifies which pages overlap the requested [startRow, endRow] range
  4. Fetches any unfetched pages in parallel
  5. Evicts distant pages if the cache exceeds maxNumPageBeforeEviction
  6. Flattens the page tree into the view model output

Configuration

Pass a StandardTableConfig to control pagination and caching:

interface StandardTableConfig {
  pageSize: number;                  // rows per page (default: 10000)
  maxNumPageBeforeEviction: number;  // max fetched pages before eviction (default: 300)
}

The constructor and setConfig() accept Partial<StandardTableConfig> - any fields you omit use the defaults above.

Implementing a StandardTableDataModel

StandardTableDataModel is abstract. You must implement two methods:

  • getData(ir: StandardDataFetchAndTransformIR) - fetches rows from your data source
  • getRangeOfColumn(field: string) - returns the distinct values or range for a column (used for filtering)
import { StandardTableDataModel } from "grid";
import { DataSchema, StandardTableConfig, StandardDataFetchAndTransformIR, GetRowsResponse, ColumnRangeValues } from "grid";

class MyTableDataModel extends StandardTableDataModel {
  private dataSource: MyDataSource;

  constructor(schema: DataSchema[], config: Partial<StandardTableConfig>, dataSource: MyDataSource) {
    super(schema, config);
    this.dataSource = dataSource;
  }

  async getData(ir: StandardDataFetchAndTransformIR): Promise<GetRowsResponse> {
    // Fetch rows from your data source using the IR
    const result = await this.dataSource.query({
      startRow: ir.startRow,
      endRow: ir.endRow,
      groupPath: ir.groupPath,
      groupBy: ir.groupBy,
      project: ir.project,
      sort: ir.sort,
      filter: ir.filter,
    });
    return { rowData: result.data, totalRowCount: result.total };
  }

  async getRangeOfColumn(field: string): Promise<ColumnRangeValues> {
    const values = await this.dataSource.getDistinctValues(field);
    return { type: "categorical", values };
  }
}

StandardDataFetchAndTransformIR

The input query that describes what data to fetch:

interface StandardDataFetchAndTransformIR {
  startRow: number;      // first row index to fetch (inclusive)
  endRow: number;        // last row index to fetch (exclusive)
  groupBy: string[];     // columns to group by (e.g. ["country", "state"])
  groupPath: string[];   // path into the group hierarchy (e.g. ["USA", "California"])
  project: string[];     // columns to include in the output
  sort: SortEntry[];     // sort instructions
  filter: ScalarFilter[];// filter conditions
}
  • groupBy defines the grouping hierarchy - an ordered list of columns that form the nesting levels. If groupBy is ["country", "state", "city"], the top level shows distinct countries, the second level shows states within a country, and the third level shows cities within a state.
  • groupPath tells the data source which level of the hierarchy to fetch from. It is empty [] for the top level. When a user expands a group row, the model calls getData with groupPath set to the values leading to that row. For example, with groupBy: ["country", "state", "city"]:
    • groupPath: [] fetches the top-level country rows
    • groupPath: ["USA"] fetches states inside "USA" (triggered by expand(["USA"]))
    • groupPath: ["USA", "California"] fetches cities inside California (triggered by expand(["USA", "California"]))
  • project lists which columns appear in the output.

GetRowsResponse

The response your getData implementation must return:

interface GetRowsResponse {
  rowData: any[][];      // column-major data arrays
  totalRowCount: number; // total number of rows at this level (for pagination)
}

rowData is column-major - each inner array holds all values for one column. For grouped queries, the first column contains the group values; remaining columns hold aggregated measures.

Row grouping with expand and collapse

When groupBy is non-empty, the model displays group rows that can be expanded to reveal children. Use expand() and collapse() to control which groups are open:

// Initial fetch - shows top-level group rows
const vm = await model.getViewModelData({
  startRow: 0,
  endRow: 100,
  groupBy: ["country", "state"],
  project: ["country", "state", "revenue"],
  sort: [],
  filter: [],
  groupPath: [],
});

// Expand the "USA" group - fetches its children (states)
const expanded = await model.expand(["USA"]);

// Expand "California" under "USA" - fetches leaf rows
const deeper = await model.expand(["USA", "California"]);

// Collapse "USA" - hides children but keeps them cached
const collapsed = await model.collapse(["USA"]);

// Re-expanding "USA" is instant (cache hit, no getData call)
const reexpanded = await model.expand(["USA"]);

Key behaviors:

  • expand(groupPath) fetches child data and marks the group as expanded
  • collapse(groupPath) marks the group as collapsed but keeps child data in cache
  • Re-expanding a collapsed group is a cache hit - no network call
  • Both methods return the updated FlattenedDataViewModelParams

Page cache and eviction

The model maintains a tree of PageNode objects. Each page holds a fixed number of rows (controlled by pageSize). Pages start with data: null and are filled on demand.

When the number of fetched pages exceeds maxNumPageBeforeEviction, the model evicts the page farthest from the current scroll position. Eviction nulls out the page's data and recursively evicts all child pages under expanded rows in that page.

Updating configuration

Call setConfig() to change pagination or cache settings at runtime. This resets the page cache:

model.setConfig({ pageSize: 5000, maxNumPageBeforeEviction: 100 });

Querying expanded state

Use getExpandedPaths() to get all currently expanded group paths:

const paths = model.getExpandedPaths();
// e.g. [["USA"], ["USA", "California"], ["Germany"]]

API Reference

Extends DataModel.

Prop

Type

configobject

Resolved configuration after merging user-provided values with defaults. See StandardTableConfig.

Type

StandardTableConfig

pagesarray

The page cache tree. Each PageNode holds a fixed slice of rows; data is null until fetched and populated on demand as the user scrolls. When groupBy is active, expanding a group row creates a nested subtree of child pages under that row's expandedRows entry - so the cache forms a tree mirroring the group hierarchy. Collapsing hides children but keeps them cached. Pages are evicted (furthest from the current scroll position first) when the total fetched page count exceeds maxNumPageBeforeEviction.

Type

PageNode[]

columnMetadata?array

Type

StandardColumnMetadata[] | undefined

getDatafunction

Subclasses must implement this to fetch rows from the data source. The StandardDataFetchAndTransformIR (intermediate representation) drives data fetching and transformation at the source - the subclass converts it into a command for its backend. For example, SqlStandardTableDataModel generates SQL from the IR. Returns a GetRowsResponse.

Type

(ir: StandardDataFetchAndTransformIR, metadataResolver?: StandardMetadataResolver<unknown>) => Promise<GetRowsResponse>

getRangeOfColumnfunction

Return the value range for a column. For dimensions (non-temporal), returns all distinct values. For measures and temporal dimensions, returns min/max. Used to populate filter UIs. Returns a ColumnRangeValues.

Type

(field: string) => Promise<ColumnRangeValues>

setConfigfunction

Replace the config and reset the page cache. Accepts a partial config; omitted fields use defaults.

Type

(config: Partial<StandardTableConfig>) => void

expandfunction

Expand a group row to reveal its children. Requires groupBy to be set in the IR. Expansion is progressive - each call drills one level deeper into the hierarchy. For example, with groupBy: ["country", "state", "city"]: expand(["USA"]) reveals states, then expand(["USA", "California"]) reveals cities. Fetches child data on first expand; re-expanding a collapsed group is a cache hit. Returns updated FlattenedDataViewModelParams.

Type

(groupPath: string[]) => Promise<FlattenedDataViewModelParams>

Parameters

groupPath -

Values identifying the group to expand (e.g. ["USA", "California"]).

collapsefunction

Collapse a group row. Requires groupBy to be set in the IR. Hides the children of the specified group but retains their data in cache, so re-expanding is instant without a getData call. Returns updated FlattenedDataViewModelParams.

Type

(groupPath: string[]) => Promise<FlattenedDataViewModelParams>

Parameters

groupPath -

Values identifying the group to collapse (e.g. ["USA"]).

getExpandedPathsfunction

Returns the current expand state - all group paths that are currently expanded after any sequence of expand/collapse calls (e.g. [["USA"], ["USA", "California"]]).

Type

() => string[][]

computeTotalLogicalRowsfunction

Returns the total number of visible logical rows. Although pages are nested in a tree structure, the data is laid out flat for rendering. Each expanded group's expanded flag determines whether its nested children count toward the total. This walks the entire page tree and sums rows from all levels where expanded is true.

Type

() => number

getViewModelDatafunction

Main entry point for fetching data. Takes a StandardDataFetchAndTransformIR describing the desired row range, grouping, projection, sort, and filter. Resets the page cache if the IR changed (groupBy, filter, project, or sort), fetches any missing pages in the requested range via getData, evicts distant pages if over the cache limit, then flattens the page tree into FlattenedDataViewModelParams for the renderer.

Type

(ir: StandardDataFetchAndTransformIR) => Promise<FlattenedDataViewModelParams>

Parameters

input -

The config or query describing what data to fetch

On this page