| name | charts |
| description | Charts and data visualization with Recharts — bar, line, area, pie/donut, scatter, and composed/radar/radial variants. Plus reference lines/areas (targets, thresholds), brush (zoom), synced charts, click drill-down, custom theme-aware tooltips, sparklines in stat cards, axis formatters (currency/percentages/dates), responsive sizing, accessibility. Use whenever the user wants graphs, charts, dashboards, analytics, KPIs, trends, or any visual representation of numeric or temporal data. |
Charts (Recharts)
Recharts is preinstalled. Composable React + D3 charts. Charts are built by nesting components inside one of LineChart, BarChart, AreaChart, PieChart, ScatterChart, RadarChart, RadialBarChart, or ComposedChart.
Project rules
- Always wrap charts in
<ResponsiveContainer width="100%" height={N}> — without it the chart renders at 0px height. The parent must have a defined width; height must be a number, not a percentage.
- Always use theme tokens for colors:
var(--chart-1) through var(--chart-5). Never hardcode hex values — they break dark mode and look generic.
- Always render a custom tooltip so it matches the app's card/border styling (default tooltip is white-on-white in dark mode).
- Always handle empty data — render a skeleton or empty state when
data.length === 0. An empty chart shows axes and confuses users.
Data shape
Recharts expects an array of objects. Each object is one data point; each numeric property can be plotted as a series.
const data = [
{ month: "Jan", revenue: 4000, expenses: 2400 },
{ month: "Feb", revenue: 3000, expenses: 1398 },
{ month: "Mar", revenue: 2000, expenses: 9800 },
]
dataKey props map object properties to chart components: dataKey="revenue", or dataKey={(entry) => entry.revenue - entry.expenses} for derived values (memoize the function with useCallback — see references/advanced.md).
Bar chart
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis dataKey="month" className="text-xs fill-muted-foreground" />
<YAxis className="text-xs fill-muted-foreground" />
<Tooltip content={<ChartTooltip />} />
<Legend />
<Bar dataKey="revenue" fill="var(--chart-1)" radius={[4, 4, 0, 0]} />
<Bar dataKey="expenses" fill="var(--chart-2)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
Stacked bars — share a stackId, only the top bar gets radius:
<Bar dataKey="completed" stackId="a" fill="var(--chart-1)" />
<Bar dataKey="pending" stackId="a" fill="var(--chart-2)" radius={[4, 4, 0, 0]} />
Horizontal bar — flip layout and swap axis types:
<BarChart layout="vertical" data={data}>
<XAxis type="number" />
<YAxis type="category" dataKey="name" />
<Bar dataKey="value" fill="var(--chart-1)" />
</BarChart>
Line chart
import { LineChart, Line } from "recharts"
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis dataKey="month" className="text-xs fill-muted-foreground" />
<YAxis className="text-xs fill-muted-foreground" />
<Tooltip content={<ChartTooltip />} />
<Line type="monotone" dataKey="value" stroke="var(--chart-1)" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="target" stroke="var(--chart-2)" strokeWidth={2} strokeDasharray="5 5" />
</LineChart>
</ResponsiveContainer>
type options: "monotone" (smooth, default for trend data), "linear", "step", "natural". Set dot={false} for dense series, connectNulls to bridge gaps.
Area chart
import { AreaChart, Area } from "recharts"
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis dataKey="month" className="text-xs fill-muted-foreground" />
<YAxis className="text-xs fill-muted-foreground" />
<Tooltip content={<ChartTooltip />} />
<Area type="monotone" dataKey="value" stroke="var(--chart-1)" fill="var(--chart-1)" fillOpacity={0.2} />
</AreaChart>
</ResponsiveContainer>
Stacked areas — share a stackId:
<Area type="monotone" dataKey="ios" stackId="1" stroke="var(--chart-1)" fill="var(--chart-1)" fillOpacity={0.4} />
<Area type="monotone" dataKey="android" stackId="1" stroke="var(--chart-2)" fill="var(--chart-2)" fillOpacity={0.4} />
Gradient fill — define once in <defs>, reference by URL:
<AreaChart data={data}>
<defs>
<linearGradient id="fillValue" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--chart-1)" stopOpacity={0.6} />
<stop offset="95%" stopColor="var(--chart-1)" stopOpacity={0} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="value" stroke="var(--chart-1)" fill="url(#fillValue)" />
</AreaChart>
Pie / donut chart
import { PieChart, Pie, Cell } from "recharts"
const COLORS = [
"var(--chart-1)",
"var(--chart-2)",
"var(--chart-3)",
"var(--chart-4)",
"var(--chart-5)",
]
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={data}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{data.map((_, i) => <Cell key={i} fill={COLORS[i % COLORS.length]} />)}
</Pie>
<Tooltip content={<ChartTooltip />} />
<Legend />
</PieChart>
</ResponsiveContainer>
innerRadius={0} for a solid pie, > 0 for a donut. Cell will be replaced by the shape prop in Recharts 4 — Cell still works today, plan to migrate when 4 ships.
Scatter chart
import { ScatterChart, Scatter } from "recharts"
<ResponsiveContainer width="100%" height={300}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis type="number" dataKey="x" name="Weight" className="text-xs fill-muted-foreground" />
<YAxis type="number" dataKey="y" name="Height" className="text-xs fill-muted-foreground" />
<Tooltip cursor={{ strokeDasharray: "3 3" }} content={<ChartTooltip />} />
<Scatter name="Sample A" data={data} fill="var(--chart-1)" />
</ScatterChart>
</ResponsiveContainer>
For bubble charts, add <ZAxis dataKey="size" range={[60, 400]} /> and the scatter dots scale with the third dimension.
Reference lines and areas
For targets, thresholds, and goal markers:
import { ReferenceLine, ReferenceArea } from "recharts"
<LineChart data={data}>
{}
<ReferenceLine y={5000} label="Target" stroke="var(--chart-3)" strokeDasharray="3 3" />
<ReferenceArea x1="Jan" x2="Mar" fill="var(--chart-2)" fillOpacity={0.1} label="Q1" />
</LineChart>
For live/streaming data, many-chart dashboards, or radar/radial/composed/synced/brush charts, Read references/advanced.md.
Click and hover events
<BarChart onClick={(state) => console.log("chart click", state)}>
<Bar
dataKey="sales"
fill="var(--chart-1)"
onClick={(payload, index) => navigate(`/items/${payload.id}`)}
/>
</BarChart>
Tooltip has interaction props for click-to-pin or controlled selection:
<Tooltip trigger="click" defaultIndex={0} cursor={false} content={<ChartTooltip />} />
Theme-aware custom tooltip
import type { TooltipProps } from "recharts"
interface TooltipPayloadEntry {
name: string
value: number
color: string
}
function ChartTooltip({ active, payload, label }: TooltipProps<number, string>) {
if (!active || !payload?.length) return null
return (
<div className="rounded-lg border bg-card p-3 shadow-md">
<p className="text-sm font-medium mb-1">{label}</p>
{payload.map((entry, i) => (
<div key={i} className="flex items-center gap-2 text-sm">
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-muted-foreground">{entry.name}:</span>
<span className="font-medium">{entry.value?.toLocaleString()}</span>
</div>
))}
</div>
)
}
Use on every chart: <Tooltip content={<ChartTooltip />} />.
Axis formatting
<YAxis tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`} />
<YAxis tickFormatter={(v) => v.toLocaleString()} />
<YAxis tickFormatter={(v) => `${v}%`} domain={[0, 100]} />
import { format, parseISO } from "date-fns"
<XAxis dataKey="date" tickFormatter={(v) => format(parseISO(v), "MMM d")} />
<XAxis dataKey="month" angle={-45} textAnchor="end" height={60} />
<XAxis interval={0} />
<XAxis interval="preserveStartEnd" />
Custom domains
<YAxis domain={[0, 100]} />
<YAxis domain={[0, "auto"]} />
<YAxis domain={[0, "dataMax + 100"]} allowDataOverflow />
<YAxis type="number" scale="log" domain={["auto", "auto"]} />
Sparkline + stat card
Project pattern for KPI tiles:
function StatCard({ title, value, trend, data }: {
title: string
value: string
trend: string
data: { v: number }[]
}) {
return (
<Card>
<CardContent className="p-4">
<p className="text-sm text-muted-foreground">{title}</p>
<p className="text-2xl font-bold">{value}</p>
<p className="text-xs text-muted-foreground">{trend}</p>
<ResponsiveContainer width="100%" height={40}>
<AreaChart data={data}>
<Area
type="monotone"
dataKey="v"
stroke="var(--chart-1)"
fill="var(--chart-1)"
fillOpacity={0.1}
strokeWidth={1.5}
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
)
}
No axes, no tooltip, no grid — just the trend.
Accessibility
accessibilityLayer is on by default (keyboard navigable, screen-reader compatible). Always add an aria-label that describes what the chart shows:
<LineChart data={data} role="img" aria-label="Monthly revenue, January through June 2026">
{}
</LineChart>
Empty / loading states
if (isPending) return <Skeleton className="h-[300px] w-full" />
if (!data.length) {
return (
<div className="flex h-[300px] items-center justify-center text-sm text-muted-foreground">
No data for this range
</div>
)
}
return <ResponsiveContainer height={300}>{/* chart */}</ResponsiveContainer>
Troubleshooting
| Symptom | Fix |
|---|
| Axis labels overlap | <XAxis angle={-45} textAnchor="end" height={80} /> or interval="preserveStartEnd". |
dataKey shows nothing | Property name typo or property is missing on some rows — Recharts silently treats undefined as a gap. |
| Animation stutters with live data | isAnimationActive={false} on the series. |
| Chart taking forever to render | Too many points — aggregate / downsample. >1000 per series is the warning sign. |
Common mistakes
Beyond the four project rules above:
Cell count mismatch — if you map Cell over data you must use the same array; mismatched length leaves slices uncolored.
- Re-creating the data array on every render — causes the chart to re-animate. Memoize with
useMemo if the source data is stable.
Resources