> Markdown version of [AI-Generated Charts](https://vaadin.com/docs/next/flow/ai-support/ai-powered-chart). Section index: [llms.txt](https://vaadin.com/docs/next/flow/llms.txt)

# AI-Powered Chart (since V25.2)

`ChartAIController` creates and updates a [`Chart`](https://vaadin.com/docs/next/components/charts.md) (Vaadin’s interactive charting component) visualization based on natural-language requests. Backed by a [`DatabaseProvider`](https://vaadin.com/docs/next/flow/ai-support/controllers.md#database-provider), the controller lets the LLM inspect the database schema, write SQL queries for one or more series, and update the Highcharts configuration independently of the data.

> **Note: Commercial Feature**
>
> A commercial Vaadin subscription is required to use AI-Powered Chart in your project.
>
> - [Start Free Trial](https://vaadin.com/trial)
>
> - [See Pricing](https://vaadin.com/pricing)

Data and configuration are kept separate: series data comes from SQL queries, while visual appearance comes from the configuration. Both updates are applied together at the end of the LLM turn, so the user never sees a half-updated chart.

The LLM can read the chart’s current state through a state tool: the SQL queries and the configuration it has set itself. Nothing in the state comes from the query results: the series, x-axis categories, and other values the chart builds from the rows stay in the application. See [Database Provider](https://vaadin.com/docs/next/flow/ai-support/controllers.md#database-provider).

## <a id="basic-usage"></a>Basic Usage

Create a `Chart`, construct a controller, and wire it to the orchestrator:

```java
Chart chart = new Chart();
MessageInput messageInput = new MessageInput();

DatabaseProvider databaseProvider = new JdbcDatabaseProvider(dataSource);
ChartAIController controller = new ChartAIController(chart, databaseProvider);

AIOrchestrator.builder(provider, systemPrompt)
        .withInput(messageInput)
        .withController(controller)
        .build();

add(messageInput, chart);
```

Example prompts:

- "Plot monthly revenue for the last year as a column chart."

- "Show revenue by region as a pie chart."

- "Compare 2025 and 2026 quarterly sales side-by-side with a legend."

- "Turn this into an area chart and add a 'Revenue (USD)' y-axis title."

> **Tip: Built-In Workflow Instructions**
>
> The controller already informs the LLM of the workflow it needs. You can focus your own system prompt on application-specific behavior. Where the system prompt conflicts with a workflow step, the system prompt takes precedence.

> **Note: Provider Compatibility**
>
> `ChartAIController` does not support OpenAI’s strict tool-calling mode. Strict mode is off by default in both LangChain4j and Spring AI; only users who explicitly opt in are affected.

## <a id="query-and-configuration-validation"></a>Query and Configuration Validation

Before queueing an update, the controller validates the LLM’s work within the turn: series queries are executed against the database, and configuration JSON is parsed eagerly. Invalid updates are rejected and never reach the chart.

The LLM can learn why a rejected update failed (since V25.3): configuration parse errors are relayed as-is, since the LLM authored the configuration. Query errors are relayed only if your `DatabaseProvider` implementation opts in by throwing a `ToolException` from `executeQuery()`; any other exception is logged and replaced with a generic error message. See [Tool Error Handling](https://vaadin.com/docs/next/flow/ai-support/controllers.md#tool-error-handling).

## <a id="persisting-chart-state"></a>Persisting Chart State

`ChartState` captures what’s needed to show the same chart again, for example in a new session. It holds the following:

- `queries()` — the SQL queries that populate the chart’s series.

- `configuration()` — the Highcharts configuration the chart was rendered with. It can hold values from the query results, such as the x-axis categories and series names.

- `llmConfiguration()` — the part of the configuration the LLM has set. After a restore, it’s the configuration the LLM sees.

Capture the state with `getState()`, which returns `null` while the chart has no queries, and apply it with `restoreState()`. Register a state change listener to persist the state automatically after each successful AI request:

```java
controller.addStateChangeListener(state ->
        sessionStore.save(sessionId, state));

// Restore on a new session
ChartState saved = sessionStore.load(sessionId);
if (saved != null) {
    controller.restoreState(saved);
}
```

`sessionStore` here is a placeholder for your own storage — a database table, a file, a `VaadinSession` attribute, or whatever fits your application.

`restoreState()` applies the configuration and runs the queries again, so the restored chart shows the current data rather than the data from when the state was saved. Listeners do not fire when `restoreState()` is called. The current state is also automatically included in session serialization, so no extra save/restore code is needed for in-session persistence.

### <a id="storing-the-state"></a>Storing the State

`ChartState` is serializable, so you can store it as a whole with Java serialization. If you store its parts instead, store every part and create the state from them again with the three-argument constructor, `new ChartState(queries, configuration, llmConfiguration)`.

After a restore, the state tool returns the LLM configuration to the LLM as is, so it must not hold values from the query results. Pass the one `llmConfiguration()` returned, not a copy of the chart configuration.

If you have only the queries and the configuration, for example from storage set up before `ChartState` had the LLM configuration, create the state with the two-argument constructor, `new ChartState(queries, configuration)`. The LLM configuration is then derived from the configuration without its series and x-axis categories. The LLM sees the rest of the configuration, including anything the application set on the chart itself. A `ChartState` stored with Java serialization before the LLM configuration was added gets its LLM configuration derived the same way when it’s deserialized.

## <a id="reconnecting-after-deserialization"></a>Reconnecting after Deserialization

`ChartAIController` is not serializable. After session restore, create a new controller, pass it to `reconnect()` together with the new provider, and optionally re-apply the saved state:

```java
ChartAIController controller = new ChartAIController(chart, databaseProvider);
orchestrator.reconnect(provider)
        .withController(controller)
        .apply();
```

## <a id="custom-data-conversion"></a>Custom Data Conversion

`DefaultDataConverter` maps SQL result rows to Highcharts series automatically. To take full control — for example, to post-process rows, merge data from multiple queries into a single series, or apply custom formatting — implement `DataConverter` and register it with `setDataConverter()`:

```java
public class CurrencyDataConverter implements DataConverter {

    @Override
    public List<Series> convertToSeries(List<Map<String, Object>> data) {
        DataSeries series = new DataSeries();
        for (Map<String, Object> row : data) {
            String name = (String) row.get("category");
            Number value = (Number) row.get("value");
            series.add(new DataSeriesItem(name, roundToCents(value)));
        }
        return List.of(series);
    }
}

controller.setDataConverter(new CurrencyDataConverter());
```

The converter receives the raw rows from `DatabaseProvider.executeQuery()` and returns one or more `Series` instances.

## <a id="combining-with-dashboard"></a>Combining with Dashboard

`ChartAIController` manages a single chart, but you can combine several of them with the [`Dashboard`](https://vaadin.com/docs/next/components/dashboard.md) component to build an AI-generated dashboard that users arrange themselves and whose layout and chart state can be saved across sessions. Each dashboard widget hosts its own `Chart` and `ChartAIController` instance; persist each controller’s state alongside the dashboard’s own layout state to let users return to the same chart set and layout in a later session.
