upd
This commit is contained in:
227
src/App.jsx
227
src/App.jsx
@@ -26,27 +26,22 @@ function polar(angleDeg, r, cx, cy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
const r = parseInt(hex.slice(1,3),16);
|
return `${parseInt(hex.slice(1,3),16)},${parseInt(hex.slice(3,5),16)},${parseInt(hex.slice(5,7),16)}`;
|
||||||
const g = parseInt(hex.slice(3,5),16);
|
|
||||||
const b = parseInt(hex.slice(5,7),16);
|
|
||||||
return `${r},${g},${b}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dominant(g) {
|
function dominant(g) {
|
||||||
return CATS.reduce((a,b) => DATA[g][a] >= DATA[g][b] ? a : b);
|
return CATS.reduce((a,b) => DATA[g][a] >= DATA[g][b] ? a : b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadarChart({ highlighted, onSelect }) {
|
function RadarChart({ highlighted, onSelect, size }) {
|
||||||
const S = 360, cx = 180, cy = 180, maxR = 130;
|
const cx = size/2, cy = size/2, maxR = size/2 - 44;
|
||||||
const step = 360 / CATS.length;
|
const step = 360 / CATS.length;
|
||||||
const rings = [1,2,3];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg width={S} height={S} viewBox={`0 0 ${S} ${S}`}>
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{maxWidth:'100%',display:'block'}}>
|
||||||
<defs>
|
<defs>
|
||||||
{GROUPS.map(g => {
|
{GROUPS.map(g => {
|
||||||
const dom = dominant(g);
|
const c = COLORS[dominant(g)];
|
||||||
const c = COLORS[dom];
|
|
||||||
return (
|
return (
|
||||||
<radialGradient key={g} id={`grad-${g.replace('№','n')}`} cx="50%" cy="50%" r="50%">
|
<radialGradient key={g} id={`grad-${g.replace('№','n')}`} cx="50%" cy="50%" r="50%">
|
||||||
<stop offset="0%" stopColor={c} stopOpacity="0.7"/>
|
<stop offset="0%" stopColor={c} stopOpacity="0.7"/>
|
||||||
@@ -56,35 +51,30 @@ function RadarChart({ highlighted, onSelect }) {
|
|||||||
})}
|
})}
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{/* rings */}
|
{[1,2,3].map(r => {
|
||||||
{rings.map(r => {
|
|
||||||
const pts = CATS.map((_,i) => polar(i*step, (r/MAX_VAL)*maxR, cx, cy));
|
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';
|
const d = pts.map((p,i) => `${i===0?'M':'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ') + ' Z';
|
||||||
return <path key={r} d={d} fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth={r===MAX_VAL?1.5:1} strokeDasharray={r===MAX_VAL?"":"4,4"}/>;
|
return <path key={r} d={d} fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth={r===3?1.5:1} strokeDasharray={r===3?"":"4,4"}/>;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* axes */}
|
|
||||||
{CATS.map((cat,i) => {
|
{CATS.map((cat,i) => {
|
||||||
const [x,y] = polar(i*step, maxR, cx, cy);
|
const [x,y] = polar(i*step, maxR, cx, cy);
|
||||||
return <line key={cat} x1={cx} y1={cy} x2={x} y2={y} stroke="rgba(255,255,255,0.12)" strokeWidth={1}/>;
|
return <line key={cat} x1={cx} y1={cy} x2={x} y2={y} stroke="rgba(255,255,255,0.12)" strokeWidth={1}/>;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* group polygons */}
|
|
||||||
{GROUPS.map(g => {
|
{GROUPS.map(g => {
|
||||||
const isHl = highlighted === g;
|
const isHl = highlighted === g;
|
||||||
const isOther = highlighted && !isHl;
|
const isOther = highlighted && !isHl;
|
||||||
const dom = dominant(g);
|
const col = COLORS[dominant(g)];
|
||||||
const col = COLORS[dom];
|
|
||||||
const pts = CATS.map((cat,i) => {
|
const pts = CATS.map((cat,i) => {
|
||||||
const r = DATA[g][cat] === 0 ? 4 : (DATA[g][cat]/MAX_VAL)*maxR;
|
const r = DATA[g][cat] === 0 ? 4 : (DATA[g][cat]/MAX_VAL)*maxR;
|
||||||
return polar(i*step, r, cx, cy);
|
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 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 (
|
return (
|
||||||
<g key={g} onClick={() => onSelect(isHl ? null : g)} style={{cursor:'pointer'}}>
|
<g key={g} onClick={() => onSelect(isHl ? null : g)} style={{cursor:'pointer'}}>
|
||||||
<path d={d}
|
<path d={d}
|
||||||
fill={isHl ? `url(#grad-${gId})` : col}
|
fill={isHl ? `url(#grad-${g.replace('№','n')})` : col}
|
||||||
fillOpacity={isHl ? 1 : isOther ? 0.03 : 0.15}
|
fillOpacity={isHl ? 1 : isOther ? 0.03 : 0.15}
|
||||||
stroke={col}
|
stroke={col}
|
||||||
strokeWidth={isHl ? 2.5 : isOther ? 0.5 : 1.2}
|
strokeWidth={isHl ? 2.5 : isOther ? 0.5 : 1.2}
|
||||||
@@ -94,32 +84,28 @@ function RadarChart({ highlighted, onSelect }) {
|
|||||||
{isHl && pts.map((p,i) => (
|
{isHl && pts.map((p,i) => (
|
||||||
<circle key={i} cx={p[0]} cy={p[1]} r={5}
|
<circle key={i} cx={p[0]} cy={p[1]} r={5}
|
||||||
fill={COLORS[CATS[i]]} stroke="#0a0a14" strokeWidth={2}
|
fill={COLORS[CATS[i]]} stroke="#0a0a14" strokeWidth={2}
|
||||||
style={{filter:`drop-shadow(0 0 6px ${COLORS[CATS[i]]})`}}
|
style={{filter:`drop-shadow(0 0 6px ${COLORS[CATS[i]]})`}}/>
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* axis labels */}
|
|
||||||
{CATS.map((cat,i) => {
|
{CATS.map((cat,i) => {
|
||||||
const [x,y] = polar(i*step, maxR+26, cx, cy);
|
const [x,y] = polar(i*step, maxR+20, cx, cy);
|
||||||
return (
|
return (
|
||||||
<text key={cat} x={x} y={y} textAnchor="middle" dominantBaseline="middle"
|
<text key={cat} x={x} y={y} textAnchor="middle" dominantBaseline="middle"
|
||||||
fill={COLORS[cat]} fontSize={11} fontFamily="'DM Mono',monospace" fontWeight="600"
|
fill={COLORS[cat]} fontSize={10} fontFamily="'DM Mono',monospace" fontWeight="600"
|
||||||
style={{filter:`drop-shadow(0 0 8px ${COLORS[cat]}80)`}}>
|
style={{filter:`drop-shadow(0 0 8px ${COLORS[cat]}80)`}}>
|
||||||
{LABELS[cat]}
|
{LABELS[cat]}
|
||||||
</text>
|
</text>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* ring numbers */}
|
{[1,2,3].map(r => {
|
||||||
{rings.map(r => {
|
|
||||||
const [x,y] = polar(0, (r/MAX_VAL)*maxR, cx, cy);
|
const [x,y] = polar(0, (r/MAX_VAL)*maxR, cx, cy);
|
||||||
return <text key={r} x={cx+5} y={y+4} fill="rgba(255,255,255,0.25)" fontSize={9} fontFamily="monospace">{r}</text>;
|
return <text key={r} x={cx+4} y={y+4} fill="rgba(255,255,255,0.25)" fontSize={8} fontFamily="monospace">{r}</text>;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* center dot */}
|
|
||||||
<circle cx={cx} cy={cy} r={3} fill="rgba(255,255,255,0.3)"/>
|
<circle cx={cx} cy={cy} r={3} fill="rgba(255,255,255,0.3)"/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -127,15 +113,14 @@ function RadarChart({ highlighted, onSelect }) {
|
|||||||
|
|
||||||
function StatBar({ g }) {
|
function StatBar({ g }) {
|
||||||
const total = CATS.reduce((s,c) => s+DATA[g][c], 0);
|
const total = CATS.reduce((s,c) => s+DATA[g][c], 0);
|
||||||
if (total === 0) return <div style={{fontSize:11,color:'rgba(255,255,255,0.2)',fontStyle:'italic'}}>нет активности</div>;
|
if (total === 0) return <div style={{fontSize:11,color:'rgba(255,255,255,0.4)',fontStyle:'italic'}}>нет активности</div>;
|
||||||
return (
|
return (
|
||||||
<div style={{display:'flex',flexDirection:'column',gap:6,marginTop:8}}>
|
<div style={{display:'flex',flexDirection:'column',gap:7,marginTop:10}}>
|
||||||
{CATS.map(cat => {
|
{CATS.map(cat => {
|
||||||
const v = DATA[g][cat];
|
const v = DATA[g][cat];
|
||||||
const pct = total > 0 ? (v/MAX_VAL)*100 : 0;
|
|
||||||
return (
|
return (
|
||||||
<div key={cat} style={{display:'flex',alignItems:'center',gap:8}}>
|
<div key={cat} style={{display:'flex',alignItems:'center',gap:8}}>
|
||||||
<div style={{width:70,fontSize:11,color:'rgba(255,255,255,0.5)',fontFamily:'DM Mono,monospace'}}>{LABELS[cat]}</div>
|
<div style={{width:70,fontSize:11,color:'rgba(255,255,255,0.6)',flexShrink:0}}>{LABELS[cat]}</div>
|
||||||
<div style={{flex:1,height:6,background:'rgba(255,255,255,0.06)',borderRadius:3,overflow:'hidden'}}>
|
<div style={{flex:1,height:6,background:'rgba(255,255,255,0.06)',borderRadius:3,overflow:'hidden'}}>
|
||||||
<div style={{width:`${(v/MAX_VAL)*100}%`,height:'100%',background:COLORS[cat],
|
<div style={{width:`${(v/MAX_VAL)*100}%`,height:'100%',background:COLORS[cat],
|
||||||
borderRadius:3,boxShadow:`0 0 8px ${COLORS[cat]}80`,transition:'width 0.5s ease'}}/>
|
borderRadius:3,boxShadow:`0 0 8px ${COLORS[cat]}80`,transition:'width 0.5s ease'}}/>
|
||||||
@@ -151,87 +136,101 @@ function StatBar({ g }) {
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const [highlighted, setHighlighted] = useState(null);
|
const [highlighted, setHighlighted] = useState(null);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [width, setWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 800);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const onResize = () => setWidth(window.innerWidth);
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
setTimeout(() => setMounted(true), 100);
|
setTimeout(() => setMounted(true), 100);
|
||||||
|
return () => window.removeEventListener('resize', onResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const totals = CATS.map(cat => ({
|
const isMobile = width < 640;
|
||||||
cat, val: GROUPS.reduce((s,g) => s+DATA[g][cat], 0)
|
const radarSize = isMobile ? Math.min(width - 64, 290) : 320;
|
||||||
}));
|
|
||||||
const maxTotal = Math.max(...totals.map(t=>t.val));
|
|
||||||
|
|
||||||
const hlData = highlighted ? DATA[highlighted] : null;
|
const totals = CATS.map(cat => ({ cat, val: GROUPS.reduce((s,g) => s+DATA[g][cat], 0) }));
|
||||||
const hlTotal = hlData ? CATS.reduce((s,c)=>s+hlData[c],0) : 0;
|
const maxTotal = Math.max(...totals.map(t=>t.val));
|
||||||
|
const hlTotal = highlighted ? CATS.reduce((s,c)=>s+DATA[highlighted][c],0) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight:'100vh', background:'#080810',
|
minHeight:'100vh', background:'#080810',
|
||||||
fontFamily:"'DM Mono', 'Courier New', monospace",
|
fontFamily:"'DM Mono','Courier New',monospace", color:'#e8e8f0',
|
||||||
color:'#e8e8f0',
|
|
||||||
display:'flex', flexDirection:'column', alignItems:'center',
|
display:'flex', flexDirection:'column', alignItems:'center',
|
||||||
padding:'40px 20px',
|
padding: isMobile ? '24px 16px 48px' : '40px 20px',
|
||||||
|
boxSizing:'border-box',
|
||||||
backgroundImage:'radial-gradient(ellipse at 20% 20%, rgba(249,115,22,0.05) 0%, transparent 60%), radial-gradient(ellipse at 80% 80%, rgba(6,182,212,0.05) 0%, transparent 60%)',
|
backgroundImage:'radial-gradient(ellipse at 20% 20%, rgba(249,115,22,0.05) 0%, transparent 60%), radial-gradient(ellipse at 80% 80%, rgba(6,182,212,0.05) 0%, transparent 60%)',
|
||||||
}}>
|
}}>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500;600&family=Syne:wght@700;800&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500;600&family=Syne:wght@700;800&display=swap" rel="stylesheet"/>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{textAlign:'center',marginBottom:36,opacity:mounted?1:0,transform:mounted?'translateY(0)':'translateY(-20px)',transition:'all 0.6s ease'}}>
|
<div style={{textAlign:'center', marginBottom: isMobile ? 20 : 36,
|
||||||
<div style={{fontSize:10,letterSpacing:8,color:'rgba(255,255,255,0.7)',textTransform:'uppercase',marginBottom:10}}>
|
opacity:mounted?1:0, transform:mounted?'translateY(0)':'translateY(-16px)', transition:'all 0.6s ease'}}>
|
||||||
|
<div style={{fontSize:9, letterSpacing: isMobile ? 4 : 7, color:'rgba(255,255,255,0.7)',
|
||||||
|
textTransform:'uppercase', marginBottom:8}}>
|
||||||
Групповой анализ
|
Групповой анализ
|
||||||
</div>
|
</div>
|
||||||
<h1 style={{margin:0,fontSize:32,fontWeight:800,fontFamily:"Syne,sans-serif",
|
<h1 style={{margin:0, fontSize: isMobile ? 24 : 32, fontWeight:800, fontFamily:'Syne,sans-serif',
|
||||||
background:'linear-gradient(135deg, #f97316, #06b6d4, #a3e635)',
|
background:'linear-gradient(135deg, #f97316, #06b6d4, #a3e635)',
|
||||||
WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent',letterSpacing:1}}>
|
WebkitBackgroundClip:'text', WebkitTextFillColor:'transparent', letterSpacing:1}}>
|
||||||
РОЗА АКТИВНОСТИ
|
РОЗА АКТИВНОСТИ
|
||||||
</h1>
|
</h1>
|
||||||
<div style={{marginTop:8,fontSize:11,color:'rgba(255,255,255,0.75)',letterSpacing:2}}>
|
<div style={{marginTop:6, fontSize:10, color:'rgba(255,255,255,0.75)', letterSpacing:2}}>
|
||||||
№1 – №11 · 4 НАПРАВЛЕНИЯ
|
№1 – №11 · 4 НАПРАВЛЕНИЯ
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div style={{display:'flex',gap:20,marginBottom:32,flexWrap:'wrap',justifyContent:'center',
|
<div style={{display:'flex', gap: isMobile ? 10 : 20, marginBottom: isMobile ? 20 : 32,
|
||||||
opacity:mounted?1:0,transition:'opacity 0.8s ease 0.2s'}}>
|
flexWrap:'wrap', justifyContent:'center',
|
||||||
|
opacity:mounted?1:0, transition:'opacity 0.8s ease 0.2s'}}>
|
||||||
{CATS.map(cat => (
|
{CATS.map(cat => (
|
||||||
<div key={cat} style={{display:'flex',alignItems:'center',gap:7}}>
|
<div key={cat} style={{display:'flex',alignItems:'center',gap:6}}>
|
||||||
<div style={{width:8,height:8,borderRadius:'50%',background:COLORS[cat],
|
<div style={{width:7,height:7,borderRadius:'50%',background:COLORS[cat],boxShadow:`0 0 8px ${COLORS[cat]}`}}/>
|
||||||
boxShadow:`0 0 8px ${COLORS[cat]}`}}/>
|
<span style={{fontSize:10, color:'rgba(255,255,255,0.75)', letterSpacing:0.5}}>{LABELS[cat]}</span>
|
||||||
<span style={{fontSize:11,color:'rgba(255,255,255,0.55)',letterSpacing:1}}>{LABELS[cat]}</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{display:'flex',flexWrap:'wrap',gap:28,justifyContent:'center',alignItems:'flex-start',width:'100%',maxWidth:940}}>
|
{/* Layout */}
|
||||||
|
<div style={{
|
||||||
|
display:'flex',
|
||||||
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
|
gap: isMobile ? 16 : 28,
|
||||||
|
justifyContent:'center',
|
||||||
|
alignItems: isMobile ? 'stretch' : 'flex-start',
|
||||||
|
width:'100%', maxWidth:940,
|
||||||
|
}}>
|
||||||
|
|
||||||
{/* Radar */}
|
{/* Radar card */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background:'rgba(255,255,255,0.02)',borderRadius:24,
|
background:'rgba(255,255,255,0.02)', borderRadius:20,
|
||||||
border:'1px solid rgba(255,255,255,0.07)',padding:24,
|
border:'1px solid rgba(255,255,255,0.07)',
|
||||||
backdropFilter:'blur(10px)',
|
padding: isMobile ? 16 : 24,
|
||||||
opacity:mounted?1:0,transform:mounted?'scale(1)':'scale(0.95)',
|
opacity:mounted?1:0, transform:mounted?'scale(1)':'scale(0.95)',
|
||||||
transition:'all 0.7s ease 0.1s',
|
transition:'all 0.7s ease 0.1s',
|
||||||
display:'flex',flexDirection:'column',alignItems:'center',gap:16,
|
display:'flex', flexDirection:'column', alignItems:'center', gap:14,
|
||||||
}}>
|
}}>
|
||||||
<RadarChart highlighted={highlighted} onSelect={setHighlighted}/>
|
<RadarChart highlighted={highlighted} onSelect={setHighlighted} size={radarSize}/>
|
||||||
|
|
||||||
{/* Highlighted info */}
|
{/* Detail panel */}
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight:80,width:'100%',maxWidth:320,
|
minHeight:64, width:'100%',
|
||||||
background: highlighted ? `rgba(${hexToRgb(COLORS[dominant(highlighted)])},0.08)` : 'rgba(255,255,255,0.03)',
|
background: highlighted ? `rgba(${hexToRgb(COLORS[dominant(highlighted)])},0.08)` : 'rgba(255,255,255,0.03)',
|
||||||
borderRadius:14,border:`1px solid ${highlighted ? COLORS[dominant(highlighted)]+'40' : 'rgba(255,255,255,0.06)'}`,
|
borderRadius:12,
|
||||||
padding:'12px 16px',transition:'all 0.3s',
|
border:`1px solid ${highlighted ? COLORS[dominant(highlighted)]+'40' : 'rgba(255,255,255,0.06)'}`,
|
||||||
|
padding:'12px 14px', transition:'all 0.3s',
|
||||||
}}>
|
}}>
|
||||||
{highlighted ? (
|
{highlighted ? (
|
||||||
<>
|
<>
|
||||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
|
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
|
||||||
<span style={{fontSize:16,fontWeight:700,fontFamily:'Syne,sans-serif',color:COLORS[dominant(highlighted)]}}>{highlighted}</span>
|
<span style={{fontSize:15,fontWeight:700,fontFamily:'Syne,sans-serif',color:COLORS[dominant(highlighted)]}}>{highlighted}</span>
|
||||||
<span style={{fontSize:11,color:'rgba(255,255,255,0.4)'}}>сумма: {hlTotal}</span>
|
<span style={{fontSize:11,color:'rgba(255,255,255,0.5)'}}>сумма: {hlTotal}</span>
|
||||||
</div>
|
</div>
|
||||||
<StatBar g={highlighted}/>
|
<StatBar g={highlighted}/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{color:'rgba(255,255,255,0.2)',fontSize:12,textAlign:'center',paddingTop:16}}>
|
<div style={{color:'rgba(255,255,255,0.35)',fontSize:12,textAlign:'center',paddingTop:10}}>
|
||||||
Нажми на группу для деталей
|
Нажми на группу для деталей
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -239,46 +238,49 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel */}
|
{/* Right panel */}
|
||||||
<div style={{display:'flex',flexDirection:'column',gap:10,minWidth:220,
|
<div style={{
|
||||||
opacity:mounted?1:0,transform:mounted?'translateX(0)':'translateX(20px)',
|
display:'flex', flexDirection:'column', gap:10,
|
||||||
transition:'all 0.7s ease 0.3s'}}>
|
flex:1, minWidth: isMobile ? 'unset' : 220,
|
||||||
|
opacity:mounted?1:0,
|
||||||
|
transform:mounted ? 'none' : isMobile ? 'translateY(20px)' : 'translateX(20px)',
|
||||||
|
transition:'all 0.7s ease 0.3s',
|
||||||
|
}}>
|
||||||
|
|
||||||
{/* Group list */}
|
{/* Group chips */}
|
||||||
<div style={{fontSize:9,letterSpacing:5,color:'rgba(255,255,255,0.75)',marginBottom:4,textTransform:'uppercase'}}>
|
<div style={{fontSize:9,letterSpacing:5,color:'rgba(255,255,255,0.75)',marginBottom:2,textTransform:'uppercase'}}>
|
||||||
Группы
|
Группы
|
||||||
</div>
|
</div>
|
||||||
<div style={{display:'flex',flexWrap:'wrap',gap:6}}>
|
<div style={{display:'flex',flexWrap:'wrap',gap:6}}>
|
||||||
{GROUPS.map((g,idx) => {
|
{GROUPS.map(g => {
|
||||||
const isHl = highlighted===g;
|
const isHl = highlighted===g;
|
||||||
const dom = dominant(g);
|
const col = COLORS[dominant(g)];
|
||||||
const col = COLORS[dom];
|
|
||||||
const total = CATS.reduce((s,c)=>s+DATA[g][c],0);
|
const total = CATS.reduce((s,c)=>s+DATA[g][c],0);
|
||||||
const isEmpty = total === 0;
|
const isEmpty = total === 0;
|
||||||
return (
|
return (
|
||||||
<div key={g} onClick={()=>setHighlighted(isHl?null:g)}
|
<div key={g} onClick={()=>setHighlighted(isHl?null:g)} style={{
|
||||||
style={{
|
display:'flex', alignItems:'center', gap:7,
|
||||||
display:'flex',alignItems:'center',gap:8,
|
padding: isMobile ? '10px 14px' : '7px 12px',
|
||||||
padding:'7px 12px',borderRadius:10,cursor:'pointer',
|
borderRadius:10, cursor:'pointer',
|
||||||
background: isHl ? `rgba(${hexToRgb(col)},0.18)` : 'rgba(255,255,255,0.03)',
|
background: isHl ? `rgba(${hexToRgb(col)},0.18)` : 'rgba(255,255,255,0.04)',
|
||||||
border:`1px solid ${isHl ? col : 'rgba(255,255,255,0.07)'}`,
|
border:`1px solid ${isHl ? col : 'rgba(255,255,255,0.08)'}`,
|
||||||
opacity: highlighted&&!isHl ? 0.4 : isEmpty ? 0.4 : 1,
|
opacity: highlighted&&!isHl ? 0.4 : isEmpty ? 0.4 : 1,
|
||||||
transition:'all 0.2s',
|
transition:'all 0.2s',
|
||||||
boxShadow: isHl ? `0 0 16px ${col}40` : 'none',
|
boxShadow: isHl ? `0 0 14px ${col}40` : 'none',
|
||||||
minWidth:70,
|
flex: isMobile ? '1 0 calc(33% - 5px)' : 'unset',
|
||||||
}}>
|
minWidth: isMobile ? 'calc(33% - 5px)' : 68,
|
||||||
|
}}>
|
||||||
<div style={{width:7,height:7,borderRadius:'50%',
|
<div style={{width:7,height:7,borderRadius:'50%',
|
||||||
background: isEmpty ? 'rgba(255,255,255,0.15)' : col,
|
background: isEmpty ? 'rgba(255,255,255,0.15)' : col,
|
||||||
boxShadow: isHl ? `0 0 8px ${col}` : 'none',
|
boxShadow: isHl ? `0 0 8px ${col}` : 'none', flexShrink:0}}/>
|
||||||
flexShrink:0}}/>
|
<span style={{fontSize: isMobile ? 14 : 13, fontWeight:isHl?700:400, flex:1}}>{g}</span>
|
||||||
<span style={{fontSize:13,fontWeight:isHl?700:400,flex:1}}>{g}</span>
|
<span style={{fontSize:10,color:'rgba(255,255,255,0.4)'}}>{total}</span>
|
||||||
<span style={{fontSize:10,color:'rgba(255,255,255,0.3)'}}>{total}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category totals */}
|
{/* Totals */}
|
||||||
<div style={{marginTop:12,padding:'16px',borderRadius:16,
|
<div style={{padding:'14px 16px',borderRadius:14,
|
||||||
background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.07)'}}>
|
background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.07)'}}>
|
||||||
<div style={{fontSize:9,letterSpacing:5,color:'rgba(255,255,255,0.75)',marginBottom:12,textTransform:'uppercase'}}>
|
<div style={{fontSize:9,letterSpacing:5,color:'rgba(255,255,255,0.75)',marginBottom:12,textTransform:'uppercase'}}>
|
||||||
Итого по направлениям
|
Итого по направлениям
|
||||||
@@ -287,8 +289,8 @@ export default function App() {
|
|||||||
<div key={cat} style={{display:'flex',alignItems:'center',gap:10,marginBottom:10}}>
|
<div key={cat} style={{display:'flex',alignItems:'center',gap:10,marginBottom:10}}>
|
||||||
<div style={{width:8,height:8,borderRadius:'50%',background:COLORS[cat],
|
<div style={{width:8,height:8,borderRadius:'50%',background:COLORS[cat],
|
||||||
boxShadow:`0 0 6px ${COLORS[cat]}`,flexShrink:0}}/>
|
boxShadow:`0 0 6px ${COLORS[cat]}`,flexShrink:0}}/>
|
||||||
<span style={{fontSize:11,flex:1,color:'rgba(255,255,255,0.6)'}}>{LABELS[cat]}</span>
|
<span style={{fontSize:11,flex:1,color:'rgba(255,255,255,0.75)'}}>{LABELS[cat]}</span>
|
||||||
<div style={{width:90,height:5,background:'rgba(255,255,255,0.06)',borderRadius:3,overflow:'hidden'}}>
|
<div style={{width: isMobile ? 60 : 90, height:5,background:'rgba(255,255,255,0.06)',borderRadius:3,overflow:'hidden'}}>
|
||||||
<div style={{width:`${(val/maxTotal)*100}%`,height:'100%',
|
<div style={{width:`${(val/maxTotal)*100}%`,height:'100%',
|
||||||
background:`linear-gradient(90deg,${COLORS[cat]},${COLORS[cat]}aa)`,
|
background:`linear-gradient(90deg,${COLORS[cat]},${COLORS[cat]}aa)`,
|
||||||
borderRadius:3,boxShadow:`0 0 6px ${COLORS[cat]}60`}}/>
|
borderRadius:3,boxShadow:`0 0 6px ${COLORS[cat]}60`}}/>
|
||||||
@@ -298,33 +300,38 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Heat summary */}
|
{/* Heatmap */}
|
||||||
<div style={{padding:'14px 16px',borderRadius:16,
|
<div style={{padding:'14px 16px',borderRadius:14,
|
||||||
background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.07)'}}>
|
background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.07)'}}>
|
||||||
<div style={{fontSize:9,letterSpacing:5,color:'rgba(255,255,255,0.75)',marginBottom:10,textTransform:'uppercase'}}>
|
<div style={{fontSize:9,letterSpacing:5,color:'rgba(255,255,255,0.75)',marginBottom:10,textTransform:'uppercase'}}>
|
||||||
Тепловая карта
|
Тепловая карта
|
||||||
</div>
|
</div>
|
||||||
<div style={{display:'grid',gridTemplateColumns:'auto repeat(4, 1fr)',gap:3,fontSize:10}}>
|
<div style={{display:'grid',gridTemplateColumns:'auto repeat(4, 1fr)',gap: isMobile ? 4 : 3}}>
|
||||||
<div/>
|
<div/>
|
||||||
{CATS.map(c => (
|
{CATS.map(c => (
|
||||||
<div key={c} style={{textAlign:'center',color:COLORS[c],fontSize:9,letterSpacing:1}}>{c.slice(0,3).toUpperCase()}</div>
|
<div key={c} style={{textAlign:'center',color:COLORS[c],fontSize:9,paddingBottom:3}}>
|
||||||
|
{c.slice(0,3).toUpperCase()}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{GROUPS.map(g => (
|
{GROUPS.map(g => (
|
||||||
<>
|
<>
|
||||||
<div key={g+'-label'} style={{color:'rgba(255,255,255,0.4)',paddingRight:4,lineHeight:'20px'}}>{g}</div>
|
<div key={g+'-label'} style={{
|
||||||
|
color:'rgba(255,255,255,0.65)', paddingRight:6,
|
||||||
|
fontSize: isMobile ? 12 : 10,
|
||||||
|
lineHeight: isMobile ? '28px' : '22px',
|
||||||
|
}}>{g}</div>
|
||||||
{CATS.map(cat => {
|
{CATS.map(cat => {
|
||||||
const v = DATA[g][cat];
|
const v = DATA[g][cat];
|
||||||
const alpha = v/MAX_VAL;
|
|
||||||
return (
|
return (
|
||||||
<div key={cat} onClick={()=>setHighlighted(highlighted===g?null:g)}
|
<div key={cat} onClick={()=>setHighlighted(highlighted===g?null:g)} style={{
|
||||||
style={{
|
height: isMobile ? 28 : 22, borderRadius:4, cursor:'pointer',
|
||||||
height:20,borderRadius:4,cursor:'pointer',
|
background: v>0 ? `rgba(${hexToRgb(COLORS[cat])},${0.15+(v/MAX_VAL)*0.75})` : 'rgba(255,255,255,0.04)',
|
||||||
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',
|
||||||
border: highlighted===g ? `1px solid ${COLORS[cat]}80` : '1px solid transparent',
|
display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
display:'flex',alignItems:'center',justifyContent:'center',
|
fontSize: isMobile ? 12 : 10,
|
||||||
fontSize:10,color:v>0?'rgba(255,255,255,0.9)':'rgba(255,255,255,0.15)',
|
color: v>0 ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.15)',
|
||||||
fontWeight:700,transition:'all 0.2s',
|
fontWeight:700, transition:'all 0.2s',
|
||||||
}}>
|
}}>
|
||||||
{v||'·'}
|
{v||'·'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -336,7 +343,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{marginTop:28,fontSize:10,color:'rgba(255,255,255,0.5)',letterSpacing:2}}>
|
<div style={{marginTop:24,fontSize:10,color:'rgba(255,255,255,0.4)',letterSpacing:2,textAlign:'center'}}>
|
||||||
НАЖМИ НА ГРУППУ · ВЫДЕЛИ НА РАДАРЕ
|
НАЖМИ НА ГРУППУ · ВЫДЕЛИ НА РАДАРЕ
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user