chart-implementation
について
このスキルにより、開発者は自動車登録データとCOEデータの可視化用インタラクティブチャートの作成と更新が可能になります。ReactベースのWebアプリケーションにおいて、新しいチャートタイプの実装、チャートのバグ修正、データ可視化機能の追加を行う際にご利用ください。Recharts、Chart.jsなどの主要なチャートライブラリをサポートし、レスポンシブでインタラクティブなチャート構築を実現します。
クイックインストール
Claude Code
推奨/plugin add https://github.com/sgcarstrends/sgcarstrendsgit clone https://github.com/sgcarstrends/sgcarstrends.git ~/.claude/skills/chart-implementationこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストールします
ドキュメント
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
GitHub リポジトリ
関連スキル
content-collections
メタThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
creating-opencode-plugins
メタThis skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.
polymarket
メタThis skill enables developers to build applications with the Polymarket prediction markets platform, including API integration for trading and market data. It also provides real-time data streaming via WebSocket to monitor live trades and market activity. Use it for implementing trading strategies or creating tools that process live market updates.
cloudflare-turnstile
メタThis skill provides comprehensive guidance for implementing Cloudflare Turnstile as a CAPTCHA-alternative bot protection system. It covers integration for forms, login pages, API endpoints, and frameworks like React/Next.js/Hono, while handling invisible challenges that maintain user experience. Use it when migrating from reCAPTCHA, debugging error codes, or implementing token validation and E2E tests.
