React Embedded AnalyticsBuilding 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 Copy Link

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:

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:

const data = useMemo(() => { 
	return {
        sources: [
            { id: 'products', name: 'Products', data: productData },
            { id: 'order_items', name: 'Order Items', data: orderItemData },
        ],
    };
}, []);

<AgStudio data={data} />

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 data = useMemo(() => { 
	return {
        sources: [
            {
                id: 'products',
                name: 'Products',
                data: productData,
                fields: productsFields
            },
            {
                id: 'order_items',
                name: 'Order Items',
                data: orderItemData,
                fields: orderItemsFields
            },
        ],
    };
}, []);

<AgStudio data={data} />

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:

const data = useMemo(() => { 
	return {
        sources: [/* ... */],
        relationships: [
            {
                id: 'order-item-product',
                source: { tableId: 'order_items', fieldId: 'product_id' },
                target: { tableId: 'products', fieldId: 'product_id' },
                type: 'many-to-one',
            },
        ],
    };
}, []);

<AgStudio data={data} />

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_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.
const data = useMemo(() => { 
	return {
        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' },
                    ],
                },
            },
        ],
    };
}, []);

<AgStudio data={data} />

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:

const [mode, setMode] = useState('view');

<AgStudio mode={mode} />

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

import { useState, useCallback } from 'react';
import { AgStudio } from 'ag-studio-react';

const App = () => {
    const [mode, setMode] = useState('view');

    const toggleMode = useCallback(() => {
        setMode((prev) => (prev === 'edit' ? 'view' : 'edit'));
    }, []);

    return (
        <>
            <div style={{ display: 'flex', justifyContent: 'flex-end', padding: '8px' }}>
                <button onClick={toggleMode}>Toggle Edit Mode</button>
            </div>
            <div style={{ height: '100%', width: '100%' }}>
                <AgStudio data={data} mode={mode} />
            </div>
        </>
    );
};

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 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 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 onStateUpdated prop:

const onStateUpdated = useCallback((event) => {
    console.log('State updated:', event.state);
}, []);

<AgStudio data={data} mode="edit" onStateUpdated={onStateUpdated} />

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:

  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:

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:

import { useRef, useCallback } from 'react';
import { AgStudio } from 'ag-studio-react';

const App = () => {
    const studioRef = useRef(null);

    const selectPage = useCallback((pageId) => {
        const state = studioRef.current.api.getState();
        studioRef.current.api.setState({
            ...state,
            selectedPageId: pageId,
        });
    }, []);

    return (
        <>
            <div style={{ display: 'flex', gap: '8px', padding: '8px' }}>
                <button onClick={() => selectPage('overview')}>Overview</button>
                <button onClick={() => selectPage('detail')}>Detail</button>
            </div>
            <div style={{ height: '100%', width: '100%' }}>
                <AgStudio ref={studioRef} data={data} initialState={initialState} mode="view" />
            </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:

  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_idorders.order_id and orders.customer_idcustomers.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:

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

Next Steps Copy Link