> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trellis.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Visualizations

> Structured chart and table data returned inline in visualization events

When the AI generates a chart or queries tabular data, the API emits a `visualization` event carrying the **complete structured payload** — no separate fetch required. Your application receives everything needed to render the chart or table directly.

<Note>
  Visualizations are only available when using the API with a connected datasource. The `visualization` event is sent before the final `message` event.
</Note>

## Event structure

Every `visualization` event follows the same envelope:

<Tabs>
  <Tab title="SSE">
    ```
    event: visualization
    data: {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", ...}
    ```
  </Tab>

  <Tab title="WebSocket">
    ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {"event": "visualization", "data": {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", ...}}
    ```
  </Tab>
</Tabs>

The `data` object contains all the information you need to render the visualization:

<ResponseField name="id" type="string" required>
  A unique 10-character alphanumeric identifier for this visualization. Use this to deduplicate events if needed.
</ResponseField>

<ResponseField name="type" type="string" required>
  The visualization type. One of `"chart"` or `"table"`.
</ResponseField>

***

## Charts

The AI produces charts when the user explicitly requests one. Seven chart types are supported, grouped into two categories:

* **Axis charts** (`bar`, `bar_horizontal`, `stacked_bar`, `line`, `area`) — display data with X and Y axes. Data points use `{value, label}` format.
* **Segment charts** (`pie`, `donut`) — display proportional parts of a whole. Data points use `{value, text}` format.

<ResponseField name="chart_type" type="string" required>
  One of `"bar"`, `"bar_horizontal"`, `"stacked_bar"`, `"line"`, `"area"`, `"pie"`, or `"donut"`.
</ResponseField>

<ResponseField name="title" type="string">
  Optional display title for the chart.
</ResponseField>

<ResponseField name="data" type="object" required>
  Container for the chart data.

  <Expandable title="data properties">
    <ResponseField name="values" type="array" required>
      Array of data points. Minimum 1 item. See per-chart-type format below.
    </ResponseField>

    <ResponseField name="x_axis_label" type="string">
      Label for the X axis. Required for axis charts (`bar`, `bar_horizontal`, `stacked_bar`, `line`, `area`). Omit for `pie` and `donut`.
    </ResponseField>

    <ResponseField name="y_axis_label" type="string">
      Label for the Y axis. Required for axis charts (`bar`, `bar_horizontal`, `stacked_bar`, `line`, `area`). Omit for `pie` and `donut`.
    </ResponseField>
  </Expandable>
</ResponseField>

<Note>
  Colors are not included in chart data — your application controls the color palette.
</Note>

### Bar chart

Use for comparing quantities across categories. Each item in `values` has:

| Field   | Type     | Required | Description                                 |
| ------- | -------- | -------- | ------------------------------------------- |
| `value` | `number` | Yes      | Numeric value for this data point           |
| `label` | `string` | No       | Category label (max \~25 chars recommended) |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "ab12xy3456",
    "type": "chart",
    "chart_type": "bar",
    "title": "Project Completion by Sector",
    "data": {
      "values": [
        {"label": "Residential", "value": 85.5},
        {"label": "Commercial", "value": 62.3},
        {"label": "Infrastructure", "value": 91.0},
        {"label": "Mixed Use", "value": 74.8}
      ],
      "x_axis_label": "Sector",
      "y_axis_label": "Completion %"
    }
  }
}
```

### Horizontal bar chart

Use for ranking items with long labels — project names, contractor names, risk descriptions. Horizontal orientation prevents label overlap. Each item in `values` has:

| Field   | Type     | Required | Description                       |
| ------- | -------- | -------- | --------------------------------- |
| `value` | `number` | Yes      | Numeric value for this data point |
| `label` | `string` | No       | Category label                    |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "ef56gh7890",
    "type": "chart",
    "chart_type": "bar_horizontal",
    "title": "Top 5 Projects by Budget (AED millions)",
    "data": {
      "values": [
        {"label": "Saadiyat Lagoons Phase 2", "value": 245.0},
        {"label": "The Grove Louvre Residence", "value": 198.5},
        {"label": "Al Raha Beach Tower", "value": 172.3},
        {"label": "Yas Bay Waterfront", "value": 156.8},
        {"label": "Mamsha Al Saadiyat", "value": 134.2}
      ],
      "x_axis_label": "Project",
      "y_axis_label": "Budget (AED M)"
    }
  }
}
```

### Stacked bar chart

Use for showing composition within categories — breakdown by severity, allocation by phase. The data shape is identical to the standard bar chart. Each item in `values` represents a stacked segment.

| Field   | Type     | Required | Description                       |
| ------- | -------- | -------- | --------------------------------- |
| `value` | `number` | Yes      | Numeric value for this data point |
| `label` | `string` | No       | Category label                    |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "ij78kl9012",
    "type": "chart",
    "chart_type": "stacked_bar",
    "title": "Risk Count by Severity",
    "data": {
      "values": [
        {"label": "High Impact", "value": 12},
        {"label": "Medium Impact", "value": 28},
        {"label": "Low Impact", "value": 45},
        {"label": "Closed", "value": 18}
      ],
      "x_axis_label": "Severity",
      "y_axis_label": "Count"
    }
  }
}
```

