11 KiB
Charts & Analytics Guide
Table of Contents
- Overview Architecture
- Parallel Routes Pattern
- Chart Components
- Stats Cards
- Skeleton Loading
- Adding a New Chart Section
Overview Architecture
The analytics dashboard at /dashboard/overview uses Next.js parallel routes to load multiple chart sections independently. Each chart slot streams in as its data becomes ready — no waterfall, no blocking.
File structure:
src/app/dashboard/overview/
├── layout.tsx # Composes all slots into a grid
├── @area_stats/
│ ├── page.tsx # Async server component (fetches data)
│ ├── loading.tsx # Skeleton shown while streaming
│ └── error.tsx # Error boundary if fetch fails
├── @bar_stats/
│ ├── page.tsx
│ ├── loading.tsx
│ └── error.tsx
├── @pie_stats/
│ ├── page.tsx
│ ├── loading.tsx
│ └── error.tsx
└── @sales/
├── page.tsx
├── loading.tsx
└── error.tsx
src/features/overview/components/
├── area-graph.tsx # Client chart component
├── area-graph-skeleton.tsx # Matching skeleton
├── bar-graph.tsx
├── bar-graph-skeleton.tsx
├── pie-graph.tsx
├── pie-graph-skeleton.tsx
├── recent-sales.tsx
└── recent-sales-skeleton.tsx
Parallel Routes Pattern
Layout (layout.tsx)
The layout receives each parallel route as a prop and arranges them in a grid:
export default function OverviewLayout({
sales,
pie_stats,
bar_stats,
area_stats
}: {
sales: React.ReactNode;
pie_stats: React.ReactNode;
bar_stats: React.ReactNode;
area_stats: React.ReactNode;
}) {
return (
<PageContainer pageTitle='Dashboard' pageDescription='Overview analytics.'>
{/* Stats cards row */}
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
<Card>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>
<Icons.billing className='h-4 w-4 text-muted-foreground' />
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>$45,231.89</div>
<p className='text-xs text-muted-foreground'>+20.1% from last month</p>
</CardContent>
</Card>
{/* ...more stat cards */}
</div>
{/* Charts grid — each slot loads independently */}
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>
<div className='col-span-4'>{area_stats}</div>
<div className='col-span-3'>{sales}</div>
<div className='col-span-4'>{bar_stats}</div>
<div className='col-span-3'>{pie_stats}</div>
</div>
</PageContainer>
);
}
Slot Page (@area_stats/page.tsx)
Each slot is an async server component that fetches data then renders the chart:
import { delay } from '@/constants/mock-api';
import { AreaGraph } from '@/features/overview/components/area-graph';
export default async function AreaStatsPage() {
await delay(2000); // Simulates API fetch
return <AreaGraph />;
}
Slot Loading (@area_stats/loading.tsx)
import { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';
export default function Loading() {
return <AreaGraphSkeleton />;
}
Slot Error (@area_stats/error.tsx)
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Icons } from '@/components/icons';
export default function AreaStatsError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<Icons.alertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load area stats: {error.message}</AlertDescription>
</Alert>
);
}
Each slot can fail independently without affecting others.
Chart Components
All chart components are 'use client' and use Recharts wrapped in shadcn's ChartContainer.
Chart Config
Every chart defines a config object mapping data keys to labels and theme colors:
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from '@/components/ui/chart';
const chartConfig = {
desktop: { label: 'Desktop', color: 'var(--chart-1)' },
mobile: { label: 'Mobile', color: 'var(--chart-2)' }
} satisfies ChartConfig;
Theme colors --chart-1 through --chart-5 are defined in each theme's CSS file and automatically adapt to light/dark mode.
Area Chart Example
'use client';
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from '@/components/ui/chart';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Icons } from '@/components/icons';
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 }
// ...more months
];
const chartConfig = {
desktop: { label: 'Desktop', color: 'var(--chart-1)' },
mobile: { label: 'Mobile', color: 'var(--chart-2)' }
} satisfies ChartConfig;
export function AreaGraph() {
return (
<Card className='@container/card'>
<CardHeader>
<CardTitle>Area Chart - Stacked</CardTitle>
<Badge variant='outline'>
<Icons.trendingUp className='mr-1 h-3 w-3' /> +12.5%
</Badge>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>
<AreaChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey='month'
tickLine={false}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={<ChartTooltipContent indicator='dot' />} />
<Area
dataKey='mobile'
type='natural'
fill='var(--color-mobile)'
stroke='var(--color-mobile)'
stackId='a'
/>
<Area
dataKey='desktop'
type='natural'
fill='var(--color-desktop)'
stroke='var(--color-desktop)'
stackId='a'
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
Bar Chart Pattern
Same structure, using BarChart + Bar:
<ChartContainer config={chartConfig}>
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey='month' tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey='desktop' fill='var(--color-desktop)' radius={4} />
<Bar dataKey='mobile' fill='var(--color-mobile)' radius={4} />
</BarChart>
</ChartContainer>
Pie/Donut Chart Pattern
<ChartContainer config={chartConfig}>
<PieChart>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
<Pie data={chartData} dataKey='visitors' nameKey='browser' innerRadius={30}>
<LabelList dataKey='visitors' className='fill-background' />
</Pie>
</PieChart>
</ChartContainer>
Stats Cards
Stats cards are simple server-rendered Card components at the top of the layout — no parallel routes needed since they render instantly:
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>
<Icons.billing className='h-4 w-4 text-muted-foreground' />
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>$45,231.89</div>
<p className='text-xs text-muted-foreground'>+20.1% from last month</p>
</CardContent>
</Card>
For dynamic stats that need data fetching, wrap in their own Suspense boundary or parallel route slot.
Skeleton Loading
Each chart has a matching skeleton component. Pattern:
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export function AreaGraphSkeleton() {
return (
<Card className='@container/card'>
<CardHeader>
<Skeleton className='h-5 w-[140px]' />
<Skeleton className='h-4 w-[80px]' />
</CardHeader>
<CardContent>
<Skeleton className='h-[250px] w-full rounded-md' />
</CardContent>
</Card>
);
}
Match the skeleton dimensions to the actual chart for smooth visual transitions.
Adding a New Chart Section
To add a new chart (e.g., line chart for user growth):
1. Create the chart component
src/features/overview/components/line-graph.tsx:
'use client';
import { Line, LineChart, CartesianGrid, XAxis } from 'recharts';
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from '@/components/ui/chart';
const chartConfig = {
users: { label: 'Users', color: 'var(--chart-3)' }
} satisfies ChartConfig;
const chartData = [
/* monthly user data */
];
export function LineGraph() {
return (
<Card>
<CardHeader>
<CardTitle>User Growth</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>
<LineChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey='month' tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<Line dataKey='users' type='monotone' stroke='var(--color-users)' strokeWidth={2} />
</LineChart>
</ChartContainer>
</CardContent>
</Card>
);
}
2. Create matching skeleton
src/features/overview/components/line-graph-skeleton.tsx
3. Create parallel route slot
src/app/dashboard/overview/@line_stats/
├── page.tsx → async, fetches data, returns <LineGraph />
├── loading.tsx → returns <LineGraphSkeleton />
├── error.tsx → error alert
└── default.tsx → return null (fallback when route doesn't match)
default.tsx is required for parallel routes — return null or a fallback:
export default function Default() {
return null;
}
4. Add slot to layout
Update src/app/dashboard/overview/layout.tsx:
export default function OverviewLayout({
sales,
pie_stats,
bar_stats,
area_stats,
line_stats // ← add new slot
}: {
/* ...types */
}) {
return (
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>
{/* existing charts */}
<div className='col-span-4'>{line_stats}</div>
</div>
);
}
Available Recharts Components
Common chart types to use with ChartContainer:
AreaChart+Area— filled area charts (stacked or standalone)BarChart+Bar— vertical/horizontal barsLineChart+Line— line/trend chartsPieChart+Pie— pie/donut chartsRadarChart+Radar— radar/spider chartsRadialBarChart+RadialBar— radial progress bars
All support ChartTooltip, ChartLegend, and theme-aware colors via var(--chart-N).