# Scatter and bubble charts in your AI client

> The words that change a scatter or bubble chart in Bonnard, and the options and limits the product enforces.

Updated: 2026-10-12

Bonnard draws a scatter chart to plot one measure against another, one point per row, and a bubble chart when a third measure sets each point's size.

[Image: A chat with the question "Footfall against conversion for each store last quarter, as a scatter plot.", three tool calls, and the scatter chart Bonnard drew: 16 stores in Q3 2026, conversion falling as footfall rises]

The examples use Acme, a demo retailer that reports in pounds. Each variant is a few words in the question. A follow-up changes a chart already drawn.

## How an agent draws it

The chat above ran three tools, in this order:

1. `explore_schema` finds the tables and their columns: here `weekly_store_sales`, with `week_start`, `store`, `footfall` and `orders`.
2. `sql_query` runs a read-only query, so the agent can check the numbers first.
3. `render_chart` takes the SQL and the chart options. It runs the query and returns the chart the chat draws.

The `render_chart` call behind the chart above:

```json
{
  "charts": [
    {
      "sql": "SELECT store, ROUND(AVG(footfall)) AS footfall, SUM(orders) * 1.0 / SUM(footfall) AS conversion FROM weekly_store_sales WHERE week_start >= DATE '2026-06-29' GROUP BY store",
      "chartType": "scatter",
      "title": "Footfall against conversion by store, Q3 2026",
      "encode": { "x": "footfall", "y": "conversion" },
      "fields": [
        { "name": "footfall", "label": "Weekly footfall", "format": "number" },
        { "name": "conversion", "label": "Conversion", "format": "percent" }
      ]
    }
  ]
}
```

A call takes up to 10 charts, shown as a carousel. Each example below shows its one entry. A connected agent reads the full option reference with `explore_docs`, topic `charts/scatter`.

## Bubbles

```prompt
Footfall against conversion for each store last quarter, with bubbles sized by revenue.
```

[Image: The same stores as bubbles, each sized by its revenue for the quarter, London the largest]

```json
{
  "sql": "SELECT store, ROUND(AVG(footfall)) AS footfall, SUM(orders) * 1.0 / SUM(footfall) AS conversion, SUM(revenue) / 1000 AS revenue FROM weekly_store_sales WHERE week_start >= DATE '2026-06-29' GROUP BY store",
  "chartType": "scatter",
  "title": "Footfall against conversion, sized by revenue, Q3 2026",
  "encode": { "x": "footfall", "y": "conversion", "size": "revenue" },
  "fields": [
    { "name": "footfall", "label": "Weekly footfall", "format": "number" },
    { "name": "conversion", "label": "Conversion", "format": "percent" },
    { "name": "revenue", "label": "Revenue (£k)", "format": "number" }
  ]
}
```

A third measure sets each point's size, scaled to the largest value on the chart. The chart names that measure under its title.

## Colour by group

```prompt
Footfall against conversion for each store last quarter, coloured by region.
```

[Image: The same stores in one colour per region, with a legend under the chart]

```json
{
  "sql": "SELECT store, region, ROUND(AVG(footfall)) AS footfall, SUM(orders) * 1.0 / SUM(footfall) AS conversion FROM weekly_store_sales WHERE week_start >= DATE '2026-06-29' GROUP BY store, region",
  "chartType": "scatter",
  "title": "Footfall against conversion by region, Q3 2026",
  "encode": { "x": "footfall", "y": "conversion", "series": "region" },
  "fields": [
    { "name": "footfall", "label": "Weekly footfall", "format": "number" },
    { "name": "conversion", "label": "Conversion", "format": "percent" }
  ]
}
```

A text column splits the points into groups, one colour each, and the legend lists the largest group first. The store, the other text column, names each point in its tooltip.

## Quadrants

```prompt
Footfall against conversion for each store last quarter, split at the median footfall and our 4% conversion target.
```

[Image: The stores split into four by a vertical line at the median footfall and a horizontal line at the 4% target]

```json
{
  "sql": "SELECT store, ROUND(AVG(footfall)) AS footfall, SUM(orders) * 1.0 / SUM(footfall) AS conversion FROM weekly_store_sales WHERE week_start >= DATE '2026-06-29' GROUP BY store",
  "chartType": "scatter",
  "title": "Stores against median footfall and the conversion target",
  "encode": { "x": "footfall", "y": "conversion" },
  "fields": [
    { "name": "footfall", "label": "Weekly footfall", "format": "number" },
    { "name": "conversion", "label": "Conversion", "format": "percent" }
  ],
  "reference": [
    { "y": 0.04, "label": "Target 4%" },
    { "x": "median", "label": "Median footfall" }
  ]
}
```

Only a scatter takes a vertical line. With one vertical and one horizontal line the points fall into quadrants, each line at a number, the average or the median.

## Axis ranges

```prompt
Footfall against conversion for each store last quarter, with footfall from zero and the conversion axis fitted to the data.
```

[Image: The same stores with the footfall axis from zero and the conversion axis from 2.5%]

```json
{
  "sql": "SELECT store, ROUND(AVG(footfall)) AS footfall, SUM(orders) * 1.0 / SUM(footfall) AS conversion FROM weekly_store_sales WHERE week_start >= DATE '2026-06-29' GROUP BY store",
  "chartType": "scatter",
  "title": "Footfall against conversion by store, Q3 2026",
  "encode": { "x": "footfall", "y": "conversion" },
  "fields": [
    { "name": "footfall", "label": "Weekly footfall", "format": "number" },
    { "name": "conversion", "label": "Conversion", "format": "percent" }
  ],
  "axes": { "x": { "min": 0 }, "y": { "min": "auto" } }
}
```

By default the x-axis fits the data and the y-axis starts at zero. Each end of either axis takes a number, or a fit to the data.

## Options and limits

Table: What a scatter chart takes

| Ask for | What Bonnard draws |
| --- | --- |
| "as a scatter plot" or "footfall against conversion" | One point per row, from two numeric columns |
| "sized by revenue" | A bubble chart, a third measure setting each point's size |
| "coloured by region" | One colour per group, with a legend |
| "France in amber" | A named colour per group, from eight colours and grey |
| "make the UK stand out" | Up to three groups in colour, the rest in grey |
| "the UK as triangles" | A point shape per group |
| "split at the median" | Vertical and horizontal lines that make quadrants |
| "a target line" or "the average" | Reference lines and bands, up to six |
| "start footfall at zero" | A fixed range on either axis, or one fitted to the data |
| "legend at the top" | The legend at the top or bottom, or hidden |

- One row is one point, and the chart sums nothing: the query does any grouping.
- Both axes take numeric columns only, and the x-axis is always a number line.
- The colour split takes a text column and ignores a numeric one. The size takes a numeric column.
- A chart query returns up to 5,000 rows by default, so up to 5,000 points.
- Points turn more translucent past 150 and again past 400, so overlaps read as density.
- A fixed axis end wins over a reference line, which is then not drawn.
- A scatter takes no trend line or moving average: those are for line, bar and area charts.
- Labels and axis titles run to 60 characters.
- A scatter chart draws no data labels. No chart draws a log scale, custom hex colours or zoom.
