shadcn/ui Charts完全ガイド - Rechartsベースの美しいチャートコンポーネント


shadcn/ui Charts完全ガイド - Rechartsベースの美しいチャートコンポーネント

shadcn/ui Chartsは、Rechartsをベースにした美しく、カスタマイズ可能なチャートコンポーネントライブラリです。shadcn/uiのデザインシステムと完全に統合され、TypeScript完全対応で型安全なデータ可視化が可能です。

この記事では、shadcn/ui Chartsの基本から高度なカスタマイズまで、実践的な使い方を解説します。

shadcn/ui Chartsとは

shadcn/ui Chartsは、Rechartsをラップした使いやすいチャートコンポーネント集です。

主な特徴

  • shadcn/ui統合: 一貫したデザインシステム
  • TypeScript対応: 完全な型安全性
  • Recharts基盤: 強力なチャート機能
  • カスタマイズ可能: テーマとスタイルの柔軟性
  • レスポンシブ: モバイルフレンドリー
  • アクセシビリティ: ARIA対応

セットアップ

前提条件

# Next.jsプロジェクト作成
npx create-next-app@latest my-charts-app

cd my-charts-app

shadcn/ui初期化

# shadcn/ui CLI
npx shadcn-ui@latest init

Chartsコンポーネント追加

# Chartコンポーネントをインストール
npx shadcn-ui@latest add chart

これにより以下がインストールされます:

  • recharts
  • @/components/ui/chart

基本的なチャート

折れ線グラフ

// components/line-chart-demo.tsx
'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Line, LineChart, XAxis, YAxis } from 'recharts'

const data = [
  { month: '1月', sales: 4000 },
  { month: '2月', sales: 3000 },
  { month: '3月', sales: 5000 },
  { month: '4月', sales: 4500 },
  { month: '5月', sales: 6000 },
  { month: '6月', sales: 5500 },
]

export function LineChartDemo() {
  return (
    <ChartContainer
      config={{
        sales: {
          label: '売上',
          color: 'hsl(var(--chart-1))',
        },
      }}
      className="h-[300px]"
    >
      <LineChart data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Line
          type="monotone"
          dataKey="sales"
          stroke="var(--color-sales)"
          strokeWidth={2}
        />
      </LineChart>
    </ChartContainer>
  )
}

棒グラフ

// components/bar-chart-demo.tsx
'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Bar, BarChart, XAxis, YAxis } from 'recharts'

const data = [
  { category: 'A', value: 400 },
  { category: 'B', value: 300 },
  { category: 'C', value: 500 },
  { category: 'D', value: 200 },
]

export function BarChartDemo() {
  return (
    <ChartContainer
      config={{
        value: {
          label: '値',
          color: 'hsl(var(--chart-2))',
        },
      }}
      className="h-[300px]"
    >
      <BarChart data={data}>
        <XAxis dataKey="category" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Bar dataKey="value" fill="var(--color-value)" radius={8} />
      </BarChart>
    </ChartContainer>
  )
}

円グラフ

// components/pie-chart-demo.tsx
'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Pie, PieChart, Cell } from 'recharts'

const data = [
  { name: 'カテゴリA', value: 400 },
  { name: 'カテゴリB', value: 300 },
  { name: 'カテゴリC', value: 300 },
  { name: 'カテゴリD', value: 200 },
]

const COLORS = [
  'hsl(var(--chart-1))',
  'hsl(var(--chart-2))',
  'hsl(var(--chart-3))',
  'hsl(var(--chart-4))',
]

export function PieChartDemo() {
  return (
    <ChartContainer
      config={{
        value: {
          label: '値',
        },
      }}
      className="h-[300px]"
    >
      <PieChart>
        <ChartTooltip content={<ChartTooltipContent />} />
        <Pie
          data={data}
          dataKey="value"
          nameKey="name"
          cx="50%"
          cy="50%"
          outerRadius={80}
          label
        >
          {data.map((entry, index) => (
            <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
          ))}
        </Pie>
      </PieChart>
    </ChartContainer>
  )
}

複数系列チャート

マルチラインチャート

'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
} from '@/components/ui/chart'
import { Line, LineChart, XAxis, YAxis } from 'recharts'

const data = [
  { month: '1月', revenue: 4000, profit: 2400 },
  { month: '2月', revenue: 3000, profit: 1398 },
  { month: '3月', revenue: 5000, profit: 3800 },
  { month: '4月', revenue: 4500, profit: 3908 },
  { month: '5月', revenue: 6000, profit: 4800 },
  { month: '6月', revenue: 5500, profit: 3800 },
]

