| name | chart-implementation |
| description | Create or update interactive charts for car registration and COE data visualization. Use when adding new chart types, fixing chart bugs, or implementing data visualizations. |
| allowed-tools | Read, Edit, Write, Grep, Glob, Bash |
Chart Implementation Skill
This skill helps you create and customize data visualization charts in apps/web/.
When to Use This Skill
- Creating new chart visualizations for car/COE data
- Updating existing charts with new features
- Implementing interactive chart features
- Optimizing chart performance
- Debugging chart rendering issues
- Adding responsive chart layouts
Chart Library
The project likely uses one of these popular React chart libraries. Check package.json:
- Recharts - Built on D3, great for React
- Chart.js with react-chartjs-2 - Simple, performant
- Tremor - Tailwind-based charts
- Victory - Composable charting
- Nivo - Feature-rich D3 wrapper
Let's use Recharts as the example (most common for Next.js):
# Install if not present
pnpm -F @sgcarstrends/web add recharts
pnpm -F @sgcarstrends/web add -D @types/recharts
Common Chart Types
1. Line Chart (Car Registrations Over Time)
"use client";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
interface CarDataPoint {
month: string;
registrations: number;
}
export function CarRegistrationTrend({ data }: { data: CarDataPoint[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="registrations"
stroke="#0070F3"
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
);
}
2. Bar Chart (Top Car Makes)
"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
interface MakeData {
make: string;
count: number;
}
export function TopCarMakes({ data }: { data: MakeData[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="make" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#0070F3" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
}
3. Multi-Line Chart (COE Categories)
"use client";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
interface COEData {
biddingNo: string;
catA: number;
catB: number;
catC: number;
catE: number;
}
export function COETrends({ data }: { data: COEData[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="biddingNo" />
<YAxis />
<Tooltip
formatter={(value) => `$${Number(value).toLocaleString()}`}
/>
<Legend />
<Line type="monotone" dataKey="catA" stroke="#8884d8" name="Cat A" />
<Line type="monotone" dataKey="catB" stroke="#82ca9d" name="Cat B" />
<Line type="monotone" dataKey="catC" stroke="#ffc658" name="Cat C" />
<Line type="monotone" dataKey="catE" stroke="#ff7c7c" name="Cat E" />
</LineChart>
</ResponsiveContainer>
);
}
4. Area Chart (Cumulative Registrations)
"use client";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
export function CumulativeRegistrations({ data }: { data: any[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Area
type="monotone"
dataKey="cumulative"
stroke="#0070F3"
fill="#0070F3"
fillOpacity={0.6}
/>
</AreaChart>
</ResponsiveContainer>
);
}
5. Pie Chart (Market Share)
"use client";
import {
PieChart,
Pie,
Cell,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
interface MarketShareData {
make: string;
value: number;
}
const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884D8"];
export function MarketShareChart({ data }: { data: MarketShareData[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
label={({ make, percent }) =>
`${make}: ${(percent * 100).toFixed(0)}%`
}
outerRadius={120}
fill="#8884d8"
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
);
}
6. Composed Chart (Combination)
"use client";
import {
ComposedChart,
Line,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
interface MixedData {
month: string;
registrations: number;
avgPrice: number;
}
export function RegistrationsAndPrices({ data }: { data: MixedData[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<ComposedChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Tooltip />
<Legend />
<Bar yAxisId="left" dataKey="registrations" fill="#0070F3" />
<Line
yAxisId="right"
type="monotone"
dataKey="avgPrice"
stroke="#ff7c7c"
/>
</ComposedChart>
</ResponsiveContainer>
);
}
Advanced Features
Custom Tooltips
import { TooltipProps } from "recharts";
function CustomTooltip({ active, payload, label }: TooltipProps<number, string>) {
if (!active || !payload || !payload.length) return null;
return (
<div className="bg-white p-4 rounded-lg shadow-lg border">
<p className="font-semibold">{label}</p>
{payload.map((entry, index) => (
<p key={index} style={{ color: entry.color }}>
{entry.name}: {entry.value?.toLocaleString()}
</p>
))}
</div>
);
}
export function ChartWithCustomTooltip({ data }: { data: any[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Line dataKey="value" stroke="#0070F3" />
</LineChart>
</ResponsiveContainer>
);
}
Interactive Features
"use client";
import { useState } from "react";
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
export function InteractiveChart({ data }: { data: any[] }) {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
return (
<div>
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={data}
onMouseMove={(e) => {
if (e.activeTooltipIndex !== undefined) {
setActiveIndex(e.activeTooltipIndex);
}
}}
onMouseLeave={() => setActiveIndex(null)}
>
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line
dataKey="value"
stroke="#0070F3"
strokeWidth={2}
dot={(props) => {
const { cx, cy, index } = props;
return (
<circle
cx={cx}
cy={cy}
r={index === activeIndex ? 6 : 3}
fill="#0070F3"
/>
);
}}
/>
</LineChart>
</ResponsiveContainer>
{activeIndex !== null && (
<div className="mt-4 p-4 bg-gray-100 rounded">
<h3 className="font-semibold">Selected Data Point</h3>
<p>Month: {data[activeIndex].month}</p>
<p>Value: {data[activeIndex].value}</p>
</div>
)}
</div>
);
}
Responsive Charts with Breakpoints
"use client";
import { useEffect, useState } from "react";
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer } from "recharts";
export function ResponsiveChart({ data }: { data: any[] }) {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
return (
<ResponsiveContainer width="100%" height={isMobile ? 300 : 400}>
<LineChart data={data}>
<XAxis
dataKey="month"
angle={isMobile ? -45 : 0}
textAnchor={isMobile ? "end" : "middle"}
height={isMobile ? 80 : 30}
/>
<YAxis />
<Line dataKey="value" stroke="#0070F3" />
</LineChart>
</ResponsiveContainer>
);
}
Data Preparation
Transforming Database Data
// Server component fetching data
import { db } from "@sgcarstrends/database";
export async function CarTrendChart() {
const rawData = await db.query.cars.findMany({
orderBy: (cars, { asc }) => [asc(cars.registrationDate)],
});
// Transform for chart
const chartData = rawData.reduce((acc, car) => {
const month = car.registrationDate.substring(0, 7); // "2024-01"
const existing = acc.find((d) => d.month === month);
if (existing) {
existing.count++;
} else {
acc.push({ month, count: 1 });
}
return acc;
}, [] as { month: string; count: number }[]);
return <CarRegistrationTrend data={chartData} />;
}
Aggregating COE Data
export async function COEPriceChart() {
const coeData = await db.query.coe.findMany({
orderBy: (coe, { desc }) => [desc(coe.biddingNo)],
limit: 20,
});
// Group by category
const chartData = coeData.map((result) => ({
biddingNo: result.biddingNo.toString(),
catA: result.category === "A" ? result.premium : 0,
catB: result.category === "B" ? result.premium : 0,
catC: result.category === "C" ? result.premium : 0,
catE: result.category === "E" ? result.premium : 0,
}));
return <COETrends data={chartData} />;
}
Performance Optimization
Lazy Loading Charts
"use client";
import dynamic from "next/dynamic";
const CarTrendChart = dynamic(
() => import("@/components/charts/car-trend-chart"),
{
ssr: false,
loading: () => <div>Loading chart...</div>,
}
);
export function DashboardPage() {
return <CarTrendChart />;
}
Memoizing Chart Data
"use client";
import { useMemo } from "react";
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer } from "recharts";
export function MemoizedChart({ rawData }: { rawData: any[] }) {
const chartData = useMemo(() => {
// Expensive data transformation
return rawData.map((item) => ({
month: formatMonth(item.date),
value: calculateValue(item),
}));
}, [rawData]);
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<XAxis dataKey="month" />
<YAxis />
<Line dataKey="value" stroke="#0070F3" />
</LineChart>
</ResponsiveContainer>
);
}
Styling Charts
Dark Mode Support
"use client";
import { useTheme } from "next-themes";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
export function ThemedChart({ data }: { data: any[] }) {
const { theme } = useTheme();
const isDark = theme === "dark";
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid
strokeDasharray="3 3"
stroke={isDark ? "#374151" : "#e5e7eb"}
/>
<XAxis dataKey="month" stroke={isDark ? "#9ca3af" : "#6b7280"} />
<YAxis stroke={isDark ? "#9ca3af" : "#6b7280"} />
<Line dataKey="value" stroke="#0070F3" />
</LineChart>
</ResponsiveContainer>
);
}
Testing Charts
// __tests__/components/car-trend-chart.test.tsx
import { render } from "@testing-library/react";
import CarTrendChart from "../car-trend-chart";
describe("CarTrendChart", () => {
const mockData = [
{ month: "2024-01", registrations: 1000 },
{ month: "2024-02", registrations: 1200 },
];
it("renders without crashing", () => {
const { container } = render(<CarTrendChart data={mockData} />);
expect(container.querySelector(".recharts-wrapper")).toBeInTheDocument();
});
it("displays correct number of data points", () => {
const { container } = render(<CarTrendChart data={mockData} />);
const dots = container.querySelectorAll(".recharts-line-dot");
expect(dots).toHaveLength(mockData.length);
});
});
Common Issues and Solutions
Chart Not Rendering
Problem: Blank space where chart should be Solution: Ensure parent has explicit height
// ❌ Wrong
<div>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>...</LineChart>
</ResponsiveContainer>
</div>
// ✅ Correct
<div className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>...</LineChart>
</ResponsiveContainer>
</div>
SSR Errors
Problem: Window is not defined Solution: Use dynamic import with ssr: false
const Chart = dynamic(() => import("./chart"), { ssr: false });
References
- Recharts Documentation: Use Context7 for latest docs
- Related files:
apps/web/src/components/charts/- Chart componentsapps/web/src/app/- Pages using chartsapps/web/CLAUDE.md- Web app documentation
Best Practices
- Responsive: Always use ResponsiveContainer
- Performance: Lazy load charts, memoize data transformations
- Accessibility: Add aria labels and descriptions
- Colors: Use consistent color scheme across charts
- Tooltips: Provide detailed, formatted tooltips
- Loading States: Show skeleton while data loads
- Error Handling: Handle empty data gracefully
- Mobile: Test on small screens, adjust labels