Render an assistant's analysis charts
Use analysis charts when a user asks an assistant a question such as “How much revenue did each customer generate?” The assistant queries its allowed data and describes a chart. Your React app fetches that query's saved rows and renders them using its chart library.
For a fixed dashboard chart whose query you already know, an API returning data may be simpler. Analysis charts are useful when the question and visualization are chosen during a conversation.
From a question to a chart
- The assistant inspects tables declared in its context and starts a read-only SQL analysis.
- It waits for that analysis to publish a successful result.
- It calls
generate_chartwith the completed analysis ID and chart configuration. - Your app reads that tool result, fetches its stored rows, and renders the chart.
The chart definition carries a reference to the result. It contains neither all the rows nor executable SQL. Reading or reopening a chart does not run the query again.
Enable the assistant capabilities
In the assistant's Python declaration, enable both capabilities when it needs to produce its own analyses:
from vortexify import Agent, AgentCapabilities, Table
agent = Agent(
prompt=(
"Answer questions about sales using the available data. "
"For charts, aggregate to a useful number of rows, wait for analysis "
"completion, and use generate_chart with columns in that result."
),
context=[Table("shop.orders")],
capabilities=AgentCapabilities(sql_analysis=True, chart_generation=True),
)Use an existing table and its actual column names. The runtime only permits analysis of the saved agent version's declared context. A chart must reference an analysis belonging to that agent version. Chart generation alone supplies the chart-description tool; SQL analysis supplies the tools that create the query result.
For example, if shop.orders has customer_id and amount, the assistant could aggregate revenue by customer. It must use the SQL aliases supplied to the analysis tool, then poll that job before generating the chart.
Recognize the chart result
A successful generate_chart result has this shape. The rows and identifiers below are illustrative:
{
"success": true,
"chart": {
"query_id": "ANALYSIS_JOB_ID",
"name": "Revenue by customer",
"config": {
"chartType": "Bar",
"xKey": "customer_id",
"yKeys": ["revenue"]
},
"row_count": 2
}
}| Field | How the app uses it |
|---|---|
query_id | Fetch the saved analysis rows |
name | Chart title or accessible label |
config.chartType | Choose a renderer |
config.xKey | Category, time, or numeric x column |
config.yKeys | One or more series columns |
row_count | Number of rows in the complete analysis result |
Supported types are Bar, StackedBar, GroupedBar, Line, MultiLine, Scatter, and Pie. The names describe presentation intent; your app supplies components, colors, axis formatting, and interactions. For a pie chart choose the intended value series; for scatter plots use numeric x/y columns.
For the example above, fetching the analysis can return rows like these:
[
{"customer_id": "customer_a", "revenue": 1200.0},
{"customer_id": "customer_b", "revenue": 850.0}
]The keys must match the definition. Choose numeric series columns and format dates, currency, and labels in your app.
Find charts in live and saved conversations
During a run, agentManager.tools contains the tool name, execution target, status, and result. Select backend generate_chart calls whose result reports success. A completed tool event can still contain an application-level error, so inspect its payload.
After a conversation is loaded or a turn finishes, agentManager.messages contains saved message content. Backend tool results are stored with type: "tool_result", tool, id, and output. Reconstruct chart cards from those messages as well; live tool state alone would lose charts when the user changes conversations or reloads the page.
Save this helper as src/components/chart-definition.ts. It validates the definition before returning it:
import type { AgentChart } from "@vortexify/react";
export function record(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
export function readChart(value: unknown): AgentChart | null {
if (typeof value === "string") {
try { value = JSON.parse(value); } catch { return null; }
}
const result = record(value);
const chart = record(result?.chart);
const config = record(chart?.config);
const types = ["Bar", "StackedBar", "GroupedBar", "Line", "MultiLine", "Scatter", "Pie"];
if (result?.success !== true || !chart || !config
|| typeof chart.query_id !== "string" || !chart.query_id
|| typeof chart.name !== "string"
|| typeof chart.row_count !== "number"
|| chart.row_count < 0 || chart.row_count > 1000
|| !types.includes(String(config.chartType))
|| typeof config.xKey !== "string"
|| !Array.isArray(config.yKeys) || !config.yKeys.length
|| !config.yKeys.every(key => typeof key === "string")) return null;
return chart as unknown as AgentChart;
}For a live tool use readChart(tool.result). For a saved message, inspect its content's type and tool, then pass content.output to the helper. Use the saved tool-call content.id and the live tool.callId to deduplicate a chart that appears in both places. These are different from the database message ID.
Render both sources through one collection, keyed by the runtime call ID. Save the conversation integration as src/components/ConversationCharts.tsx:
import type { AgentManager } from "@vortexify/react";
import { record, readChart } from "./chart-definition";
import { AnalysisBarChart } from "./AnalysisBarChart";
export function ConversationCharts({ manager }: { manager: AgentManager }) {
const charts = new Map<string, NonNullable<ReturnType<typeof readChart>>>();
for (const message of manager.messages) {
const content = record(message.content);
if (content?.type !== "tool_result" || content.tool !== "generate_chart") continue;
const chart = readChart(content.output);
if (chart && typeof content.id === "string") charts.set(content.id, chart);
}
for (const tool of manager.tools) {
if (tool.target !== "backend" || tool.name !== "generate_chart" || tool.status !== "success") continue;
const chart = readChart(tool.result);
if (chart) charts.set(tool.callId, chart);
}
return <>{[...charts].map(([id, chart]) => (
<AnalysisBarChart key={`${manager.conversationId}:${id}`} chart={chart} />
))}</>;
}Pass the same agentManager instance that consumes the conversation. The component also works after selectConversation loads its saved messages. This example only renders bar charts; other chart definitions get an explicit unsupported-type message until you add their renderer.
Fetch rows and render a bar chart
Install a chart library in your app, for example npm install recharts. Save this component as src/components/AnalysisBarChart.tsx. It handles the Bar result above. Add renderers for other chart types when your app supports them.
import { useEffect, useState } from "react";
import { useVortexify, type AgentChart } from "@vortexify/react";
import {
BarChart, Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis,
} from "recharts";
type Row = Record<string, string | number | null>;
export function AnalysisBarChart({ chart }: { chart: AgentChart }) {
const { client, ready } = useVortexify();
const [rows, setRows] = useState<Row[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [attempt, setAttempt] = useState(0);
useEffect(() => {
setRows(null);
setError(null);
if (!ready) return;
const controller = new AbortController();
let timer: ReturnType<typeof setTimeout> | undefined;
async function load() {
try {
const analysis = await client.getAnalysisData<Row>(
chart.query_id, { limit: 1000, offset: 0 },
{ signal: controller.signal },
);
if (controller.signal.aborted) return;
if (analysis.status === "failed" || analysis.status === "stopped") {
throw new Error(`Analysis ${analysis.status}`);
}
if (!analysis.data) {
timer = setTimeout(() => void load(), 1000);
return;
}
if (analysis.has_more || analysis.result?.result_truncated) {
throw new Error("Use a complete, aggregated result for this chart.");
}
setRows(analysis.data);
} catch (failure) {
if (!controller.signal.aborted) {
setError(failure instanceof Error ? failure.message : "Could not load chart data.");
}
}
}
void load();
return () => { controller.abort(); clearTimeout(timer); };
}, [client, ready, chart.query_id, attempt]);
if (chart.config.chartType !== "Bar") {
return <p>This view supports bar charts. Add a renderer for {chart.config.chartType}.</p>;
}
if (error) return <div role="alert">{error} <button onClick={() => setAttempt(n => n + 1)}>Retry</button></div>;
if (!rows) return <p role="status">Loading chart…</p>;
if (!rows.length) return <p>No rows matched this analysis.</p>;
return <figure aria-label={chart.name}>
<figcaption>{chart.name}</figcaption>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={rows}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={chart.config.xKey} />
<YAxis />
<Tooltip />
{chart.config.yKeys.map((key, index) => (
<Bar key={key} dataKey={key} fill={["#7561e8", "#28a892", "#db9958"][index % 3]} />
))}
</BarChart>
</ResponsiveContainer>
<details><summary>View chart data</summary>
<table><thead><tr><th>{chart.config.xKey}</th>
{chart.config.yKeys.map(key => <th key={key}>{key}</th>)}
</tr></thead><tbody>{rows.map((row, index) => <tr key={index}>
<td>{row[chart.config.xKey]}</td>
{chart.config.yKeys.map(key => <td key={key}>{row[key]}</td>)}
</tr>)}</tbody></table>
</details>
</figure>;
}Place the component inside VortexifyProvider and pass a definition returned by readChart. Match the row type and numeric formatting to the query output. The data table provides an alternative to the visual chart.
For stacked bars, share a stackId across series. For lines, render LineChart/Line using the same keys. Scatter and pie charts need their own data mapping rather than changing only a label.
Result size, permissions, and failures
The server rejects chart generation for truncated analyses or complete results larger than 1,000 rows. Aggregate or intentionally limit the SQL before creating the chart. A limited query answers a limited question—for example, label a top-ten chart accordingly.
getAnalysisData(queryId, page?, requestOptions?) reads the stored artifact using authenticated project scope. Pages support limit from 1 to 1,000 and an offset that is a multiple of the limit. has_more describes remaining stored rows; result.result_truncated describes rows omitted when the query result was originally produced. Fetching another page cannot recover an originally truncated result.
Pending analyses have no published data yet. Wait on the same ID; submission of another query creates different work. For a failed job or rejected chart definition, display the failure and let the user refine the question. Permissions and project context still apply when reopening a saved chart.
Inspect it in the platform
Open the agent's Conversations view to inspect saved messages and tool payloads. Use the analysis job ID to inspect execution results. The current conversation browser shows tool evidence; your app owns the chart renderer described here.
Platform screenshot placeholder: A saved
generate_charttool result in Conversations, paired with the app's chart and its “View chart data” table. Show the matching analysis ID.
See conversations and actions for managing history and agent capabilities for backend analysis tools.