Docs

AI-Powered Chart

Use ChartAIController to let users build and update Highcharts visualizations from the application database using natural language.

ChartAIController creates and updates a Chart (Vaadin’s interactive charting component) visualization based on natural-language requests. Backed by a DatabaseProvider, 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.

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.

Basic Usage

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

Source code
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.

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

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:

Source code
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.

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.

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:

Source code
Java
ChartAIController controller = new ChartAIController(chart, databaseProvider);
orchestrator.reconnect(provider)
        .withController(controller)
        .apply();

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():

Source code
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.

Combining with Dashboard

ChartAIController manages a single chart, but you can combine several of them with the Dashboard 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.

Updated –