Data visualization requires two skills: picking the right chart type for the data, and implementing it correctly with accessibility, responsive sizing, and performance for large datasets. Claude Code generates chart components with proper ARIA labels, responsive containers, and the D3 patterns for custom visualizations that library components can’t produce.
Recharts for React
Build a dashboard with: line chart (revenue over time),
bar chart (orders by category), and a pie/donut chart (traffic sources).
All charts should be responsive and show tooltips on hover.
// components/Dashboard/RevenueChart.tsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { format } from 'date-fns';
interface RevenueDataPoint {
date: string;
revenue: number;
refunds: number;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (!active || !payload?.length) return null;
return (
<div role="tooltip" aria-label="Revenue details" className="chart-tooltip">
<p className="label">{format(new Date(label), 'MMM d, yyyy')}</p>
{payload.map((entry: any) => (
<p key={entry.name} style={{ color: entry.color }}>
{entry.name}: ${entry.value.toLocaleString()}
</p>
))}
</div>
);
};
export function RevenueChart({ data }: { data: RevenueDataPoint[] }) {
const totalRevenue = data.reduce((sum, d) => sum + d.revenue, 0);
return (
// role="img" + aria-label makes the chart accessible to screen readers
<figure role="img" aria-label={`Revenue line chart showing $${totalRevenue.toLocaleString()} total over ${data.length} days`}>
<figcaption className="sr-only">
Revenue trend from {data[0]?.date} to {data[data.length - 1]?.date}.
Total revenue: ${totalRevenue.toLocaleString()}.
</figcaption>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
tickFormatter={(date) => format(new Date(date), 'MMM d')}
tick={{ fontSize: 12 }}
/>
<YAxis
tickFormatter={(value) => `$${(value / 1000).toFixed(0)}k`}
tick={{ fontSize: 12 }}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
<Line
type="monotone"
dataKey="revenue"
stroke="#2563eb"
strokeWidth={2}
dot={false}
name="Revenue"
activeDot={{ r: 6 }}
/>
<Line
type="monotone"
dataKey="refunds"
stroke="#ef4444"
strokeWidth={2}
dot={false}
name="Refunds"
strokeDasharray="4 4"
/>
</LineChart>
</ResponsiveContainer>
</figure>
);
}
// components/Dashboard/CategoryBarChart.tsx
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
const COLORS = ['#2563eb', '#7c3aed', '#059669', '#d97706', '#dc2626'];
export function CategoryBarChart({ data }: { data: { category: string; orders: number }[] }) {
const sortedData = [...data].sort((a, b) => b.orders - a.orders);
return (
<figure role="img" aria-label="Orders by category bar chart">
<figcaption className="sr-only">
Bar chart showing order counts by category.
Top category: {sortedData[0]?.category} with {sortedData[0]?.orders} orders.
</figcaption>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={sortedData} layout="vertical" margin={{ left: 80 }}>
<XAxis type="number" tick={{ fontSize: 12 }} />
<YAxis dataKey="category" type="category" tick={{ fontSize: 12 }} width={80} />
<Tooltip formatter={(value: number) => [`${value} orders`, 'Orders']} />
<Bar dataKey="orders" radius={[0, 4, 4, 0]}>
{sortedData.map((_, index) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</figure>
);
}
D3.js Custom Visualization
Build a force-directed graph showing relationships between our products
and the customers who buy them together.
// components/ProductRelationshipGraph.tsx
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
interface Node extends d3.SimulationNodeDatum {
id: string;
label: string;
type: 'product' | 'customer';
value: number; // Orders or revenue
}
interface Link extends d3.SimulationLinkDatum<Node> {
strength: number; // How often bought together
}
export function ProductRelationshipGraph({ nodes, links }: { nodes: Node[]; links: Link[] }) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
const svg = d3.select(svgRef.current!);
const { width, height } = svgRef.current!.getBoundingClientRect();
svg.selectAll('*').remove(); // Clear on re-render
const g = svg.append('g');
// Zoom and pan
svg.call(d3.zoom<SVGSVGElement, unknown>().on('zoom', ({ transform }) => {
g.attr('transform', transform);
}));
// Force simulation
const simulation = d3.forceSimulation<Node>(nodes)
.force('link', d3.forceLink<Node, Link>(links)
.id(d => d.id)
.distance(d => 80 / d.strength) // Stronger links → shorter distance
.strength(d => d.strength)
)
.force('charge', d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide(30));
// Links
const link = g.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke', '#94a3b8')
.attr('stroke-opacity', 0.5)
.attr('stroke-width', d => Math.sqrt(d.strength) * 2);
// Nodes
const node = g.append('g')
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', d => Math.sqrt(d.value) + 5)
.attr('fill', d => d.type === 'product' ? '#2563eb' : '#7c3aed')
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.attr('tabindex', 0)
.attr('aria-label', d => `${d.type}: ${d.label}`)
.call(d3.drag<SVGCircleElement, Node>()
.on('start', (event, d) => {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on('end', (event, d) => {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
})
);
// Labels
const label = g.append('g')
.selectAll('text')
.data(nodes)
.join('text')
.text(d => d.label)
.attr('font-size', 10)
.attr('dy', d => -Math.sqrt(d.value) - 8)
.attr('text-anchor', 'middle')
.attr('fill', '#374151');
// Update positions on tick
simulation.on('tick', () => {
link
.attr('x1', d => (d.source as Node).x!)
.attr('y1', d => (d.source as Node).y!)
.attr('x2', d => (d.target as Node).x!)
.attr('y2', d => (d.target as Node).y!);
node.attr('cx', d => d.x!).attr('cy', d => d.y!);
label.attr('x', d => d.x!).attr('y', d => d.y!);
});
return () => { simulation.stop(); };
}, [nodes, links]);
return (
<figure role="img" aria-label="Force-directed graph showing product relationships">
<figcaption className="sr-only">
Interactive graph with {nodes.length} nodes and {links.length} connections.
Drag nodes to reposition. Scroll to zoom.
</figcaption>
<svg
ref={svgRef}
width="100%"
height={500}
style={{ background: '#f8fafc', borderRadius: 8 }}
/>
</figure>
);
}
Real-Time Dashboard Updates
The dashboard should update live as new orders come in.
Use WebSocket for the order count, polling for the revenue chart.
// hooks/useLiveMetrics.ts
import { useState, useEffect, useCallback } from 'react';
export function useLiveOrderCount() {
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
const ws = new WebSocket(`${process.env.NEXT_PUBLIC_WS_URL}/metrics`);
ws.onmessage = (event) => {
const { type, data } = JSON.parse(event.data);
if (type === 'order_count_update') {
setCount(data.count);
}
};
ws.onerror = () => {
// Fall back to polling on WebSocket error
};
return () => ws.close();
}, []);
return count;
}
export function useRevenueData(days = 30) {
const [data, setData] = useState<RevenueDataPoint[]>([]);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
const res = await fetch(`/api/metrics/revenue?days=${days}`);
const newData = await res.json();
setData(newData);
setLoading(false);
}, [days]);
useEffect(() => {
fetchData();
// Refresh every 5 minutes
const interval = setInterval(fetchData, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [fetchData]);
return { data, loading, refresh: fetchData };
}
For building the API endpoints that feed these charts, see the API design guide. For the real-time WebSocket connections that power live updates, see the WebSocket scaling guide. The Claude Skills 360 bundle includes data visualization skill sets for Recharts, D3, and accessible dashboard patterns. Start with the free tier to try chart component generation.