### Line chart

Use for time-series or sequential data. Each item in `values` has:

| Field   | Type     | Required | Description                       |
| ------- | -------- | -------- | --------------------------------- |
| `value` | `number` | Yes      | Numeric value for this data point |
| `label` | `string` | No       | X-axis label (e.g. month, date)   |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "cd34ef5678",
    "type": "chart",
    "chart_type": "line",
    "title": "Monthly Contract Value (AED)",
    "data": {
      "values": [
        {"label": "Jan", "value": 12500000},
        {"label": "Feb", "value": 14200000},
        {"label": "Mar", "value": 11800000},
        {"label": "Apr", "value": 16400000},
        {"label": "May", "value": 18900000}
      ],
      "x_axis_label": "Month",
      "y_axis_label": "Contract Value (AED)"
    }
  }
}
```

### Area chart

Use for cumulative volume or magnitude over time. Visually similar to a line chart with a filled area beneath the line. Each item in `values` has:

| Field   | Type     | Required | Description                       |
| ------- | -------- | -------- | --------------------------------- |
| `value` | `number` | Yes      | Numeric value for this data point |
| `label` | `string` | No       | X-axis label (e.g. quarter, date) |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "mn90op1234",
    "type": "chart",
    "chart_type": "area",
    "title": "Cumulative Units Sold — Saadiyat Lagoons",
    "data": {
      "values": [
        {"label": "Q1 2025", "value": 48},
        {"label": "Q2 2025", "value": 112},
        {"label": "Q3 2025", "value": 189},
        {"label": "Q4 2025", "value": 267},
        {"label": "Q1 2026", "value": 310}
      ],
      "x_axis_label": "Quarter",
      "y_axis_label": "Units Sold"
    }
  }
}
```

### Pie chart

Use for composition of a whole (7 or fewer segments recommended). Each item in `values` has:

| Field   | Type     | Required | Description                                          |
| ------- | -------- | -------- | ---------------------------------------------------- |
| `value` | `number` | Yes      | Segment size (numeric, not necessarily a percentage) |
| `text`  | `string` | Yes      | Display label for this segment (used for the legend) |

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "gh56ij7890",
    "type": "chart",
    "chart_type": "pie",
    "title": "Budget Allocation by Category",
    "data": {
      "values": [
        {"text": "Construction", "value": 45},
        {"text": "Design", "value": 30},
        {"text": "Permits", "value": 15},
        {"text": "Other", "value": 10}
      ]
    }
  }
}
```

### Donut chart

Same structure as pie, with `"chart_type": "donut"`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "kl78mn9012",
    "type": "chart",
    "chart_type": "donut",
    "title": "Project Status Distribution",
    "data": {
      "values": [
        {"text": "On Track", "value": 35},
        {"text": "At Risk", "value": 12},
        {"text": "Delayed", "value": 7},
        {"text": "Complete", "value": 18}
      ]
    }
  }
}
```

***

## Tables

When the AI executes a SQL query that returns multiple rows, it automatically emits the results as a `visualization` event with `type: "table"`.

<Warning>
  A table visualization is only emitted for queries returning **2 or more rows**. Single-row results are described in the text `message` instead.
</Warning>

<ResponseField name="headers" type="string[]" required>
  Column header names, derived from SQL column names and formatted as title case (e.g. `project_name` becomes `"Project Name"`).
</ResponseField>

<ResponseField name="rows" type="string[][]" required>
  A 2D array of pre-formatted string values. Every cell is a string — numbers include comma separators, floats are rounded to 2 decimal places, and `null` values become empty strings.
