| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>DSMOTE — Interactive Visualization</title> |
| <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=JetBrains+Mono:wght@300;400;500&family=Syne:wght@400;700;800&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #060b14; |
| --bg2: #0d1627; |
| --bg3: #132038; |
| --cyan: #00e5ff; |
| --cyan2: #00bcd4; |
| --orange: #ff6b35; |
| --green: #00e676; |
| --red: #ff1744; |
| --purple: #7c4dff; |
| --yellow: #ffd740; |
| --text: #e0f7fa; |
| --muted: #546e7a; |
| --border: rgba(0,229,255,0.15); |
| } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: 'Rajdhani', sans-serif; |
| min-height: 100vh; |
| overflow-x: hidden; |
| } |
| |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| background-image: |
| linear-gradient(rgba(0,229,255,0.03) 1px, transparent 1px), |
| linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px); |
| background-size: 40px 40px; |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| .container { max-width: 1100px; margin: 0 auto; padding: 0 24px; position: relative; z-index: 1; } |
| |
| |
| header { |
| padding: 36px 0 24px; |
| border-bottom: 1px solid var(--border); |
| margin-bottom: 32px; |
| } |
| .header-tag { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| color: var(--cyan); |
| letter-spacing: 3px; |
| text-transform: uppercase; |
| margin-bottom: 10px; |
| opacity: 0.7; |
| } |
| header h1 { |
| font-family: 'Syne', sans-serif; |
| font-size: 2.4rem; |
| font-weight: 800; |
| letter-spacing: -1px; |
| background: linear-gradient(135deg, var(--cyan) 0%, #ffffff 60%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| header p { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 12px; |
| color: var(--muted); |
| margin-top: 8px; |
| letter-spacing: 1px; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| gap: 4px; |
| margin-bottom: 28px; |
| flex-wrap: wrap; |
| } |
| .tab { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| padding: 8px 16px; |
| border: 1px solid var(--border); |
| background: transparent; |
| color: var(--muted); |
| cursor: pointer; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| transition: all 0.2s; |
| position: relative; |
| } |
| .tab:hover { color: var(--cyan); border-color: var(--cyan); } |
| .tab.active { |
| background: var(--cyan); |
| color: var(--bg); |
| border-color: var(--cyan); |
| font-weight: 500; |
| } |
| .tab-num { |
| display: inline-block; |
| margin-right: 6px; |
| opacity: 0.5; |
| } |
| |
| |
| .panel { display: none; animation: fadeIn 0.3s ease; } |
| .panel.active { display: block; } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } |
| |
| |
| .card { |
| background: var(--bg2); |
| border: 1px solid var(--border); |
| border-radius: 2px; |
| padding: 28px; |
| margin-bottom: 20px; |
| } |
| .card-title { |
| font-family: 'Syne', sans-serif; |
| font-size: 1.3rem; |
| font-weight: 700; |
| color: var(--cyan); |
| margin-bottom: 6px; |
| } |
| .card-sub { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| color: var(--muted); |
| margin-bottom: 20px; |
| letter-spacing: 1px; |
| } |
| |
| |
| canvas { |
| display: block; |
| border: 1px solid var(--border); |
| border-radius: 2px; |
| background: var(--bg); |
| } |
| |
| .plot-row { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px; |
| align-items: start; |
| } |
| |
| .plot-label { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| text-align: center; |
| margin-top: 8px; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| } |
| |
| |
| .btn { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| padding: 10px 20px; |
| border: 1px solid var(--cyan); |
| background: transparent; |
| color: var(--cyan); |
| cursor: pointer; |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| transition: all 0.2s; |
| margin-top: 16px; |
| margin-right: 8px; |
| } |
| .btn:hover { background: var(--cyan); color: var(--bg); } |
| .btn-orange { border-color: var(--orange); color: var(--orange); } |
| .btn-orange:hover { background: var(--orange); color: var(--bg); } |
| |
| |
| .legend { |
| display: flex; |
| gap: 20px; |
| flex-wrap: wrap; |
| margin-top: 14px; |
| } |
| .legend-item { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| color: var(--muted); |
| letter-spacing: 0.5px; |
| } |
| .legend-dot { |
| width: 10px; height: 10px; border-radius: 50%; |
| flex-shrink: 0; |
| } |
| .legend-line { |
| width: 20px; height: 2px; |
| flex-shrink: 0; |
| } |
| |
| |
| .pipeline { |
| display: flex; |
| align-items: center; |
| gap: 0; |
| margin: 24px 0; |
| overflow-x: auto; |
| padding-bottom: 10px; |
| } |
| .pipe-step { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 10px; |
| flex-shrink: 0; |
| opacity: 0; |
| transform: translateY(20px); |
| transition: opacity 0.5s ease, transform 0.5s ease; |
| } |
| .pipe-step.visible { opacity: 1; transform: none; } |
| .pipe-icon { |
| width: 70px; height: 70px; |
| border-radius: 2px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 26px; |
| border: 2px solid; |
| position: relative; |
| cursor: pointer; |
| } |
| .pipe-icon::after { |
| content: ''; |
| position: absolute; |
| inset: -4px; |
| border-radius: 4px; |
| opacity: 0; |
| transition: opacity 0.3s; |
| } |
| .pipe-icon:hover::after { opacity: 1; } |
| .pipe-icon.active-step { box-shadow: 0 0 20px currentColor; } |
| |
| .pipe-label { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 9px; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| text-align: center; |
| max-width: 80px; |
| } |
| .pipe-arrow { |
| font-size: 20px; |
| color: var(--muted); |
| padding: 0 6px; |
| flex-shrink: 0; |
| margin-top: -20px; |
| } |
| |
| .step-detail { |
| background: var(--bg3); |
| border-left: 3px solid var(--cyan); |
| padding: 16px 20px; |
| margin-top: 8px; |
| border-radius: 0 2px 2px 0; |
| display: none; |
| } |
| .step-detail.visible { display: block; animation: fadeIn 0.3s; } |
| .step-detail h4 { |
| font-family: 'Syne', sans-serif; |
| font-size: 1.1rem; |
| margin-bottom: 6px; |
| } |
| .step-detail p { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 11px; |
| color: var(--muted); |
| line-height: 1.7; |
| } |
| .formula { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 12px; |
| background: var(--bg); |
| padding: 8px 14px; |
| margin-top: 10px; |
| border: 1px solid var(--border); |
| color: var(--cyan); |
| display: inline-block; |
| } |
| |
| |
| .ba-row { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 24px; |
| } |
| .ba-card { |
| background: var(--bg3); |
| border: 1px solid var(--border); |
| padding: 20px; |
| border-radius: 2px; |
| } |
| .ba-card h4 { |
| font-family: 'Syne', sans-serif; |
| font-size: 1rem; |
| margin-bottom: 16px; |
| text-align: center; |
| } |
| .bar-row { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| margin-bottom: 9px; |
| } |
| .bar-name { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 9px; |
| width: 70px; |
| text-align: right; |
| color: var(--muted); |
| flex-shrink: 0; |
| letter-spacing: 0.5px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| .bar-track { |
| flex: 1; |
| height: 14px; |
| background: var(--bg); |
| border-radius: 1px; |
| overflow: hidden; |
| border: 1px solid rgba(255,255,255,0.05); |
| } |
| .bar-fill { |
| height: 100%; |
| border-radius: 1px; |
| transition: width 1s cubic-bezier(0.4,0,0.2,1); |
| position: relative; |
| } |
| .bar-val { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 9px; |
| width: 50px; |
| color: var(--muted); |
| flex-shrink: 0; |
| } |
| |
| |
| .insight { |
| background: rgba(0,229,255,0.05); |
| border: 1px solid rgba(0,229,255,0.2); |
| padding: 14px 18px; |
| margin-top: 16px; |
| border-radius: 1px; |
| } |
| .insight-label { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 9px; |
| color: var(--cyan); |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| margin-bottom: 6px; |
| } |
| .insight p { |
| font-family: 'Rajdhani', sans-serif; |
| font-size: 1rem; |
| font-weight: 600; |
| color: var(--text); |
| line-height: 1.5; |
| } |
| |
| @media (max-width: 640px) { |
| .plot-row, .ba-row { grid-template-columns: 1fr; } |
| header h1 { font-size: 1.6rem; } |
| .pipeline { gap: 0; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <div class="header-tag">// Conference Visualization — DSMOTE</div> |
| <h1>Dynamic SMOTE: Interactive Visual Explainer</h1> |
| <p>A Hybrid Oversampling Framework for NIDS Class Imbalance</p> |
| </header> |
|
|
| <div class="tabs"> |
| <button class="tab active" onclick="switchTab(0)"><span class="tab-num">01</span>SMOTE Weakness</button> |
| <button class="tab" onclick="switchTab(1)"><span class="tab-num">02</span>DSMOTE Pipeline</button> |
| <button class="tab" onclick="switchTab(2)"><span class="tab-num">03</span>Clustering</button> |
| <button class="tab" onclick="switchTab(3)"><span class="tab-num">04</span>Density Constraint</button> |
| <button class="tab" onclick="switchTab(4)"><span class="tab-num">05</span>Before vs After</button> |
| <button class="tab" onclick="switchTab(5)"><span class="tab-num">06</span>UNSW Results</button> |
| <button class="tab" onclick="switchTab(6)"><span class="tab-num">07</span>KDD Results</button> |
| </div> |
|
|
| |
| <div class="panel active" id="panel-0"> |
| <div class="card"> |
| <div class="card-title">SMOTE is Blind</div> |
| <div class="card-sub">// Standard SMOTE interpolates between any two minority samples — crossing cluster boundaries</div> |
| <div class="plot-row"> |
| <div> |
| <canvas id="smote-canvas" width="440" height="360"></canvas> |
| <div class="plot-label" style="color:var(--orange)">⚠ Standard SMOTE — Noise Generation</div> |
| </div> |
| <div> |
| <canvas id="dsmote-canvas" width="440" height="360"></canvas> |
| <div class="plot-label" style="color:var(--green)">✓ DSMOTE — Cluster-Aware Sampling</div> |
| </div> |
| </div> |
| <div style="margin-top:12px;"> |
| <button class="btn" onclick="animateSMOTE()">▶ Animate SMOTE</button> |
| <button class="btn" onclick="animateDSMOTE()">▶ Animate DSMOTE</button> |
| <button class="btn btn-orange" onclick="resetSmote()">↺ Reset</button> |
| </div> |
| <div class="legend"> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>Cluster A — Minority</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>Cluster B — Minority</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--orange);opacity:0.6"></div>SMOTE — Noise points (wrong region)</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div>DSMOTE — Safe synthetic points</div> |
| </div> |
| <div class="insight"> |
| <div class="insight-label">// Key Message</div> |
| <p>SMOTE selects two minority samples at random — regardless of which cluster they belong to — and interpolates between them. This creates points in empty space between clusters, introducing noise and confusion for the classifier.</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-1"> |
| <div class="card"> |
| <div class="card-title">DSMOTE Algorithm Pipeline</div> |
| <div class="card-sub">// Click each step to explore the details</div> |
| <div class="pipeline" id="pipeline"> |
| <div class="pipe-step" id="pstep-0"> |
| <div class="pipe-icon" onclick="showStep(0)" |
| style="color:#ff6b35;border-color:#ff6b35;background:rgba(255,107,53,0.08)">✂️</div> |
| <div class="pipe-label" style="color:#ff6b35">Majority Reduction</div> |
| </div> |
| <div class="pipe-arrow">→</div> |
| <div class="pipe-step" id="pstep-1"> |
| <div class="pipe-icon" onclick="showStep(1)" |
| style="color:#7c4dff;border-color:#7c4dff;background:rgba(124,77,255,0.08)">📉</div> |
| <div class="pipe-label" style="color:#7c4dff">PCA Reduction</div> |
| </div> |
| <div class="pipe-arrow">→</div> |
| <div class="pipe-step" id="pstep-2"> |
| <div class="pipe-icon" onclick="showStep(2)" |
| style="color:#00e5ff;border-color:#00e5ff;background:rgba(0,229,255,0.08)">🔵</div> |
| <div class="pipe-label" style="color:#00e5ff">KMeans Clustering</div> |
| </div> |
| <div class="pipe-arrow">→</div> |
| <div class="pipe-step" id="pstep-3"> |
| <div class="pipe-icon" onclick="showStep(3)" |
| style="color:#ffd740;border-color:#ffd740;background:rgba(255,215,64,0.08)">⚡</div> |
| <div class="pipe-label" style="color:#ffd740">Smart Sampling</div> |
| </div> |
| <div class="pipe-arrow">→</div> |
| <div class="pipe-step" id="pstep-4"> |
| <div class="pipe-icon" onclick="showStep(4)" |
| style="color:#00e676;border-color:#00e676;background:rgba(0,230,118,0.08)">🎯</div> |
| <div class="pipe-label" style="color:#00e676">Density Filter</div> |
| </div> |
| <div class="pipe-arrow">→</div> |
| <div class="pipe-step" id="pstep-5"> |
| <div class="pipe-icon" onclick="showStep(5)" |
| style="color:#ff4081;border-color:#ff4081;background:rgba(255,64,129,0.08)">⚖️</div> |
| <div class="pipe-label" style="color:#ff4081">Class Weights</div> |
| </div> |
| </div> |
|
|
| <div id="step-detail" class="step-detail visible"> |
| <h4 style="color:var(--cyan)">👆 Click a Step Above</h4> |
| <p>Each step in DSMOTE solves a specific problem. Click any step icon to learn what it does and why it matters.</p> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <div class="card-title">Pipeline Data Flow</div> |
| <div class="card-sub">// How raw data transforms through each stage</div> |
| <canvas id="pipeline-canvas" width="1040" height="200" style="width:100%"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-2"> |
| <div class="card"> |
| <div class="card-title">Step 3 — KMeans Clustering of Minority Class</div> |
| <div class="card-sub">// DSMOTE understands the internal structure of minority classes</div> |
| <canvas id="cluster-canvas" width="700" height="420" style="width:100%"></canvas> |
| <div style="margin-top:12px"> |
| <button class="btn" onclick="animateClusters()">▶ Show Clustering</button> |
| <button class="btn btn-orange" onclick="resetClusters()">↺ Reset</button> |
| </div> |
| <div class="legend" style="margin-top:14px"> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>Cluster 1</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--orange)"></div>Cluster 2</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>Cluster 3</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div>Cluster 4</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--muted)"></div>Uncolored (before clustering)</div> |
| </div> |
| <div class="insight"> |
| <div class="insight-label">// Key Message</div> |
| <p>Instead of treating all minority samples as one blob, DSMOTE uses KMeans to discover sub-groups. Synthetic samples are then generated <em>within each cluster</em> — not across them.</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-3"> |
| <div class="card"> |
| <div class="card-title">Step 5 — Density Constraint Filtering</div> |
| <div class="card-sub">// Only synthetic points within the mean intra-cluster distance are accepted</div> |
| <canvas id="density-canvas" width="700" height="420" style="width:100%"></canvas> |
| <div style="margin-top:12px"> |
| <button class="btn" onclick="animateDensity()">▶ Generate Samples</button> |
| <button class="btn btn-orange" onclick="resetDensity()">↺ Reset</button> |
| </div> |
| <div class="legend" style="margin-top:14px"> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>Original minority points</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div>✓ Accepted synthetic points</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div>✗ Rejected (outside d_mean)</div> |
| <div class="legend-item"><div class="legend-line" style="background:var(--cyan);border: 1px dashed var(--cyan)"></div>d_mean radius boundary</div> |
| </div> |
| <div class="insight"> |
| <div class="insight-label">// The Innovation</div> |
| <p>A synthetic sample x_new is accepted only if ‖x_new − x_i‖ ≤ d_mean. This density gate keeps new points inside the safe zone — preventing noise, overfitting, and cluster bleeding.</p> |
| <div class="formula">if ‖x_new − xᵢ‖₂ ≤ d_mean → ACCEPT ✓ else → REJECT ✗</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-4"> |
| <div class="card"> |
| <div class="card-title">Before vs After DSMOTE — KDD Cup 99</div> |
| <div class="card-sub">// Class distribution transformation after applying DSMOTE</div> |
| <div class="ba-row"> |
| <div class="ba-card"> |
| <h4 style="color:var(--orange)">⚠ Before — Severe Imbalance</h4> |
| <div id="before-bars"></div> |
| </div> |
| <div class="ba-card"> |
| <h4 style="color:var(--green)">✓ After DSMOTE — Balanced</h4> |
| <div id="after-bars"></div> |
| </div> |
| </div> |
| <div style="margin-top:16px"> |
| <button class="btn" onclick="animateBars()">▶ Animate Transformation</button> |
| <button class="btn btn-orange" onclick="resetBars()">↺ Reset</button> |
| </div> |
| <div class="insight"> |
| <div class="insight-label">// Impact</div> |
| <p>Minority classes like <code style="color:var(--cyan)">pod</code> (264 samples) and <code style="color:var(--cyan)">warezclient</code> (1,020 samples) are boosted to ~256K–264K samples, achieving near-parity with the majority class after controlled reduction.</p> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <div class="card-title">Macro-F1 Score Comparison — UNSW-NF Dataset</div> |
| <div class="card-sub">// DSMOTE vs conventional oversampling methods</div> |
| <canvas id="f1-canvas" width="1040" height="280" style="width:100%"></canvas> |
| <button class="btn" style="margin-top:16px" onclick="animateF1()">▶ Show Results</button> |
| <div class="legend" style="margin-top:12px"> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--muted)"></div>ROS</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>SMOTE</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--orange)"></div>Borderline-SMOTE</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div>ADASYN</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>DSMOTE (Proposed)</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-5"> |
|
|
| |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px"> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(0,230,118,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">DSMOTE BEST F1</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--green)">0.588</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">RF Model</div> |
| </div> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(255,23,68,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">SMOTE BEST F1</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--red)">0.096</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">RF / DT Model</div> |
| </div> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(0,229,255,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">IMPROVEMENT</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--cyan)">6.1×</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">vs SMOTE RF</div> |
| </div> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(255,215,64,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">BAL. ACCURACY</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--yellow)">0.573</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">DSMOTE RF</div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">Macro-F1 by Model — All Methods (UNSW-NF)</div> |
| <div class="card-sub">// Real experimental results — DSMOTE is the ONLY method that meaningfully works on UNSW</div> |
| <div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap" id="unsw-model-btns"> |
| <button class="btn" style="padding:6px 12px" onclick="filterUnswModel('ALL')">All Models</button> |
| <button class="btn" style="padding:6px 12px" onclick="filterUnswModel('DT')">DT</button> |
| <button class="btn" style="padding:6px 12px" onclick="filterUnswModel('RF')">RF</button> |
| <button class="btn" style="padding:6px 12px" onclick="filterUnswModel('XGBoost')">XGBoost</button> |
| <button class="btn" style="padding:6px 12px" onclick="filterUnswModel('ANN')">ANN</button> |
| <button class="btn" style="padding:6px 12px" onclick="filterUnswModel('LSTM')">LSTM</button> |
| </div> |
| <canvas id="unsw-f1-canvas" width="1040" height="320" style="width:100%"></canvas> |
| <div class="legend" style="margin-top:12px"> |
| <div class="legend-item"><div class="legend-dot" style="background:#78909c"></div>RAW</div> |
| <div class="legend-item"><div class="legend-dot" style="background:#546e7a"></div>ROS</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>SMOTE</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--orange)"></div>BSMOTE</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div>ADASYN</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>★ DSMOTE</div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">Confusion Matrix Collapse — SMOTE vs DSMOTE (UNSW-NF, RF)</div> |
| <div class="card-sub">// SMOTE predicts EVERYTHING as "Benign" — DSMOTE correctly distributes predictions</div> |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:24px"> |
| <div> |
| <canvas id="cm-smote-canvas" width="480" height="400"></canvas> |
| <div class="plot-label" style="color:var(--red);margin-top:8px">⚠ SMOTE — Total Collapse (F1: 0.096)</div> |
| </div> |
| <div> |
| <canvas id="cm-dsmote-canvas" width="480" height="400"></canvas> |
| <div class="plot-label" style="color:var(--green);margin-top:8px">✓ DSMOTE — Proper Distribution (F1: 0.588)</div> |
| </div> |
| </div> |
| <div class="insight" style="margin-top:16px"> |
| <div class="insight-label">// The Collapse Problem</div> |
| <p>Under SMOTE, RF learns to predict every single sample as "Benign" (91.9% accuracy by doing nothing). DSMOTE forces the model to actually learn minority attack classes — Exploits, Fuzzers, Backdoor, Shellcode — that matter for security.</p> |
| </div> |
| <button class="btn" style="margin-top:12px" onclick="drawConfusionMatrices()">▶ Draw Matrices</button> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">Balanced Accuracy — DSMOTE vs All Methods (UNSW-NF)</div> |
| <div class="card-sub">// Balanced accuracy weights each class equally — exposes true minority class performance</div> |
| <canvas id="unsw-ba-canvas" width="1040" height="260" style="width:100%"></canvas> |
| <button class="btn" style="margin-top:12px" onclick="drawUnswBA()">▶ Show Balanced Accuracy</button> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-6"> |
|
|
| |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px"> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(0,230,118,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">DSMOTE RF F1</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--green)">0.9954</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">Matches RAW best</div> |
| </div> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(0,229,255,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">DSMOTE ANN F1</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--cyan)">0.9285</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">+0.013 vs SMOTE ANN</div> |
| </div> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(255,215,64,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">BAL. ACCURACY</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--yellow)">0.9940</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">DSMOTE RF</div> |
| </div> |
| <div class="card" style="padding:16px;text-align:center;border-color:rgba(124,77,255,0.3)"> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted);letter-spacing:2px;margin-bottom:6px">G-MEAN</div> |
| <div style="font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;color:var(--purple)">0.9939</div> |
| <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--muted)">DSMOTE RF</div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">Macro-F1 by Model — All Methods (KDD Cup 99)</div> |
| <div class="card-sub">// KDD is harder to fail on — DSMOTE matches top performance while improving minority class stability</div> |
| <canvas id="kdd-f1-canvas" width="1040" height="320" style="width:100%"></canvas> |
| <div class="legend" style="margin-top:12px"> |
| <div class="legend-item"><div class="legend-dot" style="background:#78909c"></div>RAW</div> |
| <div class="legend-item"><div class="legend-dot" style="background:#546e7a"></div>ROS</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>SMOTE</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--orange)"></div>BSMOTE</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>★ DSMOTE</div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">Multi-Metric Radar — Best Models Comparison (KDD)</div> |
| <div class="card-sub">// DSMOTE (RF) vs RAW (RF): Accuracy · Balanced Acc · F1 · G-Mean · Precision · Recall</div> |
| <canvas id="kdd-radar-canvas" width="1040" height="360" style="width:100%"></canvas> |
| <button class="btn" style="margin-top:12px" onclick="drawRadar()">▶ Draw Radar</button> |
| <div class="legend" style="margin-top:12px"> |
| <div class="legend-item"><div class="legend-dot" style="background:#78909c"></div>RAW RF</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--cyan)"></div>DSMOTE RF</div> |
| <div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div>SMOTE RF</div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <div class="card-title">KDD — G-Mean Comparison Across Models</div> |
| <div class="card-sub">// G-Mean = geometric mean of per-class recalls — punishes models that ignore minority classes</div> |
| <canvas id="kdd-gmean-canvas" width="1040" height="260" style="width:100%"></canvas> |
| <button class="btn" style="margin-top:12px" onclick="drawKddGmean()">▶ Show G-Mean</button> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| <script> |
| |
| |
| |
| function seededRng(seed) { |
| let s = seed; |
| return function() { |
| s = (s * 1664525 + 1013904223) & 0xffffffff; |
| return (s >>> 0) / 0xffffffff; |
| }; |
| } |
| |
| function gaussianPair(rng, mx, my, sx, sy) { |
| |
| const u1 = rng(), u2 = rng(); |
| const z0 = Math.sqrt(-2 * Math.log(u1 + 0.0001)) * Math.cos(2 * Math.PI * u2); |
| const z1 = Math.sqrt(-2 * Math.log(u1 + 0.0001)) * Math.sin(2 * Math.PI * u2); |
| return [mx + z0 * sx, my + z1 * sy]; |
| } |
| |
| function dataToCanvas(x, y, xmin, xmax, ymin, ymax, W, H, pad) { |
| const cx = pad + (x - xmin) / (xmax - xmin) * (W - 2 * pad); |
| const cy = H - pad - (y - ymin) / (ymax - ymin) * (H - 2 * pad); |
| return [cx, cy]; |
| } |
| |
| const PAD = 40; |
| |
| function drawAxes(ctx, W, H) { |
| ctx.strokeStyle = 'rgba(0,229,255,0.12)'; |
| ctx.lineWidth = 1; |
| |
| for (let i = 0; i <= 5; i++) { |
| const x = PAD + i * (W - 2 * PAD) / 5; |
| const y = PAD + i * (H - 2 * PAD) / 5; |
| ctx.beginPath(); ctx.moveTo(x, PAD); ctx.lineTo(x, H - PAD); ctx.stroke(); |
| ctx.beginPath(); ctx.moveTo(PAD, y); ctx.lineTo(W - PAD, y); ctx.stroke(); |
| } |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.25)'; |
| ctx.beginPath(); ctx.moveTo(PAD, H - PAD); ctx.lineTo(W - PAD, H - PAD); ctx.stroke(); |
| ctx.beginPath(); ctx.moveTo(PAD, PAD); ctx.lineTo(PAD, H - PAD); ctx.stroke(); |
| |
| ctx.fillStyle = 'rgba(84,110,122,0.8)'; |
| ctx.font = '10px JetBrains Mono, monospace'; |
| ctx.fillText('PC1 →', W - PAD - 2, H - PAD + 16); |
| ctx.save(); ctx.translate(PAD - 16, PAD + 10); ctx.rotate(-Math.PI / 2); |
| ctx.fillText('PC2 →', 0, 0); ctx.restore(); |
| } |
| |
| function dot(ctx, cx, cy, r, color, alpha = 1) { |
| ctx.globalAlpha = alpha; |
| ctx.fillStyle = color; |
| ctx.beginPath(); |
| ctx.arc(cx, cy, r, 0, Math.PI * 2); |
| ctx.fill(); |
| ctx.globalAlpha = 1; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| const rng1 = seededRng(42); |
| const XMIN = -5, XMAX = 5, YMIN = -5, YMAX = 5; |
| |
| function genMinorityClusters() { |
| const r = seededRng(7); |
| const pts = []; |
| |
| for (let i = 0; i < 30; i++) { |
| const [x, y] = gaussianPair(r, -2.8, 2.2, 0.6, 0.6); |
| pts.push({ x, y, cluster: 0 }); |
| } |
| |
| for (let i = 0; i < 25; i++) { |
| const [x, y] = gaussianPair(r, 2.8, -2.2, 0.55, 0.55); |
| pts.push({ x, y, cluster: 1 }); |
| } |
| |
| for (let i = 0; i < 18; i++) { |
| const [x, y] = gaussianPair(r, 1.2, 1.8, 0.4, 0.4); |
| pts.push({ x, y, cluster: 2 }); |
| } |
| return pts; |
| } |
| |
| function genMajority() { |
| const r = seededRng(11); |
| const pts = []; |
| for (let i = 0; i < 200; i++) { |
| const x = (r() - 0.5) * 8; |
| const y = (r() - 0.5) * 8; |
| pts.push({ x, y }); |
| } |
| return pts; |
| } |
| |
| const minPts = genMinorityClusters(); |
| const majPts = genMajority(); |
| let smoteAnimPts = [], dsmoteAnimPts = []; |
| let smoteAnim = null, dsmoteAnim = null; |
| |
| function drawScatterBase(canvasId, synthPts, synthColor, label) { |
| const canvas = document.getElementById(canvasId); |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| drawAxes(ctx, W, H); |
| |
| |
| majPts.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, XMIN, XMAX, YMIN, YMAX, W, H, PAD); |
| dot(ctx, cx, cy, 3, '#546e7a', 0.3); |
| }); |
| |
| |
| const clColors = ['#00e5ff', '#7c4dff', '#ffd740']; |
| minPts.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, XMIN, XMAX, YMIN, YMAX, W, H, PAD); |
| dot(ctx, cx, cy, 5, clColors[p.cluster]); |
| }); |
| |
| |
| synthPts.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, XMIN, XMAX, YMIN, YMAX, W, H, PAD); |
| dot(ctx, cx, cy, 4, synthColor, 0.75); |
| }); |
| } |
| |
| function generateSMOTEPoint() { |
| const r = seededRng(smoteAnimPts.length * 13 + 7); |
| |
| const i = Math.floor(r() * minPts.length); |
| const j = Math.floor(r() * minPts.length); |
| const lam = r(); |
| return { |
| x: minPts[i].x + lam * (minPts[j].x - minPts[i].x), |
| y: minPts[i].y + lam * (minPts[j].y - minPts[i].y) |
| }; |
| } |
| |
| function generateDSMOTEPoint(idx) { |
| const r = seededRng(idx * 17 + 3); |
| |
| const clusterPts = [ |
| minPts.filter(p => p.cluster === 0), |
| minPts.filter(p => p.cluster === 1), |
| minPts.filter(p => p.cluster === 2) |
| ]; |
| const cl = Math.floor(r() * 3); |
| const pts = clusterPts[cl]; |
| const i = Math.floor(r() * pts.length); |
| const j = Math.floor(r() * pts.length); |
| const lam = r(); |
| const nx = pts[i].x + lam * (pts[j].x - pts[i].x); |
| const ny = pts[i].y + lam * (pts[j].y - pts[i].y); |
| |
| const cx_mean = pts.reduce((a, p) => a + p.x, 0) / pts.length; |
| const cy_mean = pts.reduce((a, p) => a + p.y, 0) / pts.length; |
| const d = Math.sqrt((nx - pts[i].x) ** 2 + (ny - pts[i].y) ** 2); |
| |
| const dmean = 0.9; |
| if (d <= dmean) return { x: nx, y: ny }; |
| return null; |
| } |
| |
| function animateSMOTE() { |
| if (smoteAnim) clearInterval(smoteAnim); |
| smoteAnimPts = []; |
| let count = 0; |
| smoteAnim = setInterval(() => { |
| if (count >= 80) { clearInterval(smoteAnim); return; } |
| smoteAnimPts.push(generateSMOTEPoint()); |
| drawScatterBase('smote-canvas', smoteAnimPts, '#ff6b35', 'SMOTE'); |
| count++; |
| }, 40); |
| } |
| |
| function animateDSMOTE() { |
| if (dsmoteAnim) clearInterval(dsmoteAnim); |
| dsmoteAnimPts = []; |
| let count = 0; |
| dsmoteAnim = setInterval(() => { |
| if (count >= 80) { clearInterval(dsmoteAnim); return; } |
| let p = null, tries = 0; |
| while (!p && tries < 20) { p = generateDSMOTEPoint(count * 7 + tries); tries++; } |
| if (p) dsmoteAnimPts.push(p); |
| drawScatterBase('dsmote-canvas', dsmoteAnimPts, '#00e676', 'DSMOTE'); |
| count++; |
| }, 40); |
| } |
| |
| function resetSmote() { |
| if (smoteAnim) clearInterval(smoteAnim); |
| if (dsmoteAnim) clearInterval(dsmoteAnim); |
| smoteAnimPts = []; dsmoteAnimPts = []; |
| drawScatterBase('smote-canvas', [], '#ff6b35'); |
| drawScatterBase('dsmote-canvas', [], '#00e676'); |
| } |
| |
| |
| |
| |
| const stepDetails = [ |
| { |
| color: '#ff6b35', title: 'Step 1 — Majority Class Reduction', |
| desc: 'The dominant class (e.g., smurf with 2.8M samples) is randomly down-sampled by keeping only a fraction p of its data. This reduces bias in training without losing minority class information.', |
| formula: 'X_maj_reduced = X_maj[0 : ρ × N_maj] (ρ = 0.5)' |
| }, |
| { |
| color: '#7c4dff', title: 'Step 2 — PCA Dimensionality Reduction', |
| desc: 'Principal Component Analysis reduces the feature space while retaining 95% of the variance. This makes KMeans clustering and KNN more effective and computationally efficient.', |
| formula: 'X_pca = PCA(X, variance_ratio = 0.95)' |
| }, |
| { |
| color: '#00e5ff', title: 'Step 3 — KMeans Clustering', |
| desc: 'For each minority class, KMeans groups the samples into k sub-clusters. This lets DSMOTE understand the internal structure of each attack type rather than treating them as a uniform blob.', |
| formula: 'cluster_labels = KMeans(X_Cm, k=3)' |
| }, |
| { |
| color: '#ffd740', title: 'Step 4 — KNN + Interpolation', |
| desc: 'Within each cluster, K-nearest neighbors are computed. Synthetic samples are generated by interpolating between a selected point and one of its neighbors — just like SMOTE, but cluster-confined.', |
| formula: 'x_new = xᵢ + λ × (x_j − xᵢ), λ ~ U(0,1)' |
| }, |
| { |
| color: '#00e676', title: 'Step 5 — Density Constraint Filter', |
| desc: 'A synthetic sample is accepted only if it falls within the mean intra-cluster distance. This prevents points from being generated in sparse, noisy regions outside the true data distribution.', |
| formula: '‖x_new − xᵢ‖₂ ≤ d_mean → ACCEPT' |
| }, |
| { |
| color: '#ff4081', title: 'Step 6 — Class Weight Computation', |
| desc: 'After oversampling, class weights are computed inversely proportional to class frequency. These weights guide the loss function during training to further emphasize minority classes.', |
| formula: 'w_c = N_total / (K × N_c)' |
| } |
| ]; |
| |
| function showStep(i) { |
| const detail = document.getElementById('step-detail'); |
| const s = stepDetails[i]; |
| detail.className = 'step-detail visible'; |
| detail.style.borderLeftColor = s.color; |
| detail.innerHTML = ` |
| <h4 style="color:${s.color}">${s.title}</h4> |
| <p>${s.desc}</p> |
| <div class="formula" style="color:${s.color}">${s.formula}</div> |
| `; |
| document.querySelectorAll('.pipe-icon').forEach((el, j) => { |
| el.classList.toggle('active-step', i === j); |
| }); |
| } |
| |
| function initPipeline() { |
| document.querySelectorAll('.pipe-step').forEach((el, i) => { |
| setTimeout(() => el.classList.add('visible'), i * 120); |
| }); |
| drawPipelineCanvas(); |
| } |
| |
| function drawPipelineCanvas() { |
| const canvas = document.getElementById('pipeline-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| |
| const steps = [ |
| { label: 'RAW DATA', color: '#546e7a', val: '4.9M rows\n41 features' }, |
| { label: 'MAJORITY ↓', color: '#ff6b35', val: '2.45M rows\n41 features' }, |
| { label: 'PCA', color: '#7c4dff', val: '2.45M rows\n~18 PCs' }, |
| { label: 'CLUSTERING', color: '#00e5ff', val: 'k clusters\nper class' }, |
| { label: 'SYNTHETIC', color: '#ffd740', val: '+1.8M\nnew samples' }, |
| { label: 'BALANCED', color: '#00e676', val: '~270K\nper class' }, |
| ]; |
| |
| const bw = 120, bh = 80, gap = (W - steps.length * bw) / (steps.length + 1); |
| steps.forEach((s, i) => { |
| const x = gap + i * (bw + gap); |
| const y = (H - bh) / 2; |
| |
| ctx.strokeStyle = s.color; |
| ctx.lineWidth = 1.5; |
| ctx.strokeRect(x, y, bw, bh); |
| ctx.fillStyle = s.color + '15'; |
| ctx.fillRect(x, y, bw, bh); |
| |
| ctx.fillStyle = s.color; |
| ctx.font = 'bold 11px JetBrains Mono, monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(s.label, x + bw / 2, y + 22); |
| |
| ctx.fillStyle = 'rgba(224,247,250,0.55)'; |
| ctx.font = '10px JetBrains Mono, monospace'; |
| const lines = s.val.split('\n'); |
| lines.forEach((l, li) => ctx.fillText(l, x + bw / 2, y + 42 + li * 15)); |
| |
| if (i < steps.length - 1) { |
| const ax = x + bw + 4, ay = H / 2; |
| ctx.strokeStyle = 'rgba(84,110,122,0.6)'; |
| ctx.lineWidth = 1; |
| ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax + gap - 8, ay); ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.6)'; |
| ctx.beginPath(); |
| ctx.moveTo(ax + gap - 8, ay - 5); |
| ctx.lineTo(ax + gap - 1, ay); |
| ctx.lineTo(ax + gap - 8, ay + 5); |
| ctx.fill(); |
| } |
| }); |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| |
| |
| let clusterAnimDone = false; |
| const clRng = seededRng(55); |
| |
| function genClusterPts() { |
| const centers = [[-2, 2.5], [2.5, 0.5], [-0.5, -2.5], [3.5, -2.5]]; |
| const colors = ['#00e5ff', '#ff6b35', '#7c4dff', '#ffd740']; |
| const pts = []; |
| centers.forEach((c, ci) => { |
| const n = 22 + Math.floor(clRng() * 10); |
| for (let i = 0; i < n; i++) { |
| const [x, y] = gaussianPair(clRng, c[0], c[1], 0.55, 0.55); |
| pts.push({ x, y, cluster: ci, color: colors[ci] }); |
| } |
| }); |
| return pts; |
| } |
| |
| const clusterPts = genClusterPts(); |
| let clustersRevealed = false; |
| |
| function drawClusters(revealed) { |
| const canvas = document.getElementById('cluster-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| drawAxes(ctx, W, H); |
| |
| clusterPts.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, -5, 5, -5, 5, W, H, PAD); |
| const col = revealed ? p.color : '#546e7a'; |
| dot(ctx, cx, cy, 5.5, col, revealed ? 0.85 : 0.5); |
| }); |
| |
| if (revealed) { |
| |
| const centers = [[-2, 2.5], [2.5, 0.5], [-0.5, -2.5], [3.5, -2.5]]; |
| const colors = ['#00e5ff', '#ff6b35', '#7c4dff', '#ffd740']; |
| centers.forEach((c, ci) => { |
| const [cx, cy] = dataToCanvas(c[0], c[1], -5, 5, -5, 5, W, H, PAD); |
| ctx.strokeStyle = colors[ci]; |
| ctx.lineWidth = 2; |
| ctx.beginPath(); |
| ctx.moveTo(cx - 8, cy); ctx.lineTo(cx + 8, cy); |
| ctx.moveTo(cx, cy - 8); ctx.lineTo(cx, cy + 8); |
| ctx.stroke(); |
| ctx.font = 'bold 10px JetBrains Mono, monospace'; |
| ctx.fillStyle = colors[ci]; |
| ctx.fillText(`C${ci + 1}`, cx + 10, cy - 6); |
| }); |
| |
| |
| ctx.font = 'bold 13px Rajdhani, sans-serif'; |
| ctx.fillStyle = 'rgba(0,229,255,0.7)'; |
| ctx.fillText('✓ KMeans reveals sub-structure', PAD + 5, PAD + 18); |
| } else { |
| ctx.font = 'bold 13px Rajdhani, sans-serif'; |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; |
| ctx.fillText('Minority samples (unclustered)', PAD + 5, PAD + 18); |
| } |
| } |
| |
| function initClusters() { drawClusters(false); } |
| function animateClusters() { |
| drawClusters(false); |
| setTimeout(() => drawClusters(true), 600); |
| } |
| function resetClusters() { drawClusters(false); } |
| |
| |
| |
| |
| let densityPts = [], densityAnim = null; |
| const dRng = seededRng(88); |
| |
| function genDensitySourcePts() { |
| const pts = []; |
| for (let i = 0; i < 25; i++) { |
| const [x, y] = gaussianPair(dRng, 0, 0, 0.9, 0.9); |
| pts.push({ x, y }); |
| } |
| return pts; |
| } |
| const densitySrcPts = genDensitySourcePts(); |
| const D_MEAN = 1.1; |
| |
| function computeDMean(pts) { |
| let total = 0, count = 0; |
| pts.forEach(p => { |
| pts.forEach(q => { |
| total += Math.sqrt((p.x - q.x) ** 2 + (p.y - q.y) ** 2); |
| count++; |
| }); |
| }); |
| return total / count; |
| } |
| |
| function drawDensity(accepted, rejected) { |
| const canvas = document.getElementById('density-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| drawAxes(ctx, W, H); |
| |
| |
| densitySrcPts.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, -5, 5, -5, 5, W, H, PAD); |
| const r_px = D_MEAN / 10 * (W - 2 * PAD); |
| ctx.strokeStyle = 'rgba(0,229,255,0.12)'; |
| ctx.lineWidth = 1; |
| ctx.setLineDash([4, 4]); |
| ctx.beginPath(); ctx.arc(cx, cy, r_px, 0, Math.PI * 2); ctx.stroke(); |
| ctx.setLineDash([]); |
| }); |
| |
| |
| const refPt = densitySrcPts[5]; |
| const [rcx, rcy] = dataToCanvas(refPt.x, refPt.y, -5, 5, -5, 5, W, H, PAD); |
| const r_px = D_MEAN / 10 * (W - 2 * PAD); |
| ctx.strokeStyle = 'rgba(0,229,255,0.55)'; |
| ctx.lineWidth = 2; |
| ctx.setLineDash([6, 4]); |
| ctx.beginPath(); ctx.arc(rcx, rcy, r_px, 0, Math.PI * 2); ctx.stroke(); |
| ctx.setLineDash([]); |
| |
| |
| ctx.fillStyle = 'rgba(0,229,255,0.5)'; |
| ctx.font = '10px JetBrains Mono, monospace'; |
| ctx.fillText('d_mean', rcx + r_px + 5, rcy - 5); |
| |
| |
| densitySrcPts.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, -5, 5, -5, 5, W, H, PAD); |
| dot(ctx, cx, cy, 5.5, '#00e5ff'); |
| }); |
| |
| |
| rejected.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, -5, 5, -5, 5, W, H, PAD); |
| dot(ctx, cx, cy, 4.5, '#ff1744', 0.75); |
| |
| ctx.strokeStyle = '#ff1744'; |
| ctx.lineWidth = 1.5; |
| ctx.beginPath(); ctx.moveTo(cx - 4, cy - 4); ctx.lineTo(cx + 4, cy + 4); ctx.stroke(); |
| ctx.beginPath(); ctx.moveTo(cx + 4, cy - 4); ctx.lineTo(cx - 4, cy + 4); ctx.stroke(); |
| }); |
| |
| |
| accepted.forEach(p => { |
| const [cx, cy] = dataToCanvas(p.x, p.y, -5, 5, -5, 5, W, H, PAD); |
| dot(ctx, cx, cy, 4.5, '#00e676', 0.8); |
| }); |
| |
| |
| ctx.font = 'bold 12px JetBrains Mono, monospace'; |
| ctx.fillStyle = '#00e676'; |
| ctx.fillText(`✓ Accepted: ${accepted.length}`, PAD + 5, PAD + 16); |
| ctx.fillStyle = '#ff1744'; |
| ctx.fillText(`✗ Rejected: ${rejected.length}`, PAD + 5, PAD + 32); |
| } |
| |
| function genSyntheticDensityPt(idx) { |
| const r = seededRng(idx * 31 + 17); |
| const i = Math.floor(r() * densitySrcPts.length); |
| const j = Math.floor(r() * densitySrcPts.length); |
| const lam = r(); |
| const nx = densitySrcPts[i].x + lam * (densitySrcPts[j].x - densitySrcPts[i].x); |
| const ny = densitySrcPts[i].y + lam * (densitySrcPts[j].y - densitySrcPts[i].y); |
| const dist = Math.sqrt((nx - densitySrcPts[i].x) ** 2 + (ny - densitySrcPts[i].y) ** 2); |
| return { x: nx, y: ny, accepted: dist <= D_MEAN }; |
| } |
| |
| let densityAccepted = [], densityRejected = []; |
| |
| function initDensity() { drawDensity([], []); } |
| function animateDensity() { |
| if (densityAnim) clearInterval(densityAnim); |
| densityAccepted = []; densityRejected = []; |
| let count = 0; |
| densityAnim = setInterval(() => { |
| if (count >= 120) { clearInterval(densityAnim); return; } |
| const p = genSyntheticDensityPt(count); |
| if (p.accepted) densityAccepted.push(p); else densityRejected.push(p); |
| drawDensity(densityAccepted, densityRejected); |
| count++; |
| }, 35); |
| } |
| function resetDensity() { |
| if (densityAnim) clearInterval(densityAnim); |
| densityAccepted = []; densityRejected = []; |
| drawDensity([], []); |
| } |
| |
| |
| |
| |
| const classes = [ |
| { name: 'smurf', before: 2807886, after: 258783 }, |
| { name: 'neptune', before: 1072017, after: 269180 }, |
| { name: 'normal', before: 972781, after: 284484 }, |
| { name: 'satan', before: 15892, after: 268453 }, |
| { name: 'ipsweep', before: 12481, after: 270572 }, |
| { name: 'portsweep', before: 10413, after: 268905 }, |
| { name: 'nmap', before: 2316, after: 262351 }, |
| { name: 'back', before: 2203, after: 256081 }, |
| { name: 'warezclient', before: 1020, after: 264107 }, |
| { name: 'teardrop', before: 979, after: 252848 }, |
| { name: 'pod', before: 264, after: 240579 }, |
| ]; |
| |
| function buildBars(containerId, key, palette) { |
| const el = document.getElementById(containerId); |
| if (!el) return; |
| const maxVal = Math.max(...classes.map(c => c[key])); |
| el.innerHTML = ''; |
| classes.forEach((c, i) => { |
| const pct = (c[key] / maxVal * 100).toFixed(1); |
| const color = key === 'before' |
| ? (c.before > 100000 ? '#ff6b35' : c.before > 10000 ? '#ffd740' : '#ff1744') |
| : '#00e676'; |
| const row = document.createElement('div'); |
| row.className = 'bar-row'; |
| row.innerHTML = ` |
| <div class="bar-name">${c.name}</div> |
| <div class="bar-track"> |
| <div class="bar-fill" id="bar-${key}-${i}" style="width:0%;background:${color}"></div> |
| </div> |
| <div class="bar-val">${(c[key] / 1000).toFixed(0)}K</div> |
| `; |
| el.appendChild(row); |
| }); |
| } |
| |
| function initBars() { |
| buildBars('before-bars', 'before'); |
| buildBars('after-bars', 'after'); |
| } |
| |
| function animateBars() { |
| const maxB = Math.max(...classes.map(c => c.before)); |
| const maxA = Math.max(...classes.map(c => c.after)); |
| classes.forEach((c, i) => { |
| setTimeout(() => { |
| const bef = document.getElementById(`bar-before-${i}`); |
| const aft = document.getElementById(`bar-after-${i}`); |
| if (bef) bef.style.width = (c.before / maxB * 100) + '%'; |
| if (aft) aft.style.width = (c.after / maxA * 100) + '%'; |
| }, i * 60); |
| }); |
| } |
| |
| function resetBars() { |
| classes.forEach((c, i) => { |
| const bef = document.getElementById(`bar-before-${i}`); |
| const aft = document.getElementById(`bar-after-${i}`); |
| if (bef) bef.style.width = '0%'; |
| if (aft) aft.style.width = '0%'; |
| }); |
| } |
| |
| |
| const f1Data = [ |
| { method: 'ROS', color: '#546e7a', min: 0.002, max: 0.096 }, |
| { method: 'SMOTE', color: '#7c4dff', min: 0.007, max: 0.096 }, |
| { method: 'B-SMOTE', color: '#ff6b35', min: 0.016, max: 0.096 }, |
| { method: 'ADASYN', color: '#ffd740', min: 0.017, max: 0.114 }, |
| { method: 'DSMOTE', color: '#00e5ff', min: 0.306, max: 0.588 }, |
| ]; |
| |
| let f1Anim = null, f1Progress = 0; |
| |
| function initF1() { drawF1(0); } |
| function animateF1() { |
| if (f1Anim) cancelAnimationFrame(f1Anim); |
| f1Progress = 0; |
| function step() { |
| f1Progress = Math.min(f1Progress + 0.025, 1); |
| drawF1(f1Progress); |
| if (f1Progress < 1) f1Anim = requestAnimationFrame(step); |
| } |
| step(); |
| } |
| |
| function drawF1(progress) { |
| const canvas = document.getElementById('f1-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| |
| const padL = 60, padR = 30, padT = 30, padB = 50; |
| const chartW = W - padL - padR, chartH = H - padT - padB; |
| const maxF1 = 0.65; |
| |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.08)'; |
| ctx.lineWidth = 1; |
| for (let i = 0; i <= 6; i++) { |
| const y = padT + chartH * (1 - i / 6 * maxF1 / maxF1); |
| const val = (i / 6 * maxF1).toFixed(2); |
| ctx.beginPath(); ctx.moveTo(padL, padT + chartH - i * chartH / 6); ctx.lineTo(W - padR, padT + chartH - i * chartH / 6); ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; |
| ctx.font = '9px JetBrains Mono, monospace'; |
| ctx.textAlign = 'right'; |
| ctx.fillText(val, padL - 6, padT + chartH - i * chartH / 6 + 3); |
| } |
| |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.2)'; |
| ctx.beginPath(); ctx.moveTo(padL, padT); ctx.lineTo(padL, padT + chartH); ctx.stroke(); |
| ctx.beginPath(); ctx.moveTo(padL, padT + chartH); ctx.lineTo(W - padR, padT + chartH); ctx.stroke(); |
| |
| |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; |
| ctx.font = '10px JetBrains Mono, monospace'; |
| ctx.textAlign = 'left'; |
| ctx.fillText('Macro-F1', padL + 5, padT + 12); |
| |
| const bw = chartW / f1Data.length; |
| |
| f1Data.forEach((d, i) => { |
| const x = padL + i * bw + bw * 0.15; |
| const bWidth = bw * 0.7; |
| |
| |
| const yMax = padT + chartH - (d.max * progress / maxF1) * chartH; |
| const yMin = padT + chartH - (d.min * progress / maxF1) * chartH; |
| |
| ctx.fillStyle = d.color + '33'; |
| ctx.fillRect(x, yMax, bWidth, yMin - yMax); |
| ctx.strokeStyle = d.color; |
| ctx.lineWidth = 1.5; |
| ctx.strokeRect(x, yMax, bWidth, yMin - yMax); |
| |
| |
| ctx.strokeStyle = d.color; |
| ctx.lineWidth = 2.5; |
| ctx.beginPath(); ctx.moveTo(x, yMax); ctx.lineTo(x + bWidth, yMax); ctx.stroke(); |
| |
| |
| if (progress > 0.5) { |
| ctx.fillStyle = d.color; |
| ctx.font = 'bold 10px JetBrains Mono, monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText((d.max * progress).toFixed(3), x + bWidth / 2, yMax - 5); |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; |
| ctx.font = '9px JetBrains Mono, monospace'; |
| ctx.fillText((d.min * progress).toFixed(3), x + bWidth / 2, yMin + 12); |
| } |
| |
| |
| ctx.fillStyle = i === 4 ? d.color : 'rgba(84,110,122,0.8)'; |
| ctx.font = i === 4 ? 'bold 10px JetBrains Mono, monospace' : '9px JetBrains Mono, monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(d.method, x + bWidth / 2, padT + chartH + 20); |
| if (i === 4) ctx.fillText('★', x + bWidth / 2, padT + chartH + 33); |
| }); |
| |
| |
| if (progress > 0.7) { |
| ctx.strokeStyle = 'rgba(0,229,255,0.3)'; |
| ctx.lineWidth = 1; |
| ctx.setLineDash([4, 4]); |
| const dsmX = padL + 4 * bw; |
| ctx.strokeRect(dsmX, padT, bw, chartH); |
| ctx.setLineDash([]); |
| } |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| |
| |
| |
| const unswModels = ['DT', 'RF', 'XGBoost', 'ANN', 'CNN', 'LSTM', 'LSTM-CNN']; |
| const unswMethods = [ |
| { name: 'RAW', color: '#78909c', f1: [0.485, 0.581, 0.451, 0.299, 0.273, 0.325, 0.395] }, |
| { name: 'ROS', color: '#546e7a', f1: [0.096, 0.096, 0.003, 0.096, 0.096, 0.026, 0.016] }, |
| { name: 'SMOTE', color: '#7c4dff', f1: [0.094, 0.096, 0.025, 0.096, 0.096, 0.010, 0.008] }, |
| { name: 'BSMOTE', color: '#ff6b35', f1: [0.061, 0.096, 0.040, 0.096, 0.096, 0.016, 0.022] }, |
| { name: 'ADASYN', color: '#ffd740', f1: [0.062, 0.097, 0.114, 0.096, 0.096, 0.017, 0.018] }, |
| { name: 'DSMOTE', color: '#00e5ff', f1: [0.535, 0.588, 0.432, 0.307, 0.274, 0.332, 0.448] }, |
| ]; |
| |
| let unswFilter = 'ALL'; |
| let unswAnimProgress = 0, unswAnimFrame = null; |
| |
| function filterUnswModel(model) { |
| unswFilter = model; |
| unswAnimProgress = 0; |
| if (unswAnimFrame) cancelAnimationFrame(unswAnimFrame); |
| function step() { |
| unswAnimProgress = Math.min(unswAnimProgress + 0.04, 1); |
| drawUnswF1(unswAnimProgress); |
| if (unswAnimProgress < 1) unswAnimFrame = requestAnimationFrame(step); |
| } |
| step(); |
| } |
| |
| function drawUnswF1(progress) { |
| const canvas = document.getElementById('unsw-f1-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| |
| const models = unswFilter === 'ALL' ? unswModels : [unswFilter]; |
| const modelIndices = unswFilter === 'ALL' |
| ? unswModels.map((_, i) => i) |
| : [unswModels.indexOf(unswFilter)]; |
| |
| const padL = 45, padR = 20, padT = 30, padB = 55; |
| const chartW = W - padL - padR, chartH = H - padT - padB; |
| const maxF1 = 0.65; |
| |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.08)'; ctx.lineWidth = 1; |
| for (let i = 0; i <= 6; i++) { |
| const y = padT + chartH * (1 - i / 6); |
| ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; |
| ctx.font = '9px JetBrains Mono, monospace'; |
| ctx.textAlign = 'right'; |
| ctx.fillText((maxF1 * i / 6).toFixed(2), padL - 5, y + 3); |
| } |
| ctx.strokeStyle = 'rgba(0,229,255,0.2)'; |
| ctx.beginPath(); ctx.moveTo(padL, padT); ctx.lineTo(padL, padT + chartH); ctx.stroke(); |
| ctx.beginPath(); ctx.moveTo(padL, padT + chartH); ctx.lineTo(W - padR, padT + chartH); ctx.stroke(); |
| |
| const groupW = chartW / models.length; |
| const nMethods = unswMethods.length; |
| const bw = groupW * 0.8 / nMethods; |
| const groupPad = groupW * 0.1; |
| |
| modelIndices.forEach((mi, gi) => { |
| const gx = padL + gi * groupW + groupPad; |
| |
| ctx.fillStyle = 'rgba(224,247,250,0.7)'; |
| ctx.font = 'bold 10px JetBrains Mono, monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(unswModels[mi], padL + gi * groupW + groupW / 2, padT + chartH + 20); |
| |
| unswMethods.forEach((m, mIdx) => { |
| const val = m.f1[mi] * progress; |
| const barH = (val / maxF1) * chartH; |
| const x = gx + mIdx * bw; |
| const y = padT + chartH - barH; |
| const isDSMOTE = m.name === 'DSMOTE'; |
| |
| ctx.fillStyle = isDSMOTE ? m.color + 'cc' : m.color + '88'; |
| ctx.fillRect(x, y, bw - 1, barH); |
| if (isDSMOTE) { |
| ctx.strokeStyle = m.color; |
| ctx.lineWidth = 1.5; |
| ctx.strokeRect(x, y, bw - 1, barH); |
| } |
| |
| if (progress > 0.85 && val > 0.05) { |
| ctx.fillStyle = isDSMOTE ? m.color : 'rgba(84,110,122,0.8)'; |
| ctx.font = isDSMOTE ? 'bold 8px JetBrains Mono,monospace' : '8px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(val.toFixed(2), x + (bw - 1) / 2, y - 3); |
| } |
| }); |
| }); |
| |
| |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| const unswBA = [ |
| { method: 'RAW', color: '#78909c', vals: [0.470, 0.566, 0.425, 0.374, 0.278, 0.329, 0.382] }, |
| { method: 'SMOTE', color: '#7c4dff', vals: [0.096, 0.100, 0.111, 0.100, 0.100, 0.068, 0.124] }, |
| { method: 'ADASYN', color: '#ffd740', vals: [0.050, 0.101, 0.168, 0.100, 0.100, 0.082, 0.133] }, |
| { method: 'DSMOTE', color: '#00e5ff', vals: [0.516, 0.573, 0.408, 0.376, 0.290, 0.332, 0.479] }, |
| ]; |
| |
| function drawUnswBA() { |
| const canvas = document.getElementById('unsw-ba-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| const padL = 45, padR = 20, padT = 25, padB = 50; |
| const chartW = W - padL - padR, chartH = H - padT - padB; |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.08)'; ctx.lineWidth = 1; |
| for (let i = 0; i <= 5; i++) { |
| const y = padT + chartH * (1 - i / 5); |
| ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; ctx.font = '9px JetBrains Mono,monospace'; ctx.textAlign = 'right'; |
| ctx.fillText((i / 5).toFixed(1), padL - 4, y + 3); |
| } |
| |
| const groupW = chartW / unswModels.length; |
| unswModels.forEach((model, mi) => { |
| const gx = padL + mi * groupW + groupW * 0.08; |
| const bw = groupW * 0.84 / unswBA.length; |
| ctx.fillStyle = 'rgba(224,247,250,0.7)'; ctx.font = '10px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; ctx.fillText(model, padL + mi * groupW + groupW / 2, padT + chartH + 18); |
| unswBA.forEach((m, mIdx) => { |
| const val = m.vals[mi]; |
| const barH = val * chartH; |
| const x = gx + mIdx * bw; |
| const y = padT + chartH - barH; |
| const isDSMOTE = m.method === 'DSMOTE'; |
| ctx.fillStyle = isDSMOTE ? m.color + 'cc' : m.color + '77'; |
| ctx.fillRect(x, y, bw - 1, barH); |
| if (isDSMOTE) { |
| ctx.strokeStyle = m.color; ctx.lineWidth = 1.5; |
| ctx.strokeRect(x, y, bw - 1, barH); |
| ctx.fillStyle = m.color; ctx.font = 'bold 8px JetBrains Mono,monospace'; |
| ctx.fillText(val.toFixed(2), x + (bw - 1) / 2, y - 3); |
| } |
| }); |
| }); |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| function drawConfusionMatrices() { |
| drawCM('cm-smote-canvas', 'SMOTE RF', false); |
| drawCM('cm-dsmote-canvas', 'DSMOTE RF', true); |
| } |
| |
| function drawCM(canvasId, title, isDsmote) { |
| const canvas = document.getElementById(canvasId); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| |
| const classes = ['Benign','Exploits','Fuzzers','Recon.','Generic','DoS','Backdoor','Shellcode','Analysis','Worms']; |
| const n = classes.length; |
| |
| |
| |
| let matrix; |
| if (!isDsmote) { |
| |
| matrix = Array.from({length:n}, (_, r) => Array.from({length:n}, (_, c) => c === 0 ? [184,583,169987,805,6073,4048,1139,1787,253,21][r] : 0)); |
| } else { |
| |
| matrix = [ |
| [67,0,0,4,105,6,0,2,0,0], |
| [0,57,0,16,202,201,60,35,10,2], |
| [0,1,169977,3,5,1,0,0,0,0], |
| [11,3,1,246,324,165,25,23,8,0], |
| [69,25,0,89,4780,752,86,229,39,4], |
| [36,47,0,31,304,3485,50,91,4,0], |
| [6,43,0,15,148,133,731,51,13,1], |
| [23,13,0,18,342,404,43,921,23,0], |
| [0,9,0,2,53,45,11,27,106,0], |
| [0,0,0,1,5,0,0,0,1,14], |
| ]; |
| } |
| |
| const padL = 70, padT = 30, padR = 15, padB = 70; |
| const cellW = (W - padL - padR) / n; |
| const cellH = (H - padT - padB) / n; |
| |
| |
| const rowMaxes = matrix.map(row => Math.max(...row)); |
| const globalMax = Math.max(...rowMaxes); |
| |
| matrix.forEach((row, ri) => { |
| row.forEach((val, ci) => { |
| const x = padL + ci * cellW; |
| const y = padT + ri * cellH; |
| const intensity = val / globalMax; |
| const color = isDsmote |
| ? `rgba(0,229,255,${Math.min(intensity * 1.5, 0.9)})` |
| : `rgba(255,23,68,${Math.min(intensity * 1.5, 0.9)})`; |
| ctx.fillStyle = intensity > 0.01 ? color : 'rgba(6,11,20,0.8)'; |
| ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2); |
| |
| if (val > 0) { |
| ctx.fillStyle = intensity > 0.3 ? 'rgba(6,11,20,0.9)' : 'rgba(224,247,250,0.7)'; |
| ctx.font = `${Math.min(cellW * 0.28, 10)}px JetBrains Mono,monospace`; |
| ctx.textAlign = 'center'; |
| const display = val > 1000 ? (val/1000).toFixed(0)+'k' : val; |
| ctx.fillText(display, x + cellW/2, y + cellH/2 + 3); |
| } |
| }); |
| }); |
| |
| |
| ctx.fillStyle = 'rgba(84,110,122,0.9)'; ctx.font = '8px JetBrains Mono,monospace'; ctx.textAlign = 'center'; |
| classes.forEach((c, i) => { |
| ctx.save(); ctx.translate(padL + i * cellW + cellW/2, H - padB + 8); |
| ctx.rotate(-Math.PI / 4); ctx.fillText(c, 0, 0); ctx.restore(); |
| }); |
| |
| ctx.textAlign = 'right'; |
| classes.forEach((c, i) => { |
| ctx.fillText(c, padL - 4, padT + i * cellH + cellH/2 + 3); |
| }); |
| |
| |
| ctx.fillStyle = isDsmote ? '#00e5ff' : '#ff1744'; |
| ctx.font = 'bold 11px JetBrains Mono,monospace'; ctx.textAlign = 'center'; |
| ctx.fillText(title, W/2, 18); |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| |
| |
| const kddModels = ['DT', 'RF', 'XGBoost', 'ANN', 'CNN', 'LSTM', 'LSTM-CNN']; |
| const kddMethods = [ |
| { name: 'RAW', color: '#78909c', f1: [0.962, 0.995, 0.992, 0.952, 0.645, 0.952, 0.976] }, |
| { name: 'ROS', color: '#546e7a', f1: [0.925, 0.996, 0.992, 0.858, 0.627, 0.858, 0.975] }, |
| { name: 'SMOTE', color: '#7c4dff', f1: [0.939, 0.995, 0.992, 0.866, 0.620, 0.943, 0.974] }, |
| { name: 'BSMOTE', color: '#ff6b35', f1: [0.911, 0.995, 0.991, 0.900, 0.452, 0.924, 0.945] }, |
| { name: 'DSMOTE', color: '#00e5ff', f1: [0.962, 0.995, 0.992, 0.928, 0.505, 0.944, 0.965] }, |
| ]; |
| |
| let kddAnimFrame = null, kddProgress = 0; |
| |
| function drawKddF1(progress) { |
| const canvas = document.getElementById('kdd-f1-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| const padL = 45, padR = 20, padT = 30, padB = 55; |
| const chartW = W - padL - padR, chartH = H - padT - padB; |
| const minF1 = 0.4, maxF1 = 1.0, range = maxF1 - minF1; |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.08)'; ctx.lineWidth = 1; |
| for (let i = 0; i <= 6; i++) { |
| const val = minF1 + range * i / 6; |
| const y = padT + chartH * (1 - i / 6); |
| ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; ctx.font = '9px JetBrains Mono,monospace'; |
| ctx.textAlign = 'right'; ctx.fillText(val.toFixed(2), padL - 4, y + 3); |
| } |
| |
| ctx.fillStyle = 'rgba(84,110,122,0.5)'; ctx.font = '9px JetBrains Mono,monospace'; |
| ctx.textAlign = 'left'; ctx.fillText('* Y-axis starts at 0.40 for readability', padL + 5, padT + 12); |
| |
| const groupW = chartW / kddModels.length; |
| kddModels.forEach((model, mi) => { |
| const gx = padL + mi * groupW + groupW * 0.05; |
| const bw = groupW * 0.9 / kddMethods.length; |
| ctx.fillStyle = 'rgba(224,247,250,0.7)'; ctx.font = '10px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; ctx.fillText(model, padL + mi * groupW + groupW / 2, padT + chartH + 18); |
| kddMethods.forEach((m, mIdx) => { |
| const raw = m.f1[mi]; |
| const val = Math.max(raw - minF1, 0) * progress; |
| const barH = (val / range) * chartH; |
| const x = gx + mIdx * bw; |
| const y = padT + chartH - barH; |
| const isDSMOTE = m.name === 'DSMOTE'; |
| ctx.fillStyle = isDSMOTE ? m.color + 'cc' : m.color + '77'; |
| ctx.fillRect(x, y, bw - 1, barH); |
| if (isDSMOTE) { |
| ctx.strokeStyle = m.color; ctx.lineWidth = 1.5; |
| ctx.strokeRect(x, y, bw - 1, barH); |
| } |
| if (progress > 0.85 && raw > minF1 + 0.05) { |
| ctx.fillStyle = isDSMOTE ? m.color : 'rgba(84,110,122,0.7)'; |
| ctx.font = isDSMOTE ? 'bold 8px JetBrains Mono,monospace' : '8px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText(raw.toFixed(3), x + (bw-1)/2, y - 3); |
| } |
| }); |
| }); |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| function drawRadar() { |
| const canvas = document.getElementById('kdd-radar-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| const cx = W / 2, cy = H / 2, R = Math.min(W, H) * 0.36; |
| const axes = ['Accuracy', 'Bal. Acc.', 'Precision', 'Recall', 'F1 Macro', 'G-Mean']; |
| const n = axes.length; |
| const datasets = [ |
| { name: 'RAW RF', color: '#78909c', vals: [0.99986, 0.99402, 0.99693, 0.99402, 0.99545, 0.99393] }, |
| { name: 'DSMOTE RF', color: '#00e5ff', vals: [0.99986, 0.99402, 0.99693, 0.99402, 0.99545, 0.99393] }, |
| { name: 'SMOTE RF', color: '#7c4dff', vals: [0.9999, 0.9922, 0.9978, 0.9922, 0.9949, 0.9920] }, |
| ]; |
| |
| |
| for (let ring = 1; ring <= 5; ring++) { |
| const r = R * ring / 5; |
| ctx.beginPath(); |
| for (let i = 0; i < n; i++) { |
| const angle = (i / n) * Math.PI * 2 - Math.PI / 2; |
| const x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle); |
| i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); |
| } |
| ctx.closePath(); |
| ctx.strokeStyle = 'rgba(0,229,255,0.1)'; ctx.lineWidth = 1; ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.5)'; ctx.font = '8px JetBrains Mono,monospace'; ctx.textAlign = 'center'; |
| ctx.fillText((ring / 5).toFixed(1), cx, cy - r - 3); |
| } |
| |
| |
| for (let i = 0; i < n; i++) { |
| const angle = (i / n) * Math.PI * 2 - Math.PI / 2; |
| const ex = cx + R * Math.cos(angle), ey = cy + R * Math.sin(angle); |
| ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(ex, ey); |
| ctx.strokeStyle = 'rgba(0,229,255,0.2)'; ctx.stroke(); |
| const lx = cx + (R + 22) * Math.cos(angle), ly = cy + (R + 22) * Math.sin(angle); |
| ctx.fillStyle = 'rgba(224,247,250,0.7)'; ctx.font = '10px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; ctx.fillText(axes[i], lx, ly + 3); |
| } |
| |
| |
| datasets.forEach((ds, di) => { |
| ctx.beginPath(); |
| ds.vals.forEach((v, i) => { |
| const angle = (i / n) * Math.PI * 2 - Math.PI / 2; |
| const r = v * R; |
| const x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle); |
| i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); |
| }); |
| ctx.closePath(); |
| ctx.strokeStyle = ds.color; ctx.lineWidth = di === 1 ? 2.5 : 1.5; ctx.stroke(); |
| ctx.fillStyle = ds.color + (di === 1 ? '22' : '11'); ctx.fill(); |
| |
| ds.vals.forEach((v, i) => { |
| const angle = (i / n) * Math.PI * 2 - Math.PI / 2; |
| const r = v * R; |
| const x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle); |
| ctx.beginPath(); ctx.arc(x, y, di === 1 ? 4 : 3, 0, Math.PI * 2); |
| ctx.fillStyle = ds.color; ctx.fill(); |
| }); |
| }); |
| |
| |
| datasets.forEach((ds, i) => { |
| const lx = 20, ly = 20 + i * 18; |
| ctx.fillStyle = ds.color; ctx.fillRect(lx, ly, 14, 3); |
| ctx.fillStyle = 'rgba(224,247,250,0.8)'; ctx.font = '10px JetBrains Mono,monospace'; |
| ctx.textAlign = 'left'; ctx.fillText(ds.name, lx + 20, ly + 4); |
| }); |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| const kddGmean = [ |
| { method: 'RAW', color: '#78909c', vals: [0.959, 0.994, 0.991, 0.936, 0.005, 0.966, 0.985] }, |
| { method: 'SMOTE', color: '#7c4dff', vals: [0.993, 0.992, 0.997, 0.991, 0.972, 0.991, 0.991] }, |
| { method: 'BSMOTE', color: '#ff6b35', vals: [0.991, 0.992, 0.997, 0.958, 0.655, 0.958, 0.977] }, |
| { method: 'DSMOTE', color: '#00e5ff', vals: [0.959, 0.994, 0.991, 0.943, 0.560, 0.940, 0.959] }, |
| ]; |
| |
| function drawKddGmean() { |
| const canvas = document.getElementById('kdd-gmean-canvas'); |
| if (!canvas) return; |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.width, H = canvas.height; |
| ctx.clearRect(0, 0, W, H); |
| const padL = 45, padR = 20, padT = 25, padB = 50; |
| const chartW = W - padL - padR, chartH = H - padT - padB; |
| const minV = 0.0, maxV = 1.0; |
| |
| ctx.strokeStyle = 'rgba(0,229,255,0.08)'; ctx.lineWidth = 1; |
| for (let i = 0; i <= 5; i++) { |
| const y = padT + chartH * (1 - i / 5); |
| ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke(); |
| ctx.fillStyle = 'rgba(84,110,122,0.7)'; ctx.font = '9px JetBrains Mono,monospace'; |
| ctx.textAlign = 'right'; ctx.fillText((i / 5).toFixed(1), padL - 4, y + 3); |
| } |
| |
| const groupW = chartW / kddModels.length; |
| kddModels.forEach((model, mi) => { |
| const gx = padL + mi * groupW + groupW * 0.06; |
| const bw = groupW * 0.88 / kddGmean.length; |
| ctx.fillStyle = 'rgba(224,247,250,0.7)'; ctx.font = '10px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; ctx.fillText(model, padL + mi * groupW + groupW / 2, padT + chartH + 18); |
| kddGmean.forEach((m, mIdx) => { |
| const val = m.vals[mi]; |
| const barH = val * chartH; |
| const x = gx + mIdx * bw; |
| const y = padT + chartH - barH; |
| const isDSMOTE = m.method === 'DSMOTE'; |
| ctx.fillStyle = isDSMOTE ? m.color + 'cc' : m.color + '77'; |
| ctx.fillRect(x, y, bw - 1, barH); |
| if (isDSMOTE) { |
| ctx.strokeStyle = m.color; ctx.lineWidth = 1.5; |
| ctx.strokeRect(x, y, bw - 1, barH); |
| ctx.fillStyle = m.color; ctx.font = 'bold 8px JetBrains Mono,monospace'; |
| ctx.textAlign = 'center'; ctx.fillText(val.toFixed(3), x + (bw-1)/2, y - 3); |
| } |
| }); |
| }); |
| ctx.textAlign = 'left'; |
| } |
| |
| |
| |
| |
| window.addEventListener('load', () => { |
| resetSmote(); |
| drawScatterBase('smote-canvas', [], '#ff6b35'); |
| drawScatterBase('dsmote-canvas', [], '#00e676'); |
| }); |
| |
| |
| const _origSwitchTab = switchTab; |
| function switchTab(i) { |
| document.querySelectorAll('.tab').forEach((t, j) => t.classList.toggle('active', i === j)); |
| document.querySelectorAll('.panel').forEach((p, j) => p.classList.toggle('active', i === j)); |
| if (i === 1) setTimeout(initPipeline, 100); |
| if (i === 2) setTimeout(initClusters, 100); |
| if (i === 3) setTimeout(initDensity, 100); |
| if (i === 4) setTimeout(() => { initBars(); initF1(); }, 100); |
| if (i === 5) { |
| setTimeout(() => { |
| filterUnswModel('ALL'); |
| drawUnswBA(); |
| drawConfusionMatrices(); |
| }, 150); |
| } |
| if (i === 6) { |
| setTimeout(() => { |
| kddProgress = 0; |
| if (kddAnimFrame) cancelAnimationFrame(kddAnimFrame); |
| function step() { |
| kddProgress = Math.min(kddProgress + 0.04, 1); |
| drawKddF1(kddProgress); |
| if (kddProgress < 1) kddAnimFrame = requestAnimationFrame(step); |
| } |
| step(); |
| drawRadar(); |
| drawKddGmean(); |
| }, 150); |
| } |
| } |
| </script> |
| </body> |
| </html> |
|
|