export function MultiLineChart() {
  return (
    <ChartContainer
      config={{
        revenue: {
          label: '売上',
          color: 'hsl(var(--chart-1))',
        },
        profit: {
          label: '利益',
          color: 'hsl(var(--chart-2))',
        },
      }}
      className="h-[400px]"
    >
      <LineChart data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <ChartLegend content={<ChartLegendContent />} />
        <Line
          type="monotone"
          dataKey="revenue"
          stroke="var(--color-revenue)"
          strokeWidth={2}
        />
        <Line
          type="monotone"
          dataKey="profit"
          stroke="var(--color-profit)"
          strokeWidth={2}
        />
      </LineChart>
    </ChartContainer>
  )
}

スタックバーチャート

'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
} from '@/components/ui/chart'
import { Bar, BarChart, XAxis, YAxis } from 'recharts'

const data = [
  { month: '1月', desktop: 186, mobile: 80, tablet: 40 },
  { month: '2月', desktop: 305, mobile: 200, tablet: 100 },
  { month: '3月', desktop: 237, mobile: 120, tablet: 60 },
  { month: '4月', desktop: 273, mobile: 190, tablet: 80 },
]

export function StackedBarChart() {
  return (
    <ChartContainer
      config={{
        desktop: {
          label: 'デスクトップ',
          color: 'hsl(var(--chart-1))',
        },
        mobile: {
          label: 'モバイル',
          color: 'hsl(var(--chart-2))',
        },
        tablet: {
          label: 'タブレット',
          color: 'hsl(var(--chart-3))',
        },
      }}
      className="h-[400px]"
    >
      <BarChart data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <ChartLegend content={<ChartLegendContent />} />
        <Bar dataKey="desktop" stackId="a" fill="var(--color-desktop)" />
        <Bar dataKey="mobile" stackId="a" fill="var(--color-mobile)" />
        <Bar dataKey="tablet" stackId="a" fill="var(--color-tablet)" />
      </BarChart>
    </ChartContainer>
  )
}

エリアチャート

基本的なエリアチャート

'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Area, AreaChart, XAxis, YAxis } from 'recharts'

const data = [
  { date: '2024/01', value: 2400 },
  { date: '2024/02', value: 1398 },
  { date: '2024/03', value: 9800 },
  { date: '2024/04', value: 3908 },
  { date: '2024/05', value: 4800 },
  { date: '2024/06', value: 3800 },
]

export function AreaChartDemo() {
  return (
    <ChartContainer
      config={{
        value: {
          label: '値',
          color: 'hsl(var(--chart-1))',
        },
      }}
      className="h-[300px]"
    >
      <AreaChart data={data}>
        <defs>
          <linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
            <stop offset="5%" stopColor="var(--color-value)" stopOpacity={0.8} />
            <stop offset="95%" stopColor="var(--color-value)" stopOpacity={0} />
          </linearGradient>
        </defs>
        <XAxis dataKey="date" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Area
          type="monotone"
          dataKey="value"
          stroke="var(--color-value)"
          fillOpacity={1}
          fill="url(#colorValue)"
        />
      </AreaChart>
    </ChartContainer>
  )
}

スタックエリアチャート

'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
} from '@/components/ui/chart'
import { Area, AreaChart, XAxis, YAxis } from 'recharts'

const data = [
  { month: '1月', email: 4000, social: 2400, direct: 2400 },
  { month: '2月', email: 3000, social: 1398, direct: 2210 },
  { month: '3月', email: 2000, social: 9800, direct: 2290 },
  { month: '4月', email: 2780, social: 3908, direct: 2000 },
]

export function StackedAreaChart() {
  return (
    <ChartContainer
      config={{
        email: {
          label: 'Eメール',
          color: 'hsl(var(--chart-1))',
        },
        social: {
          label: 'ソーシャル',
          color: 'hsl(var(--chart-2))',
        },
        direct: {
          label: 'ダイレクト',
          color: 'hsl(var(--chart-3))',
        },
      }}
      className="h-[400px]"
    >
      <AreaChart data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <ChartLegend content={<ChartLegendContent />} />
        <Area
          type="monotone"
          dataKey="email"
          stackId="1"
          stroke="var(--color-email)"
          fill="var(--color-email)"
        />
        <Area
          type="monotone"
          dataKey="social"
          stackId="1"
          stroke="var(--color-social)"
          fill="var(--color-social)"
        />
        <Area
          type="monotone"
          dataKey="direct"
          stackId="1"
          stroke="var(--color-direct)"
          fill="var(--color-direct)"
        />
      </AreaChart>
    </ChartContainer>
  )
}

