Skip to main content
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.
Visualizations are only available when using the API with a connected datasource. The visualization event is sent before the final message event.

Event structure

Every visualization event follows the same envelope:
event: visualization
data: {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", ...}
The data object contains all the information you need to render the visualization:
id
string
required
A unique 10-character alphanumeric identifier for this visualization. Use this to deduplicate events if needed.
type
string
required
The visualization type. One of "chart" or "table".

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.
chart_type
string
required
One of "bar", "bar_horizontal", "stacked_bar", "line", "area", "pie", or "donut".
title
string
Optional display title for the chart.
data
object
required
Container for the chart data.
Colors are not included in chart data — your application controls the color palette.

Bar chart

Use for comparing quantities across categories. Each item in values has:
FieldTypeRequiredDescription
valuenumberYesNumeric value for this data point
labelstringNoCategory label (max ~25 chars recommended)
{
  "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:
FieldTypeRequiredDescription
valuenumberYesNumeric value for this data point
labelstringNoCategory label
{
  "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.
FieldTypeRequiredDescription
valuenumberYesNumeric value for this data point
labelstringNoCategory label
{
  "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:
FieldTypeRequiredDescription
valuenumberYesNumeric value for this data point
labelstringNoX-axis label (e.g. month, date)
{
  "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:
FieldTypeRequiredDescription
valuenumberYesNumeric value for this data point
labelstringNoX-axis label (e.g. quarter, date)
{
  "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:
FieldTypeRequiredDescription
valuenumberYesSegment size (numeric, not necessarily a percentage)
textstringYesDisplay label for this segment (used for the legend)
{
  "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".
{
  "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".
A table visualization is only emitted for queries returning 2 or more rows. Single-row results are described in the text message instead.
headers
string[]
required
Column header names, derived from SQL column names and formatted as title case (e.g. project_name becomes "Project Name").
rows
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.
{
  "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 valueFormatted 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.
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 = "";
      }
    }
  }
}