Skip to content
Back to skills

Chart Implementation

ASecurity

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.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 27, 2026
developmenttypescriptgobashreactnextjstestingdebuggingdatabaseperformancedocumentation

Works with

  • cli

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add David-Li0406/meta-skill-evloving --skill chart-implementation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Chart Implementation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Chart Implementation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/david-li0406-chart-implementation/badge)](https://www.skillsdirectory.com/skills/david-li0406-chart-implementation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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):

```bash
# 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)

```typescript
"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)

```typescript
"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)

```typescript
"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)

```typescript
"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)

```typescript
"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)

```typescript
"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

```typescript
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

```typescript
"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

```typescript
"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

```typescript
// 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

```typescript
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

```typescript
"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

```typescript
"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

```typescript
"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

```typescript
// __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

```typescript
// ❌ 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

```typescript
const Chart = dynamic(() => import("./chart"), { ssr: false });
```

## References

- Recharts Documentation: Use Context7 for latest docs
- Related files:
  - `apps/web/src/components/charts/` - Chart components
  - `apps/web/src/app/` - Pages using charts
  - `apps/web/CLAUDE.md` - Web app documentation

## Best Practices

1. **Responsive**: Always use ResponsiveContainer
2. **Performance**: Lazy load charts, memoize data transformations
3. **Accessibility**: Add aria labels and descriptions
4. **Colors**: Use consistent color scheme across charts
5. **Tooltips**: Provide detailed, formatted tooltips
6. **Loading States**: Show skeleton while data loads
7. **Error Handling**: Handle empty data gracefully
8. **Mobile**: Test on small screens, adjust labels

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…