preview: static HTML preview files for cluster UI design validation
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-hans">
|
||||
<head><meta charset="UTF-8"><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;padding:1rem 2rem}
|
||||
.card{background:#fff;border-radius:8px;padding:1.5rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
||||
a{color:#4361ee;text-decoration:none;font-size:.9rem}
|
||||
h2{font-size:1.2rem}.badge{display:inline-block;padding:.15rem .5rem;border-radius:12px;font-size:.75rem;font-weight:600}
|
||||
.badge-warning{background:#fff3cd;color:#856404}
|
||||
.feat-table{width:100%;border-collapse:collapse;font-size:.8rem;margin-top:.5rem}
|
||||
.feat-table th{background:#f5f5f5;padding:.3rem .5rem;text-align:left;font-weight:600;border-bottom:2px solid #ddd;white-space:nowrap}
|
||||
.feat-table td{padding:.25rem .5rem;border-bottom:1px solid #eee}
|
||||
.feat-table td code{font-size:.72rem}
|
||||
.feat-p{color:#2e7d32}.feat-n{color:#c62828}
|
||||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:.5rem}
|
||||
.svd-card{background:#f8f9fa;border:1px solid #e0e0e0;border-radius:8px;padding:.8rem}
|
||||
.svd-card h4{margin:0 0 .3rem;font-size:.9rem}
|
||||
.data-table{width:100%;border-collapse:collapse;font-size:.72rem;margin-top:.5rem}
|
||||
.data-table th{background:#1a1a2e;color:#e0e0e0;padding:.25rem .4rem;text-align:left;font-weight:600;white-space:nowrap;font-size:.7rem}
|
||||
.data-table td{padding:.2rem .4rem;border-bottom:1px solid #eee;font-size:.7rem}
|
||||
.data-table td code{font-size:.68rem}
|
||||
.data-table tr:nth-child(even){background:#f8f9fa}
|
||||
.scroll-x{overflow-x:auto}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<a href="#">← 聚类概览</a>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:.5rem">
|
||||
<div><h2>簇 #0 <span class="badge badge-warning" style="display:none">噪声</span></h2>
|
||||
<p style="color:#666;font-size:.9rem">847,293 条流 — 66.0% 占总 — 轮廓系数: 0.7100</p></div>
|
||||
</div>
|
||||
<div style="background:#f8f9fa;border-left:3px solid #4361ee;padding:.6rem;margin:.75rem 0;border-radius:0 4px 4px 0;font-size:.85rem;line-height:1.6;color:#333">
|
||||
低延迟 (23ms),比全局均值高3.1个标准差;大数据包 (1520 bytes),高2.3个标准差;系统端口 (443),高1.8个标准差(簇内高度集中)。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📊 特征分析 <span style="font-weight:400;font-size:.8rem;color:#888">12 个特征</span></h3>
|
||||
<table class="feat-table">
|
||||
<thead><tr><th>#</th><th>特征名</th><th>区分度</th><th>均值</th><th>标准差</th><th>中位数</th><th>P25</th><th>P75</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td><code>延迟 (4dur)</code></td><td class="feat-p">+3.213</td><td>23.0ms</td><td>5.1</td><td>21ms</td><td>18ms</td><td>26ms</td></tr>
|
||||
<tr><td>2</td><td><code>数据包 (8ppk)</code></td><td class="feat-p">+2.346</td><td>1520.0B</td><td>128</td><td>1480B</td><td>1400B</td><td>1650B</td></tr>
|
||||
<tr><td>3</td><td><code>TLS版本 (0ver)</code></td><td class="feat-n">-1.845</td><td>1.2</td><td>0.3</td><td>1.2</td><td>1.2</td><td>1.3</td></tr>
|
||||
<tr><td>4</td><td><code>目标端口 (:prd)</code></td><td class="feat-p">+1.821</td><td>443</td><td>12</td><td>443</td><td>443</td><td>443</td></tr>
|
||||
<tr><td>5</td><td><code>会话时长 (8ses)</code></td><td class="feat-n">-1.523</td><td>15s</td><td>4s</td><td>14s</td><td>12s</td><td>18s</td></tr>
|
||||
<tr><td>6</td><td><code>密钥大小 (4ksz)</code></td><td class="feat-p">+1.234</td><td>256</td><td>0</td><td>256</td><td>256</td><td>256</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔬 SVD 关键特征 <span style="font-weight:400;font-size:.8rem;color:#888">Top 5 按区分度</span></h3>
|
||||
<div class="grid-2">
|
||||
<div class="svd-card"><h4>延迟 (4dur)</h4><div style="font-size:.75rem;color:#666;margin-top:.2rem">Score: <span class="feat-p">+3.21</span> | 均值: 23.00 | 标准差: 5.10</div></div>
|
||||
<div class="svd-card"><h4>数据包 (8ppk)</h4><div style="font-size:.75rem;color:#666;margin-top:.2rem">Score: <span class="feat-p">+2.35</span> | 均值: 1520.00 | 标准差: 128.00</div></div>
|
||||
<div class="svd-card"><h4>TLS版本 (0ver)</h4><div style="font-size:.75rem;color:#666;margin-top:.2rem">Score: <span class="feat-n">-1.85</span> | 均值: 1.20 | 标准差: 0.30</div></div>
|
||||
<div class="svd-card"><h4>目标端口 (:prd)</h4><div style="font-size:.75rem;color:#666;margin-top:.2rem">Score: <span class="feat-p">+1.82</span> | 均值: 443.00 | 标准差: 12.00</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📋 数据行 <span style="font-weight:400;font-size:.8rem;color:#888">100 条</span></h3>
|
||||
<div class="scroll-x">
|
||||
<table class="data-table">
|
||||
<thead><tr><th>#</th><th>:ips</th><th>:ipd</th><th>:prs</th><th>:prd</th><th>0ver</th><th>8ack</th><th>4dur</th><th>timestamp</th><th>snam</th><th>cnam</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td><code>192.168.1.42</code></td><td><code>10.0.0.5</code></td><td><code>49152</code></td><td><code>443</code></td><td><code>TLSv1.3</code></td><td><code>1420</code></td><td><code>23</code></td><td><code>2024-01-15 08:23</code></td><td><code>api.example.com</code></td><td><code>*.example.com</code></td></tr>
|
||||
<tr><td>2</td><td><code>10.0.0.100</code></td><td><code>203.0.113.50</code></td><td><code>34567</code></td><td><code>443</code></td><td><code>TLSv1.3</code></td><td><code>2048</code></td><td><code>18</code></td><td><code>2024-01-15 08:23</code></td><td><code>cdn.example.com</code></td><td><code>*.example.com</code></td></tr>
|
||||
<tr><td>3</td><td><code>172.16.0.8</code></td><td><code>198.51.100.22</code></td><td><code>50001</code></td><td><code>8443</code></td><td><code>TLSv1.2</code></td><td><code>890</code></td><td><code>35</code></td><td><code>2024-01-15 08:24</code></td><td><code>api.internal.net</code></td><td><code>*.internal.net</code></td></tr>
|
||||
<tr><td>4</td><td><code>192.168.1.100</code></td><td><code>10.0.0.1</code></td><td><code>44321</code></td><td><code>443</code></td><td><code>TLSv1.3</code></td><td><code>5600</code></td><td><code>12</code></td><td><code>2024-01-15 08:24</code></td><td><code>www.example.com</code></td><td><code>*.example.com</code></td></tr>
|
||||
<tr><td>5</td><td><code>10.0.1.50</code></td><td><code>203.0.113.100</code></td><td><code>51000</code></td><td><code>8080</code></td><td><code>TLSv1.2</code></td><td><code>320</code></td><td><code>45</code></td><td><code>2024-01-15 08:25</code></td><td><code>api.alt.net</code></td><td><code>*.alt.net</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p style="text-align:center;color:#999;font-size:.78rem;margin-top:.5rem">显示前 100 行,共 847,293 行</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,376 @@
|
||||
<!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>
|
||||
@@ -1,356 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-hans">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>原型 — 聚类UI交互验证</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; transition:all 0.3s; min-height:calc(100vh - 48px); }
|
||||
.left-panel { flex:1; min-width:0; transition:flex 0.3s; padding:1rem; overflow-y:auto; }
|
||||
.left-panel.globe-open { flex:0 0 55%; }
|
||||
.right-panel { flex:0 0 0; overflow:hidden; transition:flex 0.3s; }
|
||||
.right-panel.globe-open { flex:0 0 45%; }
|
||||
|
||||
/* toggle handle */
|
||||
.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 0.3s ease; }
|
||||
.toggle-handle:hover { background:#4361ee; }
|
||||
|
||||
/* scatter */
|
||||
.scatter-wrap { position:relative; background:#fff; border-radius:8px; padding:0.75rem; margin-bottom:1rem; box-shadow:0 1px 3px rgba(0,0,0,0.1); }
|
||||
.scatter-wrap h3 { font-size:0.95rem; margin-bottom:0.5rem; }
|
||||
.scatter-wrap canvas { width:100%; height:360px; display:block; cursor:crosshair; border-radius:4px; background:#fafbfc; }
|
||||
|
||||
/* pills */
|
||||
.pill-row { display:flex; flex-wrap:wrap; gap:4px; margin-bottom:0.75rem; }
|
||||
.pill { display:inline-flex; align-items:center; gap:3px; padding:2px 10px; border-radius:12px; font-size:0.75rem; cursor:pointer; border:2px solid transparent; transition:all 0.15s; color:#fff; white-space:nowrap; }
|
||||
.pill.all { background:#e8e8e8; color:#333; }
|
||||
.pill.active { border-color:#333; font-weight:700; }
|
||||
|
||||
/* cluster cards */
|
||||
.card-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.75rem; margin-bottom:1rem; }
|
||||
.cluster-card { background:#fff; border:1px solid #e0e0e0; border-radius:8px; padding:0.8rem; cursor:pointer; transition:box-shadow 0.15s; }
|
||||
.cluster-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.1); }
|
||||
.cluster-card.selected { border-color:#4361ee; box-shadow:0 0 0 2px rgba(67,97,238,0.2); }
|
||||
.cluster-card .label { font-weight:700; font-size:1rem; }
|
||||
.cluster-card .meta { font-size:0.78rem; color:#666; margin-top:0.2rem; }
|
||||
.cluster-card .desc { font-size:0.8rem; color:#333; margin-top:0.4rem; padding:0.4rem; background:#f8f9fa; border-left:3px solid #4361ee; border-radius:0 4px 4px 0; line-height:1.5; }
|
||||
|
||||
/* detail panel */
|
||||
.detail-panel { position:fixed; bottom:0; left:0; right:0; z-index:100; transform:translateY(100%); transition:transform 0.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,0.15); max-height:40vh; overflow-y:auto; padding:0.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; line-height:1; border:none; background:none; }
|
||||
.detail-close:hover { color:#333; }
|
||||
.detail-body h3 { font-size:1rem; margin-bottom:0.3rem; }
|
||||
.detail-body p { font-size:0.85rem; color:#555; margin:0.2rem 0; }
|
||||
.detail-body table { width:100%; border-collapse:collapse; font-size:0.8rem; margin-top:0.5rem; }
|
||||
.detail-body th { background:#f5f5f5; padding:0.25rem 0.4rem; text-align:left; font-weight:600; border-bottom:2px solid #ddd; }
|
||||
.detail-body td { padding:0.2rem 0.4rem; border-bottom:1px solid #eee; }
|
||||
.detail-body td code { font-size:0.72rem; }
|
||||
|
||||
/* 3D地球容器 */
|
||||
#globeContainer { width:100%; height:calc(100vh - 48px); background:#0a0a1a; position:relative; }
|
||||
#globeContainer canvas { display:block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<h1>聚类概览 — 运行 #9</h1>
|
||||
<span>200 条流 · 3 个簇 · 12 个噪声点</span>
|
||||
</div>
|
||||
|
||||
<div class="main-layout" id="mainLayout">
|
||||
<!-- Left: cluster content -->
|
||||
<div class="left-panel" id="leftPanel">
|
||||
<!-- Scatter -->
|
||||
<div class="scatter-wrap">
|
||||
<h3>UMAP 嵌入 <span style="font-weight:400;font-size:0.8rem;color:#888;" id="ptCount">加载中...</span></h3>
|
||||
<canvas id="scatterChart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Cluster pills -->
|
||||
<div class="pill-row" id="pillRow">
|
||||
<span class="pill all active" data-lbl="all" onclick="clearSel()">全部</span>
|
||||
</div>
|
||||
|
||||
<!-- Cluster cards -->
|
||||
<div class="card-grid" id="cardGrid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right: 3D Globe -->
|
||||
<div class="right-panel" id="rightPanel">
|
||||
<div id="globeContainer"></div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle handle -->
|
||||
<button class="toggle-handle" id="globeHandle" onclick="toggleGlobe()">◀</button>
|
||||
</div>
|
||||
|
||||
<!-- Bottom detail panel -->
|
||||
<div class="detail-panel" id="detailPanel">
|
||||
<div class="detail-body" id="detailBody">
|
||||
<button class="detail-close" onclick="closeDetail()">✕</button>
|
||||
<div class="detail-handle" onclick="closeDetail()"></div>
|
||||
<div id="detailContent"><p style="color:#999;">选择一个簇或数据点查看详情</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="static/tianxuan/three.min.js"></script>
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════
|
||||
// MOCK DATA
|
||||
// ═══════════════════════════════════════════════
|
||||
const MOCK_CLUSTERS = [
|
||||
{ label:0, size:78, prop:0.39, silh:0.72, desc:'低延迟 (23ms),比全局均值高3.1σ;大数据包 (1520 bytes),高2.3σ;系统端口 (443)' },
|
||||
{ label:1, size:65, prop:0.33, silh:0.68, desc:'高TLS1.3比例 (92%),比均值高1.8σ;短会话 (12s),低1.5σ;ECDHE曲线 X25519' },
|
||||
{ label:2, size:45, prop:0.23, silh:0.54, desc:'高连接频率 (85次/min),比均值高4.2σ;非标准端口 (8080/8443),目标多域名' },
|
||||
{ label:-1, size:12, prop:0.06, silh:null, desc:'噪声点 — 异常TLS指纹、零散目标、低流量' },
|
||||
];
|
||||
|
||||
const COLORS = ['#4361ee','#f72585','#7209b7','#4cc9f0','#f8961e','#43aa8b','#f94144','#577590','#e9c46a','#9b5de5'];
|
||||
function colorFor(lbl) { return lbl===-1?'rgba(150,150,150,0.7)':COLORS[Math.abs(lbl)%COLORS.length]; }
|
||||
|
||||
// Generate mock UMAP scatter data (200 points)
|
||||
const SCATTER = [];
|
||||
const RNG = (seed) => { let s=seed%2147483647; return ()=> { s=s*16807%2147483647; return (s-1)/2147483646; }; };
|
||||
const rng = RNG(42);
|
||||
const centers = {0:[2,1.5],1:[-1.8,0.5],2:[0.5,-2], '-1':[3.5,-1.5]};
|
||||
const spreads = {0:[0.6,0.5],1:[0.7,0.6],2:[0.5,0.7], '-1':[1.2,0.8]};
|
||||
for (const c of MOCK_CLUSTERS) {
|
||||
for (let i=0;i<c.size;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+(rng()-0.5)*2*sx, y:cy+(rng()-0.5)*2*sy,
|
||||
label:c.label, entity:`模拟主机 ${1000+i}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// SCATTER 2D CANVAS
|
||||
// ═══════════════════════════════════════════════
|
||||
let selCluster = null, selPoint = null;
|
||||
let globeOpen = false;
|
||||
|
||||
function drawScatter(highlight) {
|
||||
const c=document.getElementById('scatterChart');
|
||||
if(!c)return;
|
||||
const W=c.parentElement.clientWidth||700, H=360;
|
||||
c.width=W*2; c.height=H*2; c.style.width=W+'px'; c.style.height=H+'px';
|
||||
const ctx=c.getContext('2d'); ctx.scale(2,2);
|
||||
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);
|
||||
// grid
|
||||
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();}
|
||||
// points
|
||||
SCATTER.forEach(d=>{
|
||||
const cx=toX(d.x),cy=toY(d.y);
|
||||
let radius=3,color=colorFor(d.label);
|
||||
if(highlight!==null&&highlight!==undefined){
|
||||
if(d.label===highlight){color=colorFor(d.label);radius=5;}
|
||||
else{color='rgba(200,200,200,0.15)';radius=2;}
|
||||
}
|
||||
ctx.beginPath();ctx.arc(cx,cy,radius,0,Math.PI*2);ctx.fillStyle=color;ctx.fill();
|
||||
});
|
||||
// labels
|
||||
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();
|
||||
|
||||
c.onclick=function(e){
|
||||
const r=c.getBoundingClientRect();
|
||||
const mx=e.clientX-r.left, my=e.clientY-r.top;
|
||||
const sx=mx/W*pw, sy=my/H*ph;
|
||||
let best=null,bestD=10;
|
||||
for(const d of SCATTER){
|
||||
const dx=toX(d.x)-mx, dy=toY(d.y)-my;
|
||||
const dist=Math.sqrt(dx*dx+dy*dy);
|
||||
if(dist<bestD){bestD=dist;best=d;}
|
||||
}
|
||||
if(best){selCluster=MOCK_CLUSTERS.find(c=>c.label===best.label);selPoint=best;showDetail(selCluster,selPoint);}
|
||||
};
|
||||
document.getElementById('ptCount').textContent=SCATTER.length+' 个点';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CLUSTER SELECTION
|
||||
// ═══════════════════════════════════════════════
|
||||
function selectCluster(lbl){
|
||||
selCluster=MOCK_CLUSTERS.find(c=>c.label===lbl);
|
||||
selPoint=null;
|
||||
drawScatter(lbl);
|
||||
document.querySelectorAll('.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,null);
|
||||
}
|
||||
function clearSel(){
|
||||
selCluster=null;selPoint=null;
|
||||
drawScatter(null);
|
||||
document.querySelectorAll('.pill,.cluster-card').forEach(p=>p.classList.remove('active','selected'));
|
||||
closeDetail();
|
||||
if(globeOpen)globeFilter(null);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// DETAIL PANEL
|
||||
// ═══════════════════════════════════════════════
|
||||
function showDetail(cluster,point){
|
||||
const dc=document.getElementById('detailContent');
|
||||
let html='';
|
||||
if(point){
|
||||
html+='<h3>数据点详情</h3><table><thead><tr><th>字段</th><th>值</th></tr></thead><tbody>'+
|
||||
'<tr><td>标识</td><td><code>'+point.entity+'</code></td></tr>'+
|
||||
'<tr><td>所属簇</td><td>#'+(point.label===-1?'噪声':point.label)+'</td></tr>'+
|
||||
'<tr><td>UMAP-x</td><td>'+point.x.toFixed(4)+'</td></tr>'+
|
||||
'<tr><td>UMAP-y</td><td>'+point.y.toFixed(4)+'</td></tr>'+
|
||||
'</tbody></table>';
|
||||
}else if(cluster){
|
||||
html+='<h3>簇 #'+(cluster.label===-1?'噪声':cluster.label)+'</h3>'+
|
||||
'<p>大小: '+cluster.size+' | 占比: '+(cluster.prop*100).toFixed(0)+'%'+
|
||||
(cluster.silh!==null?' | 轮廓系数: '+cluster.silh.toFixed(4):'')+'</p>'+
|
||||
'<p style="background:#f8f9fa;border-left:3px solid #4361ee;padding:0.5rem;margin:0.5rem 0;border-radius:0 4px 4px 0;font-size:0.85rem;line-height:1.6;">'+cluster.desc+'</p>'+
|
||||
'<table><thead><tr><th>特征</th><th>区分度</th><th>均值</th><th>标准差</th></tr></thead><tbody>'+
|
||||
'<tr><td>延迟 (4dur)</td><td style="color:#2e7d32;">+3.2</td><td>23ms</td><td>5.1</td></tr>'+
|
||||
'<tr><td>数据包大小 (8ppk)</td><td style="color:#2e7d32;">+2.3</td><td>1520B</td><td>128</td></tr>'+
|
||||
'<tr><td>TLS 版本 (0ver)</td><td style="color:#c62828;">-1.8</td><td>1.2</td><td>0.3</td></tr>'+
|
||||
'</tbody></table>';
|
||||
}
|
||||
dc.innerHTML=html;
|
||||
document.getElementById('detailPanel').classList.add('open');
|
||||
}
|
||||
function closeDetail(){document.getElementById('detailPanel').classList.remove('open');}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// GLOBE SIDEBAR
|
||||
// ═══════════════════════════════════════════════
|
||||
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&&!window._globeInit)initGlobe3D();
|
||||
}
|
||||
|
||||
function globeFilter(label){
|
||||
if(!window._arcMeshes)return;
|
||||
window._arcMeshes.forEach(m=>{
|
||||
if(label===null){m.material.opacity=0.5;m.material.color.setHex(parseInt(m.userData.color,16));}
|
||||
else{m.material.opacity=0.06;m.material.color.setHex(0x666666);}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 3D GLOBE (Three.js, simplified)
|
||||
// ═══════════════════════════════════════════════
|
||||
function initGlobe3D(){
|
||||
if(window._globeInit)return;
|
||||
const cont=document.getElementById('globeContainer');
|
||||
if(!cont)return;
|
||||
const W=cont.clientWidth||450, 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
|
||||
const earth=new THREE.Mesh(new THREE.SphereGeometry(2,48,48),new THREE.MeshPhongMaterial({color:0x1a3a5c,emissive:0x0a1a2a}));
|
||||
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})));
|
||||
// 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 s=new THREE.BufferGeometry();const p=new Float32Array(600*3);
|
||||
for(let i=0;i<600*3;i++)p[i]=(Math.random()-0.5)*100;
|
||||
s.setAttribute('position',new THREE.BufferAttribute(p,3));
|
||||
scene.add(new THREE.Points(s,new THREE.PointsMaterial({color:0xffffff,size:0.15})));
|
||||
|
||||
// mock flows → arcs
|
||||
const tlsC={'TLSv1.3':0x4cc9f0,'TLSv1.2':0x43aa8b,'TLSv1.1':0xf8961e,'TLSv1.0':0xf94144};
|
||||
const arcGrp=new THREE.Group();scene.add(arcGrp);
|
||||
const arcs=[];
|
||||
function llz(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));}
|
||||
|
||||
// generate ~30 mock flows
|
||||
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 tlsOpts=['TLSv1.3','TLSv1.2','TLSv1.3','TLSv1.2','TLSv1.1'];
|
||||
for(let i=0;i<30;i++){
|
||||
const src=cities[i%cities.length],dst=cities[(i+3)%cities.length];
|
||||
const f=llz(src.lat,src.lon,2),t=llz(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=tlsOpts[i%tlsOpts.length];
|
||||
const mat=new THREE.MeshBasicMaterial({color:tlsC[c],transparent:true,opacity:0.5});
|
||||
const mesh=new THREE.Mesh(new THREE.TubeGeometry(curve,16,0.015,4,false),mat);
|
||||
mesh.userData={color:tlsC[c].toString(16)};
|
||||
arcGrp.add(mesh);arcs.push(mesh);
|
||||
}
|
||||
window._arcMeshes=arcs;
|
||||
|
||||
// drag
|
||||
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;arcGrp.rotation.y=earth.rotation.y;arcGrp.rotation.x=earth.rotation.x;pm={x:e.clientX,y:e.clientY};};
|
||||
|
||||
!function anim(){requestAnimationFrame(anim);if(!isD){earth.rotation.y+=0.003;arcGrp.rotation.y=earth.rotation.y;}ren.render(scene,cam);}();
|
||||
window._globeInit=true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// RENDER PILLS & CARDS
|
||||
// ═══════════════════════════════════════════════
|
||||
(function init(){
|
||||
const pr=document.getElementById('pillRow');
|
||||
MOCK_CLUSTERS.forEach(c=>{
|
||||
if(c.label===-1)return;
|
||||
const p=document.createElement('span');p.className='pill';p.style.background=colorFor(c.label);
|
||||
p.dataset.lbl=c.label;p.textContent='#'+c.label+' ('+c.size+')';p.onclick=()=>selectCluster(c.label);
|
||||
pr.appendChild(p);
|
||||
});
|
||||
// noise pill
|
||||
const np=document.createElement('span');np.className='pill';np.style.background='#999';
|
||||
np.dataset.lbl='-1';np.textContent='噪声 ('+MOCK_CLUSTERS.find(c=>c.label===-1).size+')';np.onclick=()=>selectCluster(-1);
|
||||
pr.appendChild(np);
|
||||
|
||||
// cards
|
||||
const cg=document.getElementById('cardGrid');
|
||||
MOCK_CLUSTERS.forEach(c=>{
|
||||
if(c.label===-1)return;
|
||||
const card=document.createElement('div');card.className='cluster-card';card.dataset.label=c.label;
|
||||
card.innerHTML='<div class="label">簇 #'+c.label+'</div>'+
|
||||
'<div class="meta">'+c.size+' 个实体 · '+(c.prop*100).toFixed(0)+'%'+(c.silh!==null?' · 轮廓系数: '+c.silh.toFixed(4):'')+'</div>'+
|
||||
'<div class="desc">'+c.desc+'</div>';
|
||||
card.onclick=()=>selectCluster(c.label);
|
||||
cg.appendChild(card);
|
||||
});
|
||||
|
||||
drawScatter(null);
|
||||
// set initial handle pos
|
||||
document.getElementById('globeHandle').style.left='calc(100% - 20px)';
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user