---
title: "Building a Dashboard"
framework: angular
version: "2.1.2"
---

# Building a Dashboard

This guide demonstrates how to build a dashboard from scratch, covering all of the features required to create an interactive, multi-page dashboard.

## Overview

This tutorial covers the following:

1. Loading external JSON data into Studio
2. Configuring data with fields, relationships and expressions
3. Controlling view and edit modes via the UI
4. Handling state changes, and pre-configuring reports
5. 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**:

#### Complete Tutorial Example

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Complete Tutorial Example](https://www.ag-grid.com/studio/examples/tutorial/tutorial-complete/angular/)

> **Note**
>
> This tutorial assumes you have completed the [Quick Start](https://www.ag-grid.com/studio/angular/quick-start/) and have AG Studio installed.

## Create an Empty Dashboard

Complete our Quick Start (or open the example below in CodeSandbox / Plunker) to start with a basic instance of AG Studio:

#### Empty Dashboard

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Empty Dashboard](https://www.ag-grid.com/studio/examples/tutorial/tutorial-empty/angular/)

## Loading External Data

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:

```ts
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:

```ts
<ag-studio
    [data]="data"
    /* other studio properties ... */ />

this.data = {
    sources: [
        { id: 'products', name: 'Products', data: productData },
        { id: 'order_items', name: 'Order Items', data: orderItemData },
    ],
};
```

### Data Source Fields

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:

```ts
// 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',
    },
    /* ... */
]
```

> **Note**
>
> 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:

```ts
<ag-studio
    [data]="data"
    /* other studio properties ... */ />

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.

#### External Data with Fields

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: External Data with Fields](https://www.ag-grid.com/studio/examples/tutorial/tutorial-data/angular/)

## Configuring Data

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

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:

```ts
<ag-studio
    [data]="data"
    /* other studio properties ... */ />

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

**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_gross`** multiplies `quantity` by `unit_price` from the same table.
- **`margin`** subtracts `unit_price` from `list_price`. This crosses tables, which works because the relationship links order items to their products.

```ts
<ag-studio
    [data]="data"
    /* other studio properties ... */ />

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

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`:

```ts
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](https://www.ag-grid.com/studio/angular/formatting/) for the full format-string syntax.

Try dragging `Line Gross` or `Margin` onto the canvas to see them in action:

#### Relationships & Expressions

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Relationships & Expressions](https://www.ag-grid.com/studio/examples/tutorial/tutorial-relationships/angular/)

> **Note**
>
> Refer to the [Expressions](https://www.ag-grid.com/studio/angular/expressions/) guide for the full list of operators, conditional logic, and cross-source expressions, and the [Configuring Data](https://www.ag-grid.com/studio/angular/data#relationships) guide for more on relationship types and chaining.

## Controlling Modes

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:

```ts
<ag-studio
    [mode]="mode"
    /* other studio properties ... */ />

this.mode = 'view';
```

To switch modes at runtime, update the `mode` property. For example, you could add a toggle button above Studio:

```ts
import { Component } from '@angular/core';
import { AgStudio } from 'ag-studio-angular';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [AgStudio],
    template: `
        <div style="display: flex; justify-content: flex-end; padding: 8px;">
            <button (click)="toggleMode()">Toggle Edit Mode</button>
        </div>
        <ag-studio [data]="data" [mode]="mode"></ag-studio>
    `,
})
export class AppComponent {
    mode = 'view';

    toggleMode() {
        this.mode = this.mode === '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:

#### Mode Toggling

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Mode Toggling](https://www.ag-grid.com/studio/examples/tutorial/tutorial-modes/angular/)

## Managing State

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

The Studio's **state** is a complete, serialisable snapshot of the entire dashboard:

```ts
{
    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 mappings
  - `widgetLayout` - where widgets are positioned within the canvas
  - `filter` - optional page-level or widget-level filters
- **`panels`** - the current state of the sidebar panels (collapsed, width, etc.).
- **`selectedPageId`** - the `id` of 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](https://www.ag-grid.com/studio/angular/state/) for the full API reference.

### Listening for State Changes

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 `(stateUpdated)` output:

```ts
// In the template:
// <ag-studio [data]="data" mode="edit" (stateUpdated)="onStateUpdated($event)"></ag-studio>

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.

#### State Change Events

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: State Change Events](https://www.ag-grid.com/studio/examples/tutorial/tutorial-state-events/angular/)

### Defining an Initial State

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:

1. Design a dashboard in edit mode,
2. Copy the state from the console log (from the `onStateUpdated` callback you just added),
3. Paste it into your code as the `initialState` value.

Alternatively, view the code in the example below to see the pre-configured state of the report in the example:

#### Pre-built Report

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Pre-built Report](https://www.ag-grid.com/studio/examples/tutorial/tutorial-initial-state/angular/)

## Navigating Between Pages

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:

```ts
// 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:

```ts
import { Component, ViewChild } from '@angular/core';
import { AgStudio } from 'ag-studio-angular';

@Component({
    selector: 'app-root',
    standalone: true,
    imports: [AgStudio],
    template: `
        <div style="display: flex; gap: 8px; padding: 8px;">
            <button (click)="selectPage('overview')">Overview</button>
            <button (click)="selectPage('detail')">Detail</button>
        </div>
        <ag-studio #myStudio [data]="data" [initialState]="initialState" mode="view"></ag-studio>
    `,
})
export class AppComponent {
    @ViewChild('myStudio') studio!: AgStudio;

    selectPage(pageId: string) {
        const state = this.studio.api.getState();
        this.studio.api.setState({
            ...state,
            selectedPageId: pageId,
        });
    }
}
```

#### Navigating Between Pages

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Navigating Between Pages](https://www.ag-grid.com/studio/examples/tutorial/tutorial-pages/angular/)

## Test Your Knowledge

Put what you've learnt into practice. Using the dashboard you've built so far, try the following challenges:

1. **Add a new data source** - Load the `customers.json` file alongside products and order items. Define field definitions for the customer fields: `customer_id`, `customer_name`, `region`, `segment`, and `industry`.
2. **Define a new relationship** - The `orders.json` file contains an `order_id` column and a `customer_id` column. Load the orders data, then add relationships linking `order_items.order_id` → `orders.order_id` and `orders.customer_id` → `customers.customer_id`.
3. **Create a new calculated field** - Add an expression called `line_net` that computes the net line total after discount: `(quantity × unit_price) - ((quantity × unit_price) × discount_pct)`. This requires nesting operators, refer to the `line_gross` expression for the pattern.
4. **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:

#### Complete Tutorial Example

```ts
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component.ts';

const app = bootstrapApplication(AppComponent, {
    providers: [provideHttpClient()],
});
```

[Live example: Complete Tutorial Example](https://www.ag-grid.com/studio/examples/tutorial/tutorial-complete/angular/)

## Summary

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 with `setState()` or `initialState`.
- **Pages** - multiple canvases within a single report, navigated by updating the `selectedPageId` in the state.

## Next Steps

**[API Reference](https://www.ag-grid.com/studio/angular/reference/)**

Browse the full API reference for AG Studio properties, events, and methods.

**[Configuring Data](https://www.ag-grid.com/studio/angular/data/)**

Learn how to configure different types of data sources, as well as share data between multiple instances.
