/* ── v4 Charts: NodeNetwork, SkillRadar, Dashboard Charts ── */
/* ── Node Network Canvas ── */
function NodeNetwork({ style }) {
const canvasRef = React.useRef(null);
const animRef = React.useRef(null);
const nodesRef = React.useRef([]);
const mouseRef = React.useRef({ x: -1000, y: -1000 });
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
React.useEffect(() => {
if (isMobile) return; // skip on mobile
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
let cw, ch;
const resize = () => {
cw = canvas.offsetWidth; ch = canvas.offsetHeight;
canvas.width = cw * dpr; canvas.height = ch * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
const initNodes = () => {
const count = Math.min(70, Math.floor((cw * ch) / 14000));
nodesRef.current = Array.from({ length: count }, () => ({
x: Math.random() * cw, y: Math.random() * ch,
vx: (Math.random() - 0.5) * 0.35, vy: (Math.random() - 0.5) * 0.35,
r: Math.random() * 2 + 1, pulse: Math.random() * Math.PI * 2,
}));
};
const draw = () => {
ctx.clearRect(0, 0, cw, ch);
const nodes = nodesRef.current;
const mx = mouseRef.current.x, my = mouseRef.current.y;
nodes.forEach(n => {
n.x += n.vx; n.y += n.vy; n.pulse += 0.02;
if (n.x < 0 || n.x > cw) n.vx *= -1;
if (n.y < 0 || n.y > ch) n.vy *= -1;
const dxm = n.x - mx, dym = n.y - my, dm = Math.sqrt(dxm*dxm + dym*dym);
if (dm < 150) { const f = (150-dm)/150*0.8; n.x += (dxm/dm)*f; n.y += (dym/dm)*f; }
});
for (let i = 0; i < nodes.length; i++) {
for (let j = i+1; j < nodes.length; j++) {
const dx = nodes[i].x-nodes[j].x, dy = nodes[i].y-nodes[j].y, d = Math.sqrt(dx*dx+dy*dy);
if (d < 130) { ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.strokeStyle = `rgba(124,58,237,${(1-d/130)*0.14})`; ctx.lineWidth = 0.5; ctx.stroke(); }
}
const dxm = nodes[i].x-mx, dym = nodes[i].y-my, dm = Math.sqrt(dxm*dxm+dym*dym);
if (dm < 200) { ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(mx, my); ctx.strokeStyle = `rgba(45,212,191,${(1-dm/200)*0.3})`; ctx.lineWidth = 0.8; ctx.stroke(); }
}
nodes.forEach(n => {
const g = Math.sin(n.pulse)*0.3+0.7;
ctx.beginPath(); ctx.arc(n.x, n.y, n.r*g, 0, Math.PI*2);
ctx.fillStyle = `rgba(124,58,237,${0.4+g*0.3})`; ctx.fill();
});
animRef.current = requestAnimationFrame(draw);
};
resize(); initNodes(); draw();
const onMove = e => { const r = canvas.getBoundingClientRect(); mouseRef.current = { x: e.clientX-r.left, y: e.clientY-r.top }; };
const onLeave = () => { mouseRef.current = { x: -1000, y: -1000 }; };
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseleave', onLeave);
const onVis = () => { if (document.hidden) cancelAnimationFrame(animRef.current); else draw(); };
document.addEventListener('visibilitychange', onVis);
const onResize = () => { resize(); initNodes(); };
window.addEventListener('resize', onResize);
return () => { cancelAnimationFrame(animRef.current); canvas.removeEventListener('mousemove', onMove); canvas.removeEventListener('mouseleave', onLeave); document.removeEventListener('visibilitychange', onVis); window.removeEventListener('resize', onResize); };
}, [isMobile]);
if (isMobile) return null;
return ;
}
window.NodeNetwork = NodeNetwork;
/* ── Typing Code ── */
function TypingCode() {
const lines = [
{ text: 'import pandas as pd', color: '#c084fc' },
{ text: 'from sklearn.ensemble import GradientBoostingClassifier', color: '#c084fc' },
{ text: '', color: '' },
{ text: 'df = pd.read_sql("SELECT * FROM customers", conn)', color: '#2dd4bf' },
{ text: 'X_train, X_test = train_test_split(df, test_size=0.2)', color: '#2dd4bf' },
{ text: '', color: '' },
{ text: 'model = GradientBoostingClassifier(n_estimators=200)', color: '#f9a8d4' },
{ text: 'model.fit(X_train, y_train)', color: '#f9a8d4' },
{ text: 'accuracy = model.score(X_test, y_test) # 0.94', color: '#fbbf24' },
];
const [displayLines, setDisplayLines] = React.useState([]);
const [curLine, setCurLine] = React.useState(0);
const [curChar, setCurChar] = React.useState(0);
const [blink, setBlink] = React.useState(true);
React.useEffect(() => { const iv = setInterval(() => setBlink(b => !b), 530); return () => clearInterval(iv); }, []);
React.useEffect(() => {
if (curLine >= lines.length) { const t = setTimeout(() => { setDisplayLines([]); setCurLine(0); setCurChar(0); }, 3000); return () => clearTimeout(t); }
const line = lines[curLine];
if (line.text === '') { setDisplayLines(p => [...p, { text: '', color: '' }]); setCurLine(l => l+1); setCurChar(0); return; }
if (curChar <= line.text.length) {
const t = setTimeout(() => {
setDisplayLines(p => { const n = [...p]; if (n.length <= curLine) n.push({ text: '', color: line.color }); n[curLine] = { text: line.text.substring(0, curChar), color: line.color }; return n; });
setCurChar(c => c+1);
}, 25 + Math.random()*20);
return () => clearTimeout(t);
} else { const t = setTimeout(() => { setCurLine(l => l+1); setCurChar(0); }, 400); return () => clearTimeout(t); }
}, [curLine, curChar]);
return (
{displayLines.map((l, i) => (
{String(i+1).padStart(2,' ')}
{l.text}
{i === displayLines.length-1 && curLine < lines.length && ▊}
))}
{curLine >= lines.length && displayLines.length > 0 && (
✓ Model trained — Accuracy: 94.2%
)}
);
}
window.TypingCode = TypingCode;
/* ── Skill Radar ── */
function SkillRadar({ skills, size = 280 }) {
const canvasRef = React.useRef(null);
const [ref, visible] = useReveal(0.2);
const [hovered, setHovered] = React.useState(-1);
const animProgress = React.useRef(0);
React.useEffect(() => {
if (!visible) return;
const canvas = canvasRef.current; if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = size*dpr; canvas.height = size*dpr; ctx.scale(dpr, dpr);
const cx = size/2, cy = size/2, maxR = size/2-40, n = skills.length, step = (Math.PI*2)/n;
let startTime = null;
const animate = (ts) => {
if (!startTime) startTime = ts;
const p = Math.min((ts-startTime)/1200, 1), eased = 1-Math.pow(1-p, 3);
animProgress.current = eased;
ctx.clearRect(0, 0, size, size);
// Grid + percentage labels
for (let ring = 1; ring <= 5; ring++) {
const r = (maxR/5)*ring;
ctx.beginPath();
for (let i = 0; i <= n; i++) { const a = step*i - Math.PI/2, x = cx+Math.cos(a)*r, y = cy+Math.sin(a)*r; i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y); }
ctx.closePath(); ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; ctx.stroke();
// % labels on right axis
ctx.font = '500 9px "DM Sans", sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.15)'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.fillText(`${ring*20}%`, cx + r + 4, cy);
}
skills.forEach((_, i) => { const a = step*i - Math.PI/2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx+Math.cos(a)*maxR, cy+Math.sin(a)*maxR); ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.stroke(); });
ctx.beginPath();
skills.forEach((s, i) => { const a = step*i - Math.PI/2, r = (s.value/100)*maxR*eased; const x = cx+Math.cos(a)*r, y = cy+Math.sin(a)*r; i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y); });
ctx.closePath();
const grad = ctx.createLinearGradient(0, 0, size, size);
grad.addColorStop(0, 'rgba(124,58,237,0.25)'); grad.addColorStop(1, 'rgba(45,212,191,0.15)');
ctx.fillStyle = grad; ctx.fill(); ctx.strokeStyle = 'rgba(124,58,237,0.6)'; ctx.lineWidth = 2; ctx.stroke();
skills.forEach((s, i) => {
const a = step*i - Math.PI/2, r = (s.value/100)*maxR*eased;
const px = cx+Math.cos(a)*r, py = cy+Math.sin(a)*r;
ctx.beginPath(); ctx.arc(px, py, hovered===i ? 5 : 3.5, 0, Math.PI*2);
ctx.fillStyle = hovered===i ? C.teal : C.purple; ctx.fill();
if (hovered===i) { ctx.beginPath(); ctx.arc(px, py, 10, 0, Math.PI*2); ctx.fillStyle = 'rgba(45,212,191,0.15)'; ctx.fill(); }
const lr = maxR+24, lx = cx+Math.cos(a)*lr, ly = cy+Math.sin(a)*lr;
ctx.font = `${hovered===i ? '600' : '500'} 11px "DM Sans", sans-serif`;
ctx.fillStyle = hovered===i ? C.text : C.textMuted; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(s.label, lx, ly);
if (hovered===i) { ctx.font = '700 12px "DM Sans"'; ctx.fillStyle = C.teal; ctx.fillText(`${s.value}%`, lx, ly+14); }
});
if (p < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [visible, skills, size, hovered]);
const handleMove = (e) => {
const canvas = canvasRef.current; if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX-rect.left, my = e.clientY-rect.top;
const cx = size/2, cy = size/2, maxR = size/2-40, n = skills.length, step = (Math.PI*2)/n;
let closest = -1, closestDist = 30;
skills.forEach((s, i) => { const a = step*i-Math.PI/2, r = (s.value/100)*maxR*animProgress.current; const d = Math.sqrt((mx-cx-Math.cos(a)*r)**2 + (my-cy-Math.sin(a)*r)**2); if (d < closestDist) { closestDist = d; closest = i; } });
setHovered(closest);
};
return (
);
}
window.SkillRadar = SkillRadar;
/* ── Sales Forecast Line Chart ── */
function ForecastChart({ width = 360, height = 200 }) {
const canvasRef = React.useRef(null);
const [ref, visible] = useReveal(0.2);
const actual = [42, 48, 55, 52, 61, 68, 65, 72, 78, 74, 82, 88];
const forecast = [null,null,null,null,null,null,null,null,null,null,null,null, 92, 97, 103];
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','Jan','Feb','Mar'];
const allVals = [...actual, 92, 97, 103];
const maxV = Math.max(...allVals), minV = Math.min(...allVals);
React.useEffect(() => {
if (!visible) return;
const canvas = canvasRef.current; if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = width*dpr; canvas.height = height*dpr; ctx.scale(dpr, dpr);
const pad = { t: 20, r: 20, b: 30, l: 40 };
const cw = width-pad.l-pad.r, ch = height-pad.t-pad.b;
const toX = i => pad.l + (i/(months.length-1))*cw;
const toY = v => pad.t + (1-(v-minV+5)/(maxV-minV+10))*ch;
let startTime = null;
const draw = (ts) => {
if (!startTime) startTime = ts;
const p = Math.min((ts-startTime)/1500, 1), e = 1-Math.pow(1-p, 3);
ctx.clearRect(0, 0, width, height);
// Grid
ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1;
for (let v = Math.ceil(minV/10)*10; v <= maxV+5; v += 20) {
const y = toY(v); ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(width-pad.r, y); ctx.stroke();
ctx.font = '500 10px "DM Sans"'; ctx.fillStyle = C.textDim; ctx.textAlign = 'right'; ctx.fillText(`$${v}K`, pad.l-6, y+3);
}
months.forEach((m, i) => { if (i % 2 === 0) { ctx.font = '500 9px "DM Sans"'; ctx.fillStyle = C.textDim; ctx.textAlign = 'center'; ctx.fillText(m, toX(i), height-8); } });
// Actual line
const drawCount = Math.floor(e * actual.length);
ctx.beginPath(); ctx.strokeStyle = C.purple; ctx.lineWidth = 2.5; ctx.lineJoin = 'round';
actual.forEach((v, i) => { if (i > drawCount) return; const x = toX(i), y = toY(v); i===0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); });
ctx.stroke();
// Forecast line (dashed)
if (e > 0.7) {
const fp = (e-0.7)/0.3;
ctx.beginPath(); ctx.strokeStyle = C.teal; ctx.lineWidth = 2; ctx.setLineDash([6, 4]);
const forecastData = [actual[actual.length-1], 92, 97, 103];
const drawF = Math.floor(fp * (forecastData.length));
forecastData.forEach((v, i) => { if (i > drawF) return; const x = toX(11+i), y = toY(v); i===0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); });
ctx.stroke(); ctx.setLineDash([]);
}
// Dots
actual.forEach((v, i) => { if (i > drawCount) return; ctx.beginPath(); ctx.arc(toX(i), toY(v), 3, 0, Math.PI*2); ctx.fillStyle = C.purple; ctx.fill(); });
if (p < 1) requestAnimationFrame(draw);
};
requestAnimationFrame(draw);
}, [visible]);
return (
Revenue Forecast
12-month actual + 3-month forecast
Actual
Forecast
);
}
window.ForecastChart = ForecastChart;
/* ── Customer Segment Donut ── */
function DonutChart({ size = 220 }) {
const canvasRef = React.useRef(null);
const [ref, visible] = useReveal(0.2);
const [hovered, setHovered] = React.useState(-1);
const segments = [
{ label: 'Champions', value: 35, color: '#7c3aed', count: '4,200' },
{ label: 'Loyal', value: 28, color: '#2dd4bf', count: '3,360' },
{ label: 'At Risk', value: 22, color: '#f59e0b', count: '2,640' },
{ label: 'Lost', value: 15, color: '#ef4444', count: '1,800' },
];
React.useEffect(() => {
if (!visible) return;
const canvas = canvasRef.current; if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = size*dpr; canvas.height = size*dpr; ctx.scale(dpr, dpr);
const cx = size/2, cy = size/2, outerR = size/2-10, innerR = outerR*0.6;
let startTime = null;
const draw = (ts) => {
if (!startTime) startTime = ts;
const p = Math.min((ts-startTime)/1200, 1), e = 1-Math.pow(1-p, 3);
ctx.clearRect(0, 0, size, size);
let angle = -Math.PI/2;
const totalAngle = Math.PI*2*e;
segments.forEach((s, i) => {
const sweep = (s.value/100)*totalAngle;
const r = hovered === i ? outerR + 4 : outerR;
ctx.beginPath(); ctx.arc(cx, cy, r, angle, angle+sweep); ctx.arc(cx, cy, innerR, angle+sweep, angle, true); ctx.closePath();
ctx.fillStyle = s.color; ctx.globalAlpha = hovered === i ? 1 : 0.85; ctx.fill(); ctx.globalAlpha = 1;
angle += sweep;
});
// Center text
ctx.font = '700 22px "DM Sans"'; ctx.fillStyle = C.white; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
if (hovered >= 0) {
ctx.fillText(`${segments[hovered].value}%`, cx, cy-6);
ctx.font = '500 11px "DM Sans"'; ctx.fillStyle = C.textMuted; ctx.fillText(segments[hovered].label, cx, cy+12);
} else {
ctx.fillText('12K', cx, cy-6);
ctx.font = '500 11px "DM Sans"'; ctx.fillStyle = C.textMuted; ctx.fillText('Customers', cx, cy+12);
}
if (p < 1) requestAnimationFrame(draw);
};
requestAnimationFrame(draw);
}, [visible, hovered]);
const handleMove = (e) => {
const canvas = canvasRef.current; if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX-rect.left-size/2, my = e.clientY-rect.top-size/2;
const d = Math.sqrt(mx*mx+my*my), outerR = size/2-10, innerR = outerR*0.6;
if (d < innerR || d > outerR+5) { setHovered(-1); return; }
let a = Math.atan2(my, mx) + Math.PI/2; if (a < 0) a += Math.PI*2;
let cum = 0;
for (let i = 0; i < segments.length; i++) { cum += segments[i].value/100; if (a/(Math.PI*2) < cum) { setHovered(i); return; } }
setHovered(-1);
};
return (
Customer Segments
RFM segmentation analysis
);
}
window.DonutChart = DonutChart;
/* ── NLP Sentiment Bars ── */
function SentimentChart() {
const [ref, visible] = useReveal(0.2);
const data = [
{ label: 'Positive', value: 64, color: '#22c55e' },
{ label: 'Neutral', value: 24, color: '#94a3b8' },
{ label: 'Negative', value: 12, color: '#ef4444' },
];
return (
Sentiment Analysis
NLP classification on 100K reviews
{data.map((d, i) => (
))}
Overall sentiment score: +0.72 (bullish)
);
}
window.SentimentChart = SentimentChart;