592 lines
18 KiB
TypeScript
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 };
|