---
title: "Server-Side Implementation"
framework: javascript
version: "2.1.2"
---

# Server-Side Implementation

Studio calls your engine's `execute()` method with one or more `AgExecuteRequest` objects. Each request carries an `AgStudioQuery` that your engine translates into your backend's native query language. Return one `AgExecuteResult` per request, in the same order.

Implement the optional `executeCube()` method as well if your dashboard uses Pivot Grid, Treemap, Sunburst, or legend-grouped charts. See [Pivot and Hierarchy Queries](#pivot-and-hierarchy-queries).

## Query Anatomy

Every field reference in an `AgStudioQuery` is an `AgStudioQueryField` object carrying `key`, `fieldId`, `sourceId`, and optional properties like `aggregation`.

### Core Fields

| Field | Purpose | Shape |
| --- | --- | --- |
| `axes` | Group-by dimensions for aggregation | `[{ dimensions: [{ field: { key, fieldId, sourceId } }] }]` |
| `measures` | Aggregated columns | `[{ field: { key, fieldId, sourceId, aggregation: 'sum' } }]` |
| `projection` | Raw columns when not aggregating (mutually exclusive with `axes`/`measures`) | `[{ field: { key, fieldId, sourceId } }]` |
| `filter` | WHERE clause tree (recursive groups with `combinator`, leaves with `operator`) | `{ combinator: 'and', conditions: [{ field, operator: 'equals', value }] }` |
| `sort` | ORDER BY specification | `[{ field: { key, fieldId, sourceId }, direction: 'desc' }]` |
| `limit` | LIMIT and OFFSET | `{ count: 100, offset: 20 }` |
| `computedFields` | Computed columns with expression ASTs and evaluation phase | See [Computed Fields](#computed-fields) |

### Additional Fields

These fields appear when the dashboard uses features that require them. If your engine does not support a given field, the corresponding UI feature will not work correctly.

| Field | Purpose |
| --- | --- |
| `having` | Post-aggregation filter (SQL `HAVING`) |
| `window` | Window functions (`rank`, `denseRank`, `rowNumber`) |
| `joins` | Multi-source joins (see [Sources and Joins](#sources-and-joins)) |
| `scope` | Dimension-member slice for slicer/page-filter semantics |
| `distinct` | Row deduplication for projection-mode queries |
| `from` | Derived-table composition (SQL `FROM (SELECT ...)`) |

Properties available on the `AgStudioQuery` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `axes` | `AgAxisDefinition[]` |  | Dimension axes for OLAP aggregation. Empty `axes: []` combined with measures yields a single grand-total group. Mutually exclusive with `projection`. |
| `measures` | `AgMeasureDefinition[]` |  | Measures aggregated at each cell. Required when `axes` is present. |
| `projection` | `AgDimensionDefinition[]` |  | Output columns for detail/raw-row queries. Mutually exclusive with `axes`/`measures`. |
| `joins` | `AgJoinClause[]` |  | Ordered join chain - the query's source topology. Omit for a single-source query. Engines must consume clauses in order and must not perform schema lookup to reconstruct the topology. |
| `filter` | `AgStudioFilterDefinition` |  | Row-level predicate applied BEFORE any aggregation (SQL `WHERE` semantics). Leaves reference source fields on the joined plan. Affects both the result set and the denominator used by measures with `totalsScope: 'filtered'`. |
| `having` | `AgStudioFilterDefinition` |  | Post-aggregation predicate applied to cells, AFTER measures are computed (SQL `HAVING` semantics). Leaves must reference measure aliases or dimension fields (via `AgExprFieldRef`); row-level source fields are rejected. Cells the predicate rejects are dropped; the measure denominators are NOT recomputed. |
| `scope` | `AgScopeDefinition` |  | Dimension-member slice (MDX-inspired). Restricts the visible cells to the chosen members but - unlike `filter` - does NOT remove the excluded rows from the denominator used by measures with `totalsScope: 'unrestricted'`. Use for slicer/page-filter semantics where "% of total" should still divide by the full dataset. |
| `sort` | `AgStudioSortDefinition[]` |  | Result ordering (SQL `ORDER BY`). Applied AFTER aggregation, computed fields, and windows. Multiple entries form a tie-break chain in array order. Sort fields must reference output columns (measure aliases, dimension fields, or computed-field outputs). |
| `window` | `AgStudioWindowDefinition[]` |  | Window function evaluations (SQL `OVER (...)`). Each entry produces an output column computed over a partition of rows defined by `partitionBy` and ordered by `orderBy`. Evaluated after aggregation; outputs may be referenced by post-aggregation computed fields and `having`. |
| `limit` | `AgStudioLimitDefinition` |  | Hard cap on returned rows, with optional offset (SQL `LIMIT` / `OFFSET`). Applied as the final step after `sort`; pagination is therefore stable only when `sort` produces a total ordering. |
| `distinct` | `boolean` |  | Projection-mode row deduplication (SQL `SELECT DISTINCT`). In aggregation mode, use distinct-within-measure aggregations (`countd` etc.) instead. |
| `computedFields` | `AgStudioComputedFieldDefinition[]` |  | Evaluation schedule for computed expression fields. Topologically sorted - engines evaluate in array order. Each entry carries the expression in `AgStudioExpression` AST form and an explicit evaluation phase. Synthetic entries decompose cross-source expressions into single-source intermediates. Absent when the query has no expression fields. |
| `from` | `AgStudioQuery` |  | Derived-table composition (SQL `FROM (SELECT ...)`). Engines execute the inner query, materialise its result, and run the outer query over those rows. Each materialised level costs memory and latency, so engines should consider imposing a depth limit and rejecting deeper nesting with an error rather than silently truncating. |

### Computed Fields

`computedFields` is a topologically sorted evaluation schedule. Each entry carries an expression AST and an explicit evaluation phase:

- **`phase: 'pre-agg'`**: evaluated on source rows before grouping (SQL column expression)
- **`phase: 'post-agg'`**: evaluated on grouped output after measures are computed
- **`synthetic: true`**: internally generated intermediates; exclude from user-facing result columns

Engines evaluate entries in array order. Each entry's dependencies are satisfied by prior entries or source scans.

Expression nodes are either operations (`{ operator, inputs, options? }`) or leaves: field references (`{ field: AgStudioQueryField }`), values (`{ type, value }`, e.g. `{ type: 'number', value: 2 }`), or alias references (`{ ref: string }`).

## Field Identifiers

Every field reference in a query is an `AgStudioQueryField`:

| Property | Where it comes from | What to use it for |
| --- | --- | --- |
| `fieldId` | Source-qualified column ID from `getDataSources()`: `"sales.revenue"`. For computed fields (`sourceId: ''`), a bare identifier like `"pct_of_total"`. | Map to a backend column. Strip the source prefix for source fields; use as-is for computed fields. |
| `sourceId` | The `id` of the source the field belongs to: `"sales"`. | Pick which backend table or endpoint the query targets. |
| `key` | An opaque string Studio builds per-field per-query. | The column name in your result rows. Your result `rows[i][field.key]` must round-trip cleanly. **Treat `key` as opaque; do not parse it.** |

Measure fields also carry `aggregation` (e.g. `'sum'`, `'avg'`, `'count'`). Access it via `measure.field.aggregation`.

Properties available on the `AgStudioQueryField` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` |  | Composite identity key and output column alias. Treat as opaque. When the wrapping definition (window / measure / dimension / projection entry) carries an `as`, that alias - not this `key` - becomes the output column identity. This `key` then only carries the field's *source* identity for resolution. |
| `sourceId` | `string` |  | Owning source ID; `''` for computed/inline fields. |
| `fieldId` | `string` |  | The underlying field identifier within the source. |
| `aggregation` | `AgAggregationFunction` |  | Aggregation function for measure fields; omitted for raw dimensions. |
| `determinant` | `string` |  | Additional identity component for fields that would otherwise collide on `(sourceId, fieldId, aggregation)` - e.g. window outputs with different orderings. |
| `sourceAlias` | `string` |  | Alias for self-joins - disambiguates multiple instances of the same underlying source in one query. |
| `expression` | `AgStudioExpression` |  | Inline expression AST - present for computed / inline fields (where `sourceId === ''`) and also the mechanism for granularity transforms such as `dateTrunc` and numeric bucketing. |
| `isMeasure` | `boolean` |  | Inline-field intent disambiguator. For inline fields, `true` marks a measure (lives under `AgStudioQuery.measures`), `false` marks a dimension (lives under `axes[].dimensions`). Validator rejects mismatched slot/intent pairs. |
| `dataType` | `AgDataType` |  | Optional data-type hint. Studio-produced fields set this from the schema; hand-constructed queries may omit it. |

## Filter Structure

The `filter` and `having` fields share the same recursive tree structure:

**`AgStudioFilterGroup`**: a boolean combinator (`'and'`, `'or'`, or `'not'`) wrapping child nodes:

```ts
{
    combinator: 'and',
    conditions: [/* AgStudioFilterCondition | AgStudioFilterGroup */],
}
```

**`AgStudioFilterCondition`**: a leaf predicate on a single field:

```ts
{
    field: { key, fieldId, sourceId },
    operator: 'equals',
    value: 'EMEA',
}
```

Walk the tree recursively: groups become parenthesised boolean expressions; conditions become backend predicates. See the [reference examples](#reference-examples) for complete filter translation.

Properties available on the `AgStudioFilterCondition` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `field` | `AgStudioQueryField` |  | The field tested by the predicate. |
| `operator` | `"contains" \| "equals" \| "between" \| "endsWith" \| "startsWith" \| "notEqual" \| "greaterThan" \| "lessThan" \| "greaterThanOrEqual" \| "lessThanOrEqual" \| "inRelativeRange" \| "isNull" \| "isNotNull" \| "isUndefined" \| "isNotUndefined" \| "isBlank" \| "isNotBlank" \| "isNaN" \| "notContains" \| "isIn" \| "isTrue" \| "isFalse"` |  | The comparison applied between `field` and `value`. `rank` is excluded rank-style filtering is expressed via `AgDimensionDefinition.topN`. |
| `value` | `AgPrimitive \| AgPrimitive[] \| AgRelativeRangeFilterValue \| [AgPrimitive, AgPrimitive]` |  | `undefined` for `isNull` / `isNotNull` / `isTrue` / `isFalse`; a 2-element array for `between` (check `operator` to distinguish from `isIn`); an array for `isIn`; a relative-range descriptor for `inRelativeRange`; single primitive otherwise. |
| `options` | `{ crossFilter?: boolean; scopeDerived?: boolean; matchCase?: boolean; escapeChar?: string }` |  | Routing metadata carried to the engine and explain output. |

Properties available on the `AgStudioFilterGroup` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `combinator` | `"and" \| "or" \| "not"` |  | How `conditions` are combined. `not` negates the conjunction of its children (`NOT (c1 AND c2 AND ...)`); to negate a single predicate, wrap it in a `not` group with one child. |
| `conditions` | `AgStudioFilterDefinition[]` |  | Child filters - leaf conditions or nested groups. |

## Result Format

Return an `AgExecuteResult` per request, discriminated on `dataShape`. Return `'rows'` (default) or `'columns'` depending on `options.shape`.

> **Note**
>
> The `dataShape` discriminator on each result must match the data you return. If you set `dataShape: 'columns'` but populate `rows` (or vice versa), widgets will render empty.

Both shapes carry an `AgResultMetadata` object. `rowCount` is the number of rows in the returned result. When your engine applies a `limit`, set `totalRowCount` to the pre-limit row count.

### Rows Format (default)

```ts
return {
    dataShape: 'rows',
    rows: [
        { 'sales.region': 'EMEA', 'sales.revenue': 123456 },
        { 'sales.region': 'APAC', 'sales.revenue': 234567 },
    ],
    metadata: { rowCount: 2, totalRowCount: 5432 },
};
```

### Columns Format

```ts
const columns = new Map<string, ReadonlyArray<AgPrimitive | null>>();
columns.set('sales.region', ['EMEA', 'APAC']);
columns.set('sales.revenue', [123456, 234567]);

return {
    dataShape: 'columns',
    columns,
    metadata: { rowCount: 2 },
};
```

Properties available on the `AgExecuteRequest&lt;TShape extends AgResultShape = AgResultShape&gt;` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `query` | `AgStudioQuery` |  | The query to execute against the data source. |
| `options` | `AgRequestOptions<TShape>` |  | Per-request options controlling result shape, cancellation, and validation. |
| `info` | `AgEngineCallInfo` |  | Advisory metadata for logging, tracing, and batch coalescing. |

Properties available on the `AgRowsResult` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `metadata` | `AgResultMetadata` |  | Row count and optional pre-limit total. |
| `dataShape` | `"rows"` |  | Discriminator - always `'rows'` for this shape. |
| `rows` | `Record<string, unknown>[]` |  | Result rows keyed by output field alias. |

Properties available on the `AgColumnsResult` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `metadata` | `AgResultMetadata` |  | Row count and optional pre-limit total. |
| `dataShape` | `"columns"` |  | Discriminator - always `'columns'` for this shape. |
| `columns` | `AgColumnsMap` |  | Column arrays keyed by output field alias. |

## Sources and Joins

When a widget pulls fields from multiple related sources (declared via [Relationships](https://www.ag-grid.com/studio/javascript/data#relationships)), Studio produces a single `AgStudioQuery` with `joins` populated. Your `execute()` receives one query that spans multiple sources. Each field's `sourceId` and `fieldId` identify which backend table it belongs to, so your translator can resolve every reference.

If your backend does not support joins, throw a descriptive error when a query contains `joins`.

## Batching and Cancellation

Studio calls `execute(...requests)` with every request in the current render cycle. Each request carries an `info` object:

- **`batchId`:** All requests in the same `execute()` call share a `batchId`. Use it to coalesce backend round-trips (e.g. one combined request per batch). When absent, treat each request independently.
- **`queryId`:** Distinguishes independent queries from the same widget (e.g. `'rows'` vs `'grandTotal'`). Studio uses `widgetId + queryId` as the cancellation key.

Each request carries an optional `options.signal: AbortSignal`. Propagate it into your backend call to cancel superseded queries:

```ts
const res = await fetch(url, { signal: request.options?.signal });
```

## Errors

Throw from `execute()` when the backend fails. Any failed request in a batch will cause the whole batch to fail. This is to avoid partial data flowing into Studio.

```ts
async execute(...requests: AgExecuteRequest<AgResultShape>[]): Promise<AgExecuteResult[]> {
    try {
        return await this.runQueries(requests);
    } catch (error) {
        throw { message: 'Backend query failed', cause: error };
    }
}
```

## Pivot and Hierarchy Queries

Some widgets need cube-aggregated data that `execute()` can't express: multiple independent grouping axes with dense cell coverage, and subtotal placement within a hierarchy. For these, Studio calls an optional second method, `executeCube(...requests: AgCubeResolvedExecuteRequest[])`, and expects one `AgCubeResult` per request - axis tuples and a sparse cell store, not flat rows.

`executeCube` is required for:

- **Pivot Grid** widgets, always.
- **Treemap** and **Sunburst** widgets, always - hierarchy subtotal placement (`includeSubtotals`) has no flat equivalent.
- Any chart configured with a **legend field** - legend grouping is a pivot query under the hood.

A chart's own group-by fields, without a legend field, never need `executeCube`: they're a flat `execute()` query regardless of how many are configured.

### If Your Engine Doesn't Implement It

`executeCube` is optional. If you omit it:

- Pivot Grid, Treemap, and Sunburst widget types are omitted from the widget picker entirely, with a validation warning explaining why.
- A chart configured with a legend field still renders, but without the legend grouping - one ungrouped series, plus a validation warning.

Properties available on the `AgCubeResolvedExecuteRequest` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `query` | `AgCubeResolvedStudioQuery` |  | AgCubeResolvedStudioQuery |
| `options` | `AgCubeExecuteOptions` |  | AgCubeExecuteOptions |
| `info` | `AgEngineCallInfo` |  | AgEngineCallInfo |

Properties available on the `AgCubeResult&lt;TValue = AgPrimitive&gt;` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `dataShape` | `"cube"` |  | "cube" |
| `axes` | `AgResultAxis[]` |  | Axes in query order; length ≥ 2. |
| `measures` | `AgResultMeasure[]` |  | Ordered measure descriptors; each key matches a cell record key. |
| `cells` | `AgCubeCells` |  | Sparse N-D cell store keyed by axis-tuple index vector. |
| `metadata` | `AgResultMetadata` |  | Row count and optional pre-limit total. |

## Reference Examples

> **Note**
>
> These are reference implementations for learning purposes. They are not production-ready integrations and will not cover every query feature. Use them as starting points for your own engine.

### ClickHouse over HTTP

Translates `AgStudioQuery` to ClickHouse SQL, posts it over HTTP, and returns the response. The dashboard queries ClickHouse's [uk_price_paid](https://clickhouse.com/docs/getting-started/example-datasets/uk-price-paid) dataset (~28M rows) without downloading any of it.

#### ClickHouse Server-Side

```ts
import {
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
} from "ag-studio";
import { ClickHouseDataEngine } from "./engine.ts";

// Dashboard over the `uk_price_paid` dataset on ClickHouse's public
// playground. Fields are prefixed with the source id (`uk_price_paid.*`).
const initialState: AgReportState = {
  panels: {
    filters: {
      collapsed: true,
    },
  },

  pages: [
    {
      id: "properties-dashboard",
      widgets: {
        "county-filter": {
          type: "list-filter",
          dataMapping: { value: [{ id: "uk_price_paid.county" }] },
          format: { title: { enabled: true, text: "Filter by County" } },
        },
        "type-filter": {
          type: "list-filter",
          dataMapping: { value: [{ id: "uk_price_paid.type" }] },
          format: { title: { enabled: true, text: "Filter by Property Type" } },
        },
        "avg-by-county": {
          type: "bar-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "uk_price_paid.county" }],
            valueKey: [{ id: "uk_price_paid.price", aggregation: "avg" }],
          },
          format: {
            title: {
              enabled: true,
              text: "Average Price by County",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
          },
        },
        "avg-by-type": {
          type: "bar-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "uk_price_paid.type" }],
            valueKey: [{ id: "uk_price_paid.price", aggregation: "avg" }],
          },
          format: {
            title: {
              enabled: true,
              text: "Average Price by Property Type",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
          },
        },
        "summary-grid": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "uk_price_paid.county" },
              { id: "uk_price_paid.type" },
              { id: "uk_price_paid.price", aggregation: "count" },
              { id: "uk_price_paid.price", aggregation: "avg" },
              { id: "uk_price_paid.price", aggregation: "min" },
              { id: "uk_price_paid.price", aggregation: "max" },
            ],
          },
          format: {
            title: {
              enabled: true,
              text: "Price Summary",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
            style: {
              grandTotalRow: { enabled: true },
              theme: { rowHeight: 28 },
            },
          },
        },
      },
      widgetLayout: {
        "county-filter": { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 12 },
        "type-filter": { xTrack: 0, yTrack: 12, xSpan: 6, ySpan: 12 },
        "avg-by-county": { xTrack: 6, yTrack: 0, xSpan: 9, ySpan: 24 },
        "avg-by-type": { xTrack: 15, yTrack: 0, xSpan: 9, ySpan: 24 },
        "summary-grid": { xTrack: 0, yTrack: 24, xSpan: 24, ySpan: 14 },
      },
      filter: { page: [] },
    },
  ],
  selectedPageId: "properties-dashboard",
};

let studioApi: AgStudioApi;

function toggleMode() {
  const currentMode = studioApi.getProperty("mode");
  const newMode = currentMode === "edit" ? "view" : "edit";
  studioApi.setProperty("mode", newMode);
  updateModeButton(newMode);
}

function updateModeButton(mode: "view" | "edit") {
  document.getElementById("toggleMode")!.textContent =
    mode === "edit" ? "View Mode" : "Edit Mode";
}

function loadData() {
  document.getElementById("myStudio")!.style.display = "";
  studioApi.setProperty("data", new ClickHouseDataEngine());
  document.getElementById("loadData")!.style.display = "none";
}

const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
const studioProperties: AgStudioProperties = {
  mode: "view",
  initialState,
};
studioApi = createStudio(studioDiv, studioProperties);
updateModeButton(studioApi.getProperty("mode")!);
document.getElementById("loadData")!.addEventListener("click", loadData);

(window as any).toggleMode = toggleMode;

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).toggleMode = toggleMode;
}
```

[Live example: ClickHouse Server-Side](https://www.ag-grid.com/studio/examples/server-side-data-implementation/clickhouse-serverside/typescript/)

Contains HM Land Registry data © Crown copyright and database right 2021. This data is licensed under the Open Government Licence v3.0. ([Source](https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads), [Fields](https://www.gov.uk/guidance/about-the-price-paid-data))

### ClickHouse Server-Side Pivot

Adds `executeCube` (see [Pivot and Hierarchy Queries](#pivot-and-hierarchy-queries)) to the ClickHouse engine above, so it can also serve Pivot Grid, Treemap, and Sunburst widgets. The implementation groups by both axes' dimensions in a single query, then reshapes the flat result into the cube shape.

#### ClickHouse Server-Side Pivot

```ts
import {
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
} from "ag-studio";
import { ClickHousePivotDataEngine } from "./engine.ts";

