Files
fymio.us/index.html
2026-02-28 08:49:54 +03:00

242 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>