カスタムツールチップ

リッチツールチップ

'use client'

import {
  ChartContainer,
  ChartTooltip,
} from '@/components/ui/chart'
import { Line, LineChart, XAxis, YAxis } from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

const data = [
  { date: '2024/01', sales: 4000, users: 240 },
  { date: '2024/02', sales: 3000, users: 139 },
  { date: '2024/03', sales: 5000, users: 380 },
]

function CustomTooltip({ active, payload }: any) {
  if (!active || !payload) return null

  return (
    <Card>
      <CardHeader className="p-3">
        <CardTitle className="text-sm">
          {payload[0].payload.date}
        </CardTitle>
      </CardHeader>
      <CardContent className="p-3 pt-0 space-y-1">
        <div className="flex items-center justify-between">
          <span className="text-sm text-muted-foreground">売上</span>
          <span className="text-sm font-bold">
            ¥{payload[0].value.toLocaleString()}
          </span>
        </div>
        <div className="flex items-center justify-between">
          <span className="text-sm text-muted-foreground">ユーザー</span>
          <span className="text-sm font-bold">
            {payload[1].value.toLocaleString()}
          </span>
        </div>
      </CardContent>
    </Card>
  )
}

export function CustomTooltipChart() {
  return (
    <ChartContainer
      config={{
        sales: {
          label: '売上',
          color: 'hsl(var(--chart-1))',
        },
        users: {
          label: 'ユーザー',
          color: 'hsl(var(--chart-2))',
        },
      }}
      className="h-[300px]"
    >
      <LineChart data={data}>
        <XAxis dataKey="date" />
        <YAxis />
        <ChartTooltip content={<CustomTooltip />} />
        <Line
          type="monotone"
          dataKey="sales"
          stroke="var(--color-sales)"
          strokeWidth={2}
        />
        <Line
          type="monotone"
          dataKey="users"
          stroke="var(--color-users)"
          strokeWidth={2}
        />
      </LineChart>
    </ChartContainer>
  )
}

レスポンシブチャート

ResponsiveContainer使用

'use client'

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Line, LineChart, XAxis, YAxis, ResponsiveContainer } from 'recharts'

const data = [
  { month: '1月', value: 4000 },
  { month: '2月', value: 3000 },
  { month: '3月', value: 5000 },
]

export function ResponsiveChart() {
  return (
    <div className="w-full h-[300px] md:h-[400px] lg:h-[500px]">
      <ChartContainer
        config={{
          value: {
            label: '値',
            color: 'hsl(var(--chart-1))',
          },
        }}
        className="h-full"
      >
        <ResponsiveContainer width="100%" height="100%">
          <LineChart data={data}>
            <XAxis dataKey="month" />
            <YAxis />
            <ChartTooltip content={<ChartTooltipContent />} />
            <Line
              type="monotone"
              dataKey="value"
              stroke="var(--color-value)"
              strokeWidth={2}
            />
          </LineChart>
        </ResponsiveContainer>
      </ChartContainer>
    </div>
  )
}

インタラクティブチャート

クリックイベント

'use client'

import { useState } from 'react'
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Bar, BarChart, XAxis, YAxis } from 'recharts'

const data = [
  { category: 'A', value: 400 },
  { category: 'B', value: 300 },
  { category: 'C', value: 500 },
]

export function InteractiveChart() {
  const [selectedBar, setSelectedBar] = useState<string | null>(null)

  const handleClick = (data: any) => {
    setSelectedBar(data.category)
  }

  return (
    <div>
      <ChartContainer
        config={{
          value: {
            label: '値',
            color: 'hsl(var(--chart-1))',
          },
        }}
        className="h-[300px]"
      >
        <BarChart data={data} onClick={handleClick}>
          <XAxis dataKey="category" />
          <YAxis />
          <ChartTooltip content={<ChartTooltipContent />} />
          <Bar
            dataKey="value"
            fill="var(--color-value)"
            opacity={0.8}
            cursor="pointer"
          />
        </BarChart>
      </ChartContainer>
      {selectedBar && (
        <p className="mt-4 text-center">
          選択: カテゴリ {selectedBar}
        </p>
      )}
    </div>
  )
}

リアルタイムデータ

ライブアップデート

'use client'

import { useState, useEffect } from 'react'
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Line, LineChart, XAxis, YAxis } from 'recharts'

