This guide demonstrates how to build a dashboard from scratch, covering all of the features required to create an interactive, multi-page dashboard.
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:
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:
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://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:
<ag-studio
:data="data"
/* other studio properties ... */>
</ag-studio>
this.data = {
sources: [
{ id: 'products', name: 'Products', data: productData },
{ id: 'order_items', name: 'Order Items', data: orderItemData },
],
}; 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:
<ag-studio
:data="data"
/* other studio properties ... */>
</ag-studio>
this.data = {
sources: [
{
id: 'products',
name: 'Products',
data: productData,
fields: productsFields
},
{
id: 'order_items',
name: 'Order Items',
data: orderItemData,
fields: orderItemsFields
},
],
};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.
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:
<ag-studio
:data="data"
/* other studio properties ... */>
</ag-studio>
this.data = {
sources: [/* ... */],
relationships: [
{
id: 'order-item-product',
source: { tableId: 'order_items', fieldId: 'product_id' },
target: { tableId: 'products', fieldId: 'product_id' },
type: 'many-to-one',
},
],
};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.
<ag-studio
:data="data"
/* other studio properties ... */>
</ag-studio>
this.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' },
],
},
},
],
};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 format can be customised by providing your own formatter function.
Pass an Intl.NumberFormat instance via the options.format property on the line_gross expression definition:
const compactCurrency = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
currencyDisplay: 'narrowSymbol',
notation: 'compact',
compactDisplay: 'short',
maximumFractionDigits: 1,
});
const expressions: AgExpressionFieldDefinition[] = [
{
id: 'line_gross',
name: 'Line Gross',
format: 'currencyFormat',
options: { format: compactCurrency },
expression: { /* ... */ },
},
/* ... */
];
This works on any field or expression. The options.format property accepts any Intl.NumberFormat instance. See Data Types for more on format customisation.
Try dragging Line Gross or Margin onto the canvas to see them in action:
Refer to the Expressions guide for the full list of operators, conditional logic, and cross-source expressions, and the Configuring Data 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:
<ag-studio
:mode="mode"
/* other studio properties ... */>
</ag-studio>
this.mode = 'view';To switch modes at runtime, update the mode property. For example, you could add a toggle button above Studio:
<template>
<div style="display: flex; justify-content: flex-end; padding: 8px;">
<button @click="toggleMode">Toggle Edit Mode</button>
</div>
<AgStudio :data="data" :mode="mode" />
</template>
<script setup>
import { ref } from 'vue';
import { AgStudio } from 'ag-studio-vue3';
const mode = ref('view');
function toggleMode() {
mode.value = mode.value === 'edit' ? 'view' : 'edit';
}
</script>
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:
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 via the @state-updated binding:
<template>
<AgStudio :data="data" mode="edit" @state-updated="onStateUpdated" />
</template>
<script setup>
function onStateUpdated(event) {
console.log('State updated:', event.state);
}
</script>
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.
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:
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:
<template>
<div style="display: flex; gap: 8px; padding: 8px;">
<button @click="selectPage('overview')">Overview</button>
<button @click="selectPage('detail')">Detail</button>
</div>
<AgStudio ref="studioRef" :data="data" :initialState="initialState" mode="view" />
</template>
<script setup>
import { ref } from 'vue';
import { AgStudio } from 'ag-studio-vue3';
const studioRef = ref(null);
function selectPage(pageId) {
const state = studioRef.value.api.getState();
studioRef.value.api.setState({
...state,
selectedPageId: pageId,
});
}
</script>
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:
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.