// Pivot over the `uk_price_paid` dataset on ClickHouse's public playground,
// served entirely server-side via `AgDataEngine.executeCube`. Fields are
// prefixed with the source id (`uk_price_paid.*`).
const initialState: AgReportState = {
  panels: {
    filters: {
      collapsed: true,
    },
  },
  pages: [
    {
      id: "properties-pivot",
      widgets: {
        "price-pivot": {
          type: "pivot-grid",
          dataMapping: {
            rows: [{ id: "uk_price_paid.county" }],
            columns: [{ id: "uk_price_paid.type" }],
            values: [
              { id: "uk_price_paid.price", aggregation: "avg" },
              { id: "uk_price_paid.price", aggregation: "count" },
            ],
          },
          format: {
            title: {
              enabled: true,
              text: "Average Price and Sales Count by County and Property Type",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
            style: { totalColumns: true },
          },
        },
        "price-treemap": {
          type: "treemap-chart",
          dataMapping: {
            categoryKey: [
              { id: "uk_price_paid.duration" },
              { id: "uk_price_paid.type" },
            ],
            valueKey: [{ id: "uk_price_paid.price", aggregation: "sum" }],
          },
          format: {
            title: {
              enabled: true,
              text: "Total Price by Property Type and Tenure",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
          },
        },
        "price-sunburst": {
          type: "sunburst-chart",
          dataMapping: {
            categoryKey: [
              { id: "uk_price_paid.duration" },
              { id: "uk_price_paid.type" },
            ],
            valueKey: [{ id: "uk_price_paid.price", aggregation: "sum" }],
          },
          format: {
            title: {
              enabled: true,
              text: "Total Price by Property Type and Tenure",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
          },
        },
      },
      widgetLayout: {
        "price-pivot": { xTrack: 0, yTrack: 0, xSpan: 24, ySpan: 19 },
        "price-treemap": { xTrack: 0, yTrack: 19, xSpan: 12, ySpan: 19 },
        "price-sunburst": { xTrack: 12, yTrack: 19, xSpan: 12, ySpan: 19 },
      },
      filter: { page: [] },
    },
  ],
  selectedPageId: "properties-pivot",
};

let studioApi: AgStudioApi;

function toggleMode() {
  const currentMode = studioApi.getProperty("mode");
  const newMode = currentMode === "edit" ? "view" : "edit";
  studioApi.setProperty("mode", newMode);
  updateModeButton(newMode);
}

function updateModeButton(mode: "view" | "edit") {
  document.getElementById("toggleMode")!.textContent =
    mode === "edit" ? "View Mode" : "Edit Mode";
}

function loadData() {
  document.getElementById("myStudio")!.style.display = "";
  studioApi.setProperty("data", new ClickHousePivotDataEngine());
  document.getElementById("loadData")!.style.display = "none";
}

const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
const studioProperties: AgStudioProperties = {
  mode: "view",
  initialState,
};
studioApi = createStudio(studioDiv, studioProperties);
updateModeButton(studioApi.getProperty("mode")!);
document.getElementById("loadData")!.addEventListener("click", loadData);

(window as any).toggleMode = toggleMode;

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).toggleMode = toggleMode;
}
```

[Live example: ClickHouse Server-Side Pivot](https://www.ag-grid.com/studio/examples/server-side-data-implementation/clickhouse-serverside-pivot/typescript/)

Contains HM Land Registry data © Crown copyright and database right 2021. This data is licensed under the Open Government Licence v3.0. ([Source](https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads), [Fields](https://www.gov.uk/guidance/about-the-price-paid-data))

### REST API: World Bank Countries

Queries the World Bank Open Data API. Supported predicates are sent as URL parameters; remaining filters, aggregation, and sorting are applied locally.

#### World Bank Countries Server-Side

```ts
import {
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
} from "ag-studio";
import { RestCountriesDataEngine } from "./engine.ts";

// Dashboard over the World Bank countries API. Fields are prefixed with the
// source id (`countries.*`). The dashboard's filters on region / income
// level / lending type become URL query parameters on the fetch - this is
// the core pedagogy of the example: a filter change produces a narrower
// REST call.
const initialState: AgReportState = {
  panels: {
    filters: {
      collapsed: true,
    },
  },
  pages: [
    {
      id: "countries-dashboard",
      widgets: {
        "region-filter": {
          type: "list-filter",
          dataMapping: { value: [{ id: "countries.region" }] },
          format: { title: { enabled: true, text: "Filter by Region" } },
        },
        "income-filter": {
          type: "list-filter",
          dataMapping: { value: [{ id: "countries.incomeLevel" }] },
          format: { title: { enabled: true, text: "Filter by Income Level" } },
        },
        "count-by-region": {
          type: "bar-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "countries.region" }],
            valueKey: [{ id: "countries.country", aggregation: "count" }],
          },
          format: {
            title: {
              enabled: true,
              text: "Countries by Region",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
          },
        },
        "count-by-income": {
          type: "bar-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "countries.incomeLevel" }],
            valueKey: [{ id: "countries.country", aggregation: "count" }],
          },
          format: {
            title: {
              enabled: true,
              text: "Countries by Income Level",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
          },
        },
        "countries-grid": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "countries.region" },
              { id: "countries.incomeLevel" },
              { id: "countries.lendingType" },
              { id: "countries.country", aggregation: "count" },
            ],
          },
          format: {
            title: {
              enabled: true,
              text: "Countries Summary",
              typography: { fontSize: 16, fontWeight: "bold" },
            },
            style: {
              grandTotalRow: { enabled: true },
              theme: { rowHeight: 28 },
            },
          },
        },
      },
      widgetLayout: {
        "region-filter": { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 12 },
        "income-filter": { xTrack: 0, yTrack: 12, xSpan: 6, ySpan: 12 },
        "count-by-region": { xTrack: 6, yTrack: 0, xSpan: 9, ySpan: 24 },
        "count-by-income": { xTrack: 15, yTrack: 0, xSpan: 9, ySpan: 24 },
        "countries-grid": { xTrack: 0, yTrack: 24, xSpan: 24, ySpan: 14 },
      },
      filter: { page: [] },
    },
  ],
  selectedPageId: "countries-dashboard",
};

