377 lines
22 KiB
HTML
377 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-hans">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>聚类概览 — 预览</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f7fa;color:#1a1a2e}
|
||
.header{background:#1a1a2e;color:#fff;padding:0.8rem 1.5rem;display:flex;justify-content:space-between;align-items:center}
|
||
.header h1{font-size:1.1rem}.header span{font-size:0.8rem;color:#aaa}
|
||
.main-layout{display:flex;position:relative;min-height:calc(100vh - 48px)}
|
||
.left{flex:1;min-width:0;padding:1rem;overflow-y:auto;transition:flex .3s}
|
||
.left.globe-open{flex:0 0 55%}
|
||
.right{flex:0 0 0;overflow:hidden;transition:flex .3s}
|
||
.right.globe-open{flex:0 0 45%}
|
||
.toggle-handle{
|
||
position:absolute;z-index:10;top:50%;width:20px;height:44px;background:#1a1a2e;color:#fff;
|
||
border:none;border-radius:4px 0 0 4px;cursor:pointer;display:flex;align-items:center;
|
||
justify-content:center;font-size:12px;transition:left .3s;left:calc(100% - 20px)
|
||
}
|
||
.toggle-handle:hover{background:#4361ee}
|
||
.card{background:#fff;border-radius:8px;padding:1rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
||
.card h3{font-size:.95rem;margin-bottom:.5rem}
|
||
.scatter-wrap{padding:.75rem}
|
||
.scatter-wrap canvas{width:100%;height:380px;display:block;border-radius:4px;background:#fafbfc}
|
||
.pill-row{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:.75rem}
|
||
.pill{display:inline-flex;align-items:center;gap:3px;padding:2px 10px;border-radius:12px;font-size:.75rem;cursor:pointer;border:2px solid transparent;color:#fff;white-space:nowrap}
|
||
.pill.active{border-color:#333;font-weight:700}
|
||
.card-grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
|
||
.cluster-card{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:.8rem;cursor:pointer;transition:box-shadow .15s}
|
||
.cluster-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.1)}
|
||
.cluster-card.selected{border-color:#4361ee;box-shadow:0 0 0 2px rgba(67,97,238,.2)}
|
||
.cluster-card .lbl{font-weight:700;font-size:1rem}
|
||
.cluster-card .meta{font-size:.78rem;color:#666;margin-top:.2rem}
|
||
.cluster-card .desc{font-size:.8rem;color:#333;margin-top:.4rem;padding:.4rem;background:#f8f9fa;border-left:3px solid #4361ee;border-radius:0 4px 4px 0;line-height:1.5}
|
||
.detail-panel{position:fixed;bottom:0;left:0;right:0;z-index:100;transform:translateY(100%);transition:transform .35s cubic-bezier(.4,0,.2,1)}
|
||
.detail-panel.open{transform:translateY(0)}
|
||
.detail-body{background:#fff;border-radius:16px 16px 0 0;box-shadow:0 -4px 24px rgba(0,0,0,.15);max-height:40vh;overflow-y:auto;padding:.5rem 1.5rem 1.5rem}
|
||
.detail-handle{width:36px;height:4px;background:#ccc;border-radius:2px;margin:8px auto;cursor:pointer}
|
||
.detail-close{float:right;cursor:pointer;font-size:1.3rem;color:#999;border:none;background:none}
|
||
.detail-close:hover{color:#333}
|
||
.detail-body h3{font-size:1rem;margin-bottom:.3rem}
|
||
.detail-body p{font-size:.85rem;color:#555;margin:.2rem 0}
|
||
.detail-body table{width:100%;border-collapse:collapse;font-size:.8rem;margin-top:.5rem}
|
||
.detail-body th{background:#f5f5f5;padding:.25rem .4rem;text-align:left;font-weight:600;border-bottom:2px solid #ddd;white-space:nowrap}
|
||
.detail-body td{padding:.2rem .4rem;border-bottom:1px solid #eee}
|
||
.feat-p{color:#2e7d32}.feat-n{color:#c62828}
|
||
#globeCont{width:100%;height:calc(100vh - 48px);background:#0a0a1a;position:relative}
|
||
.btn-sm{display:inline-block;padding:.25rem .6rem;border-radius:4px;font-size:.78rem;text-decoration:none;background:#4361ee;color:#fff}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<h1>聚类概览 — 运行 #9</h1>
|
||
<span>1,284,592 条流 · 3 个簇 · 12,847 个噪声</span>
|
||
</div>
|
||
|
||
<div class="main-layout" id="main">
|
||
<div class="left" id="leftPanel">
|
||
<!-- UMAP 散点图 -->
|
||
<div class="card scatter-wrap">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<h3>UMAP 嵌入 <span style="font-weight:400;font-size:.8rem;color:#888" id="ptCount">加载中...</span></h3>
|
||
<span id="viewToggle" style="display:none">
|
||
<button id="btn2D" class="btn-sm" style="font-weight:700">2D</button>
|
||
<button id="btn3D" class="btn-sm btn-outline">3D</button>
|
||
</span>
|
||
</div>
|
||
<canvas id="scatterChart"></canvas>
|
||
<div class="pill-row" id="legendPills"></div>
|
||
</div>
|
||
|
||
<!-- 簇标签 -->
|
||
<div class="pill-row" id="clusterPills"></div>
|
||
|
||
<!-- 簇卡片 -->
|
||
<div class="card-grid" id="cardGrid"></div>
|
||
</div>
|
||
|
||
<div class="right" id="rightPanel">
|
||
<div id="globeCont"></div>
|
||
</div>
|
||
<button class="toggle-handle" id="globeHandle" onclick="toggleGlobe()">◀</button>
|
||
</div>
|
||
|
||
<!-- 详情面板 -->
|
||
<div class="detail-panel" id="detailPanel">
|
||
<div class="detail-body">
|
||
<button class="detail-close" onclick="closeDetail()">✕</button>
|
||
<div class="detail-handle" onclick="closeDetail()"></div>
|
||
<div id="detailContent"><p style="color:#999;padding:.5rem 0">选择一个簇或数据点查看详情</p></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="static/tianxuan/three.min.js"></script>
|
||
<script>
|
||
// ═══ 模拟数据 ═══
|
||
const COLORS = ['#4361ee','#f72585','#7209b7','#4cc9f0','#f8961e','#43aa8b','#f94144','#577590','#e9c46a','#9b5de5'];
|
||
const CLUSTERS = [
|
||
{label:0,size:847293,prop:0.66,silh:0.71,
|
||
desc:'低延迟 (23ms),比全局均值高3.1个标准差;大数据包 (1520 bytes),高2.3个标准差;系统端口 (443)',
|
||
detail:'簇 #0 由 847,293 条流组成(66%)。\n特征:延迟(4dur)均值23ms,区分度+3.2;数据包大小(8ppk)均值1520B,区分度+2.3;端口(:prd)高度集中于443(92%);TLS版本以1.3为主(78%)。'},
|
||
|
||
{label:1,size:311482,prop:0.24,silh:0.65,
|
||
desc:'高TLS1.3比例 (92%),比均值高1.8σ;短会话 (12s),低1.5σ;ECDHE曲线 X25519',
|
||
detail:'簇 #1 由 311,482 条流组成(24%)。\n特征:TLS1.3比率(0ver)92%,区分度+1.8;会话时长(8ses)均值12s,区分度-1.5;密钥交换曲线集中在X25519(88%);目标端口包括443和8443。'},
|
||
|
||
{label:2,size:112970,prop:0.09,silh:0.52,
|
||
desc:'高连接频率 (85次/min),比均值高4.2σ;非标准端口 (8080/8443);目标多域名',
|
||
detail:'簇 #2 由 112,970 条流组成(9%)。\n特征:连接频率(8seq/min)均值85,区分度+4.2;非标准端口比例高(76%),以8080/8443为主;SNI名称(snam)分散在8个不同域名;源IP来自不同/24网段。'},
|
||
|
||
{label:-1,size:12847,prop:0.01,silh:null,
|
||
desc:'噪声 — 异常TLS指纹、零散目标、单次连接',
|
||
detail:''},
|
||
];
|
||
|
||
const RNG = (s)=>{let seed=s%2147483647;return()=>{seed=seed*16807%2147483647;return(seed-1)/2147483646;}};
|
||
const r=RNG(42);
|
||
const centers={0:[2.2,1.8],1:[-2.0,0.3],2:[0.3,-2.2],'-1':[3.8,-1.2]};
|
||
const spreads={0:[0.7,0.6],1:[0.8,0.7],2:[0.6,0.8],'-1':[1.5,1.0]};
|
||
const SCATTER=[];
|
||
let totalPts=0;
|
||
for(const c of CLUSTERS){
|
||
const n=c.label===-1?200:Math.min(c.size/2000,200);
|
||
totalPts+=n;
|
||
for(let i=0;i<n;i++){
|
||
const cx=centers[c.label][0],cy=centers[c.label][1];
|
||
const sx=spreads[c.label][0],sy=spreads[c.label][1];
|
||
SCATTER.push({
|
||
x:cx+(r()-0.5)*2*sx, y:cy+(r()-0.5)*2*sy,
|
||
z:(r()-0.5)*2*0.8, label:c.label,
|
||
entity:c.label===-1?'噪声':`主机_${1000+i}`
|
||
});
|
||
}
|
||
}
|
||
|
||
let selCluster=null,globeOpen=false,globeInit=false;
|
||
|
||
// ═══ 2D Canvas 散点 ═══
|
||
function drawScatter(highlight){
|
||
const c=document.getElementById('scatterChart');
|
||
if(!c)return;
|
||
const W=c.parentElement.clientWidth||700,H=380;
|
||
c.width=W;c.height=H;
|
||
const ctx=c.getContext('2d');
|
||
const pad={top:25,bottom:30,left:50,right:20};
|
||
const pw=W-pad.left-pad.right,ph=H-pad.top-pad.bottom;
|
||
const xs=SCATTER.map(d=>d.x),ys=SCATTER.map(d=>d.y);
|
||
const xMin=Math.min(...xs),xMax=Math.max(...xs),yMin=Math.min(...ys),yMax=Math.max(...ys);
|
||
const xR=xMax-xMin||1,yR=yMax-yMin||1;
|
||
const toX=v=>pad.left+(v-xMin)/xR*pw,toY=v=>pad.top+ph-(v-yMin)/yR*ph;
|
||
ctx.clearRect(0,0,W,H);
|
||
ctx.strokeStyle='#eee';ctx.lineWidth=1;
|
||
for(let i=0;i<=5;i++){ctx.beginPath();ctx.moveTo(pad.left,pad.top+i*ph/5);ctx.lineTo(W-pad.right,pad.top+i*ph/5);ctx.stroke();}
|
||
SCATTER.forEach(d=>{
|
||
const cx=toX(d.x),cy=toY(d.y);
|
||
let color=d.label===-1?'rgba(150,150,150,0.4)':COLORS[Math.abs(d.label)%COLORS.length];
|
||
let r2=d.label===-1?2:4;
|
||
if(highlight!==null&&highlight!==undefined){
|
||
if(d.label===highlight){color=d.label===-1?'rgba(150,150,150,0.9)':COLORS[Math.abs(d.label)%COLORS.length];r2=6;}
|
||
else{color='rgba(200,200,200,0.15)';r2=2;}
|
||
}
|
||
ctx.beginPath();ctx.arc(cx,cy,r2,0,Math.PI*2);ctx.fillStyle=color;ctx.fill();
|
||
});
|
||
ctx.fillStyle='#999';ctx.font='11px sans-serif';ctx.textAlign='center';
|
||
ctx.fillText('UMAP-1',W/2,H-3);
|
||
ctx.save();ctx.translate(14,H/2);ctx.rotate(-Math.PI/2);ctx.fillText('UMAP-2',0,0);ctx.restore();
|
||
document.getElementById('ptCount').textContent=SCATTER.length+' 个点';
|
||
// legend pills (from scatter data)
|
||
const leg=document.getElementById('legendPills');leg.innerHTML='';
|
||
const seen={};
|
||
SCATTER.forEach(d=>{if(!seen[d.label]){seen[d.label]=true;
|
||
const p=document.createElement('span');p.className='pill';
|
||
p.style.background=d.label===-1?'#999':COLORS[Math.abs(d.label)%COLORS.length];
|
||
p.textContent=d.label===-1?'噪声':'#'+d.label;
|
||
p.onclick=()=>selectCluster(d.label);leg.appendChild(p);
|
||
}});
|
||
}
|
||
|
||
// ═══ 簇选择 ═══
|
||
function selectCluster(lbl){
|
||
selCluster=CLUSTERS.find(c=>c.label===lbl);
|
||
drawScatter(lbl);
|
||
document.querySelectorAll('#clusterPills .pill').forEach(p=>p.classList.toggle('active',parseInt(p.dataset.lbl)===lbl));
|
||
document.querySelectorAll('.cluster-card').forEach(p=>p.classList.toggle('selected',parseInt(p.dataset.label)===lbl));
|
||
if(globeOpen)globeFilter(lbl);
|
||
showDetail(selCluster);
|
||
}
|
||
|
||
function clearSel(){selCluster=null;drawScatter(null);closeDetail();
|
||
document.querySelectorAll('.pill,.cluster-card').forEach(p=>p.classList.remove('active','selected'));
|
||
if(globeOpen)globeFilter(null);
|
||
}
|
||
|
||
// ═══ 详情面板 ═══
|
||
function showDetail(c){
|
||
const dc=document.getElementById('detailContent');
|
||
if(!c||c.label===-1){dc.innerHTML='<p style="color:#999;">选择一个簇查看详情</p>';document.getElementById('detailPanel').classList.remove('open');return;}
|
||
dc.innerHTML='<h3>簇 #'+c.label+'</h3><p>大小: '+c.size.toLocaleString()+' | 占比: '+(c.prop*100).toFixed(1)+'%'+(c.silh!==null?' | 轮廓系数: '+c.silh.toFixed(4):'')+'</p>'+
|
||
'<p style="background:#f8f9fa;border-left:3px solid #4361ee;padding:.5rem;margin:.5rem 0;border-radius:0 4px 4px 0;font-size:.85rem;line-height:1.6;white-space:pre-wrap;">'+c.detail+'</p>'+
|
||
'<table><thead><tr><th>特征</th><th>区分度</th><th>均值</th><th>标准差</th></tr></thead><tbody>'+
|
||
(c.label===0?'<tr><td>延迟 (4dur)</td><td class="feat-p">+3.2</td><td>23.0ms</td><td>5.1</td></tr><tr><td>数据包 (8ppk)</td><td class="feat-p">+2.3</td><td>1520.0B</td><td>128</td></tr><tr><td>TLS版本 (0ver)</td><td class="feat-n">-1.8</td><td>1.2</td><td>0.3</td></tr>':
|
||
c.label===1?'<tr><td>TLS1.3比率</td><td class="feat-p">+1.8</td><td>92%</td><td>8%</td></tr><tr><td>会话时长</td><td class="feat-n">-1.5</td><td>12s</td><td>4s</td></tr><tr><td>端口</td><td class="feat-p">+1.2</td><td>443</td><td>12</td></tr>':
|
||
'<tr><td>连接频率</td><td class="feat-p">+4.2</td><td>85/min</td><td>12</td></tr><tr><td>非标准端口</td><td class="feat-p">+2.8</td><td>76%</td><td>9%</td></tr><tr><td>SNI域名数</td><td class="feat-p">+2.1</td><td>8</td><td>2</td></tr>')+
|
||
'</tbody></table>';
|
||
document.getElementById('detailPanel').classList.add('open');
|
||
}
|
||
function closeDetail(){document.getElementById('detailPanel').classList.remove('open');}
|
||
|
||
// ═══ 3D UMAP scatter ═══
|
||
let s3DRenderer,s3DScene,s3DCamera,s3DPts,s3DInit=false,is3DMode=false;
|
||
function hasWG2(){try{var c=document.createElement('canvas');return!!c.getContext('webgl2');}catch(e){return false;}}
|
||
|
||
(function(){
|
||
if(!hasWG2())return;
|
||
document.getElementById('viewToggle').style.display='inline';
|
||
const b2=document.getElementById('btn2D'),b3=document.getElementById('btn3D');
|
||
function show2D(){is3DMode=false;document.querySelector('#scatterChart').style.display='';
|
||
document.getElementById('viewToggle').style.display='none';drawScatter(selCluster?selCluster.label:null);
|
||
setTimeout(()=>document.getElementById('viewToggle').style.display='inline',50);
|
||
b2.style.fontWeight='700';b3.style.fontWeight='normal';}
|
||
function show3D(){is3DMode=true;document.querySelector('#scatterChart').style.display='none';
|
||
if(!s3DInit)init3D();b3.style.fontWeight='700';b2.style.fontWeight='normal';}
|
||
b2.onclick=show2D;b3.onclick=show3D;
|
||
})();
|
||
|
||
function init3D(){
|
||
if(s3DInit)return;
|
||
const cont=document.getElementById('scatterChart').parentElement;
|
||
const W=cont.clientWidth||700,H=450;
|
||
const container=document.createElement('div');container.id='scatter3D';container.style.cssText='width:100%;height:450px';
|
||
cont.appendChild(container);
|
||
|
||
const xs=SCATTER.map(d=>d.x),ys=SCATTER.map(d=>d.y),zs=SCATTER.map(d=>d.z||0);
|
||
const xM=Math.min(...xs),xX=Math.max(...xs),yM=Math.min(...ys),yX=Math.max(...ys),zM=Math.min(...zs),zX=Math.max(...zs);
|
||
const cx=(xM+xX)/2,cy=(yM+yX)/2,cz=(zM+zX)/2,rg=Math.max(xX-xM,yX-yM,zX-zM)||1;
|
||
const nrm=(v,c,r)=>((v-c)/r)*3;
|
||
const scene=new THREE.Scene();scene.background=new THREE.Color(0xf8f9fa);
|
||
const camera=new THREE.PerspectiveCamera(50,W/H,0.1,50);camera.position.set(4.5,3.5,6);camera.lookAt(0,0,0);
|
||
const renderer=new THREE.WebGLRenderer({antialias:true});renderer.setSize(W,H);renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
|
||
container.appendChild(renderer.domElement);
|
||
|
||
const pos=new Float32Array(SCATTER.length*3);
|
||
const cols=new Float32Array(SCATTER.length*3);
|
||
SCATTER.forEach((d,i)=>{
|
||
pos[i*3]=nrm(d.x,cx,rg);pos[i*3+1]=nrm(d.y,cy,rg);pos[i*3+2]=d.z!=null?nrm(d.z,cz,rg):0;
|
||
const c3=new THREE.Color(d.label===-1?'#999999':COLORS[Math.abs(d.label)%COLORS.length]);
|
||
cols[i*3]=c3.r;cols[i*3+1]=c3.g;cols[i*3+2]=c3.b;
|
||
});
|
||
const geo=new THREE.BufferGeometry();
|
||
geo.setAttribute('position',new THREE.BufferAttribute(pos,3));
|
||
geo.setAttribute('color',new THREE.BufferAttribute(cols,3));
|
||
const mat=new THREE.PointsMaterial({size:0.12,vertexColors:true,sizeAttenuation:true,transparent:true,opacity:0.85});
|
||
const pts=new THREE.Points(geo,mat);scene.add(pts);
|
||
|
||
let isD=false,pm={x:0,y:0};
|
||
renderer.domElement.onmousedown=e=>{isD=true;pm={x:e.clientX,y:e.clientY};};
|
||
window.onmouseup=()=>isD=false;
|
||
window.onmousemove=e=>{if(!isD||!is3DMode)return;const dx=e.clientX-pm.x,dy=e.clientY-pm.y;
|
||
pts.rotation.y+=dx*0.005;pts.rotation.x+=dy*0.005;pts.rotation.x=Math.max(-Math.PI/2,Math.min(Math.PI/2,pts.rotation.x));
|
||
pm={x:e.clientX,y:e.clientY};};
|
||
renderer.domElement.onwheel=e=>{if(!is3DMode)return;e.preventDefault();camera.position.z+=e.deltaY*0.008;camera.position.z=Math.max(1.5,Math.min(15,camera.position.z));};
|
||
!function anim(){requestAnimationFrame(anim);if(!is3DMode)return;renderer.render(scene,camera);}();
|
||
s3DRenderer=renderer;s3DScene=scene;s3DCamera=camera;s3DPts=pts;s3DInit=true;
|
||
}
|
||
|
||
// ═══ 地球侧栏 ═══
|
||
function toggleGlobe(){
|
||
globeOpen=!globeOpen;
|
||
document.getElementById('leftPanel').classList.toggle('globe-open',globeOpen);
|
||
document.getElementById('rightPanel').classList.toggle('globe-open',globeOpen);
|
||
const h=document.getElementById('globeHandle');
|
||
h.innerHTML=globeOpen?'▶':'◀';
|
||
h.style.left=globeOpen?'calc(55% - 20px)':'calc(100% - 20px)';
|
||
if(globeOpen&&!globeInit)initGlobe();
|
||
}
|
||
function globeFilter(label){
|
||
if(!window._arcs)return;
|
||
window._arcs.forEach(m=>{
|
||
if(label===null){m.material.opacity=0.5;m.material.color.setHex(parseInt(m.userData.c,16));}
|
||
else{m.material.opacity=0.04;m.material.color.setHex(0x444444);}
|
||
});
|
||
}
|
||
|
||
function initGlobe(){
|
||
if(globeInit)return;
|
||
const cont=document.getElementById('globeCont');
|
||
if(!cont)return;
|
||
const W=cont.clientWidth||500,H=cont.clientHeight||500;
|
||
const scene=new THREE.Scene();scene.background=new THREE.Color(0x0a0a1a);
|
||
const cam=new THREE.PerspectiveCamera(45,W/H,0.1,100);cam.position.set(0,0,6);
|
||
const ren=new THREE.WebGLRenderer({antialias:true});ren.setSize(W,H);ren.setPixelRatio(Math.min(window.devicePixelRatio,2));
|
||
cont.appendChild(ren.domElement);
|
||
|
||
// Earth with texture
|
||
const tex=new THREE.TextureLoader().load('static/tianxuan/earth_atmos_2048.jpg');
|
||
const earth=new THREE.Mesh(new THREE.SphereGeometry(2,64,64),new THREE.MeshPhongMaterial({map:tex,specular:new THREE.Color(0x333333),shininess:5}));
|
||
scene.add(earth);
|
||
// Glow
|
||
scene.add(new THREE.Mesh(new THREE.SphereGeometry(2.06,48,48),new THREE.MeshBasicMaterial({color:0x224488,transparent:true,opacity:0.12})));
|
||
// Grid
|
||
const gridMat=new THREE.LineBasicMaterial({color:0x6699cc,transparent:true,opacity:0.08});
|
||
for(let lat=-80;lat<=80;lat+=20){const pts=[];for(let lon=0;lon<=360;lon+=5){const p=(90-lat)*Math.PI/180,t=(lon+180)*Math.PI/180;pts.push(new THREE.Vector3(-2.02*Math.sin(p)*Math.cos(t),2.02*Math.cos(p),2.02*Math.sin(p)*Math.sin(t)));}scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),gridMat));}
|
||
for(let lon=0;lon<360;lon+=20){const pts=[];for(let lat=-90;lat<=90;lat+=5){const p=(90-lat)*Math.PI/180,t=(lon+180)*Math.PI/180;pts.push(new THREE.Vector3(-2.02*Math.sin(p)*Math.cos(t),2.02*Math.cos(p),2.02*Math.sin(p)*Math.sin(t)));}scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),gridMat));}
|
||
|
||
// Lights
|
||
scene.add(new THREE.AmbientLight(0x222244));
|
||
const dl=new THREE.DirectionalLight(0xffffff,1);dl.position.set(5,10,7);scene.add(dl);
|
||
scene.add(new THREE.DirectionalLight(0x4488ff,0.3));
|
||
// Stars
|
||
const sg=new THREE.BufferGeometry();const sp=new Float32Array(600*3);
|
||
for(let i=0;i<600*3;i++)sp[i]=(Math.random()-0.5)*100;
|
||
sg.setAttribute('position',new THREE.BufferAttribute(sp,3));
|
||
scene.add(new THREE.Points(sg,new THREE.PointsMaterial({color:0xffffff,size:0.15})));
|
||
|
||
// Flow arcs
|
||
const tC={'TLSv1.3':0x4cc9f0,'TLSv1.2':0x43aa8b,'TLSv1.1':0xf8961e,'TLSv1.0':0xf94144};
|
||
const ag=new THREE.Group();scene.add(ag);
|
||
const arcs=[];
|
||
const cities=[{lat:31.2,lon:121.5},{lat:39.9,lon:116.4},{lat:35.7,lon:139.7},{lat:37.6,lon:127.0},{lat:51.5,lon:-0.13},{lat:48.9,lon:2.35},{lat:40.7,lon:-74.0},{lat:37.8,lon:-122.4},{lat:1.35,lon:103.8},{lat:-33.9,lon:151.2}];
|
||
const tls=['TLSv1.3','TLSv1.2','TLSv1.3','TLSv1.3','TLSv1.2','TLSv1.1','TLSv1.2','TLSv1.3','TLSv1.2','TLSv1.3'];
|
||
function ll(lat,lon,r){const p=(90-lat)*Math.PI/180,t=(lon+180)*Math.PI/180;return new THREE.Vector3(-r*Math.sin(p)*Math.cos(t),r*Math.cos(p),r*Math.sin(p)*Math.sin(t));}
|
||
for(let i=0;i<cities.length;i++){
|
||
for(let j=0;j<cities.length;j++){
|
||
if(i===j)continue;
|
||
const src=cities[i],dst=cities[j];
|
||
const f=ll(src.lat,src.lon,2),t=ll(dst.lat,dst.lon,2);
|
||
const mid=new THREE.Vector3().addVectors(f,t).multiplyScalar(0.5).normalize().multiplyScalar(3.2);
|
||
const curve=new THREE.QuadraticBezierCurve3(f,mid,t);
|
||
const c=tls[(i+j)%tls.length];
|
||
const mat=new THREE.MeshBasicMaterial({color:tC[c],transparent:true,opacity:0.4});
|
||
const mesh=new THREE.Mesh(new THREE.TubeGeometry(curve,16,0.012,4,false),mat);
|
||
mesh.userData={c:tC[c].toString(16)};ag.add(mesh);arcs.push(mesh);
|
||
}
|
||
}
|
||
window._arcs=arcs;
|
||
|
||
let isD=false,pm={x:0,y:0};
|
||
ren.domElement.onmousedown=e=>{isD=true;pm={x:e.clientX,y:e.clientY};};
|
||
window.onmouseup=()=>isD=false;
|
||
window.onmousemove=e=>{if(!isD)return;const dx=e.clientX-pm.x,dy=e.clientY-pm.y;earth.rotation.y+=dx*0.005;earth.rotation.x+=dy*0.005;ag.rotation.y=earth.rotation.y;ag.rotation.x=earth.rotation.x;pm={x:e.clientX,y:e.clientY};};
|
||
|
||
!function anim(){requestAnimationFrame(anim);if(!isD){earth.rotation.y+=0.003;ag.rotation.y=earth.rotation.y;}ren.render(scene,cam);}();
|
||
globeInit=true;
|
||
}
|
||
|
||
// ═══ 渲染 ═══
|
||
(function init(){
|
||
const pr=document.getElementById('clusterPills');
|
||
const ap=document.createElement('span');ap.className='pill';ap.style.background='#e8e8e8';ap.style.color='#333';
|
||
ap.textContent='全部';ap.onclick=clearSel;pr.appendChild(ap);
|
||
CLUSTERS.forEach(c=>{
|
||
if(c.label===-1)return;
|
||
const p=document.createElement('span');p.className='pill';p.style.background=COLORS[c.label%COLORS.length];
|
||
p.dataset.lbl=c.label;p.textContent='#'+c.label+' ('+(c.size/1000).toFixed(0)+'K)';p.onclick=()=>selectCluster(c.label);
|
||
pr.appendChild(p);
|
||
});
|
||
const np=document.createElement('span');np.className='pill';np.style.background='#999';
|
||
np.dataset.lbl='-1';np.textContent='噪声 (12.8K)';np.onclick=()=>selectCluster(-1);pr.appendChild(np);
|
||
|
||
const cg=document.getElementById('cardGrid');
|
||
CLUSTERS.forEach(c=>{
|
||
if(c.label===-1)return;
|
||
const cd=document.createElement('div');cd.className='cluster-card';cd.dataset.label=c.label;
|
||
cd.innerHTML='<div class="lbl">簇 #'+c.label+'</div><div class="meta">'+c.size.toLocaleString()+' 条流 · '+(c.prop*100).toFixed(0)+'%'+(c.silh!==null?' · 轮廓系数: '+c.silh.toFixed(4):'')+'</div><div class="desc">'+c.desc+'</div>';
|
||
cd.onclick=()=>selectCluster(c.label);cg.appendChild(cd);
|
||
});
|
||
|
||
drawScatter(null);
|
||
document.getElementById('globeHandle').style.left='calc(100% - 20px)';
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|