opncrafter
Module 6 of 10: Generative UI

Building Generative Charts

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:

  1. Tool Call: LLM decides to view a chart. It calls get_stock_history(symbol="AAPL").
  2. Data Fetching: Your Server Action fetches real data from an API (e.g. Yahoo Finance).
  3. 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.