---
title: "Custom Widgets"
framework: react
version: "2.1.2"
---

# Custom Widgets

Custom widgets allow you to add your own widgets to AG Studio. Use them when the provided widgets do not meet your requirements.

#### Custom Widget

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgStudio, AgStudioRef } from "ag-studio-react";
import {
  AgDataEngine,
  AgDataSourcesDefinition,
  AgDefaultRegistry,
  AgReportState,
  AgStudioApi,
  AgStudioApiReadyEvent,
  AgStudioMode,
  AgStudioProperties,
  AgWidgetFormParams,
  AgWidgetsConfig,
  createWidgets,
} from "ag-studio";
import { CustomDef, MyRegistry } from "./interfaces.tsx";
import CustomWidget from "./customWidget.tsx";

const StudioExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const studioStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [data, setData] = useState<AgDataSourcesDefinition | AgDataEngine>();
  const initialState = useMemo<AgReportState<MyRegistry>>(() => {
    return {
      pages: [
        {
          id: "page1",
          widgets: {
            "1": {
              type: "customWidget",
              dataMapping: {
                value: [
                  {
                    id: "medals.gold",
                    aggregation: "sum",
                  },
                ],
              },
            },
          },
          widgetLayout: {
            "1": {
              xTrack: 0,
              yTrack: 0,
              xSpan: 24,
              ySpan: 12,
            },
          },
          selection: {
            type: "widget",
            id: "1",
          },
        },
      ],
      selectedPageId: "page1",
      panels: {
        filters: {
          collapsed: true,
        },
        data: {
          collapsed: true,
        },
      },
    };
  }, []);
  const widgets = useMemo<
    | AgWidgetsConfig<MyRegistry>
    | ((
        widgets: AgWidgetsConfig<AgDefaultRegistry>,
      ) => AgWidgetsConfig<MyRegistry>)
  >(() => {
    return createWidgets<MyRegistry>({
      additionalTypes: [
        {
          id: "customWidget",
          icon: {
            url: "https://www.ag-grid.com/studio/images/brandmark.svg",
          },
          label: "Custom Widget",
          dataMapping: {
            value: {
              type: "field",
              supportedRoles: ["numeric"],
              requires: { cardinality: "one" },
              required: true,
            },
          },
          form: (params: AgWidgetFormParams<CustomDef>) => {
            const defaultForm = params.createDefaults({
              dataMappingItems: [
                {
                  key: "value",
                  label: "Value",
                },
              ],
            });
            defaultForm.items[1].items.push({
              type: "number",
              id: "format.style.valueFontSize",
              label: "Value Font Size",
              defaultValue: 48,
            });
            return defaultForm;
          },
          comp: CustomWidget,
          defaultSize: {
            width: 400,
            height: 300,
          },
          minSize: {
            width: 200,
            height: 100,
          },
          ai: {
            description:
              "Custom widget used for displaying values in an interesting way.",
          },
        },
      ],
      menu: [
        {
          label: "Custom",
          widgetIds: ["customWidget"],
        },
      ],
    });
  }, []);

  const onApiReady = useCallback((params: AgStudioApiReadyEvent) => {
    fetch("https://www.ag-grid.com/studio/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: any[]) => setData({ sources: [{ id: "medals", data }] }));
  }, []);

  return (
    <div style={containerStyle}>
      <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
        <AgStudio<MyRegistry>
          style={studioStyle}
          className="my-studio-container"
          data={data}
          initialState={initialState}
          widgets={widgets}
          mode={"edit"}
          onApiReady={onApiReady}
        />
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <StudioExample />
  </StrictMode>,
);
```

[Live example: Custom Widget](https://www.ag-grid.com/studio/examples/custom-widgets/custom-widget/reactFunctionalTs/)

The example above demonstrates a custom widget showing a single data value.

## Implementing a Custom Widget

To provide a custom widget, implement the `AgWidgetDefinition` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `TWidgetType` |  | Unique widget identifier (e.g., 'grid', 'value', 'column-chart-grouped'). |
| `label` | `string` |  | Display label, or localisation key. |
| `icon` | `AgCustomIcon` |  | Optional icon for widget display. One of: `string` - an SVG string `{ className: string }` - A CSS class name `{ url: string }` - A URL to an SVG |
| `dataMapping` | `AgDataMappingDefinitions<keyof TOut["dataMapping"] & string>` |  | Optional data mappings for the widget (if required). This defines the type of fields and how they are used by the widget. |
| `formatShape` | `Function` |  | Optional format shape. The shape created by this function will be used for parsing the `format` configuration. If provided, `format` state will be passed through the `parse()` method before being loaded into Studio. If using AI, the shape is required, as it uses the schema. |
| `form` | `Function` |  | Form configuration using typed form builder. |
| `defaultState` | `Partial<TOut>` |  | Optional default state. If provided, this will override the default values in the form. It also allows defaults to be set for the data mapping and sort. |
| `extends` | `"value" \| "grid" \| "pivot-grid" \| "column-chart-grouped" \| "column-chart-stacked" \| "column-chart-stacked-100" \| "bar-chart-grouped" \| "bar-chart-stacked" \| "bar-chart-stacked-100" \| "line-chart" \| "area-chart" \| "area-chart-stacked" \| "area-chart-stacked-100" \| "scatter-chart" \| "bubble-chart" \| "radar-line-chart" \| "radar-area-chart" \| "radial-column-chart" \| "nightingale-chart" \| "radial-bar-chart" \| "pie-chart" \| "donut-chart" \| "funnel-chart" \| "cone-funnel-chart" \| "pyramid-chart" \| "treemap-chart" \| "sunburst-chart" \| "radial-gauge" \| "linear-gauge" \| "button-filter" \| "list-filter" \| "date-filter" \| "text" \| "image" \| "combo-chart-grouped-column-line" \| "combo-chart-stacked-column-line"` |  | Optional default widget ID that this definition extends. Use this if overriding or pre-configuring one of the default widgets. This ensure that any values that no longer appear in the form are still correctly mapped / set to the right default value. For custom widgets, this should not be set. |
| `comp` | `string \| ComponentType<AgWidgetParams<TOut>> \| (new () => AgTypeScriptComponent<AgWidgetParams<TOut>>)` |  | Custom component. Either: a `string` that matches a custom component in the studio `components` property; a custom React component; or a custom TypeScript component class (no React; working directly with the DOM). |
| `defaultSize` | `AgWidgetSize` |  | Default widget size when created. |
| `minSize` | `AgWidgetSize` |  | Minimum widget size constraints. |
| `toolbar` | `(AgWidgetToolbarButton \| AgDefaultWidgetAction)[]` | `['duplicate', 'delete']` | Optional toolbar configuration. Can be a built-in item (`'delete'` or `'duplicate'`) or a custom item. If the item provides an action, that will be performed, otherwise it will be dispatched as a `'toolbarAction'` event to the widget. If providing a custom value, `'duplicate'` and `'delete'` must be provided if required. |
| `frame` | `AgWidgetFrameStyle` |  | Optional frame style (defaults to standard widget frame). |
| `featureConfig` | `AgWidgetFeatureConfig` |  | Optional feature configuration. |
| `options` | `TOptions` |  | Optional options passed directly to widget. |
| `ai` | `AgWidgetAiMetadata` |  | Optional structured AI metadata for this widget type. |

Custom widgets are provided to the `widgets` property, similar to [Customising the Available Widgets](https://www.ag-grid.com/studio/react/widget-configuration#customise-the-available-widgets).

Provide the custom widget definitions to the `createWidgets(params)` helper function, and add them to the menu.

```jsx
const widgets = (widgetConfig) => createWidgets<CustomRegistry>({
    additionalTypes: [customWidgetDefinition],
    menu: [
        ...widgetConfig.menu,
        {
            label: 'Custom',
            widgetIds: ['customWidget'],
        },
    ]
});

<AgStudio widgets={widgets} />
```

For the types to work correctly, the custom widgets should be defined in the [Registry Type](https://www.ag-grid.com/studio/react/registry-type/).

```ts
interface CustomRegistry extends AgBaseRegistry {
    widgets: readonly (AgDefaultWidgetDefinition | CustomWidgetDefinition)[]
}
```

## Data Mapping

The `dataMapping` property defines the fields or fieldsets that are required to configure the widget, and the required relationships between them.

For example, to configure a line chart, the data mapping might look like this:

```ts
const dataMapping = {
    xAxisKey: {
        type: 'field', // Only a single field allowed
        // All types of value allowed
        supportedRoles: ['category', 'numeric', 'temporal'],
        requires: { cardinality: 'many' }, // Many values are accepted
        required: true, // Required field - widget cannot be displayed without it
        sort: true, // Show the sort menu
        aiDescription: 'Field for the x axis.', // Description when used with AI
    },
    yAxisKey: {
        type: 'fieldset', // Multiple fields allowed
        supportedRoles: ['numeric'], // Only numeric values allowed
        // For each `xAxisKey`, this must map to a single value
        requires: { per: 'dataMapping.xAxisKey', cardinality: 'one' },
        required: true, // Required field - widget cannot be displayed without it
        sort: true, // Show the sort menu
        // Description when used with AI
        aiDescription: 'Field(s) for the y axis. Each field becomes a separate series.',
    },
};
```

## Custom Widget Component

The custom component is a React component that receives props of type `AgWidgetParams`.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `widgetId` | `string` |  | Widget ID. |
| `widgetType` | `string` |  | Widget type. |
| `format` | `TWidget["format"]` |  | Widget format as constructed from the widget form. |
| `dataMapping` | `AgWidgetDataMapping<TWidget>` |  | Widget data mapping values. |
| `sort` | `AgWidgetSort[] \| undefined` |  | Widget sort if defined. |

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `api` | [`AgStudioApi`](https://www.ag-grid.com/studio/react/studio-api/) |  | Studio API. |
| `context` | `TContext` |  | Application context as set on `context` Studio property. |
| `widgetId` | `string` |  | Widget ID. |
| `widgetType` | `string` |  | Widget type. |
| `format` | `TWidget["format"]` |  | Widget format as constructed from the widget form. |
| `dataMapping` | `AgWidgetDataMapping<TWidget>` |  | Widget data mapping values. |
| `sort` | `AgWidgetSort[] \| undefined` |  | Widget sort if defined. |
| `config` | `AgWidgetConfig<TWidget, TOptions>` |  | Widget configuration. |
| `widgetApi` | `AgWidgetApi` |  | Widget API. Provides access to retrieve data. |

### Custom Widget Params

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `api` | [`AgStudioApi`](https://www.ag-grid.com/studio/react/studio-api/) |  | Studio API. |
| `context` | `TContext` |  | Application context as set on `context` Studio property. |
| `widgetId` | `string` |  | Widget ID. |
| `widgetType` | `string` |  | Widget type. |
| `format` | `TWidget["format"]` |  | Widget format as constructed from the widget form. |
| `dataMapping` | `AgWidgetDataMapping<TWidget>` |  | Widget data mapping values. |
| `sort` | `AgWidgetSort[] \| undefined` |  | Widget sort if defined. |
| `config` | `AgWidgetConfig<TWidget, TOptions>` |  | Widget configuration. |
| `widgetApi` | `AgWidgetApi` |  | Widget API. Provides access to retrieve data. |

### Display State

Widgets have four different display states that can be set via `widgetApi.setDisplayState(state, metadata?)`. These will trigger different overlays to be displayed by the layout on top of the widget. The states are:

- `displayed` - The widget has data and is ready to display. Studio will show no overlay; the widget is rendered normally.
- `loading` - The widget is loading. Metadata defaults to `{ prominent: true }` for a solid loading overlay; use `{ prominent: false }` for an unobtrusive refresh indicator that keeps the previous content visible.
- `noData` - The widget has no data (e.g. everything is filtered out or the data is empty). Studio will show an overlay with "No data to display".
- `incompleteDataMapping` - The widget does not have all of the required fields set. Studio will show an overlay with the field selection inputs.

Each time the widget updates, set the relevant status as needed.

```ts
widgetApi.setDisplayState('loading');
const response = await widgetApi.getData(request);
// ... process the response
widgetApi.setDisplayState('displayed');
```

### Loading Data

Data is loaded via `widgetApi.getData(request)`. The request can be constructed from the data mapping values in the params.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `kind` | `"flat"` |  | Flat row query. Omit for backwards compatibility. |
| `fields` | `AgWidgetField[]` |  | List of fields to return in the query. Unaggregated fields will automatically be used for grouping. |
| `sort` | `AgSort[]` |  | Sort the result based on the provided fields. |
| `filter` | `AgFilter[]` |  | Additional filter to apply. Page-level filters and widget-level filters (including from filter widgets and cross filters) will be automatically applied. |
| `limit` | `AgLimit` |  | Limit the number of rows returned, or for pagination. |

## Form

The widget `form` configures the form displayed in the edit panel. The values from the form are passed in the widget params.

See the [Form Setup](https://www.ag-grid.com/studio/react/custom-widgets-form/) page for more details.

## Cross-Filtering

#### Custom Widget with Cross Filter

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgStudio, AgStudioRef } from "ag-studio-react";
import {
  AgDataEngine,
  AgDataSourcesDefinition,
  AgDefaultRegistry,
  AgReportState,
  AgStudioApi,
  AgStudioApiReadyEvent,
  AgStudioMode,
  AgStudioProperties,
  AgWidgetFormParams,
  AgWidgetsConfig,
  createWidgets,
} from "ag-studio";
import { CustomDef, MyRegistry } from "./interfaces.tsx";
import CustomWidget from "./customWidget.tsx";

const StudioExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const studioStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [data, setData] = useState<AgDataSourcesDefinition | AgDataEngine>();
  const initialState = useMemo<AgReportState<MyRegistry>>(() => {
    return {
      pages: [
        {
          id: "page1",
          widgets: {
            "1": {
              type: "customWidget",
              dataMapping: {
                category: [
                  {
                    id: "medals.country",
                  },
                ],
                value: [
                  {
                    id: "medals.gold",
                    aggregation: "sum",
                  },
                ],
              },
            },
            "2": {
              type: "grid",
              dataMapping: {
                cols: [
                  {
                    id: "medals.country",
                  },
                  {
                    id: "medals.athlete",
                  },
                  {
                    id: "medals.gold",
                    aggregation: "sum",
                  },
                ],
              },
            },
            "3": {
              type: "column-chart-grouped",
              dataMapping: {
                categoryKey: [
                  {
                    id: "medals.year",
                  },
                ],
                valueKey: [
                  {
                    id: "medals.gold",
                    aggregation: "sum",
                  },
                ],
              },
            },
          },
          widgetLayout: {
            "1": {
              xTrack: 0,
              yTrack: 0,
              xSpan: 12,
              ySpan: 36,
            },
            "2": {
              xTrack: 12,
              yTrack: 18,
              xSpan: 12,
              ySpan: 18,
            },
            "3": {
              xTrack: 12,
              yTrack: 0,
              xSpan: 12,
              ySpan: 18,
            },
          },
        },
      ],
      selectedPageId: "page1",
      panels: {
        filters: {
          collapsed: true,
        },
      },
    };
  }, []);
  const widgets = useMemo<
    | AgWidgetsConfig<MyRegistry>
    | ((
        widgets: AgWidgetsConfig<AgDefaultRegistry>,
      ) => AgWidgetsConfig<MyRegistry>)
  >(() => {
    return (widgetConfig: AgWidgetsConfig<MyRegistry>) =>
      createWidgets<MyRegistry>({
        additionalTypes: [
          {
            id: "customWidget",
            icon: {
              url: "https://www.ag-grid.com/studio/images/brandmark.svg",
            },
            label: "Custom Widget",
            dataMapping: {
              category: {
                type: "field",
                supportedRoles: ["category"],
                requires: { cardinality: "many" },
                required: true,
              },
              value: {
                type: "field",
                supportedRoles: ["numeric"],
                requires: { per: "dataMapping.category", cardinality: "one" },
                required: true,
              },
            },
            form: (params: AgWidgetFormParams<CustomDef>) => {
              return params.createDefaults({
                dataMappingItems: [
                  {
                    key: "category",
                    label: "Category",
                  },
                  {
                    key: "value",
                    label: "Value",
                  },
                ],
                supportsCrossHighlight: true,
              });
            },
            comp: CustomWidget,
            defaultSize: {
              width: 400,
              height: 300,
            },
            minSize: {
              width: 200,
              height: 100,
            },
            featureConfig: {
              crossFilter: {
                supportsHighlight: true,
              },
            },
          },
        ],
        menu: [
          {
            label: "Custom",
            widgetIds: ["customWidget"],
          },
          ...widgetConfig.menu,
        ],
      });
  }, []);

  const onApiReady = useCallback((params: AgStudioApiReadyEvent) => {
    fetch("https://www.ag-grid.com/studio/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: any[]) => setData({ sources: [{ id: "medals", data }] }));
  }, []);

  return (
    <div style={containerStyle}>
      <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
        <AgStudio<MyRegistry>
          style={studioStyle}
          className="my-studio-container"
          data={data}
          initialState={initialState}
          widgets={widgets}
          mode={"view"}
          onApiReady={onApiReady}
        />
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <StudioExample />
  </StrictMode>,
);
```

[Live example: Custom Widget with Cross Filter](https://www.ag-grid.com/studio/examples/custom-widgets/cross-filter-widget/reactFunctionalTs/)

To implement cross-filtering from within a custom widget, the cross filter methods can be used from the widget API.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `toggleCrossFilter` | `Function` |  | Set a cross filter. |
| `resetCrossFilter` | `Function` |  | Clear cross filter. |
| `getCrossFilterSelections` | `Function` |  | Get the current cross filter for this widget. |

To support the cross filter highlight behaviour (similar to some of the default charts, e.g. column charts), enable it in the widget definition.

```ts
 const widgetDefinition = {
    // ...
    featureConfig: {
        crossFilter: {
            supportsHighlight: true
        }
    }
 };
```

When this is enabled, the data response will contain two datasets. The original data (`response.results`), and the cross-filtered data (`response.crossFilter`).

## AI Integration

For a custom widget to work with [AI](https://www.ag-grid.com/studio/react/ai/), the `formatShape` and `ai` properties must be defined in the widget definition.

The shape returned by `formatShape` is used to provide the AI with the schema for the `format` property of the widget, and to validate the `format` value the AI sets via state.

## Using AG Grid and AG Charts in Custom Widgets

As well as the built-in AG Grid and AG Charts widgets, it is possible to create your own custom widgets using AG Grid and AG Charts.

To match the AG Grid theming in Studio, use `studioGridTheme` and pass it to the `theme` grid option.

For AG Charts, set the following in chart options (where `api` is the Studio API in the widget params):

```ts
const chartOptions = {
    // ... other options
    theme: getChartTheme(api),
    background: {
        fill: 'transparent'
    }
};
```

> **Note**
>
> Using AG Grid Enterprise or AG Charts Enterprise in a custom widget requires the relevant AG Grid Enterprise or AG Charts Enterprise licence.

## Popups within Custom Widgets

If a custom widget creates its own popup that is anchored outside of the custom widget DOM element (e.g. like a third-party date picker), then the popup element needs to have the 'ag-custom-component-popup' CSS class. This allows Studio to determine correctly when focus is within a widget.

## Sankey Chart

A Sankey diagram visualises flow between two sets of nodes, sized by a numeric measure. This example maps a sales dataset (channel → product category) and supports cross-filtering: clicking a node filters the bar chart alongside it, and holding Ctrl/Cmd adds to the selection.

#### Custom Widget: Sankey

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgStudio, AgStudioRef } from "ag-studio-react";
import {
  AgDataEngine,
  AgDataSourcesDefinition,
  AgDefaultRegistry,
  AgReportState,
  AgStudioApi,
  AgStudioMode,
  AgStudioProperties,
  AgWidgetFormParams,
  AgWidgetsConfig,
  createWidgets,
} from "ag-studio";
import { SOURCE_ID, ordersData } from "./data.tsx";
import { MyRegistry, SankeyDef } from "./interfaces.tsx";
import CustomWidget from "./customWidget.tsx";

const StudioExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const studioStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [data, setData] = useState<AgDataSource>(ordersData);
  const widgets = useMemo<
    | AgWidgetsConfig<MyRegistry>
    | ((
        widgets: AgWidgetsConfig<AgDefaultRegistry>,
      ) => AgWidgetsConfig<MyRegistry>)
  >(() => {
    return (widgetConfig: AgWidgetsConfig<MyRegistry>) =>
      createWidgets<MyRegistry>({
        additionalTypes: [
          {
            id: "sankey-widget",
            label: "Sankey",
            icon: {
              url: "https://www.ag-grid.com/studio/images/brandmark.svg",
            },
            dataMapping: {
              from: {
                type: "field",
                supportedRoles: ["category"],
                requires: { cardinality: "one" },
                required: true,
                aiDescription: "Source nodes of the flow.",
              },
              to: {
                type: "field",
                supportedRoles: ["category"],
                requires: { cardinality: "one" },
                required: true,
                aiDescription: "Target nodes of the flow.",
              },
              value: {
                type: "field",
                supportedRoles: ["numeric"],
                requires: { cardinality: "one" },
                required: true,
                aiDescription: "Numeric measure that sizes each flow.",
              },
            },
            form: (params: AgWidgetFormParams<SankeyDef>) =>
              params.createDefaults({
                dataMappingItems: [
                  { key: "from", label: "From" },
                  { key: "to", label: "To" },
                  { key: "value", label: "Value" },
                ],
              }),
            comp: CustomWidget,
            defaultSize: { width: 600, height: 400 },
            minSize: { width: 300, height: 200 },
          },
        ],
        menu: [
          { label: "Custom", widgetIds: ["sankey-widget"] },
          ...widgetConfig.menu,
        ],
      });
  }, []);
  const initialState = useMemo<AgReportState<MyRegistry>>(() => {
    return {
      panels: {
        filters: { collapsed: true },
        edit: { collapsed: true },
        data: { collapsed: true },
      },
      pages: [
        {
          id: "page-1",
          widgets: {
            sankey: {
              type: "sankey-widget",
              dataMapping: {
                from: [{ id: `${SOURCE_ID}.channel` }],
                to: [{ id: `${SOURCE_ID}.product` }],
                value: [{ id: `${SOURCE_ID}.revenue`, aggregation: "sum" }],
              },
            },
            "revenue-by-channel": {
              type: "bar-chart-grouped",
              dataMapping: {
                categoryKey: [{ id: `${SOURCE_ID}.channel` }],
                valueKey: [{ id: `${SOURCE_ID}.revenue`, aggregation: "sum" }],
              },
              format: {
                crossFilter: "highlight",
                title: { enabled: true, text: "Revenue by Channel" },
              },
            },
          },
          widgetLayout: {
            sankey: { xTrack: 0, yTrack: 0, xSpan: 16, ySpan: 29 },
            "revenue-by-channel": {
              xTrack: 16,
              yTrack: 0,
              xSpan: 8,
              ySpan: 29,
            },
          },
        },
      ],
      selectedPageId: "page-1",
    };
  }, []);

  return (
    <div style={containerStyle}>
      <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
        <AgStudio<MyRegistry>
          style={studioStyle}
          className="my-studio-container"
          data={data}
          widgets={widgets}
          initialState={initialState}
          mode={"view"}
        />
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <StudioExample />
  </StrictMode>,
);
```

[Live example: Custom Widget: Sankey](https://www.ag-grid.com/studio/examples/custom-widgets/sankey-widget/reactFunctionalTs/)

The widget uses two parallel `getData` calls on every refresh - one that ignores cross-filters to keep the full node set stable, and one that respects them to compute which nodes and links to dim. This prevents the chart from reflowing every time a filter changes.

## Choropleth Map

A choropleth map shades geographic regions by a numeric measure. This example renders UK county boundaries using AG Charts' `map-shape` series and supports click-to-cross-filter alongside a regional bar chart.

#### Custom Widget: Choropleth Map

```tsx
"use client";

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgStudio, AgStudioRef } from "ag-studio-react";
import {
  AgDataEngine,
  AgDataSourcesDefinition,
  AgDefaultRegistry,
  AgReportState,
  AgStudioApi,
  AgStudioMode,
  AgStudioProperties,
  AgWidgetFormParams,
  AgWidgetsConfig,
  createWidgets,
} from "ag-studio";
import { SOURCE_ID, storeData } from "./data.tsx";
import { MapDef, MyRegistry } from "./interfaces.tsx";
import CustomWidget from "./customWidget.tsx";

const StudioExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const studioStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [data, setData] = useState<AgDataSource>(storeData);
  const widgets = useMemo<
    | AgWidgetsConfig<MyRegistry>
    | ((
        widgets: AgWidgetsConfig<AgDefaultRegistry>,
      ) => AgWidgetsConfig<MyRegistry>)
  >(() => {
    return (widgetConfig: AgWidgetsConfig<MyRegistry>) =>
      createWidgets<MyRegistry>({
        additionalTypes: [
          {
            id: "map-widget",
            label: "Choropleth Map",
            icon: {
              url: "https://www.ag-grid.com/studio/images/brandmark.svg",
            },
            dataMapping: {
              county: {
                type: "field",
                supportedRoles: ["category"],
                requires: { cardinality: "one" },
                required: true,
                aiDescription:
                  "Category field whose values match UK county names.",
              },
              value: {
                type: "field",
                supportedRoles: ["numeric"],
                requires: { cardinality: "one" },
                required: true,
                aiDescription:
                  "Numeric measure that drives the colour intensity of each county.",
              },
            },
            form: (params: AgWidgetFormParams<MapDef>) =>
              params.createDefaults({
                dataMappingItems: [
                  { key: "county", label: "County" },
                  { key: "value", label: "Value" },
                ],
              }),
            comp: CustomWidget,
            defaultSize: { width: 500, height: 500 },
            minSize: { width: 300, height: 300 },
          },
        ],
        menu: [
          { label: "Custom", widgetIds: ["map-widget"] },
          ...widgetConfig.menu,
        ],
      });
  }, []);
  const initialState = useMemo<AgReportState<MyRegistry>>(() => {
    return {
      panels: {
        filters: { collapsed: true },
        edit: { collapsed: true },
        data: { collapsed: true },
      },
      pages: [
        {
          id: "page-1",
          widgets: {
            map: {
              type: "map-widget",
              dataMapping: {
                county: [{ id: `${SOURCE_ID}.county` }],
                value: [{ id: `${SOURCE_ID}.revenue`, aggregation: "sum" }],
              },
            },
            "revenue-by-region": {
              type: "bar-chart-grouped",
              dataMapping: {
                categoryKey: [{ id: `${SOURCE_ID}.region` }],
                valueKey: [{ id: `${SOURCE_ID}.revenue`, aggregation: "sum" }],
              },
              format: {
                crossFilter: "highlight",
                title: { enabled: true, text: "Revenue by Region" },
              },
            },
          },
          widgetLayout: {
            map: { xTrack: 0, yTrack: 0, xSpan: 14, ySpan: 35 },
            "revenue-by-region": {
              xTrack: 14,
              yTrack: 0,
              xSpan: 10,
              ySpan: 35,
            },
          },
        },
      ],
      selectedPageId: "page-1",
    };
  }, []);

  return (
    <div style={containerStyle}>
      <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
        <AgStudio<MyRegistry>
          style={studioStyle}
          className="my-studio-container"
          data={data}
          widgets={widgets}
          initialState={initialState}
          mode={"view"}
        />
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <StudioExample />
  </StrictMode>,
);
```

[Live example: Custom Widget: Choropleth Map](https://www.ag-grid.com/studio/examples/custom-widgets/map-widget/reactFunctionalTs/)

The colour domain is clamped to the p10-p90 range of the dataset, preventing a single high-value region from compressing all other counties into a narrow band near the minimum. When counties are selected, a background layer renders the full distribution at reduced opacity to preserve geographic context.
