continue re-design
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface RadarChartProps {
|
||||
data: {
|
||||
label: string;
|
||||
score: number;
|
||||
maxScore?: number;
|
||||
}[];
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function RadarChart({ data, size = 300 }: RadarChartProps) {
|
||||
const [isAnimated, setIsAnimated] = useState(false);
|
||||
const center = size / 2;
|
||||
const radius = (size / 2) - 60; // Increased padding for labels
|
||||
const angleStep = (Math.PI * 2) / data.length;
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimated(true);
|
||||
}, []);
|
||||
|
||||
// Calculate points for the polygon
|
||||
const getPoint = (value: number, index: number) => {
|
||||
const angle = index * angleStep - Math.PI / 2;
|
||||
const r = (value / 100) * radius;
|
||||
return {
|
||||
x: center + r * Math.cos(angle),
|
||||
y: center + r * Math.sin(angle)
|
||||
};
|
||||
};
|
||||
|
||||
// Create polygon points string
|
||||
const polygonPoints = data
|
||||
.map((item, i) => {
|
||||
const point = getPoint(isAnimated ? item.score : 0, i);
|
||||
return `${point.x},${point.y}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
// Grid levels
|
||||
const gridLevels = [20, 40, 60, 80, 100];
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg width={size} height={size} className="overflow-visible">
|
||||
<defs>
|
||||
<linearGradient id="radar-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#FF4A00" stopOpacity="0.8" />
|
||||
<stop offset="100%" stopColor="#FF8533" stopOpacity="0.3" />
|
||||
</linearGradient>
|
||||
<filter id="radar-glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Grid circles */}
|
||||
{gridLevels.map((level) => (
|
||||
<circle
|
||||
key={level}
|
||||
cx={center}
|
||||
cy={center}
|
||||
r={(level / 100) * radius}
|
||||
fill="none"
|
||||
stroke="rgba(0,0,0,0.05)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Axis lines */}
|
||||
{data.map((_, i) => {
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const x2 = center + radius * Math.cos(angle);
|
||||
const y2 = center + radius * Math.sin(angle);
|
||||
return (
|
||||
<line
|
||||
key={i}
|
||||
x1={center}
|
||||
y1={center}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke="rgba(0,0,0,0.05)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Data polygon */}
|
||||
<motion.polygon
|
||||
points={polygonPoints}
|
||||
fill="url(#radar-gradient)"
|
||||
stroke="#FF4A00"
|
||||
strokeWidth="2"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
filter="url(#radar-glow)"
|
||||
/>
|
||||
|
||||
{/* Data points */}
|
||||
{data.map((item, i) => {
|
||||
const point = getPoint(item.score, i);
|
||||
return (
|
||||
<motion.circle
|
||||
key={i}
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r="4"
|
||||
fill="#FF4A00"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: isAnimated ? 1 : 0 }}
|
||||
transition={{ delay: 0.8 + i * 0.1, duration: 0.3 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Labels */}
|
||||
{data.map((item, i) => {
|
||||
const angle = i * angleStep - Math.PI / 2;
|
||||
const labelRadius = radius + 40; // Increased label distance
|
||||
const x = center + labelRadius * Math.cos(angle);
|
||||
const y = center + labelRadius * Math.sin(angle);
|
||||
|
||||
// Better text anchor logic based on quadrant
|
||||
let textAnchor = "middle";
|
||||
let dy = 0;
|
||||
|
||||
// Left side
|
||||
if (x < center - 20) {
|
||||
textAnchor = "end";
|
||||
}
|
||||
// Right side
|
||||
else if (x > center + 20) {
|
||||
textAnchor = "start";
|
||||
}
|
||||
|
||||
// Top
|
||||
if (y < center - 20) {
|
||||
dy = -5;
|
||||
}
|
||||
// Bottom
|
||||
else if (y > center + 20) {
|
||||
dy = 5;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.g key={i}>
|
||||
{/* Background for better readability */}
|
||||
<motion.rect
|
||||
x={x - (textAnchor === "middle" ? 30 : textAnchor === "end" ? 60 : 0)}
|
||||
y={y - 10}
|
||||
width={60}
|
||||
height={20}
|
||||
fill="white"
|
||||
fillOpacity={0.9}
|
||||
rx={4}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.9 + i * 0.05 }}
|
||||
/>
|
||||
<motion.text
|
||||
x={x}
|
||||
y={y + dy}
|
||||
textAnchor={textAnchor as any}
|
||||
dominantBaseline="middle"
|
||||
className="text-xs fill-black-alpha-80 font-medium"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 + i * 0.05 }}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{item.label}
|
||||
</motion.text>
|
||||
{/* Score value */}
|
||||
<motion.text
|
||||
x={x}
|
||||
y={y + dy + 12}
|
||||
textAnchor={textAnchor as any}
|
||||
dominantBaseline="middle"
|
||||
className="text-[10px] fill-heat-100 font-bold"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.1 + i * 0.05 }}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{item.score}%
|
||||
</motion.text>
|
||||
</motion.g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-16 flex justify-center">
|
||||
<div className="inline-flex flex-row gap-16 text-xs text-black-alpha-48 bg-white px-16 py-8 rounded-6 shadow-sm">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-12 h-12 rounded-full bg-heat-200" />
|
||||
<span className="whitespace-nowrap">80-100%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-12 h-12 rounded-full bg-heat-100" />
|
||||
<span className="whitespace-nowrap">60-79%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-12 h-12 rounded-full bg-heat-50" />
|
||||
<span className="whitespace-nowrap"><60%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user