export function LiveChart() {
  const [data, setData] = useState([
    { time: '0s', value: 0 },
  ])

  useEffect(() => {
    const interval = setInterval(() => {
      setData((prev) => {
        const newData = [
          ...prev,
          {
            time: `${prev.length}s`,
            value: Math.random() * 100,
          },
        ].slice(-20) // 最新20件のみ保持

        return newData
      })
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return (
    <ChartContainer
      config={{
        value: {
          label: '値',
          color: 'hsl(var(--chart-1))',
        },
      }}
      className="h-[300px]"
    >
      <LineChart data={data}>
        <XAxis dataKey="time" />
        <YAxis domain={[0, 100]} />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Line
          type="monotone"
          dataKey="value"
          stroke="var(--color-value)"
          strokeWidth={2}
          dot={false}
          isAnimationActive={false}
        />
      </LineChart>
    </ChartContainer>
  )
}

ダッシュボード例

総合ダッシュボード

// app/dashboard/page.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { LineChartDemo } from '@/components/line-chart-demo'
import { BarChartDemo } from '@/components/bar-chart-demo'
import { PieChartDemo } from '@/components/pie-chart-demo'

export default function DashboardPage() {
  return (
    <div className="p-8 space-y-8">
      <h1 className="text-3xl font-bold">ダッシュボード</h1>

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              総売上
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">¥45,231</div>
            <p className="text-xs text-muted-foreground">
              前月比 +20.1%
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              新規ユーザー
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">+2,350</div>
            <p className="text-xs text-muted-foreground">
              前月比 +180.1%
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              アクティブユーザー
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">+12,234</div>
            <p className="text-xs text-muted-foreground">
              前月比 +19%
            </p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">
              コンバージョン率
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">3.2%</div>
            <p className="text-xs text-muted-foreground">
              前月比 +0.5%
            </p>
          </CardContent>
        </Card>
      </div>

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
        <Card className="col-span-4">
          <CardHeader>
            <CardTitle>月次売上推移</CardTitle>
          </CardHeader>
          <CardContent>
            <LineChartDemo />
          </CardContent>
        </Card>

        <Card className="col-span-3">
          <CardHeader>
            <CardTitle>カテゴリ別売上</CardTitle>
          </CardHeader>
          <CardContent>
            <PieChartDemo />
          </CardContent>
        </Card>
      </div>

      <Card>
        <CardHeader>
          <CardTitle>地域別売上</CardTitle>
        </CardHeader>
        <CardContent>
          <BarChartDemo />
        </CardContent>
      </Card>
    </div>
  )
}

テーマカスタマイズ

カスタムカラー

/* app/globals.css */
@layer base {
  :root {
    --chart-1: 220 70% 50%;
    --chart-2: 160 60% 45%;
    --chart-3: 30 80% 55%;
    --chart-4: 280 65% 60%;
    --chart-5: 340 75% 55%;
  }

  .dark {
    --chart-1: 220 70% 50%;
    --chart-2: 160 60% 45%;
    --chart-3: 30 80% 55%;
    --chart-4: 280 65% 60%;
    --chart-5: 340 75% 55%;
  }
}

パフォーマンス最適化

大規模データセット

'use client'

import { useMemo } from 'react'
import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
import { Line, LineChart, XAxis, YAxis } from 'recharts'

export function LargeDataChart({ rawData }: { rawData: any[] }) {
  // データをメモ化
  const processedData = useMemo(() => {
    return rawData.slice(0, 100) // 最新100件のみ
  }, [rawData])

  return (
    <ChartContainer
      config={{
        value: {
          label: '値',
          color: 'hsl(var(--chart-1))',
        },
      }}
      className="h-[300px]"
    >
      <LineChart data={processedData}>
        <XAxis dataKey="date" />
        <YAxis />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Line
          type="monotone"
          dataKey="value"
          stroke="var(--color-value)"
          strokeWidth={2}
          dot={false} // 大規模データではドットを非表示
          isAnimationActive={false} // アニメーション無効化
        />
      </LineChart>
    </ChartContainer>
  )
}

まとめ

shadcn/ui Chartsは、Rechartsの強力な機能とshadcn/uiの美しいデザインを組み合わせた最適なチャートソリューションです。

主なメリット:

  • 簡単統合: shadcn/uiとシームレス
  • TypeScript: 完全な型安全性
  • カスタマイズ性: 柔軟なスタイリング
  • パフォーマンス: 最適化された描画

データ可視化が必要なプロジェクトには、shadcn/ui Chartsが最適な選択です。

参考リンク