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.
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| Class | What it does |
|---|---|
DataModel | Abstract base. Holds the schema and exposes getViewModelData. |
PivotTableDataModel | Reshapes flat query results into a 2D pivot grid with row/column facets. |
StandardTableDataModel | Produces flat rows with optional row grouping, expand/collapse, and pagination. |
SqlPivotTableDataModel | Extends PivotTableDataModel with SQL generation for SQL-backed data sources. |
SqlStandardTableDataModel | Extends 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:
- Create a data source.
const ds = await DuckDBWasmDataSource.create();
await ds.loadData({ schema, data });- Create a data model, passing the schema and data source.
const model = new SqlPivotTableDataModel(schema, ds);- Call
getViewModelDatawith 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);- 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:
- Define
TInput- what the caller passes in (the config describing what data to produce) - Define
TViewModelData- what the renderer expects back - 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
schemaarrayDataSchema column definitions describing name, type, display name, and aggregation for each field. Set once at construction and read-only.
Type
DataSchema[]
schemaIndexobjectType
Map<string, number>
getColumnfunctionLook 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.
getViewModelDatafunctionTransform (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