Building Generative Charts
Jan 3, 2026 • 20 min read
Library: Recharts + Vercel AI SDK
One of the "Killer Apps" of Generative UI is dynamic data visualization. Instead of a static dashboard, users can ask: "Compare Apple and Microsoft revenue for the last 5 years" and get an interactive chart instantly.
1. The Architecture: Don't Let the LLM Guess Numbers
CRITICAL WARNING: Never ask an LLM to "generate a chart of Apple's stock price". It doesn't know. It will hallucinate numbers.
Instead, follow this 3-step pipeline:
- Tool Call: LLM decides to view a chart. It calls
get_stock_history(symbol="AAPL"). - Data Fetching: Your Server Action fetches real data from an API (e.g. Yahoo Finance).
- Component Rendering: You return a
<StockChart data={realData} />to the client.
2. The Tool Definition
We use a generator function to yield a specific component.
// actions.tsx
return {
show_stock_price: {
description: 'Show stock price history',
parameters: z.object({ symbol: z.string() }),
generate: async function* ({ symbol }) {
// 1. Show Skeleton instantly
yield <BotCard><StockSkeleton /></BotCard>;
try {
// 2. Fetch Real Data
const data = await fetchYahooFinance(symbol);
// 3. Render Final Chart (Recharts)
return (
<BotCard>
<StockChart data={data} symbol={symbol} />
</BotCard>
);
} catch (err) {
return <BotCard>Failed to load data for {symbol}</BotCard>;
}
}
}
}3. The Chart Component (Recharts)
We wrap Recharts to make it beautiful and responsive.
'use client';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
export function StockChart({ data, symbol }) {
return (
<div className="h-[300px] w-full mt-4">
<h3 className="text-lg font-bold mb-2">{symbol} Pricing</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<defs>
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#8884d8" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="date" tick={{fontSize: 10}} />
<YAxis tick={{fontSize: 10}} domain={['auto', 'auto']} />
<Tooltip
contentStyle={{ borderRadius: '8px', border: 'none', background: '#333' }}
itemStyle={{ color: '#fff' }}
/>
<Area
type="monotone"
dataKey="value"
stroke="#8884d8"
fillOpacity={1}
fill="url(#colorValue)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}4. Advanced: Dynamic "Compare" Charts
What if the user asks "Compare AAPL and MSFT"?
Strategy: The LLM calls a different tool: compare_stocks(symbols: string[]).
compare_stocks: {
parameters: z.object({ symbols: z.array(z.string()) }),
generate: async function* ({ symbols }) {
yield <Skeleton count={symbols.length} />;
// Fetch all in parallel
const datasets = await Promise.all(
symbols.map(s => fetchYahooFinance(s))
);
// Transform for Recharts (merge by date)
const mergedData = mergeDatasets(datasets);
return <MultiLineChart data={mergedData} keys={symbols} />;
}
}Conclusion
Generative Charts are the "Wow" factor of AI apps. By separating Intent (LLM) from Data (API) and Presentation (Recharts), you ensure accuracy and beauty.