</ResponseField>

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "visualization",
  "data": {
    "id": "op90qr1234",
    "type": "table",
    "headers": [
      "Project Name",
      "Status",
      "Budget",
      "Completion %"
    ],
    "rows": [
      ["Al Raha Beach Tower", "Construction", "12,800,000", "73.46"],
      ["Yas Mall Extension", "Design", "8,400,000", "15.00"],
      ["Admin Building Retrofit", "DLP and Project Closeout", "5,200,000", "100.00"],
      ["Marina Gate Phase 2", "Contractor Procurement", "22,100,000", "8.50"],
      ["Green Spine Boulevard", "Construction", "31,600,000", "44.20"]
    ]
  }
}
```

### Cell formatting rules

| Source value               | Formatted string     |
| -------------------------- | -------------------- |
| `5200000` (integer)        | `"5,200,000"`        |
| `73.456` (float)           | `"73.46"`            |
| `None` / `null`            | `""` (empty string)  |
| `True` / `False` (boolean) | `"True"` / `"False"` |
| `"text"` (string)          | `"text"` (unchanged) |

***

## Handling visualizations in your application

Use the `type` field to branch between charts and tables, then `chart_type` to select the appropriate chart renderer.

<Tabs>
  <Tab title="JavaScript">
    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    interface VisualizationData {
      id: string;
      type: "chart" | "table";
      chart_type?: "bar" | "bar_horizontal" | "stacked_bar" | "line" | "area" | "pie" | "donut";
      title?: string;
      data?: {
        values: Array<{ label?: string; text?: string; value: number }>;
        x_axis_label?: string;
        y_axis_label?: string;
      };
      headers?: string[];
      rows?: string[][];
    }

    function handleVisualization(viz: VisualizationData) {
      if (viz.type === "table") {
        renderTable(viz.headers!, viz.rows!);
        return;
      }

      const axisOptions = {
        xLabel: viz.data!.x_axis_label,
        yLabel: viz.data!.y_axis_label,
      };

      switch (viz.chart_type) {
        case "bar":
          renderBarChart(viz.title, viz.data!.values, axisOptions);
          break;
        case "bar_horizontal":
          renderHorizontalBarChart(viz.title, viz.data!.values, axisOptions);
          break;
        case "stacked_bar":
          renderStackedBarChart(viz.title, viz.data!.values, axisOptions);
          break;
        case "line":
          renderLineChart(viz.title, viz.data!.values, axisOptions);
          break;
        case "area":
          renderAreaChart(viz.title, viz.data!.values, axisOptions);
          break;
        case "pie":
        case "donut":
          renderPieChart(viz.title, viz.data!.values, viz.chart_type);
          break;
      }
    }

    // Wire into your SSE event handler
    async function streamChat(message: string, integrationId: string, token: string) {
      const response = await fetch("https://api.trellis.sh/v1/chats", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
          Accept: "text/event-stream",
        },
        body: JSON.stringify({ message, integration_id: integrationId }),
      });

      const reader = response.body!.getReader();
      const decoder = new TextDecoder();
      let buffer = "";
      let currentEvent = "";

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() || "";

        for (const line of lines) {
          if (line.startsWith("event: ")) {
            currentEvent = line.slice(7);
          } else if (line.startsWith("data: ") && currentEvent) {
            const data = JSON.parse(line.slice(6));
            if (currentEvent === "visualization") {
              handleVisualization(data);
            } else if (currentEvent === "message") {
              displayMessage(data.content);
            }
            currentEvent = "";
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import requests
    import json

    def handle_visualization(viz: dict):
        """Dispatch visualization data to the appropriate renderer."""
        if viz["type"] == "table":
            render_table(viz["headers"], viz["rows"])
            return

        chart_type = viz.get("chart_type")
        title = viz.get("title")
        values = viz["data"]["values"]
        axis_kwargs = dict(
            x_label=viz["data"].get("x_axis_label"),
            y_label=viz["data"].get("y_axis_label"),
        )

        if chart_type == "bar":
            render_bar_chart(title, values, **axis_kwargs)
        elif chart_type == "bar_horizontal":
            render_horizontal_bar_chart(title, values, **axis_kwargs)
        elif chart_type == "stacked_bar":
            render_stacked_bar_chart(title, values, **axis_kwargs)
        elif chart_type == "line":
            render_line_chart(title, values, **axis_kwargs)
        elif chart_type == "area":
            render_area_chart(title, values, **axis_kwargs)
        elif chart_type in ("pie", "donut"):
            render_pie_chart(title, values, hole=(chart_type == "donut"))


    def stream_chat(message: str, integration_id: str, token: str):
        """Stream a chat message and handle visualization events."""
        response = requests.post(
            "https://api.trellis.sh/v1/chats",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
                "Accept": "text/event-stream",
            },
            json={"message": message, "integration_id": integration_id},
            stream=True,
        )

        current_event = None
        for line in response.iter_lines(decode_unicode=True):
            if not line:
                continue
            if line.startswith("event: "):
                current_event = line[7:]
            elif line.startswith("data: ") and current_event:
                data = json.loads(line[6:])
                if current_event == "visualization":
                    handle_visualization(data)
                elif current_event == "message":
                    print(data["content"])
                current_event = None
    ```
  </Tab>
</Tabs>
