DSMOTE / index.html
eyad222's picture
Update index.html
6d50ddf verified
<!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;
}
/* Grid noise overlay */
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 */
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 */
.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;
}
/* PANELS */
.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 */
.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 */
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 */
.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 */
.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 */
.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;
}
/* BEFORE/AFTER */
.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 BOX */
.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>
<!-- ======================== PANEL 1: SMOTE WEAKNESS ======================== -->
<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>
<!-- ======================== PANEL 2: PIPELINE ======================== -->
<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>
<!-- Mini pipeline canvas -->
<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>
<!-- ======================== PANEL 3: CLUSTERING ======================== -->
<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>
<!-- ======================== PANEL 4: DENSITY CONSTRAINT ======================== -->
<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>
<!-- ======================== PANEL 5: BEFORE / AFTER ======================== -->
<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>
<!-- F1 comparison -->
<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>
<!-- ======================== PANEL 6: UNSW RESULTS ======================== -->
<div class="panel" id="panel-5">
<!-- Stat strip -->
<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>
<!-- ======================== PANEL 7: KDD RESULTS ======================== -->
<div class="panel" id="panel-6">
<!-- Stat strip -->
<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><!-- /container -->
<script>
// ============================================================
// UTILITIES
// ============================================================
function seededRng(seed) {
let s = seed;
return function() {
s = (s * 1664525 + 1013904223) & 0xffffffff;
return (s >>> 0) / 0xffffffff;
};
}
function gaussianPair(rng, mx, my, sx, sy) {
// Box-Muller
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;
// grid lines
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();
}
// axes
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();
// labels
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;
}
// ============================================================
// TAB SWITCHING — handled at bottom of script
// ============================================================
// ============================================================
// PANEL 1: SMOTE vs DSMOTE
// ============================================================
const rng1 = seededRng(42);
const XMIN = -5, XMAX = 5, YMIN = -5, YMAX = 5;
function genMinorityClusters() {
const r = seededRng(7);
const pts = [];
// Cluster A: top-left
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 });
}
// Cluster B: bottom-right
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 });
}
// Cluster C: middle-right (smaller)
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);
// majority
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);
});
// minority originals
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]);
});
// synthetic
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);
// pick ANY two minority pts (ignoring cluster)
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);
// Pick a cluster
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);
// density check: within cluster
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);
// compute mean intra dist (simplified)
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');
}
// ============================================================
// PANEL 2: PIPELINE
// ============================================================
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;
// box
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);
// label
ctx.fillStyle = s.color;
ctx.font = 'bold 11px JetBrains Mono, monospace';
ctx.textAlign = 'center';
ctx.fillText(s.label, x + bw / 2, y + 22);
// value
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));
// arrow
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';
}
// ============================================================
// PANEL 3: CLUSTERING
// ============================================================
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) {
// draw centroid markers
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);
});
// label
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); }
// ============================================================
// PANEL 4: DENSITY CONSTRAINT
// ============================================================
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; // in data units
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);
// draw d_mean circles for each original point
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([]);
});
// draw one prominent circle
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([]);
// label
ctx.fillStyle = 'rgba(0,229,255,0.5)';
ctx.font = '10px JetBrains Mono, monospace';
ctx.fillText('d_mean', rcx + r_px + 5, rcy - 5);
// original pts
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
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);
// X mark
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
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);
});
// counts
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([], []);
}
// ============================================================
// PANEL 5: BEFORE / AFTER
// ============================================================
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%';
});
}
// F1 Chart
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;
// grid
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);
}
// axes
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();
// axis label
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;
// range bar (min to max)
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);
// max line
ctx.strokeStyle = d.color;
ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(x, yMax); ctx.lineTo(x + bWidth, yMax); ctx.stroke();
// value labels
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);
}
// x label
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);
});
// DSMOTE highlight
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';
}
// ============================================================
// PANEL 6: UNSW RESULTS
// ============================================================
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;
// Grid
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;
// model label
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);
}
});
});
// Method legend inside chart
ctx.textAlign = 'left';
}
// UNSW Balanced Accuracy
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';
}
// Confusion Matrix renderer
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;
// SMOTE RF: predicts everything as Benign (col 0)
// DSMOTE RF: approximate from RAW_RF confusion data (spread across diagonal)
let matrix;
if (!isDsmote) {
// SMOTE collapse: everything predicted as Benign
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 {
// DSMOTE / RAW RF-like: diagonal dominant with some spread
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;
// Normalize
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);
// value
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);
}
});
});
// X labels
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();
});
// Y labels
ctx.textAlign = 'right';
classes.forEach((c, i) => {
ctx.fillText(c, padL - 4, padT + i * cellH + cellH/2 + 3);
});
// Title
ctx.fillStyle = isDsmote ? '#00e5ff' : '#ff1744';
ctx.font = 'bold 11px JetBrains Mono,monospace'; ctx.textAlign = 'center';
ctx.fillText(title, W/2, 18);
ctx.textAlign = 'left';
}
// ============================================================
// PANEL 7: KDD RESULTS
// ============================================================
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);
}
// note: zoom view
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';
}
// Radar chart
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] },
];
// Grid rings
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);
}
// Axis lines & labels
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);
}
// Data polygons
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();
// dots
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();
});
});
// Legend
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';
}
// KDD G-Mean
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';
}
// ============================================================
// INIT
// ============================================================
window.addEventListener('load', () => {
resetSmote();
drawScatterBase('smote-canvas', [], '#ff6b35');
drawScatterBase('dsmote-canvas', [], '#00e676');
});
// Auto-draw on tab switch
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>