242 lines
12 KiB
HTML
242 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Роза показателей</title>
|
||
<script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
|
||
<script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.2/babel.min.js"></script>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { background: #0d0d1a; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="text/babel">
|
||
const RAW_DATA = {
|
||
N1: { интер: 3, поток: 1, чист: 0, сеть: 1 },
|
||
N7: { интер: 0, поток: 1, чист: 0, сеть: 0 },
|
||
N9: { интер: 1, поток: 3, чист: 1, сеть: 0 },
|
||
N6: { интер: 2, поток: 3, чист: 2, сеть: 1 },
|
||
N3: { интер: 1, поток: 1, чист: 3, сеть: 0 },
|
||
N8: { интер: 0, поток: 2, чист: 2, сеть: 1 },
|
||
N2: { интер: 1, поток: 1, чист: 2, сеть: 0 },
|
||
N11: { интер: 1, поток: 1, чист: 2, сеть: 1 },
|
||
N5: { интер: 1, поток: 1, чист: 1, сеть: 2 },
|
||
N10: { интер: 2, поток: 1, чист: 0, сеть: 1 },
|
||
};
|
||
|
||
const CATEGORIES = ["интер", "поток", "чист", "сеть"];
|
||
const CAT_LABELS = { интер: "Интер", поток: "Поток", чист: "Чист трод", сеть: "Сеть" };
|
||
const CAT_COLORS = { интер: "#ff6b6b", поток: "#ffd93d", чист: "#6bcb77", сеть: "#4d96ff" };
|
||
const PARTICIPANTS = Object.keys(RAW_DATA);
|
||
|
||
function polarToXY(angle, r, cx, cy) {
|
||
const rad = (angle - 90) * (Math.PI / 180);
|
||
return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
|
||
}
|
||
|
||
function RadarChart({ highlighted, onSelect }) {
|
||
const size = 340, cx = 170, cy = 170, maxR = 122, maxVal = 3;
|
||
const angleStep = 90;
|
||
const rings = [1, 2, 3];
|
||
|
||
const axes = CATEGORIES.map((cat, i) => {
|
||
const [x2, y2] = polarToXY(i * angleStep, maxR, cx, cy);
|
||
return { cat, i, x2, y2 };
|
||
});
|
||
|
||
return (
|
||
<svg width={size} height={size} style={{display:"block", cursor:"pointer"}}>
|
||
{rings.map(ring => {
|
||
const pts = CATEGORIES.map((_, i) => polarToXY(i * angleStep, (ring/maxVal)*maxR, cx, cy));
|
||
const d = pts.map((pt,i) => (i===0?`M${pt[0]},${pt[1]}`:`L${pt[0]},${pt[1]}`)).join(" ")+" Z";
|
||
return <path key={ring} d={d} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth={1}/>;
|
||
})}
|
||
{axes.map(({cat,x2,y2}) => (
|
||
<line key={cat} x1={cx} y1={cy} x2={x2} y2={y2} stroke="rgba(255,255,255,0.15)" strokeWidth={1}/>
|
||
))}
|
||
{PARTICIPANTS.map(p => {
|
||
const isHl = highlighted === p;
|
||
const dominant = CATEGORIES.reduce((a,b) => RAW_DATA[p][a]>=RAW_DATA[p][b]?a:b);
|
||
const color = CAT_COLORS[dominant];
|
||
const pts = CATEGORIES.map((cat,i) => {
|
||
const r = (RAW_DATA[p][cat]/maxVal)*maxR;
|
||
return polarToXY(i*angleStep, r, cx, cy);
|
||
});
|
||
const d = pts.map((pt,i)=>(i===0?`M${pt[0]},${pt[1]}`:`L${pt[0]},${pt[1]}`)).join(" ")+" Z";
|
||
return (
|
||
<path key={p} d={d}
|
||
fill={color} fillOpacity={isHl?0.55:highlighted?0.04:0.18}
|
||
stroke={color} strokeWidth={isHl?2.5:highlighted?0.5:1}
|
||
strokeOpacity={isHl?1:highlighted?0.25:0.6}
|
||
onClick={()=>onSelect(isHl?null:p)}
|
||
style={{transition:"all 0.25s"}}
|
||
/>
|
||
);
|
||
})}
|
||
{highlighted && CATEGORIES.map((cat,i) => {
|
||
const r = (RAW_DATA[highlighted][cat]/maxVal)*maxR;
|
||
const [x,y] = polarToXY(i*angleStep, r, cx, cy);
|
||
return <circle key={i} cx={x} cy={y} r={5} fill={CAT_COLORS[cat]} stroke="#0d0d1a" strokeWidth={1.5}/>;
|
||
})}
|
||
{axes.map(({cat,i}) => {
|
||
const [lx,ly] = polarToXY(i*angleStep, maxR+26, cx, cy);
|
||
return (
|
||
<text key={cat} x={lx} y={ly} textAnchor="middle" dominantBaseline="middle"
|
||
fill={CAT_COLORS[cat]} fontSize={12} fontFamily="'Courier New',monospace" fontWeight="bold">
|
||
{CAT_LABELS[cat]}
|
||
</text>
|
||
);
|
||
})}
|
||
{rings.map(ring => (
|
||
<text key={ring} x={cx+4} y={cy-(ring/maxVal)*maxR+4}
|
||
fill="rgba(255,255,255,0.3)" fontSize={9} fontFamily="monospace">{ring}</text>
|
||
))}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
function App() {
|
||
const [highlighted, setHighlighted] = React.useState(null);
|
||
const [view, setView] = React.useState("radar");
|
||
|
||
const totals = CATEGORIES.map(cat => ({
|
||
cat, val: PARTICIPANTS.reduce((s,p)=>s+RAW_DATA[p][cat],0)
|
||
}));
|
||
const maxTotal = Math.max(...totals.map(t=>t.val));
|
||
|
||
return (
|
||
<div style={{minHeight:"100vh",background:"#0d0d1a",fontFamily:"'Courier New',monospace",
|
||
color:"#e0e0f0",display:"flex",flexDirection:"column",alignItems:"center",padding:"32px 16px"}}>
|
||
|
||
<div style={{textAlign:"center",marginBottom:28}}>
|
||
<div style={{fontSize:11,letterSpacing:6,color:"#4d96ff",textTransform:"uppercase",marginBottom:6}}>
|
||
Анализ активности
|
||
</div>
|
||
<h1 style={{margin:0,fontSize:28,fontWeight:900,letterSpacing:2,color:"#fff"}}>
|
||
РОЗА ПОКАЗАТЕЛЕЙ
|
||
</h1>
|
||
<div style={{marginTop:8,fontSize:12,color:"rgba(255,255,255,0.4)"}}>N1 – N11 · 4 категории</div>
|
||
</div>
|
||
|
||
<div style={{display:"flex",gap:0,marginBottom:28,border:"1px solid rgba(255,255,255,0.15)",
|
||
borderRadius:8,overflow:"hidden"}}>
|
||
{["radar","bar"].map(v => (
|
||
<button key={v} onClick={()=>setView(v)} style={{
|
||
padding:"8px 24px",background:view===v?"#4d96ff":"transparent",
|
||
border:"none",color:view===v?"#fff":"rgba(255,255,255,0.5)",
|
||
cursor:"pointer",fontFamily:"monospace",fontSize:13,
|
||
fontWeight:view===v?700:400,transition:"all 0.2s"}}>
|
||
{v==="radar"?"🕸 Радар":"📊 Столбцы"}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{display:"flex",flexWrap:"wrap",gap:32,justifyContent:"center",
|
||
alignItems:"flex-start",width:"100%",maxWidth:900}}>
|
||
|
||
<div style={{background:"rgba(255,255,255,0.03)",borderRadius:20,
|
||
border:"1px solid rgba(255,255,255,0.08)",padding:20,
|
||
display:"flex",flexDirection:"column",alignItems:"center"}}>
|
||
|
||
{view==="radar" ? (
|
||
<RadarChart highlighted={highlighted} onSelect={setHighlighted}/>
|
||
) : (
|
||
<div style={{width:340,padding:"20px 10px"}}>
|
||
{PARTICIPANTS.map(p => {
|
||
const total = CATEGORIES.reduce((s,c)=>s+RAW_DATA[p][c],0);
|
||
const isHl = highlighted===p;
|
||
return (
|
||
<div key={p} onClick={()=>setHighlighted(isHl?null:p)}
|
||
style={{display:"flex",alignItems:"center",gap:10,marginBottom:10,
|
||
cursor:"pointer",opacity:highlighted&&!isHl?0.35:1,transition:"opacity 0.2s"}}>
|
||
<div style={{width:30,fontSize:11,color:isHl?"#fff":"rgba(255,255,255,0.6)",fontWeight:isHl?700:400}}>{p}</div>
|
||
<div style={{flex:1,display:"flex",height:18,borderRadius:4,overflow:"hidden"}}>
|
||
{CATEGORIES.map(cat => {
|
||
const w = total>0?(RAW_DATA[p][cat]/total)*100:0;
|
||
return <div key={cat} style={{width:`${w}%`,background:CAT_COLORS[cat],transition:"width 0.3s"}}
|
||
title={`${CAT_LABELS[cat]}: ${RAW_DATA[p][cat]}`}/>;
|
||
})}
|
||
</div>
|
||
<div style={{width:18,fontSize:11,color:"rgba(255,255,255,0.4)",textAlign:"right"}}>{total}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<div style={{display:"flex",gap:14,marginTop:12,flexWrap:"wrap",justifyContent:"center"}}>
|
||
{CATEGORIES.map(cat => (
|
||
<div key={cat} style={{display:"flex",alignItems:"center",gap:5,fontSize:11}}>
|
||
<div style={{width:10,height:10,borderRadius:"50%",background:CAT_COLORS[cat]}}/>
|
||
<span style={{color:"rgba(255,255,255,0.6)"}}>{CAT_LABELS[cat]}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{display:"flex",flexDirection:"column",gap:6,minWidth:200}}>
|
||
<div style={{fontSize:10,letterSpacing:4,color:"rgba(255,255,255,0.3)",marginBottom:8,textTransform:"uppercase"}}>
|
||
Участники
|
||
</div>
|
||
{PARTICIPANTS.map(p => {
|
||
const total = CATEGORIES.reduce((s,c)=>s+RAW_DATA[p][c],0);
|
||
const dominant = CATEGORIES.reduce((a,b)=>RAW_DATA[p][a]>=RAW_DATA[p][b]?a:b);
|
||
const isHl = highlighted===p;
|
||
const col = CAT_COLORS[dominant];
|
||
return (
|
||
<div key={p} onClick={()=>setHighlighted(isHl?null:p)} style={{
|
||
display:"flex",alignItems:"center",gap:12,padding:"8px 14px",
|
||
borderRadius:10,cursor:"pointer",
|
||
background:isHl?`rgba(${parseInt(col.slice(1,3),16)},${parseInt(col.slice(3,5),16)},${parseInt(col.slice(5,7),16)},0.2)`:"rgba(255,255,255,0.03)",
|
||
border:`1px solid ${isHl?col:"rgba(255,255,255,0.06)"}`,
|
||
transition:"all 0.2s",opacity:highlighted&&!isHl?0.4:1}}>
|
||
<div style={{width:8,height:8,borderRadius:"50%",background:col,
|
||
boxShadow:isHl?`0 0 8px ${col}`:"none"}}/>
|
||
<span style={{fontWeight:isHl?700:400,fontSize:14,flex:1}}>{p}</span>
|
||
<div style={{display:"flex",gap:3,alignItems:"flex-end",height:28}}>
|
||
{CATEGORIES.map(cat => (
|
||
<div key={cat} style={{width:8,height:Math.max((RAW_DATA[p][cat]/3)*28,2),
|
||
background:CAT_COLORS[cat],borderRadius:2}}
|
||
title={`${CAT_LABELS[cat]}: ${RAW_DATA[p][cat]}`}/>
|
||
))}
|
||
</div>
|
||
<span style={{fontSize:11,color:"rgba(255,255,255,0.35)",minWidth:16,textAlign:"right"}}>{total}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
<div style={{marginTop:16,padding:"12px 14px",borderRadius:10,
|
||
background:"rgba(255,255,255,0.04)",border:"1px solid rgba(255,255,255,0.08)"}}>
|
||
<div style={{fontSize:10,letterSpacing:3,color:"rgba(255,255,255,0.3)",marginBottom:8,textTransform:"uppercase"}}>
|
||
Итого по категориям
|
||
</div>
|
||
{totals.map(({cat,val}) => (
|
||
<div key={cat} style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
|
||
<div style={{width:8,height:8,borderRadius:"50%",background:CAT_COLORS[cat]}}/>
|
||
<span style={{fontSize:12,flex:1,color:"rgba(255,255,255,0.7)"}}>{CAT_LABELS[cat]}</span>
|
||
<div style={{width:80,height:6,background:"rgba(255,255,255,0.08)",borderRadius:3,overflow:"hidden"}}>
|
||
<div style={{width:`${(val/maxTotal)*100}%`,height:"100%",background:CAT_COLORS[cat],borderRadius:3}}/>
|
||
</div>
|
||
<span style={{fontSize:12,color:CAT_COLORS[cat],fontWeight:700,minWidth:20,textAlign:"right"}}>{val}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{marginTop:32,fontSize:11,color:"rgba(255,255,255,0.2)",textAlign:"center"}}>
|
||
Нажми на участника чтобы выделить · Переключай вид выше
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
|
||
</script>
|
||
</body>
|
||
</html>
|