iris_backend / src /components /ApplicationTrendsChart.jsx
Saandraahh's picture
Fixed Admin side UI, dynamic weights settings, etc...
84d4394
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;