trello_fake/components/statistics/charts.tsx

592 lines
18 KiB
TypeScript

"use client";
import {
PieChart,
Pie,
Cell,
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend,
RadialBarChart,
RadialBar,
AreaChart,
Area,
CartesianGrid,
LineChart,
Line,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
// Colores del tema
const COLORS = {
primary: "#3b82f6", // blue-500
secondary: "#8b5cf6", // purple-500
success: "#22c55e", // green-500
warning: "#f59e0b", // amber-500
danger: "#ef4444", // red-500
info: "#06b6d4", // cyan-500
gray: "#6b7280", // gray-500
orange: "#f97316", // orange-500
pink: "#ec4899", // pink-500
indigo: "#6366f1", // indigo-500
};
const STATUS_COLORS = {
borrador: "#9ca3af", // gray-400
abierto: "#3b82f6", // blue-500
cerrado: "#22c55e", // green-500
};
const PROGRESS_COLORS = ["#ef4444", "#f97316", "#f59e0b", "#22c55e"];
// Tooltip personalizado
interface CustomTooltipProps {
active?: boolean;
payload?: Array<{ name: string; value: number; color?: string }>;
label?: string;
formatter?: (value: number) => string;
}
function CustomTooltip({ active, payload, label, formatter }: CustomTooltipProps) {
if (active && payload && payload.length) {
return (
<div className="bg-white border border-gray-200 shadow-lg rounded-lg p-3">
{label && <p className="text-sm font-medium text-gray-700 mb-1">{label}</p>}
{payload.map((entry, index) => (
<p key={index} className="text-sm" style={{ color: entry.color }}>
{entry.name}: {formatter ? formatter(entry.value) : entry.value}
</p>
))}
</div>
);
}
return null;
}
// ==================== PIE CHART ====================
interface PieChartData {
name: string;
value: number;
color?: string;
}
interface StatusPieChartProps {
data: PieChartData[];
title: string;
subtitle?: string;
}
export function StatusPieChart({ data, title, subtitle }: StatusPieChartProps) {
const total = data.reduce((acc, item) => acc + item.value, 0);
const colors = [STATUS_COLORS.borrador, STATUS_COLORS.abierto, STATUS_COLORS.cerrado];
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={90}
paddingAngle={2}
dataKey="value"
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
labelLine={false}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color || colors[index % colors.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex justify-center gap-4 mt-2">
{data.map((entry, index) => (
<div key={entry.name} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color || colors[index % colors.length] }}
/>
<span className="text-xs text-gray-600">
{entry.name}: {entry.value}
</span>
</div>
))}
</div>
</CardContent>
</Card>
);
}
// ==================== DONUT CHART ====================
interface DonutChartProps {
data: PieChartData[];
title: string;
centerLabel?: string;
centerValue?: string | number;
}
export function DonutChart({ data, title, centerLabel, centerValue }: DonutChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64 relative">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={70}
outerRadius={100}
paddingAngle={3}
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color || PROGRESS_COLORS[index % PROGRESS_COLORS.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
{centerLabel && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<span className="text-2xl font-bold text-gray-900">{centerValue}</span>
<span className="text-xs text-gray-500">{centerLabel}</span>
</div>
)}
</div>
<div className="flex flex-wrap justify-center gap-3 mt-2">
{data.map((entry, index) => (
<div key={entry.name} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color || PROGRESS_COLORS[index % PROGRESS_COLORS.length] }}
/>
<span className="text-xs text-gray-600">{entry.name}</span>
</div>
))}
</div>
</CardContent>
</Card>
);
}
// ==================== RADIAL BAR CHART ====================
interface RadialBarData {
name: string;
value: number;
fill: string;
}
interface RadialProgressChartProps {
data: RadialBarData[];
title: string;
subtitle?: string;
}
export function RadialProgressChart({ data, title, subtitle }: RadialProgressChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart
cx="50%"
cy="50%"
innerRadius="30%"
outerRadius="100%"
data={data}
startAngle={180}
endAngle={0}
>
<RadialBar
background
dataKey="value"
cornerRadius={10}
label={{ position: "insideStart", fill: "#fff", fontSize: 12 }}
/>
<Tooltip content={<CustomTooltip formatter={(v) => `${v}%`} />} />
<Legend
iconSize={10}
layout="horizontal"
verticalAlign="bottom"
wrapperStyle={{ fontSize: "12px" }}
/>
</RadialBarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// ==================== SINGLE RADIAL GAUGE ====================
interface RadialGaugeProps {
value: number;
maxValue?: number;
title: string;
subtitle?: string;
color?: string;
}
export function RadialGauge({ value, maxValue = 100, title, subtitle, color = COLORS.primary }: RadialGaugeProps) {
const percentage = Math.min((value / maxValue) * 100, 100);
const data = [
{ name: title, value: percentage, fill: color },
];
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-48 relative">
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart
cx="50%"
cy="50%"
innerRadius="60%"
outerRadius="90%"
data={data}
startAngle={180}
endAngle={0}
>
<RadialBar
background={{ fill: "#f3f4f6" }}
dataKey="value"
cornerRadius={15}
/>
</RadialBarChart>
</ResponsiveContainer>
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none" style={{ marginTop: "-20px" }}>
<span className="text-3xl font-bold text-gray-900">{value}%</span>
</div>
</div>
</CardContent>
</Card>
);
}
// ==================== BAR CHART ====================
interface BarChartData {
name: string;
value: number;
value2?: number;
}
interface HorizontalBarChartProps {
data: BarChartData[];
title: string;
subtitle?: string;
dataKey?: string;
color?: string;
formatter?: (value: number) => string;
}
export function HorizontalBarChart({
data,
title,
subtitle,
dataKey = "value",
color = COLORS.primary,
formatter,
}: HorizontalBarChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} layout="vertical" margin={{ left: 20, right: 20 }}>
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" tickFormatter={formatter} />
<YAxis type="category" dataKey="name" width={100} tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip formatter={formatter} />} />
<Bar dataKey={dataKey} fill={color} radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// ==================== VERTICAL BAR CHART ====================
interface VerticalBarChartProps {
data: BarChartData[];
title: string;
subtitle?: string;
color?: string;
secondaryColor?: string;
showSecondary?: boolean;
formatter?: (value: number) => string;
}
export function VerticalBarChart({
data,
title,
subtitle,
color = COLORS.primary,
secondaryColor = COLORS.secondary,
showSecondary = false,
formatter,
}: VerticalBarChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
<YAxis tickFormatter={formatter} tick={{ fontSize: 11 }} />
<Tooltip content={<CustomTooltip formatter={formatter} />} />
<Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} name="Presupuesto" />
{showSecondary && (
<Bar dataKey="value2" fill={secondaryColor} radius={[4, 4, 0, 0]} name="Gastado" />
)}
{showSecondary && <Legend wrapperStyle={{ fontSize: "12px" }} />}
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// ==================== AREA CHART ====================
interface AreaChartData {
name: string;
value: number;
value2?: number;
}
interface AreaChartProps {
data: AreaChartData[];
title: string;
subtitle?: string;
color?: string;
gradientId?: string;
}
export function AreaChartComponent({
data,
title,
subtitle,
color = COLORS.primary,
gradientId = "colorValue",
}: AreaChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
<stop offset="95%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="value"
stroke={color}
fillOpacity={1}
fill={`url(#${gradientId})`}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// ==================== LINE CHART ====================
interface LineChartData {
name: string;
presupuesto: number;
gastado: number;
}
interface BudgetLineChartProps {
data: LineChartData[];
title: string;
subtitle?: string;
}
export function BudgetLineChart({ data, title, subtitle }: BudgetLineChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="name" tick={{ fontSize: 10 }} angle={-45} textAnchor="end" height={60} />
<YAxis tick={{ fontSize: 11 }} tickFormatter={(v) => `${(v / 1000).toFixed(0)}k`} />
<Tooltip
content={<CustomTooltip formatter={(v) => `€${v.toLocaleString("es-ES")}`} />}
/>
<Legend wrapperStyle={{ fontSize: "12px" }} />
<Line
type="monotone"
dataKey="presupuesto"
stroke={COLORS.primary}
strokeWidth={2}
dot={{ r: 3 }}
name="Presupuesto"
/>
<Line
type="monotone"
dataKey="gastado"
stroke={COLORS.success}
strokeWidth={2}
dot={{ r: 3 }}
name="Gastado"
/>
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// ==================== STACKED BAR CHART ====================
interface StackedBarData {
name: string;
completado: number;
restante: number;
}
interface StackedProgressChartProps {
data: StackedBarData[];
title: string;
subtitle?: string;
}
export function StackedProgressChart({ data, title, subtitle }: StackedProgressChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} layout="vertical" margin={{ left: 0, right: 20 }}>
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" domain={[0, 100]} tickFormatter={(v) => `${v}%`} />
<YAxis type="category" dataKey="name" width={120} tick={{ fontSize: 11 }} />
<Tooltip content={<CustomTooltip formatter={(v) => `${v}%`} />} />
<Legend wrapperStyle={{ fontSize: "12px" }} />
<Bar dataKey="completado" stackId="a" fill={COLORS.success} name="Completado" radius={[0, 0, 0, 0]} />
<Bar dataKey="restante" stackId="a" fill="#e5e7eb" name="Restante" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// ==================== MULTI RADIAL CHART ====================
interface MultiRadialData {
name: string;
value: number;
fill: string;
}
interface MultiRadialChartProps {
data: MultiRadialData[];
title: string;
subtitle?: string;
}
export function MultiRadialChart({ data, title, subtitle }: MultiRadialChartProps) {
return (
<Card className="border-gray-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
</CardHeader>
<CardContent>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart
cx="50%"
cy="50%"
innerRadius="20%"
outerRadius="90%"
data={data}
startAngle={90}
endAngle={-270}
>
<RadialBar
background={{ fill: "#f3f4f6" }}
dataKey="value"
cornerRadius={5}
/>
<Tooltip content={<CustomTooltip formatter={(v) => `${v}%`} />} />
<Legend
iconSize={10}
layout="vertical"
verticalAlign="middle"
align="right"
wrapperStyle={{ fontSize: "11px", right: 0 }}
/>
</RadialBarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
export { COLORS, STATUS_COLORS, PROGRESS_COLORS };