DataModel

Fetches raw data from a DataSource and transforms it into a structure the renderer can display.

DataModel<TInput, TViewModelData> is the abstract base class that sits between a data source and the renderer. Subclasses fetch raw data from the data source and transform it into a structure the renderer can display directly.

Outline
abstract class DataModel<TInput, TViewModelData> {
  protected schema: DataSchema[];
  protected schemaIndex: Map<string, number>;
  constructor(schema: DataSchema[]);
  protected getColumn(name: string): DataSchema;
  abstract getViewModelData(input: TInput): Promise<TViewModelData>;
}

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";
  • TInput - the input that describes what data to produce (e.g. a pivot config, a row range query)
  • TViewModelData - the output shape the renderer expects

Example:

A pivot data model takes a pivot config and returns reshaped pivot data:

const viewModelData = await pivotModel.getViewModelData({
  rows: "region",
  columns: cross("department", "revenue"),
});

A standard table data model takes a row query and returns flat rows:

const viewModelData = await flatModel.getViewModelData({
  startRow: 0,
  endRow: 100,
  groupBy: ["region", "country"],
  project: ["revenue", "cost"],
  sort: [],
  filter: [],
  groupPath: [],
});

Responsibilities

A data model does two things:

  • Decides which command to send to the data source to fetch the data
    • A WASM SQL implementation prepares a SQL query that the data source executes to transform (groupby, select, project, window etc) fetch and transform the data
    • An API implementation sends a payload JSON object to the server, which handles the transformation and returns the rows
  • Reshapes the result (transforms the output if required) so that it can be rendered

The data source executes the command. The data model decides what command to run and what to do with the result.

Built-in implementations

DataModel<TInput, TViewModelData>
|-- PivotTableDataModel
|   |-- SqlPivotTableDataModel
|-- StandardTableDataModel
    |-- SqlStandardTableDataModel
ClassWhat it does
DataModelAbstract base. Holds the schema and exposes getViewModelData.
PivotTableDataModelReshapes flat query results into a 2D pivot grid with row/column facets.
StandardTableDataModelProduces flat rows with optional row grouping, expand/collapse, and pagination.
SqlPivotTableDataModelExtends PivotTableDataModel with SQL generation for SQL-backed data sources.
SqlStandardTableDataModelExtends StandardTableDataModel with SQL generation for SQL-backed data sources.

Usage

DataModel is abstract and cannot be instantiated directly. The library provides built-in implementations for the most common data modelling patterns - PivotTableDataModel and StandardTableDataModel.

Most use cases can be achieved by using or extending those classes. If you need a completely new data modelling layer with no overlap with the built-in implementations, extend DataModel directly.

Typical flow:

  1. Create a data source.
const ds = await DuckDBWasmDataSource.create();
await ds.loadData({ schema, data });
  1. Create a data model, passing the schema and data source.
const model = new SqlPivotTableDataModel(schema, ds);
  1. Call getViewModelData with a config or query. The data model builds a command, sends it to the data source, and returns view model data.
const viewModelData = await model.getViewModelData(pivotConfig);
  1. Pass the view model data to a view model, which the renderer consumes.

Implementing your own DataModel

If the built-in data models don't fit your use case, extend DataModel directly. You need to:

  1. Define TInput - what the caller passes in (the config describing what data to produce)
  2. Define TViewModelData - what the renderer expects back
  3. Implement getViewModelData - build a command, send it to the data source, reshape the result

Example: a Kanban board data model backed by SQL. Columns are statuses, rows are swimlanes (e.g. priority). Each cell holds an array of task cards.

import { DataModel } from "grid";
import { SqlDataSource } from "grid";
import { DataSchema } from "grid";

type KanbanInput = {
  statusField: string;
  swimlaneField: string;
  cardFields: string[];
};

type KanbanCard = Record<string, any>;

type KanbanViewModelData = {
  statuses: string[];
  swimlanes: string[];
  cells: Map<string, KanbanCard[]>; // key: "swimlane::status"
};

class WasmSqlKanbanDataModel extends DataModel<KanbanInput, KanbanViewModelData> {
  private dataSource: SqlDataSource;

  constructor(schema: DataSchema[], dataSource: SqlDataSource) {
    super(schema);
    this.dataSource = dataSource;
  }

  async getViewModelData(input: KanbanInput): Promise<KanbanViewModelData> {
    const { statusField, swimlaneField, cardFields } = input;

    // Validate all fields against the schema (throws if not found)
    const allFields = [statusField, swimlaneField, ...cardFields];
    const quoted = allFields.map((f) => `"${this.getColumn(f).name}"`);

    // 1. Build the SQL command
    const sql = `SELECT ${quoted.join(", ")} FROM ${this.dataSource.table} ORDER BY "${statusField}", "${swimlaneField}"`;

    // 2. Send to data source
    const rows = await this.dataSource.execute(sql);

    // 3. Reshape into view model data
    const statuses = [...new Set(rows.map((r) => r[statusField]))];
    const swimlanes = [...new Set(rows.map((r) => r[swimlaneField]))];
    const cells = new Map<string, KanbanCard[]>();

    for (const row of rows) {
      const key = `${row[swimlaneField]}::${row[statusField]}`;
      if (!cells.has(key)) cells.set(key, []);
      cells.get(key)!.push(row);
    }

    return { statuses, swimlanes, cells };
  }
}

Usage:

const model = new WasmSqlKanbanDataModel(schema, dataSource);
const board = await model.getViewModelData({
  statusField: "status",
  swimlaneField: "priority",
  cardFields: ["title", "assignee", "due_date"],
});
// board.statuses  → ["todo", "in_progress", "done"]
// board.swimlanes → ["high", "medium", "low"]
// board.cells.get("high::todo") → [{ title: "Fix bug", ... }]

API Reference

Abstract base class that fetches raw data from a DataSource and transforms it into a structure the renderer can display.

Subclasses implement getViewModelData to build a command, send it to the data source, and reshape the result.

Prop

Type

schemaarray

DataSchema column definitions describing name, type, display name, and aggregation for each field. Set once at construction and read-only.

Type

DataSchema[]

schemaIndexobject

Type

Map<string, number>

getColumnfunction

Look up a column definition by name. Throws if the column does not exist in the schema.

Type

(name: string) => DataSchema

Parameters

name -

The column name to look up.

getViewModelDatafunction

Transform (if supported) and fetch data from the data source based on input. Transform the data in memory and return the result that's ready for the renderer to consume.

For SQL-backed sources, you can transform data at the source (sort, group, window, project, select, etc), get the result in JS memory, and reshape if necessary for renderer consumption.

For other sources like directly loading a CSV file, download the data and transform the whole data in memory.

Type

(input: TInput) => Promise<TViewModelData>

Parameters

input -

The config or query describing what data to fetch

On this page