import { useState, useEffect } from "react"; const DATA = { "№1": { интер: 3, поток: 1, сети: 1, инст: 0 }, "№2": { интер: 1, поток: 1, сети: 0, инст: 2 }, "№3": { интер: 1, поток: 1, сети: 3, инст: 0 }, "№4": { интер: 0, поток: 0, сети: 0, инст: 0 }, "№5": { интер: 1, поток: 1, сети: 2, инст: 1 }, "№6": { интер: 2, поток: 2, сети: 1, инст: 2 }, "№7": { интер: 0, поток: 1, сети: 0, инст: 0 }, "№8": { интер: 0, поток: 2, сети: 1, инст: 2 }, "№9": { интер: 1, поток: 3, сети: 0, инст: 1 }, "№10": { интер: 2, поток: 1, сети: 1, инст: 0 }, "№11": { интер: 1, поток: 1, сети: 1, инст: 2 }, }; const CATS = ["интер", "поток", "сети", "инст"]; const LABELS = { интер: "Интеракция", поток: "Потоки", сети: "Сети", инст: "Институц." }; const COLORS = { интер: "#f97316", поток: "#06b6d4", сети: "#a3e635", инст: "#e879f9" }; const GROUPS = Object.keys(DATA); const MAX_VAL = 3; function polar(angleDeg, r, cx, cy) { const rad = (angleDeg - 90) * Math.PI / 180; return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)]; } function hexToRgb(hex) { const r = parseInt(hex.slice(1,3),16); const g = parseInt(hex.slice(3,5),16); const b = parseInt(hex.slice(5,7),16); return `${r},${g},${b}`; } function dominant(g) { return CATS.reduce((a,b) => DATA[g][a] >= DATA[g][b] ? a : b); } function RadarChart({ highlighted, onSelect }) { const S = 360, cx = 180, cy = 180, maxR = 130; const step = 360 / CATS.length; const rings = [1,2,3]; return ( {GROUPS.map(g => { const dom = dominant(g); const c = COLORS[dom]; return ( ); })} {/* rings */} {rings.map(r => { const pts = CATS.map((_,i) => polar(i*step, (r/MAX_VAL)*maxR, cx, cy)); const d = pts.map((p,i) => `${i===0?'M':'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ') + ' Z'; return ; })} {/* axes */} {CATS.map((cat,i) => { const [x,y] = polar(i*step, maxR, cx, cy); return ; })} {/* group polygons */} {GROUPS.map(g => { const isHl = highlighted === g; const isOther = highlighted && !isHl; const dom = dominant(g); const col = COLORS[dom]; const pts = CATS.map((cat,i) => { const r = DATA[g][cat] === 0 ? 4 : (DATA[g][cat]/MAX_VAL)*maxR; return polar(i*step, r, cx, cy); }); const d = pts.map((p,i) => `${i===0?'M':'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ') + ' Z'; const gId = g.replace('№','n'); return ( onSelect(isHl ? null : g)} style={{cursor:'pointer'}}> {isHl && pts.map((p,i) => ( ))} ); })} {/* axis labels */} {CATS.map((cat,i) => { const [x,y] = polar(i*step, maxR+26, cx, cy); return ( {LABELS[cat]} ); })} {/* ring numbers */} {rings.map(r => { const [x,y] = polar(0, (r/MAX_VAL)*maxR, cx, cy); return {r}; })} {/* center dot */} ); } function StatBar({ g }) { const total = CATS.reduce((s,c) => s+DATA[g][c], 0); if (total === 0) return
нет активности
; return (
{CATS.map(cat => { const v = DATA[g][cat]; const pct = total > 0 ? (v/MAX_VAL)*100 : 0; return (
{LABELS[cat]}
{v}
); })}
); } export default function App() { const [highlighted, setHighlighted] = useState(null); const [mounted, setMounted] = useState(false); useEffect(() => { setTimeout(() => setMounted(true), 100); }, []); const totals = CATS.map(cat => ({ cat, val: GROUPS.reduce((s,g) => s+DATA[g][cat], 0) })); const maxTotal = Math.max(...totals.map(t=>t.val)); const hlData = highlighted ? DATA[highlighted] : null; const hlTotal = hlData ? CATS.reduce((s,c)=>s+hlData[c],0) : 0; return (
{/* Header */}
Групповой анализ

РОЗА АКТИВНОСТИ

№1 – №11 · 4 НАПРАВЛЕНИЯ
{/* Legend */}
{CATS.map(cat => (
{LABELS[cat]}
))}
{/* Radar */}
{/* Highlighted info */}
{highlighted ? ( <>
{highlighted} сумма: {hlTotal}
) : (
Нажми на группу для деталей
)}
{/* Right panel */}
{/* Group list */}
Группы
{GROUPS.map((g,idx) => { const isHl = highlighted===g; const dom = dominant(g); const col = COLORS[dom]; const total = CATS.reduce((s,c)=>s+DATA[g][c],0); const isEmpty = total === 0; return (
setHighlighted(isHl?null:g)} style={{ display:'flex',alignItems:'center',gap:8, padding:'7px 12px',borderRadius:10,cursor:'pointer', background: isHl ? `rgba(${hexToRgb(col)},0.18)` : 'rgba(255,255,255,0.03)', border:`1px solid ${isHl ? col : 'rgba(255,255,255,0.07)'}`, opacity: highlighted&&!isHl ? 0.4 : isEmpty ? 0.4 : 1, transition:'all 0.2s', boxShadow: isHl ? `0 0 16px ${col}40` : 'none', minWidth:70, }}>
{g} {total}
); })}
{/* Category totals */}
Итого по направлениям
{totals.map(({cat,val}) => (
{LABELS[cat]}
{val}
))}
{/* Heat summary */}
Тепловая карта
{CATS.map(c => (
{c.slice(0,3).toUpperCase()}
))} {GROUPS.map(g => ( <>
{g}
{CATS.map(cat => { const v = DATA[g][cat]; const alpha = v/MAX_VAL; return (
setHighlighted(highlighted===g?null:g)} style={{ height:20,borderRadius:4,cursor:'pointer', background: v>0 ? `rgba(${hexToRgb(COLORS[cat])},${0.15+alpha*0.75})` : 'rgba(255,255,255,0.04)', border: highlighted===g ? `1px solid ${COLORS[cat]}80` : '1px solid transparent', display:'flex',alignItems:'center',justifyContent:'center', fontSize:10,color:v>0?'rgba(255,255,255,0.9)':'rgba(255,255,255,0.15)', fontWeight:700,transition:'all 0.2s', }}> {v||'·'}
); })} ))}
НАЖМИ НА ГРУППУ · ВЫДЕЛИ НА РАДАРЕ
); }