Spaces:
Sleeping
Sleeping
| import React, { useState, useMemo } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| const ApplicationTrendsChart = ({ data }) => { | |
| // --- DATA LOGIC --- | |
| const safeData = useMemo(() => { | |
| const applicants = data || []; | |
| const last30Days = Array.from({ length: 30 }, (_, i) => { | |
| const d = new Date(); | |
| d.setDate(d.getDate() - (29 - i)); | |
| return d.toISOString().split('T')[0]; | |
| }); | |
| return last30Days.map(dateStr => { | |
| const count = applicants.filter(a => a.created_at && a.created_at.startsWith(dateStr)).length; | |
| return { name: dateStr.split('-')[2], value: count }; | |
| }); | |
| }, [data]); | |
| const maxValue = 4; | |
| const [hoverIndex, setHoverIndex] = useState(null); | |
| // ✅ FIX: High Resolution Coordinate System (was 100x100) | |
| const width = 800; | |
| const height = 300; | |
| const padding = { | |
| top: 40, | |
| right: 20, | |
| bottom: 50, | |
| left: 40 | |
| }; | |
| const chartWidth = width - padding.left - padding.right; | |
| const chartHeight = height - padding.top - padding.bottom; | |
| const getX = i => padding.left + (i / (safeData.length - 1)) * chartWidth; | |
| const getY = v => padding.top + chartHeight - (v / maxValue) * chartHeight; | |
| const path = safeData | |
| .map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.value)}`) | |
| .join(' '); | |
| const formatDate = (index) => { | |
| const date = new Date(); | |
| date.setDate(date.getDate() - (safeData.length - 1 - index)); | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| }; | |
| const getIndexFromX = (svgX) => { | |
| const ratio = (svgX - padding.left) / chartWidth; | |
| const index = Math.round(ratio * (safeData.length - 1)); | |
| return Math.max(0, Math.min(safeData.length - 1, index)); | |
| }; | |
| return ( | |
| <div style={{ width: '100%', height: '100%', overflow: 'hidden' }}> | |
| <svg | |
| viewBox={`0 0 ${width} ${height}`} | |
| preserveAspectRatio="none" | |
| style={{ width: '100%', height: '100%', display: 'block', overflow: 'visible' }} | |
| onMouseMove={(e) => { | |
| const rect = e.currentTarget.getBoundingClientRect(); | |
| const x = ((e.clientX - rect.left) / rect.width) * width; | |
| setHoverIndex(getIndexFromX(x)); | |
| }} | |
| onMouseLeave={() => setHoverIndex(null)} | |
| > | |
| {/* --- DEFS for Gradients/Shadows --- */} | |
| <defs> | |
| <linearGradient id="lineGradient" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stopColor="#d42d43" stopOpacity={0.2} /> | |
| <stop offset="100%" stopColor="#d42d43" stopOpacity={0} /> | |
| </linearGradient> | |
| </defs> | |
| {/* --- GRID LINES --- */} | |
| {/* X axis line */} | |
| <line | |
| x1={padding.left} | |
| y1={padding.top + chartHeight} | |
| x2={padding.left + chartWidth} | |
| y2={padding.top + chartHeight} | |
| stroke="rgba(255,255,255,0.2)" | |
| strokeWidth="2" // Thicker stroke for clarity | |
| /> | |
| {/* Y axis line */} | |
| <line | |
| x1={padding.left} | |
| y1={padding.top} | |
| x2={padding.left} | |
| y2={padding.top + chartHeight} | |
| stroke="rgba(255,255,255,0.2)" | |
| strokeWidth="2" | |
| /> | |
| {/* --- Y LABELS --- */} | |
| {[0, 2, 4].map((value) => ( | |
| <text | |
| key={`y-${value}`} | |
| x={padding.left - 10} | |
| y={getY(value) + 5} | |
| textAnchor="end" | |
| fontSize="12" // Larger font size for high-res | |
| fontWeight="500" | |
| fill="rgba(255,255,255,0.7)" | |
| style={{ fontFamily: 'Inter, sans-serif' }} | |
| > | |
| {value} | |
| </text> | |
| ))} | |
| {/* --- HOVER LINE --- */} | |
| {hoverIndex !== null && ( | |
| <line | |
| x1={getX(hoverIndex)} | |
| x2={getX(hoverIndex)} | |
| y1={padding.top} | |
| y2={padding.top + chartHeight} | |
| stroke="rgba(255,255,255,0.4)" | |
| strokeWidth="2" | |
| strokeDasharray="5,5" | |
| /> | |
| )} | |
| {/* --- AREA FILL (Optional: Adds depth) --- */} | |
| <path | |
| d={`${path} L ${padding.left + chartWidth} ${padding.top + chartHeight} L ${padding.left} ${padding.top + chartHeight} Z`} | |
| fill="url(#lineGradient)" | |
| /> | |
| {/* --- MAIN TREND LINE --- */} | |
| <motion.path | |
| d={path} | |
| fill="none" | |
| stroke="#d42d43" | |
| strokeWidth="3" // Thicker line | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| initial={{ pathLength: 0, opacity: 0 }} | |
| animate={{ pathLength: 1, opacity: 1 }} | |
| transition={{ duration: 1.5, ease: 'easeOut' }} | |
| /> | |
| {/* --- DOTS --- */} | |
| {safeData.map((d, i) => ( | |
| <circle | |
| key={i} | |
| cx={getX(i)} | |
| cy={getY(d.value)} | |
| r="4" // Larger radius | |
| fill="#1e1e2e" // Dark center | |
| stroke="#ce6925" | |
| strokeWidth="2" | |
| /> | |
| ))} | |
| {/* --- TOOLTIP --- */} | |
| <AnimatePresence> | |
| {hoverIndex !== null && ( | |
| <motion.g | |
| initial={{ opacity: 0, y: 5 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0 }} | |
| transform={`translate(${Math.min(getX(hoverIndex) - 60, width - 140)}, ${padding.top - 30})`} | |
| > | |
| <rect | |
| width="120" | |
| height="50" | |
| rx="8" | |
| fill="white" | |
| fillOpacity="0.95" | |
| stroke="rgba(0,0,0,0.1)" | |
| strokeWidth="1" | |
| filter="drop-shadow(0px 4px 4px rgba(0,0,0,0.25))" | |
| /> | |
| <text | |
| x="60" | |
| y="20" | |
| textAnchor="middle" | |
| fontSize="12" | |
| fontWeight="bold" | |
| fill="#0f172a" | |
| style={{ fontFamily: 'Inter, sans-serif' }} | |
| > | |
| {formatDate(hoverIndex)} | |
| </text> | |
| <text | |
| x="60" | |
| y="38" | |
| textAnchor="middle" | |
| fontSize="12" | |
| fill="#334155" | |
| style={{ fontFamily: 'Inter, sans-serif' }} | |
| > | |
| Applications: {safeData[hoverIndex].value} | |
| </text> | |
| </motion.g> | |
| )} | |
| </AnimatePresence> | |
| {/* --- X LABELS --- */} | |
| {safeData.map((_, i) => { | |
| const isLast = i === safeData.length - 1; | |
| const isSecondLast = i === safeData.length - 2; | |
| // Logic to prevent label crowding | |
| if (i % 5 !== 0 && !isLast) return null; | |
| if (isSecondLast) return null; | |
| return ( | |
| <text | |
| key={`x-label-${i}`} | |
| x={Math.round(getX(i))} | |
| y={Math.round(padding.top + chartHeight + 25)} | |
| textAnchor="middle" | |
| fontSize="11" | |
| fill="rgba(255,255,255,0.6)" | |
| style={{ fontFamily: 'Inter, sans-serif' }} | |
| > | |
| {formatDate(i)} | |
| </text> | |
| ); | |
| })} | |
| </svg> | |
| </div> | |
| ); | |
| }; | |
| export default ApplicationTrendsChart; |