let studioApi: AgStudioApi;

function toggleMode() {
  const currentMode = studioApi.getProperty("mode");
  const newMode = currentMode === "edit" ? "view" : "edit";
  studioApi.setProperty("mode", newMode);
  updateModeButton(newMode);
}

function updateModeButton(mode: "view" | "edit") {
  document.getElementById("toggleMode")!.textContent =
    mode === "edit" ? "View Mode" : "Edit Mode";
}

function loadData() {
  document.getElementById("myStudio")!.style.display = "";
  studioApi.setProperty("data", new RestCountriesDataEngine());
  document.getElementById("loadData")!.style.display = "none";
}

const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
const studioProperties: AgStudioProperties = {
  mode: "view",
  initialState,
};
studioApi = createStudio(studioDiv, studioProperties);
updateModeButton(studioApi.getProperty("mode")!);
document.getElementById("loadData")!.addEventListener("click", loadData);

(window as any).toggleMode = toggleMode;

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).toggleMode = toggleMode;
}
```

[Live example: World Bank Countries Server-Side](https://www.ag-grid.com/studio/examples/server-side-data-implementation/restcountries-serverside/typescript/)

Contains data from The World Bank: Countries API, licensed under [Creative Commons Attribution 4.0 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/). ([Terms of Use](https://www.worldbank.org/en/about/legal/terms-of-use-for-datasets))
