モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
データを視覚的に比較できる棒グラフ。アニメーション、ツールチップ、レスポンシブ対応
ホバー時のツールチップとアニメーション効果
カラー、グリッド、値表示など柔軟にカスタマイズ
あらゆる画面サイズで美しく表示
プロンプト例:
ダッシュボード用の棒グラフを実装してください。月別・四半期別の切り替え、前年比表示、エクスポート機能(PNG/CSV)、リアルタイムデータ更新機能を含めてください。
プロンプト例:
複数データセットの比較棒グラフを作成してください。積み上げ棒グラフ、グループ化表示、凡例表示、データフィルタリング、パーセンテージ表示モードを実装してください。
プロンプト例:
高度なアニメーション効果を持つ棒グラフを実装してください。データ更新時のトランジション、ソート機能、ズーム・パン操作、タッチジェスチャー対応を含めてください。
import React, { useState, useRef, useEffect } from 'react';
interface DataPoint {
label: string;
value: number;
color?: string;
}
interface BarChartProps {
data: DataPoint[];
height?: number;
showGrid?: boolean;
showValues?: boolean;
animated?: boolean;
interactive?: boolean;
className?: string;
}
export const BarChart: React.FC<BarChartProps> = ({
data,
height = 300,
showGrid = true,
showValues = true,
animated = true,
interactive = true,
className = ''
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [animationProgress, setAnimationProgress] = useState(0);
const chartRef = useRef<HTMLDivElement>(null);
// 最大値を計算
const maxValue = Math.max(...data.map(d => d.value));
const roundedMax = Math.ceil(maxValue / 10) * 10;
// Y軸の目盛りを生成
const yAxisSteps = 5;
const yAxisValues = Array.from(
{ length: yAxisSteps + 1 },
(_, i) => (roundedMax / yAxisSteps) * i
);
// デフォルトカラー
const defaultColors = [
'#3B82F6', // blue
'#10B981', // green
'#F59E0B', // yellow
'#EF4444', // red
'#8B5CF6', // purple
'#EC4899', // pink
];
// アニメーション
useEffect(() => {
if (animated) {
const timer = setTimeout(() => {
const interval = setInterval(() => {
setAnimationProgress(prev => {
if (prev >= 1) {
clearInterval(interval);
return 1;
}
return prev + 0.05;
});
}, 20);
}, 100);
return () => {
clearTimeout(timer);
};
} else {
setAnimationProgress(1);
}
}, [animated]);
// バーの高さを計算
const getBarHeight = (value: number) => {
const percentage = (value / roundedMax) * animationProgress;
return percentage * (height - 60); // 軸とラベルのスペースを確保
};
return (
<div className={`w-full ${className}`} ref={chartRef}>
<div className="relative" style={{ height: `${height}px` }}>
{/* Y軸とグリッド */}
<div className="absolute left-0 top-0 bottom-12 w-12 flex flex-col justify-between">
{yAxisValues.reverse().map((value, index) => (
<div key={index} className="relative flex items-center">
<span className="text-xs text-gray-500 pr-2">{value}</span>
{showGrid && index !== yAxisValues.length - 1 && (
<div
className="absolute left-12 w-screen h-px bg-gray-200"
style={{ width: `calc(100vw - 3rem)` }}
/>
)}
</div>
))}
</div>
{/* チャート本体 */}
<div className="ml-12 h-full relative">
<div className="flex items-end justify-around h-full pb-12">
{data.map((item, index) => {
const barHeight = getBarHeight(item.value);
const color = item.color || defaultColors[index % defaultColors.length];
const isHovered = hoveredIndex === index;
return (
<div
key={index}
className="relative flex-1 mx-1 flex flex-col items-center justify-end"
onMouseEnter={() => interactive && setHoveredIndex(index)}
onMouseLeave={() => interactive && setHoveredIndex(null)}
>
{/* 値表示 */}
{showValues && (
<div
className={`
absolute bottom-full mb-2 text-sm font-semibold
transition-all duration-200
${isHovered ? 'text-gray-900' : 'text-gray-600'}
`}
style={{
bottom: `${barHeight}px`,
opacity: animationProgress
}}
>
{item.value}
</div>
)}
{/* バー */}
<div
className={`
w-full rounded-t-md transition-all duration-200
${interactive ? 'cursor-pointer' : ''}
${isHovered ? 'opacity-80' : ''}
`}
style={{
backgroundColor: color,
height: `${barHeight}px`,
transform: isHovered ? 'translateY(-2px)' : 'translateY(0)'
}}
/>
{/* ツールチップ */}
{interactive && isHovered && (
<div className="absolute bottom-full mb-8 px-3 py-2 bg-gray-900 text-white text-xs rounded shadow-lg whitespace-nowrap z-10">
<div className="font-semibold">{item.label}</div>
<div>値: {item.value}</div>
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full">
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900" />
</div>
</div>
)}
{/* X軸ラベル */}
<div className="absolute top-full mt-2 text-xs text-gray-600 whitespace-nowrap">
{item.label}
</div>
</div>
);
})}
</div>
{/* X軸 */}
<div className="absolute bottom-12 left-0 right-0 h-px bg-gray-300" />
</div>
</div>
</div>
);
};
// 使用例
export const BarChartExample = () => {
const [chartType, setChartType] = useState<'quarterly' | 'monthly'>('quarterly');
const quarterlyData: DataPoint[] = [
{ label: 'Q1', value: 50, color: '#3B82F6' },
{ label: 'Q2', value: 68, color: '#10B981' },
{ label: 'Q3', value: 38, color: '#F59E0B' },
{ label: 'Q4', value: 62, color: '#EF4444' },
];
const monthlyData: DataPoint[] = [
{ label: '1月', value: 45 },
{ label: '2月', value: 52 },
{ label: '3月', value: 48 },
{ label: '4月', value: 61 },
{ label: '5月', value: 58 },
{ label: '6月', value: 65 },
];
return (
<div className="p-8 max-w-4xl mx-auto space-y-8">
<div>
<h3 className="text-lg font-semibold mb-4">四半期売上高</h3>
<BarChart
data={quarterlyData}
height={300}
showGrid={true}
showValues={true}
animated={true}
interactive={true}
/>
</div>
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">月別データ</h3>
<div className="flex gap-2">
<button
onClick={() => setChartType('quarterly')}
className={`px-4 py-2 text-sm rounded-md ${
chartType === 'quarterly'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
四半期
</button>
<button
onClick={() => setChartType('monthly')}
className={`px-4 py-2 text-sm rounded-md ${
chartType === 'monthly'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
月別
</button>
</div>
</div>
<BarChart
data={chartType === 'quarterly' ? quarterlyData : monthlyData}
height={250}
showGrid={true}
showValues={true}
animated={true}
interactive={true}
/>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">シンプルな棒グラフ(静的)</h3>
<BarChart
data={[
{ label: 'A', value: 30 },
{ label: 'B', value: 50 },
{ label: 'C', value: 20 },
{ label: 'D', value: 40 },
]}
height={200}
showGrid={false}
showValues={false}
animated={false}
interactive={false}
/>
</div>
</div>
);
};
// 基本的な使用例
import { BarChart } from './bar-chart';
function SalesChart() {
const data = [
{ label: 'Q1', value: 50, color: '#3B82F6' },
{ label: 'Q2', value: 68, color: '#10B981' },
{ label: 'Q3', value: 38, color: '#F59E0B' },
{ label: 'Q4', value: 72, color: '#8B5CF6' },
];
return (
<BarChart
data={data}
height={300}
showGrid={true}
showValues={true}
animated={true}
interactive={true}
/>
);
}
// カスタマイズ例
function SimpleChart() {
const data = [
{ label: 'A', value: 30 },
{ label: 'B', value: 50 },
{ label: 'C', value: 20 },
];
return (
<BarChart
data={data}
height={200}
showGrid={false}
showValues={false}
animated={false}
/>
);
}