Building a dashboard from scratch uses every feature an interactive, multi-page dashboard needs.
Overview Copy Link
This tutorial covers the following:
- Loading external JSON data into Studio
- Configuring data with fields, relationships and expressions
- Controlling view and edit modes via the UI
- Handling state changes, and pre-configuring reports
- Setting up and navigating between multiple pages
Once complete, you'll have an interactive, multi-page analytics dashboard with pre-built widgets, backed by multiple data sources with custom fields and relationships. Try it out for yourself by switching pages, toggling between view and edit mode, and editing reports:
import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgReportState,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
// =============================================================================
// 1. Loading External Data
// =============================================================================
// Fetch JSON files from the server. The files contain native JSON types -
// numbers are numbers and booleans are booleans - so no extra parsing is needed.
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
// =============================================================================
// 2. Field Definitions
// =============================================================================
// Field definitions tell Studio how to display and aggregate each column.
// Each field has an `id` matching a property in the row data, a display `name`,
// and a `format` that controls rendering (e.g. text, number, currency, boolean).
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
// Test Your Knowledge #1: Customer field definitions
const customersFields: AgFieldDefinition[] = [
{ id: "customer_id", name: "Customer ID", format: "textFormat" },
{ id: "customer_name", name: "Customer", format: "textFormat" },
{ id: "region", name: "Region", format: "textFormat" },
{ id: "segment", name: "Segment", format: "textFormat" },
{ id: "industry", name: "Industry", format: "textFormat" },
];
// Test Your Knowledge #2: Order field definitions
const ordersFields: AgFieldDefinition[] = [
{
id: "order_id",
name: "Order ID",
format: "textFormat",
cardinality: "high",
notBlank: true,
},
{ id: "customer_id", name: "Customer ID", format: "textFormat" },
{ id: "order_datetime", name: "Order Date/Time", format: "dateTimeFormat" },
{ id: "channel", name: "Channel", format: "textFormat" },
{ id: "status", name: "Status", format: "textFormat" },
{ id: "payment_method", name: "Payment Method", format: "textFormat" },
{ id: "currency", name: "Currency", format: "textFormat" },
];
// =============================================================================
// 3. Expressions (Calculated Fields)
// =============================================================================
// Expressions create derived columns computed at query time. They use operators
// like multiply, subtract, and divide, referencing fields via `sourceId.fieldId`.
const expressions: AgExpressionFieldDefinition[] = [
// line_gross = quantity × unit_price
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
// margin = list_price - unit_price (crosses tables via the relationship)
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
// Test Your Knowledge #3: line_net = (quantity × unit_price) - ((quantity × unit_price) × discount_pct)
{
id: "line_net",
name: "Line Net",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [
// line_gross: quantity × unit_price
{
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
// discount_amount: (quantity × unit_price) × discount_pct
{
operator: "multiply",
inputs: [
{
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
{ id: "order_items.discount_pct" },
],
},
],
},
},
];
// =============================================================================
// 4. Initial State (Pre-built Reports)
// =============================================================================
// The state is a serialisable snapshot of the entire dashboard. Define it in
// code to pre-build reports users see on load. Each page has widgets, a layout
// grid, and optional filters.
const initialState: AgReportState = {
pages: [
// -----------------------------------------------------------------
// Page 1: Overview - KPIs and charts built from products + order items
// -----------------------------------------------------------------
{
id: "overview",
widgets: {
"kpi-gross-sales": {
type: "value",
dataMapping: { value: [{ id: "line_gross", aggregation: "sum" }] },
format: { caption: { enabled: true, text: "Gross Sales" } },
},
"kpi-order-count": {
type: "value",
dataMapping: {
value: [{ id: "orders.order_id", aggregation: "countd" }],
},
format: { caption: { enabled: true, text: "Order Count" } },
},
"kpi-avg-qty": {
type: "value",
dataMapping: {
value: [{ id: "order_items.quantity", aggregation: "avg" }],
},
format: { caption: { enabled: true, text: "Avg Qty per Line" } },
},
// Bar chart: Gross Sales by product subcategory
"sales-by-category": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.subcategory" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: {
title: { enabled: true, text: "Gross Sales by Subcategory" },
},
},
// Bar chart: Gross Sales by brand
"sales-by-brand": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.brand" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Gross Sales by Brand" } },
},
// Grid: top products by gross sales
"top-products": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.category" },
{ id: "order_items.quantity", aggregation: "sum" },
{ id: "line_gross", aggregation: "sum" },
{ id: "margin", aggregation: "avg" },
],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Top Products" } },
},
},
widgetLayout: {
"kpi-gross-sales": { xTrack: 0, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-order-count": { xTrack: 8, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-avg-qty": { xTrack: 16, yTrack: 0, xSpan: 8, ySpan: 6 },
"sales-by-category": { xTrack: 0, yTrack: 6, xSpan: 12, ySpan: 16 },
"sales-by-brand": { xTrack: 12, yTrack: 6, xSpan: 12, ySpan: 16 },
"top-products": { xTrack: 0, yTrack: 22, xSpan: 24, ySpan: 16 },
},
},
// -----------------------------------------------------------------
// Page 2: Detail - subcategory filter driving a data grid
// -----------------------------------------------------------------
{
id: "detail",
widgets: {
"subcategory-filter": {
type: "list-filter",
dataMapping: {
value: [{ id: "products.subcategory" }],
},
format: {
title: { enabled: true, text: "Subcategory" },
},
},
"order-grid": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.subcategory" },
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
{ id: "line_gross" },
{ id: "line_net" },
],
},
format: {
title: { enabled: true, text: "Order Details" },
},
},
},
widgetLayout: {
"subcategory-filter": { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 32 },
"order-grid": { xTrack: 6, yTrack: 0, xSpan: 18, ySpan: 32 },
},
},
// -----------------------------------------------------------------
// Test Your Knowledge #4: Customers page
// KPI for unique customers, bar chart of revenue by region, and a
// customer detail grid.
// -----------------------------------------------------------------
{
id: "customers",
widgets: {
"kpi-unique-customers": {
type: "value",
dataMapping: {
value: [{ id: "customers.customer_id", aggregation: "countd" }],
},
format: { caption: { enabled: true, text: "Unique Customers" } },
},
"revenue-by-region": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "customers.region" }],
valueKey: [{ id: "line_net", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_net", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Revenue by Region" } },
},
"customer-grid": {
type: "grid",
dataMapping: {
cols: [
{ id: "customers.customer_name" },
{ id: "customers.region" },
{ id: "customers.segment" },
{ id: "customers.industry" },
{ id: "orders.order_id", aggregation: "countd" },
{ id: "line_net", aggregation: "sum" },
],
},
sort: [
{
field: { id: "line_net", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Customer Details" } },
},
},
widgetLayout: {
"kpi-unique-customers": { xTrack: 0, yTrack: 0, xSpan: 24, ySpan: 6 },
"revenue-by-region": { xTrack: 0, yTrack: 6, xSpan: 24, ySpan: 16 },
"customer-grid": { xTrack: 0, yTrack: 22, xSpan: 24, ySpan: 16 },
},
},
],
selectedPageId: "overview",
panels: {
filters: {
collapsed: true,
},
},
};
// =============================================================================
// 5. Controlling Modes & Navigating Pages
// =============================================================================
// The Studio API lets you toggle between edit and view mode at runtime, and
// navigate between pages by updating the selectedPageId in the state.
let studioApi: AgStudioApi;
// Switch the currently visible page
function selectPage(pageId: string) {
const state = studioApi.getState();
studioApi.setState({
...state,
selectedPageId: pageId,
});
}
// Toggle between edit (design-time) and view (presentation) mode
function toggleMode() {
const currentMode = studioApi.getProperty("mode");
studioApi.setProperty("mode", currentMode === "edit" ? "view" : "edit");
}
// Expose to HTML onclick handlers
(window as any).selectPage = selectPage;
(window as any).toggleMode = toggleMode;
// =============================================================================
// 6. Create the Studio
// =============================================================================
const studioProperties: AgStudioProperties = {
mode: "edit",
initialState,
// Listen for state changes - useful for auto-saving or debugging
onStateUpdated: (event) => {
console.log("State updated:", event.state);
},
onApiReady: (params) => {
Promise.all([
loadJson("products.json"),
loadJson("order_items.json"),
loadJson("customers.json"),
loadJson("orders.json"),
]).then(([productData, orderItemData, customerData, orderData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
{
id: "customers",
name: "Customers",
data: customerData,
fields: customersFields,
},
{
id: "orders",
name: "Orders",
data: orderData,
fields: ordersFields,
},
],
// Relationships link data sources together for cross-table queries
relationships: [
// Each order item refers to exactly one product
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
// Test Your Knowledge #2: Link order items → orders → customers
{
id: "order-item-order",
source: { tableId: "order_items", fieldId: "order_id" },
target: { tableId: "orders", fieldId: "order_id" },
type: "many-to-one",
},
{
id: "order-customer",
source: { tableId: "orders", fieldId: "customer_id" },
target: { tableId: "customers", fieldId: "customer_id" },
type: "many-to-one",
},
],
// Calculated fields available across the dashboard
expressions,
});
});
},
};
// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).selectPage = selectPage;
(<any>window).toggleMode = toggleMode;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div class="example-controls">
<div class="controls-row" style="display: flex; justify-content: space-between">
<div style="display: flex; gap: 8px">
<button onclick="selectPage('overview')">Overview</button>
<button onclick="selectPage('detail')">Detail</button>
<button onclick="selectPage('customers')">Customers</button>
</div>
<button onclick="toggleMode()">Toggle Edit Mode</button>
</div>
</div>
<div id="myStudio" class="my-studio-container"></div>
</div>
This tutorial assumes you have completed the Quick Start and have AG Studio installed.
Create an Empty Dashboard Copy Link
Complete our Quick Start (or open the example below in CodeSandbox / Plunker) to start with a basic instance of AG Studio:
import {
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
const productData = [
{
product_name: "Printer",
category: "Printing & Imaging",
brand: "CleanSlate Office",
list_price: 109.09,
unit_cost: 92.26,
},
{
product_name: "Notebook",
category: "Paper & Notebooks",
brand: "PaperLine",
list_price: 24.7,
unit_cost: 14.19,
},
{
product_name: "Highlighters",
category: "Writing Instruments",
brand: "PaperLine",
list_price: 10.34,
unit_cost: 5.03,
},
];
const data = {
sources: [{ id: "products", data: productData }],
};
const studioProperties: AgStudioProperties = {
data,
mode: "edit",
};
let studioApi: AgStudioApi;
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
<div style="display: flex; flex-direction: column; height: 100%">
<div id="myStudio" class="my-studio-container"></div>
</div>
Loading External Data Copy Link
The Quick Start example loads data from local, hardcoded arrays. Let's update the code to pull in some larger JSON files: products.json & order_items.json, which will give us enough data to build a more complex dashboard:
async function loadJson(filename: string): Promise<any[]> {
const url = `https://www.ag-grid.com/studio/example-assets/main-demo/${filename}`;
const response = await fetch(url);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productData = await loadJson('products.json');
const orderItemData = await loadJson('order_items.json');Once we have the data, we can update the sources array to include each of the new data sources:
const studioProperties = {
data: {
sources: [
{ id: 'products', name: 'Products', data: productData },
{ id: 'order_items', name: 'Order Items', data: orderItemData },
],
},
// other studio properties ...
} Data Source Fields Copy Link
When providing data sources to AG Studio synchronously, certain information about the fields within the data source will be automatically inferred, including their format (e.g. text, number, boolean, etc.).
You can override these defaults, and provide more detailed information about the fields within the data source, by providing a fields array alongside the data. Each field has an id that matches a property in the row data, a display name, and a format that tells Studio how to display and aggregate the values:
// Sample of productsFields configuration
const productsFields: AgFieldDefinition[] = [
{
id: 'product_id',
name: 'Product ID',
format: 'textFormat',
},
/* ... */
];
// Sample of orderItemsFields configuration
const orderItemsFields: AgFieldDefinition[] = [
{
id: 'order_item_id',
name: 'Order Item ID',
format: 'textFormat',
},
/* ... */
]Click the Code button in the example below for the full field definitions for each data source
The field definitions are then applied to their respective data source via the fields property:
const studioProperties = {
data: {
sources: [
{
id: 'products',
name: 'Products',
data: productData,
fields: productsFields
},
{
id: 'order_items',
name: 'Order Items',
data: orderItemData,
fields: orderItemsFields
},
],
},
// other studio properties ...
}We should now see Studio loaded with two data sources. Open the data panel on the right to browse the available fields from both tables, and try dragging fields onto the canvas to create widgets.
import {
AgFieldDefinition,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
let studioApi: AgStudioApi;
const studioProperties: AgStudioProperties = {
mode: "edit",
onApiReady: (params) => {
Promise.all([loadJson("products.json"), loadJson("order_items.json")]).then(
([productData, orderItemData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
],
});
},
);
},
};
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
<div style="display: flex; flex-direction: column; height: 100%">
<div id="myStudio" class="my-studio-container"></div>
</div>
Configuring Data Copy Link
So far we have two independent data sources. Studio treats each source as a standalone table, meaning widgets can use fields from one source, but not from both at the same time. To unlock cross-table queries, we need two more concepts: Relationships and Expressions.
Relationships Copy Link
A Relationship tells Studio how two data sources are connected. The order_items table has a product_id column that maps to products.product_id - each order item refers to exactly one product. Let's define this by adding a relationships array to the data configuration:
const studioProperties = {
data: {
sources: [/* ... */],
relationships: [
{
id: 'order-item-product',
source: { tableId: 'order_items', fieldId: 'product_id' },
target: { tableId: 'products', fieldId: 'product_id' },
type: 'many-to-one',
},
],
},
// other studio properties ...
}The type: 'many-to-one' tells Studio that many order items map to one product. With this relationship in place, a chart can now use products.category as its axis while aggregating order_items.quantity as the values - something that wasn't possible with independent sources.
Calculated Fields Copy Link
Expressions create derived columns that are computed at query time. They are defined in the expressions array alongside sources and relationships, and appear in the Studio UI just like regular fields.
Each expression has an operator (such as multiply, subtract, add, divide) and an array of inputs that reference fields using the format sourceId.fieldId.
Let's add two calculated fields:
line_grossmultipliesquantitybyunit_pricefrom the same table.marginsubtractsunit_pricefromlist_price. This crosses tables, which works because the relationship links order items to their products.
const studioProperties = {
data: {
sources: [/* ... */],
relationships: [/* ... */],
expressions: [
{
id: 'line_gross',
name: 'Line Gross',
isMeasure: false,
format: 'currencyFormat',
expression: {
operator: 'multiply',
inputs: [
{ id: 'order_items.quantity' },
{ id: 'order_items.unit_price' },
],
},
},
{
id: 'margin',
name: 'Margin',
isMeasure: false,
format: 'currencyFormat',
expression: {
operator: 'subtract',
inputs: [
{ id: 'products.list_price' },
{ id: 'order_items.unit_price' },
],
},
},
],
},
// other studio properties ...
}These fields should now be available within AG Studio. They behave exactly like standard fields and can be used in any widget.
Formatting Expressions Copy Link
An expression's display can be customised with a format string passed via options.format. Format strings follow Excel's number-format syntax, with conditional sections that select a pattern based on the value's magnitude.
The line_gross expression below uses conditional sections to compact large values to K:
const expressions: AgExpressionFieldDefinition[] = [
{
id: 'line_gross',
name: 'Line Gross',
format: 'currencyFormat',
formatOptions: { format: '$#,##0.0,K' },
expression: { /* ... */ },
},
/* ... */
];This works on any field or expression. See Formatting for the full format-string syntax.
Try dragging Line Gross or Margin onto the canvas to see them in action:
import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
// Compact currency formatter for aggregated values
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
// Calculated fields: line_gross and margin
const expressions: AgExpressionFieldDefinition[] = [
// line_gross = quantity × unit_price
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
// margin = list_price - unit_price (crosses tables via the relationship)
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
];
let studioApi: AgStudioApi;
const studioProperties: AgStudioProperties = {
mode: "edit",
onApiReady: (params) => {
Promise.all([loadJson("products.json"), loadJson("order_items.json")]).then(
([productData, orderItemData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
],
relationships: [
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
],
expressions,
});
},
);
},
};
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
<div style="display: flex; flex-direction: column; height: 100%">
<div id="myStudio" class="my-studio-container"></div>
</div>
Refer to the Expressions guide for the full list of operators, conditional logic, and cross-source expressions, and the Data Modelling guide for more on relationship types and chaining.
Controlling Modes Copy Link
AG Studio has two Modes. In edit mode, users can add, remove, resize, and configure widgets. In view mode, the dashboard is locked - users can filter, sort, and explore data, but cannot change the layout.
You set the initial mode when creating Studio:
const studioProperties = {
mode: 'view',
// other studio properties ...
}To switch modes at runtime, update the mode property. For example, you could add a toggle button above Studio:
<div style="display: flex; justify-content: flex-end; padding: 8px;">
<button onclick="toggleMode()">Toggle Edit Mode</button>
</div>
<div id="myStudio" style="height: 100%; width: 100%"></div>let studioApi;
studioApi = createStudio(document.getElementById('myStudio')!, {
data,
mode: 'view',
});
function toggleMode() {
const currentMode = studioApi.getProperty('mode');
studioApi.setProperty('mode', currentMode === 'edit' ? 'view' : 'edit');
}Try toggling between the modes. In edit mode you'll see the data panel and editing controls appear, allowing you to add and configure widgets. In view mode, these panels are hidden and the layout is locked:
import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
const expressions: AgExpressionFieldDefinition[] = [
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
];
let studioApi: AgStudioApi;
// Toggle between edit and view mode
function toggleMode() {
const currentMode = studioApi.getProperty("mode");
const newMode = currentMode === "edit" ? "view" : "edit";
studioApi.setProperty("mode", newMode);
document.getElementById("toggleMode")!.textContent =
`Switch to ${newMode === "edit" ? "View" : "Edit"} Mode`;
}
(window as any).toggleMode = toggleMode;
const studioProperties: AgStudioProperties = {
mode: "edit",
onApiReady: (params) => {
Promise.all([loadJson("products.json"), loadJson("order_items.json")]).then(
([productData, orderItemData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
],
relationships: [
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
],
expressions,
});
},
);
},
};
// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).toggleMode = toggleMode;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div class="example-controls">
<div class="controls-row" style="display: flex; justify-content: flex-end">
<button id="toggleMode" onclick="toggleMode()">Switch to View Mode</button>
</div>
</div>
<div id="myStudio" class="my-studio-container"></div>
</div>
Managing State Copy Link
State management is a key feature within AG Studio, with two main functionalities: Defining an initial state (e.g. pre-built reports), and updating an existing state (e.g. building or editing reports).
State Object Copy Link
The Studio's state is a complete, serialisable snapshot of the entire dashboard:
{
pages: [
{
id: '...',
widgets: { /* ... */ },
widgetLayout: { /* ... */ },
filter: { /* ... */ }, // optional
},
],
panels: { /* ... */ },
selectedPageId: '...',
}The state object contains (amongst other things):
pages- an array of page objects, each containing:widgets- the widgets displayed on the page, including their data mappingswidgetLayout- where widgets are positioned within the canvasfilter- optional page-level or widget-level filters
panels- the current state of the sidebar panels (collapsed, width, etc.).selectedPageId- theidof the currently visible page.
Because the state is plain JSON, you can serialise it with JSON.stringify(), store it in a database or localStorage, and restore it later to reload the dashboard exactly as it was.
See State for the full API reference.
Listening for State Changes Copy Link
Every time the dashboard state changes, e.g. when a widget is moved, a filter is applied, or a page is switched, AG Studio fires a stateUpdated event.
You can listen for this event by providing an onStateUpdated callback:
const studioApi = createStudio(document.getElementById('myStudio')!, {
data,
onStateUpdated: (event) => {
console.log('State updated:', event.state);
},
});This is useful for auto-saving, syncing state to a backend, or simply inspecting what the state object looks like as you interact with the dashboard.
import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
const expressions: AgExpressionFieldDefinition[] = [
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
];
let studioApi: AgStudioApi;
function toggleMode() {
const currentMode = studioApi.getProperty("mode");
const newMode = currentMode === "edit" ? "view" : "edit";
studioApi.setProperty("mode", newMode);
document.getElementById("toggleMode")!.textContent =
`Switch to ${newMode === "edit" ? "View" : "Edit"} Mode`;
}
(window as any).toggleMode = toggleMode;
const studioProperties: AgStudioProperties = {
mode: "edit",
onStateUpdated: (event) => {
console.log("State updated:", event.state);
},
onApiReady: (params) => {
Promise.all([loadJson("products.json"), loadJson("order_items.json")]).then(
([productData, orderItemData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
],
relationships: [
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
],
expressions,
});
},
);
},
};
// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).toggleMode = toggleMode;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div class="example-controls">
<div class="controls-row" style="display: flex; justify-content: flex-end">
<button id="toggleMode" onclick="toggleMode()">Switch to View Mode</button>
</div>
</div>
<div id="myStudio" class="my-studio-container"></div>
</div>
Defining an Initial State Copy Link
Now that you can see the state object in the console, let's use it to pre-build a report. The initialState property accepts an AgReportState object that defines the dashboard layout on load.
The easiest way to build an initial state is to:
- Design a dashboard in edit mode,
- Copy the state from the console log (from the
onStateUpdatedcallback you just added), - Paste it into your code as the
initialStatevalue.
Alternatively, view the code in the example below to see the pre-configured state of the report in the example:
import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgReportState,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
const expressions: AgExpressionFieldDefinition[] = [
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
];
// Pre-built report: overview page with KPIs, charts, and a grid
const initialState: AgReportState = {
pages: [
{
id: "overview",
widgets: {
"kpi-gross-sales": {
type: "value",
dataMapping: { value: [{ id: "line_gross", aggregation: "sum" }] },
format: { caption: { enabled: true, text: "Gross Sales" } },
},
"kpi-avg-margin": {
type: "value",
dataMapping: { value: [{ id: "margin", aggregation: "avg" }] },
format: { caption: { enabled: true, text: "Avg Margin" } },
},
"kpi-avg-qty": {
type: "value",
dataMapping: {
value: [{ id: "order_items.quantity", aggregation: "avg" }],
},
format: { caption: { enabled: true, text: "Avg Qty per Line" } },
},
"sales-by-category": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.subcategory" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: {
title: { enabled: true, text: "Gross Sales by Subcategory" },
},
},
"sales-by-brand": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.brand" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Gross Sales by Brand" } },
},
"top-products": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.category" },
{ id: "order_items.quantity", aggregation: "sum" },
{ id: "line_gross", aggregation: "sum" },
{ id: "margin", aggregation: "avg" },
],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Top Products" } },
},
},
widgetLayout: {
"kpi-gross-sales": { xTrack: 0, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-avg-margin": { xTrack: 8, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-avg-qty": { xTrack: 16, yTrack: 0, xSpan: 8, ySpan: 6 },
"sales-by-category": { xTrack: 0, yTrack: 6, xSpan: 12, ySpan: 16 },
"sales-by-brand": { xTrack: 12, yTrack: 6, xSpan: 12, ySpan: 16 },
"top-products": { xTrack: 0, yTrack: 22, xSpan: 24, ySpan: 16 },
},
},
],
selectedPageId: "overview",
panels: {
filters: {
collapsed: true,
},
},
};
let studioApi: AgStudioApi;
function toggleMode() {
const currentMode = studioApi.getProperty("mode");
const newMode = currentMode === "edit" ? "view" : "edit";
studioApi.setProperty("mode", newMode);
document.getElementById("toggleMode")!.textContent =
`Switch to ${newMode === "edit" ? "View" : "Edit"} Mode`;
}
(window as any).toggleMode = toggleMode;
const studioProperties: AgStudioProperties = {
mode: "edit",
initialState,
onStateUpdated: (event) => {
console.log("State updated:", event.state);
},
onApiReady: (params) => {
Promise.all([loadJson("products.json"), loadJson("order_items.json")]).then(
([productData, orderItemData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
],
relationships: [
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
],
expressions,
});
},
);
},
};
// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).toggleMode = toggleMode;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div class="example-controls">
<div class="controls-row" style="display: flex; justify-content: flex-end">
<button id="toggleMode" onclick="toggleMode()">Switch to View Mode</button>
</div>
</div>
<div id="myStudio" class="my-studio-container"></div>
</div>
Navigating Between Pages Copy Link
A dashboard can contain multiple Pages - think of them as tabs, each with its own set of widgets, layout, and filters. You define pages in the initialState.pages array, and control which page is currently displayed via the selectedPageId property.
Let's add a second page to our dashboard: a detail page with a subcategory filter driving a data grid:
// Add this as a second entry in the pages array
{
id: 'detail',
widgets: {
'subcategory-filter': {
type: 'list-filter',
dataMapping: {
value: [{ id: 'products.subcategory' }],
},
format: {
title: { enabled: true, text: 'Subcategory' },
},
},
'order-grid': {
type: 'grid',
dataMapping: {
cols: [
{ id: 'products.product_name' },
{ id: 'products.subcategory' },
{ id: 'order_items.quantity' },
{ id: 'order_items.unit_price' },
{ id: 'margin' },
{ id: 'line_gross' },
],
},
format: {
title: { enabled: true, text: 'Order Details' },
},
},
},
widgetLayout: {
'subcategory-filter': { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 32 },
'order-grid': { xTrack: 6, yTrack: 0, xSpan: 18, ySpan: 32 },
},
}To switch between pages at runtime, use getState() and setState() on the Studio API to update the selectedPageId. You can wire this up to navigation buttons in your application UI:
function selectPage(pageId: string) {
const state = studioApi.getState();
studioApi.setState({
...state,
selectedPageId: pageId,
});
}<div style="display: flex; gap: 8px; padding: 8px;">
<button onclick="selectPage('overview')">Overview</button>
<button onclick="selectPage('detail')">Detail</button>
</div>
<div id="myStudio" style="height: 100%; width: 100%"></div>import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgReportState,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
const expressions: AgExpressionFieldDefinition[] = [
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
];
// Two-page report: overview with KPIs/charts, and a detail page with a
// subcategory filter driving a data grid
const initialState: AgReportState = {
pages: [
{
id: "overview",
widgets: {
"kpi-gross-sales": {
type: "value",
dataMapping: { value: [{ id: "line_gross", aggregation: "sum" }] },
format: { caption: { enabled: true, text: "Gross Sales" } },
},
"kpi-avg-margin": {
type: "value",
dataMapping: { value: [{ id: "margin", aggregation: "avg" }] },
format: { caption: { enabled: true, text: "Avg Margin" } },
},
"kpi-avg-qty": {
type: "value",
dataMapping: {
value: [{ id: "order_items.quantity", aggregation: "avg" }],
},
format: { caption: { enabled: true, text: "Avg Qty per Line" } },
},
"sales-by-category": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.subcategory" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: {
title: { enabled: true, text: "Gross Sales by Subcategory" },
},
},
"sales-by-brand": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.brand" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Gross Sales by Brand" } },
},
"top-products": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.category" },
{ id: "order_items.quantity", aggregation: "sum" },
{ id: "line_gross", aggregation: "sum" },
{ id: "margin", aggregation: "avg" },
],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Top Products" } },
},
},
widgetLayout: {
"kpi-gross-sales": { xTrack: 0, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-avg-margin": { xTrack: 8, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-avg-qty": { xTrack: 16, yTrack: 0, xSpan: 8, ySpan: 6 },
"sales-by-category": { xTrack: 0, yTrack: 6, xSpan: 12, ySpan: 16 },
"sales-by-brand": { xTrack: 12, yTrack: 6, xSpan: 12, ySpan: 16 },
"top-products": { xTrack: 0, yTrack: 22, xSpan: 24, ySpan: 16 },
},
},
{
id: "detail",
widgets: {
"subcategory-filter": {
type: "list-filter",
dataMapping: { value: [{ id: "products.subcategory" }] },
format: { title: { enabled: true, text: "Subcategory" } },
},
"order-grid": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.subcategory" },
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
{ id: "margin" },
{ id: "line_gross" },
],
},
format: { title: { enabled: true, text: "Order Details" } },
},
},
widgetLayout: {
"subcategory-filter": { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 32 },
"order-grid": { xTrack: 6, yTrack: 0, xSpan: 18, ySpan: 32 },
},
},
],
selectedPageId: "overview",
panels: {
filters: {
collapsed: true,
},
},
};
let studioApi: AgStudioApi;
function selectPage(pageId: string) {
const state = studioApi.getState();
studioApi.setState({ ...state, selectedPageId: pageId });
}
function toggleMode() {
const currentMode = studioApi.getProperty("mode");
const newMode = currentMode === "edit" ? "view" : "edit";
studioApi.setProperty("mode", newMode);
document.getElementById("toggleMode")!.textContent =
`Switch to ${newMode === "edit" ? "View" : "Edit"} Mode`;
}
(window as any).selectPage = selectPage;
(window as any).toggleMode = toggleMode;
const studioProperties: AgStudioProperties = {
mode: "edit",
initialState,
onStateUpdated: (event) => {
console.log("State updated:", event.state);
},
onApiReady: (params) => {
Promise.all([loadJson("products.json"), loadJson("order_items.json")]).then(
([productData, orderItemData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
],
relationships: [
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
],
expressions,
});
},
);
},
};
// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).selectPage = selectPage;
(<any>window).toggleMode = toggleMode;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div class="example-controls">
<div class="controls-row" style="display: flex; justify-content: space-between">
<div style="display: flex; gap: 8px">
<button onclick="selectPage('overview')">Overview</button>
<button onclick="selectPage('detail')">Detail</button>
</div>
<button id="toggleMode" onclick="toggleMode()">Switch to View Mode</button>
</div>
</div>
<div id="myStudio" class="my-studio-container"></div>
</div>
Test Your Knowledge Copy Link
Put what you've learnt into practice. Using the dashboard you've built so far, try the following challenges:
Add a new data source - Load the
customers.jsonfile alongside products and order items. Define field definitions for the customer fields:customer_id,customer_name,region,segment, andindustry.Define a new relationship - The
orders.jsonfile contains anorder_idcolumn and acustomer_idcolumn. Load the orders data, then add relationships linkingorder_items.order_id→orders.order_idandorders.customer_id→customers.customer_id.Create a new calculated field - Add an expression called
line_netthat computes the net line total after discount:(quantity × unit_price) - ((quantity × unit_price) × discount_pct). This requires nesting operators, refer to theline_grossexpression for the pattern.Add a new page - Create a third page called "Customers" with a KPI showing the total number of unique customers, a bar chart showing revenue by
customers.region, and a grid listing customer details.
Expand the example below to see a completed version with all four challenges implemented:
import {
AgExpressionFieldDefinition,
AgFieldDefinition,
AgReportState,
AgStudioApi,
AgStudioProperties,
createStudio,
enableStudioDevValidations,
} from "ag-studio";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableStudioDevValidations();
}
// =============================================================================
// 1. Loading External Data
// =============================================================================
// Fetch JSON files from the server. The files contain native JSON types -
// numbers are numbers and booleans are booleans - so no extra parsing is needed.
const BASE_URL = "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/main-demo";
async function loadJson(filename: string): Promise<any[]> {
const response = await fetch(`${BASE_URL}/${filename}`);
if (!response.ok) {
console.error(`Failed to load ${filename}: ${response.status}`);
return [];
}
return response.json();
}
// =============================================================================
// 2. Field Definitions
// =============================================================================
// Field definitions tell Studio how to display and aggregate each column.
// Each field has an `id` matching a property in the row data, a display `name`,
// and a `format` that controls rendering (e.g. text, number, currency, boolean).
const productsFields: AgFieldDefinition[] = [
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "product_name", name: "Product", format: "textFormat" },
{ id: "category", name: "Category", format: "textFormat" },
{ id: "subcategory", name: "Subcategory", format: "textFormat" },
{ id: "brand", name: "Brand", format: "textFormat" },
{ id: "launch_date", name: "Launch Date", format: "dateFormat" },
{ id: "list_price", name: "List Price", format: "currencyFormat" },
{ id: "unit_cost", name: "Unit Cost", format: "currencyFormat" },
{ id: "is_discontinued", name: "Discontinued", format: "booleanFormat" },
];
const orderItemsFields: AgFieldDefinition[] = [
{ id: "order_item_id", name: "Order Item ID", format: "textFormat" },
{ id: "order_id", name: "Order ID", format: "textFormat" },
{ id: "product_id", name: "Product ID", format: "textFormat" },
{ id: "quantity", name: "Qty", format: "integerFormat" },
{ id: "unit_price", name: "Unit Price", format: "currencyFormat" },
{ id: "discount_pct", name: "Discount", format: "percentageFormat" },
{ id: "tax_rate", name: "Tax Rate", format: "percentageFormat" },
{ id: "returned", name: "Returned", format: "booleanFormat" },
{ id: "return_reason", name: "Return Reason", format: "textFormat" },
];
// Test Your Knowledge #1: Customer field definitions
const customersFields: AgFieldDefinition[] = [
{ id: "customer_id", name: "Customer ID", format: "textFormat" },
{ id: "customer_name", name: "Customer", format: "textFormat" },
{ id: "region", name: "Region", format: "textFormat" },
{ id: "segment", name: "Segment", format: "textFormat" },
{ id: "industry", name: "Industry", format: "textFormat" },
];
// Test Your Knowledge #2: Order field definitions
const ordersFields: AgFieldDefinition[] = [
{
id: "order_id",
name: "Order ID",
format: "textFormat",
cardinality: "high",
notBlank: true,
},
{ id: "customer_id", name: "Customer ID", format: "textFormat" },
{ id: "order_datetime", name: "Order Date/Time", format: "dateTimeFormat" },
{ id: "channel", name: "Channel", format: "textFormat" },
{ id: "status", name: "Status", format: "textFormat" },
{ id: "payment_method", name: "Payment Method", format: "textFormat" },
{ id: "currency", name: "Currency", format: "textFormat" },
];
// =============================================================================
// 3. Expressions (Calculated Fields)
// =============================================================================
// Expressions create derived columns computed at query time. They use operators
// like multiply, subtract, and divide, referencing fields via `sourceId.fieldId`.
const expressions: AgExpressionFieldDefinition[] = [
// line_gross = quantity × unit_price
{
id: "line_gross",
name: "Line Gross",
isMeasure: false,
format: "currencyFormat",
formatOptions: { format: "$#,##0.0,K" },
expression: {
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
},
// margin = list_price - unit_price (crosses tables via the relationship)
{
id: "margin",
name: "Margin",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [{ id: "products.list_price" }, { id: "order_items.unit_price" }],
},
},
// Test Your Knowledge #3: line_net = (quantity × unit_price) - ((quantity × unit_price) × discount_pct)
{
id: "line_net",
name: "Line Net",
isMeasure: false,
format: "currencyFormat",
expression: {
operator: "subtract",
inputs: [
// line_gross: quantity × unit_price
{
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
// discount_amount: (quantity × unit_price) × discount_pct
{
operator: "multiply",
inputs: [
{
operator: "multiply",
inputs: [
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
],
},
{ id: "order_items.discount_pct" },
],
},
],
},
},
];
// =============================================================================
// 4. Initial State (Pre-built Reports)
// =============================================================================
// The state is a serialisable snapshot of the entire dashboard. Define it in
// code to pre-build reports users see on load. Each page has widgets, a layout
// grid, and optional filters.
const initialState: AgReportState = {
pages: [
// -----------------------------------------------------------------
// Page 1: Overview - KPIs and charts built from products + order items
// -----------------------------------------------------------------
{
id: "overview",
widgets: {
"kpi-gross-sales": {
type: "value",
dataMapping: { value: [{ id: "line_gross", aggregation: "sum" }] },
format: { caption: { enabled: true, text: "Gross Sales" } },
},
"kpi-order-count": {
type: "value",
dataMapping: {
value: [{ id: "orders.order_id", aggregation: "countd" }],
},
format: { caption: { enabled: true, text: "Order Count" } },
},
"kpi-avg-qty": {
type: "value",
dataMapping: {
value: [{ id: "order_items.quantity", aggregation: "avg" }],
},
format: { caption: { enabled: true, text: "Avg Qty per Line" } },
},
// Bar chart: Gross Sales by product subcategory
"sales-by-category": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.subcategory" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: {
title: { enabled: true, text: "Gross Sales by Subcategory" },
},
},
// Bar chart: Gross Sales by brand
"sales-by-brand": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "products.brand" }],
valueKey: [{ id: "line_gross", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Gross Sales by Brand" } },
},
// Grid: top products by gross sales
"top-products": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.category" },
{ id: "order_items.quantity", aggregation: "sum" },
{ id: "line_gross", aggregation: "sum" },
{ id: "margin", aggregation: "avg" },
],
},
sort: [
{
field: { id: "line_gross", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Top Products" } },
},
},
widgetLayout: {
"kpi-gross-sales": { xTrack: 0, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-order-count": { xTrack: 8, yTrack: 0, xSpan: 8, ySpan: 6 },
"kpi-avg-qty": { xTrack: 16, yTrack: 0, xSpan: 8, ySpan: 6 },
"sales-by-category": { xTrack: 0, yTrack: 6, xSpan: 12, ySpan: 16 },
"sales-by-brand": { xTrack: 12, yTrack: 6, xSpan: 12, ySpan: 16 },
"top-products": { xTrack: 0, yTrack: 22, xSpan: 24, ySpan: 16 },
},
},
// -----------------------------------------------------------------
// Page 2: Detail - subcategory filter driving a data grid
// -----------------------------------------------------------------
{
id: "detail",
widgets: {
"subcategory-filter": {
type: "list-filter",
dataMapping: {
value: [{ id: "products.subcategory" }],
},
format: {
title: { enabled: true, text: "Subcategory" },
},
},
"order-grid": {
type: "grid",
dataMapping: {
cols: [
{ id: "products.product_name" },
{ id: "products.subcategory" },
{ id: "order_items.quantity" },
{ id: "order_items.unit_price" },
{ id: "line_gross" },
{ id: "line_net" },
],
},
format: {
title: { enabled: true, text: "Order Details" },
},
},
},
widgetLayout: {
"subcategory-filter": { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 32 },
"order-grid": { xTrack: 6, yTrack: 0, xSpan: 18, ySpan: 32 },
},
},
// -----------------------------------------------------------------
// Test Your Knowledge #4: Customers page
// KPI for unique customers, bar chart of revenue by region, and a
// customer detail grid.
// -----------------------------------------------------------------
{
id: "customers",
widgets: {
"kpi-unique-customers": {
type: "value",
dataMapping: {
value: [{ id: "customers.customer_id", aggregation: "countd" }],
},
format: { caption: { enabled: true, text: "Unique Customers" } },
},
"revenue-by-region": {
type: "bar-chart-grouped",
dataMapping: {
categoryKey: [{ id: "customers.region" }],
valueKey: [{ id: "line_net", aggregation: "sum" }],
},
sort: [
{
field: { id: "line_net", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Revenue by Region" } },
},
"customer-grid": {
type: "grid",
dataMapping: {
cols: [
{ id: "customers.customer_name" },
{ id: "customers.region" },
{ id: "customers.segment" },
{ id: "customers.industry" },
{ id: "orders.order_id", aggregation: "countd" },
{ id: "line_net", aggregation: "sum" },
],
},
sort: [
{
field: { id: "line_net", aggregation: "sum" },
direction: "desc",
},
],
format: { title: { enabled: true, text: "Customer Details" } },
},
},
widgetLayout: {
"kpi-unique-customers": { xTrack: 0, yTrack: 0, xSpan: 24, ySpan: 6 },
"revenue-by-region": { xTrack: 0, yTrack: 6, xSpan: 24, ySpan: 16 },
"customer-grid": { xTrack: 0, yTrack: 22, xSpan: 24, ySpan: 16 },
},
},
],
selectedPageId: "overview",
panels: {
filters: {
collapsed: true,
},
},
};
// =============================================================================
// 5. Controlling Modes & Navigating Pages
// =============================================================================
// The Studio API lets you toggle between edit and view mode at runtime, and
// navigate between pages by updating the selectedPageId in the state.
let studioApi: AgStudioApi;
// Switch the currently visible page
function selectPage(pageId: string) {
const state = studioApi.getState();
studioApi.setState({
...state,
selectedPageId: pageId,
});
}
// Toggle between edit (design-time) and view (presentation) mode
function toggleMode() {
const currentMode = studioApi.getProperty("mode");
studioApi.setProperty("mode", currentMode === "edit" ? "view" : "edit");
}
// Expose to HTML onclick handlers
(window as any).selectPage = selectPage;
(window as any).toggleMode = toggleMode;
// =============================================================================
// 6. Create the Studio
// =============================================================================
const studioProperties: AgStudioProperties = {
mode: "edit",
initialState,
// Listen for state changes - useful for auto-saving or debugging
onStateUpdated: (event) => {
console.log("State updated:", event.state);
},
onApiReady: (params) => {
Promise.all([
loadJson("products.json"),
loadJson("order_items.json"),
loadJson("customers.json"),
loadJson("orders.json"),
]).then(([productData, orderItemData, customerData, orderData]) => {
params.api.setProperty("data", {
sources: [
{
id: "products",
name: "Products",
data: productData,
fields: productsFields,
},
{
id: "order_items",
name: "Order Items",
data: orderItemData,
fields: orderItemsFields,
},
{
id: "customers",
name: "Customers",
data: customerData,
fields: customersFields,
},
{
id: "orders",
name: "Orders",
data: orderData,
fields: ordersFields,
},
],
// Relationships link data sources together for cross-table queries
relationships: [
// Each order item refers to exactly one product
{
id: "order-item-product",
source: { tableId: "order_items", fieldId: "product_id" },
target: { tableId: "products", fieldId: "product_id" },
type: "many-to-one",
acceptFanout: true,
},
// Test Your Knowledge #2: Link order items → orders → customers
{
id: "order-item-order",
source: { tableId: "order_items", fieldId: "order_id" },
target: { tableId: "orders", fieldId: "order_id" },
type: "many-to-one",
},
{
id: "order-customer",
source: { tableId: "orders", fieldId: "customer_id" },
target: { tableId: "customers", fieldId: "customer_id" },
type: "many-to-one",
},
],
// Calculated fields available across the dashboard
expressions,
});
});
},
};
// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).selectPage = selectPage;
(<any>window).toggleMode = toggleMode;
}
<div style="display: flex; flex-direction: column; height: 100%">
<div class="example-controls">
<div class="controls-row" style="display: flex; justify-content: space-between">
<div style="display: flex; gap: 8px">
<button onclick="selectPage('overview')">Overview</button>
<button onclick="selectPage('detail')">Detail</button>
<button onclick="selectPage('customers')">Customers</button>
</div>
<button onclick="toggleMode()">Toggle Edit Mode</button>
</div>
</div>
<div id="myStudio" class="my-studio-container"></div>
</div>
Summary Copy Link
Congratulations! You've built a fully interactive, multi-page analytics dashboard. Here's a recap of the key concepts covered:
- Data Sources - arrays of row data passed to Studio via
data.sources, each with field definitions describing the data shape. - Field Definitions - describe the columns within a data source, including display names, formats, and visibility.
- Relationships - links between data sources that enable cross-table queries, such as joining order items to products via a shared
product_id. - Expressions - calculated fields defined with operator trees (
multiply,subtract, etc.) that are computed at query time and behave like regular fields. - Modes - edit mode for designing dashboards and view mode for presenting them, toggled at runtime via the Studio API.
- State - a serialisable snapshot of the entire dashboard, captured with
getState()and restored withsetState()orinitialState. - Pages - multiple canvases within a single report, navigated by updating the
selectedPageIdin the state.