Upload 18 files
Browse files- templates/Apriori-Simulator-three.html +446 -0
- templates/Eclat-Algorithm-three.html +858 -0
- templates/Gaussian-Mixture-Models.html +46 -0
- templates/Gradient-Descen.html +47 -0
- templates/Independent-Component-Analysis.html +47 -0
- templates/Linear-Discriminant-Analysis.html +47 -0
- templates/Naive-Bayes-Simulator.html +730 -0
- templates/Neural-Networks-for-Classification-three.html +916 -0
- templates/Neural-Networks-for-Classification.html +48 -0
- templates/Optimization.html +12 -0
- templates/XGBoost-Regression.html +94 -0
- templates/gmm-threejs.html +977 -0
- templates/gradient-descent-three.html +695 -0
- templates/ica-threejs.html +456 -0
- templates/lda-three.html +688 -0
- templates/pca-threejs.html +524 -0
- templates/xboost-tree-three.html +645 -0
- templates/xbost-graph-three.html +693 -0
templates/Apriori-Simulator-three.html
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Apriori Algorithm Simulator | Learn Data Mining Interactively</title>
|
| 7 |
+
<!-- Dependencies -->
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 10 |
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| 11 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 13 |
+
|
| 14 |
+
<style>
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'Inter', sans-serif;
|
| 17 |
+
background-color: #f8fafc;
|
| 18 |
+
}
|
| 19 |
+
.gradient-hero {
|
| 20 |
+
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
|
| 21 |
+
}
|
| 22 |
+
.text-gradient {
|
| 23 |
+
background: linear-gradient(to right, #2563eb, #7c3aed);
|
| 24 |
+
-webkit-background-clip: text;
|
| 25 |
+
-webkit-text-fill-color: transparent;
|
| 26 |
+
}
|
| 27 |
+
.shadow-card {
|
| 28 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
| 29 |
+
}
|
| 30 |
+
.shadow-soft {
|
| 31 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
| 32 |
+
}
|
| 33 |
+
@keyframes slideUp {
|
| 34 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 35 |
+
to { opacity: 1; transform: translateY(0); }
|
| 36 |
+
}
|
| 37 |
+
.animate-slide-up {
|
| 38 |
+
animation: slideUp 0.5s ease-out forwards;
|
| 39 |
+
}
|
| 40 |
+
@keyframes scaleIn {
|
| 41 |
+
from { opacity: 0; transform: scale(0.9); }
|
| 42 |
+
to { opacity: 1; transform: scale(1); }
|
| 43 |
+
}
|
| 44 |
+
.animate-scale-in {
|
| 45 |
+
animation: scaleIn 0.3s ease-out forwards;
|
| 46 |
+
}
|
| 47 |
+
.gradient-primary {
|
| 48 |
+
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
|
| 49 |
+
}
|
| 50 |
+
</style>
|
| 51 |
+
</head>
|
| 52 |
+
<body>
|
| 53 |
+
<div id="root"></div>
|
| 54 |
+
|
| 55 |
+
<script type="text/babel">
|
| 56 |
+
const { useState, useEffect, useMemo } = React;
|
| 57 |
+
|
| 58 |
+
// --- Data & Helpers ---
|
| 59 |
+
const groceryItems = [
|
| 60 |
+
{ id: "bread", name: "Bread", emoji: "🍞", category: "bakery" },
|
| 61 |
+
{ id: "milk", name: "Milk", emoji: "🥛", category: "dairy" },
|
| 62 |
+
{ id: "butter", name: "Butter", emoji: "🧈", category: "dairy" },
|
| 63 |
+
{ id: "eggs", name: "Eggs", emoji: "🥚", category: "dairy" },
|
| 64 |
+
{ id: "cheese", name: "Cheese", emoji: "🧀", category: "dairy" },
|
| 65 |
+
{ id: "apple", name: "Apple", emoji: "🍎", category: "fruits" },
|
| 66 |
+
{ id: "banana", name: "Banana", emoji: "🍌", category: "fruits" },
|
| 67 |
+
{ id: "coffee", name: "Coffee", emoji: "☕", category: "beverages" },
|
| 68 |
+
{ id: "cereal", name: "Cereal", emoji: "🥣", category: "breakfast" },
|
| 69 |
+
{ id: "yogurt", name: "Yogurt", emoji: "🥛", category: "dairy" },
|
| 70 |
+
];
|
| 71 |
+
|
| 72 |
+
const getItemEmoji = (id) => groceryItems.find(i => i.id === id)?.emoji || "📦";
|
| 73 |
+
const getItemDisplayName = (id) => groceryItems.find(i => i.id === id)?.name || id;
|
| 74 |
+
|
| 75 |
+
// --- Logic Engine ---
|
| 76 |
+
const transactions = [
|
| 77 |
+
{ id: 1, items: ["bread", "milk", "butter"], customer: "👩" },
|
| 78 |
+
{ id: 2, items: ["bread", "eggs", "milk"], customer: "👨" },
|
| 79 |
+
{ id: 3, items: ["milk", "butter", "cheese"], customer: "👵" },
|
| 80 |
+
{ id: 4, items: ["bread", "milk", "butter", "eggs"], customer: "👦" },
|
| 81 |
+
{ id: 5, items: ["bread", "butter"], customer: "👧" },
|
| 82 |
+
{ id: 6, items: ["milk", "eggs", "cheese"], customer: "👴" },
|
| 83 |
+
{ id: 7, items: ["bread", "milk", "butter", "cheese"], customer: "👩🦰" },
|
| 84 |
+
{ id: 8, items: ["bread", "milk"], customer: "👨🦱" },
|
| 85 |
+
];
|
| 86 |
+
|
| 87 |
+
// --- UI Components ---
|
| 88 |
+
|
| 89 |
+
const Icon = ({ name, size = 20, className = "" }) => {
|
| 90 |
+
const icons = {
|
| 91 |
+
"lightbulb": <><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .5 2.2 1.5 3.1.7.9 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></>,
|
| 92 |
+
"search": <><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>,
|
| 93 |
+
"rotate-ccw": <><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></>,
|
| 94 |
+
"chevron-left": <path d="m15 18-6-6 6-6"/>,
|
| 95 |
+
"chevron-right": <path d="m9 18 6-6 6-6"/>,
|
| 96 |
+
"shopping-cart": <><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></>,
|
| 97 |
+
"sparkles": <><path d="m12 3 1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></>,
|
| 98 |
+
"arrow-right": <><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></>,
|
| 99 |
+
"trending-up": <><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></>
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
<svg
|
| 104 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 105 |
+
width={size}
|
| 106 |
+
height={size}
|
| 107 |
+
viewBox="0 0 24 24"
|
| 108 |
+
fill="none"
|
| 109 |
+
stroke="currentColor"
|
| 110 |
+
strokeWidth="2"
|
| 111 |
+
strokeLinecap="round"
|
| 112 |
+
strokeLinejoin="round"
|
| 113 |
+
className={className}
|
| 114 |
+
>
|
| 115 |
+
{icons[name] || null}
|
| 116 |
+
</svg>
|
| 117 |
+
);
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
const Button = ({ children, onClick, disabled, variant = "default", size = "md", className = "" }) => {
|
| 121 |
+
const variants = {
|
| 122 |
+
default: "bg-slate-900 text-white hover:bg-slate-800",
|
| 123 |
+
outline: "border border-slate-200 bg-white hover:bg-slate-50 text-slate-700",
|
| 124 |
+
hero: "gradient-primary text-white shadow-lg hover:opacity-90",
|
| 125 |
+
icon: "p-2 border border-slate-200 hover:bg-slate-50"
|
| 126 |
+
};
|
| 127 |
+
const sizes = {
|
| 128 |
+
md: "px-4 py-2",
|
| 129 |
+
lg: "px-8 py-3 text-lg font-bold"
|
| 130 |
+
};
|
| 131 |
+
return (
|
| 132 |
+
<button
|
| 133 |
+
onClick={onClick}
|
| 134 |
+
disabled={disabled}
|
| 135 |
+
className={`inline-flex items-center justify-center rounded-xl transition-all active:scale-95 disabled:opacity-50 disabled:active:scale-100 ${variants[variant]} ${sizes[size]} ${className}`}
|
| 136 |
+
>
|
| 137 |
+
{children}
|
| 138 |
+
</button>
|
| 139 |
+
);
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
// --- Main Simulator ---
|
| 143 |
+
const steps = [
|
| 144 |
+
{ id: 0, title: "Meet Sarah, the Store Owner 👩💼", subtitle: "She wants to understand her customers better" },
|
| 145 |
+
{ id: 1, title: "Sarah looks at shopping receipts 🧾", subtitle: "8 customers visited her store today" },
|
| 146 |
+
{ id: 2, title: "Let's count each item! 🔢", subtitle: "How many times did each item appear?" },
|
| 147 |
+
{ id: 3, title: "Find the popular items! ⭐", subtitle: "Items bought by at least 3 customers are 'frequent'" },
|
| 148 |
+
{ id: 4, title: "Which items are bought TOGETHER? 🤝", subtitle: "Let's check pairs of frequent items" },
|
| 149 |
+
{ id: 5, title: "Sarah discovers patterns! 💡", subtitle: "These are called 'Association Rules'" },
|
| 150 |
+
{ id: 6, title: "Sarah can use these insights! 🎯", subtitle: "Now she knows how to arrange her store" },
|
| 151 |
+
];
|
| 152 |
+
|
| 153 |
+
const AprioriSimulator = () => {
|
| 154 |
+
const [currentStep, setCurrentStep] = useState(0);
|
| 155 |
+
|
| 156 |
+
const nextStep = () => currentStep < steps.length - 1 && setCurrentStep(currentStep + 1);
|
| 157 |
+
const prevStep = () => currentStep > 0 && setCurrentStep(currentStep - 1);
|
| 158 |
+
const reset = () => setCurrentStep(0);
|
| 159 |
+
|
| 160 |
+
// Audio Handler
|
| 161 |
+
const playSound = (e) => {
|
| 162 |
+
e.preventDefault(); // Prevent default anchor behavior to allow sound to play
|
| 163 |
+
const audio = document.getElementById('clickSound');
|
| 164 |
+
if (audio) {
|
| 165 |
+
audio.currentTime = 0;
|
| 166 |
+
audio.play().catch(err => console.log("Audio play prevented:", err));
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// Navigate after a short delay for the sound
|
| 170 |
+
setTimeout(() => {
|
| 171 |
+
window.location.href = "/xgboost-regression";
|
| 172 |
+
}, 150);
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
// Calculations
|
| 176 |
+
const itemCounts = useMemo(() => {
|
| 177 |
+
const counts = {};
|
| 178 |
+
transactions.forEach(t => t.items.forEach(item => counts[item] = (counts[item] || 0) + 1));
|
| 179 |
+
return counts;
|
| 180 |
+
}, []);
|
| 181 |
+
|
| 182 |
+
const frequentItems = Object.entries(itemCounts).filter(([_, c]) => c >= 3).map(([i]) => i);
|
| 183 |
+
|
| 184 |
+
const pairCounts = useMemo(() => {
|
| 185 |
+
const counts = {};
|
| 186 |
+
transactions.forEach(t => {
|
| 187 |
+
for (let i = 0; i < t.items.length; i++) {
|
| 188 |
+
for (let j = i + 1; j < t.items.length; j++) {
|
| 189 |
+
const pair = [t.items[i], t.items[j]].sort().join("+");
|
| 190 |
+
if (frequentItems.includes(t.items[i]) && frequentItems.includes(t.items[j])) {
|
| 191 |
+
counts[pair] = (counts[pair] || 0) + 1;
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
return counts;
|
| 197 |
+
}, [frequentItems]);
|
| 198 |
+
|
| 199 |
+
const frequentPairs = Object.entries(pairCounts).filter(([_, c]) => c >= 3).sort((a,b) => b[1]-a[1]);
|
| 200 |
+
|
| 201 |
+
// Define progress bar style outside of JSX to avoid double-curly-brace pattern
|
| 202 |
+
const progressStyle = { width: `${((currentStep + 1) / steps.length) * 100}%` };
|
| 203 |
+
|
| 204 |
+
const renderStepContent = () => {
|
| 205 |
+
switch (currentStep) {
|
| 206 |
+
case 0: return (
|
| 207 |
+
<div className="text-center animate-scale-in">
|
| 208 |
+
<div className="text-8xl mb-6">👩💼</div>
|
| 209 |
+
<div className="bg-white rounded-3xl p-8 shadow-card max-w-md mx-auto border border-blue-50">
|
| 210 |
+
<p className="text-xl leading-relaxed text-slate-700">
|
| 211 |
+
"Hi! I'm <strong>Sarah</strong>. I own a small grocery store.
|
| 212 |
+
I noticed some customers buy certain items together..."
|
| 213 |
+
</p>
|
| 214 |
+
<p className="text-xl leading-relaxed mt-4 text-slate-700">
|
| 215 |
+
"I want to find these <strong>patterns</strong> so I can place
|
| 216 |
+
related items close together!"
|
| 217 |
+
</p>
|
| 218 |
+
</div>
|
| 219 |
+
<div className="mt-8 flex justify-center gap-6 text-5xl">
|
| 220 |
+
🍞 🥛 🧈 🥚 🧀
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
);
|
| 224 |
+
case 1: return (
|
| 225 |
+
<div className="animate-slide-up">
|
| 226 |
+
<div className="grid gap-3 max-w-2xl mx-auto">
|
| 227 |
+
{transactions.map((t, idx) => {
|
| 228 |
+
// Removed double-curly-brace style pattern for Flask compatibility
|
| 229 |
+
const animStyle = { animationDelay: `${idx * 100}ms` };
|
| 230 |
+
return (
|
| 231 |
+
<div key={t.id} className="flex items-center gap-4 bg-white rounded-2xl p-4 shadow-soft border border-slate-100 animate-slide-up" style={animStyle}>
|
| 232 |
+
<div className="text-3xl">{t.customer}</div>
|
| 233 |
+
<div className="text-2xl opacity-50">🛒</div>
|
| 234 |
+
<div className="flex flex-wrap gap-2">
|
| 235 |
+
{t.items.map(item => (
|
| 236 |
+
<span key={item} className="bg-slate-100 text-slate-700 px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1 border border-slate-200">
|
| 237 |
+
<span className="text-lg">{getItemEmoji(item)}</span>
|
| 238 |
+
{getItemDisplayName(item)}
|
| 239 |
+
</span>
|
| 240 |
+
))}
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
);
|
| 244 |
+
})}
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
);
|
| 248 |
+
case 2: return (
|
| 249 |
+
<div className="animate-scale-in">
|
| 250 |
+
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 max-w-3xl mx-auto">
|
| 251 |
+
{Object.entries(itemCounts).sort((a,b) => b[1]-a[1]).map(([item, count], idx) => (
|
| 252 |
+
<div key={item} className="bg-white rounded-2xl p-6 shadow-soft text-center border border-slate-100">
|
| 253 |
+
<div className="text-5xl mb-2">{getItemEmoji(item)}</div>
|
| 254 |
+
<div className="font-bold text-lg text-slate-800">{getItemDisplayName(item)}</div>
|
| 255 |
+
<div className="mt-2 text-4xl font-extrabold text-blue-600">{count}</div>
|
| 256 |
+
<div className="text-sm text-slate-400 font-medium uppercase tracking-wider">Times Bought</div>
|
| 257 |
+
</div>
|
| 258 |
+
))}
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
);
|
| 262 |
+
case 3: return (
|
| 263 |
+
<div className="animate-scale-in">
|
| 264 |
+
<div className="text-center mb-8">
|
| 265 |
+
<div className="inline-block bg-blue-50 text-blue-700 px-6 py-3 rounded-2xl border border-blue-100">
|
| 266 |
+
<p className="text-lg font-medium">
|
| 267 |
+
<strong>Rule:</strong> Bought by <strong>at least 3</strong> customers
|
| 268 |
+
</p>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
<div className="grid sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
| 272 |
+
{Object.entries(itemCounts).sort((a,b) => b[1]-a[1]).map(([item, count]) => (
|
| 273 |
+
<div key={item} className={`rounded-2xl p-5 flex items-center justify-between border-2 transition-all ${count >= 3 ? "bg-green-50 border-green-200 shadow-sm" : "bg-slate-50 border-slate-100 opacity-50"}`}>
|
| 274 |
+
<div className="flex items-center gap-4">
|
| 275 |
+
<span className="text-4xl">{getItemEmoji(item)}</span>
|
| 276 |
+
<div>
|
| 277 |
+
<div className="font-bold text-slate-800">{getItemDisplayName(item)}</div>
|
| 278 |
+
<div className="text-sm text-slate-500 font-medium">{count} / 8 customers</div>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
<div className="text-2xl">{count >= 3 ? "✅" : "❌"}</div>
|
| 282 |
+
</div>
|
| 283 |
+
))}
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
);
|
| 287 |
+
case 4: return (
|
| 288 |
+
<div className="animate-scale-in">
|
| 289 |
+
<div className="grid sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
| 290 |
+
{frequentPairs.map(([pair, count], idx) => {
|
| 291 |
+
const [i1, i2] = pair.split("+");
|
| 292 |
+
// Removed double-curly-brace style pattern for Flask compatibility
|
| 293 |
+
const animStyle = { animationDelay: `${idx * 150}ms` };
|
| 294 |
+
return (
|
| 295 |
+
<div key={pair} className="bg-white rounded-2xl p-6 shadow-card border border-blue-50 text-center animate-slide-up" style={animStyle}>
|
| 296 |
+
<div className="flex items-center justify-center gap-4 mb-3">
|
| 297 |
+
<span className="text-5xl">{getItemEmoji(i1)}</span>
|
| 298 |
+
<span className="text-3xl font-bold text-blue-400">+</span>
|
| 299 |
+
<span className="text-5xl">{getItemEmoji(i2)}</span>
|
| 300 |
+
</div>
|
| 301 |
+
<div className="font-bold text-slate-800 text-lg">{getItemDisplayName(i1)} & {getItemDisplayName(i2)}</div>
|
| 302 |
+
<div className="mt-3 text-blue-600 font-extrabold text-2xl">Together {count}x</div>
|
| 303 |
+
</div>
|
| 304 |
+
);
|
| 305 |
+
})}
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
);
|
| 309 |
+
case 5: return (
|
| 310 |
+
<div className="animate-scale-in max-w-2xl mx-auto space-y-4">
|
| 311 |
+
{[
|
| 312 |
+
{ from: "bread", to: "milk", p: 86, tip: "Most bread buyers also get milk!" },
|
| 313 |
+
{ from: "bread", to: "butter", p: 71, tip: "Bread and butter are best friends!" },
|
| 314 |
+
{ from: "milk", to: "butter", p: 57, tip: "Dairy items stick together!" }
|
| 315 |
+
].map((rule, idx) => (
|
| 316 |
+
<div key={idx} className="bg-white rounded-2xl p-6 shadow-card border border-blue-50 border-l-4 border-l-blue-600">
|
| 317 |
+
<div className="flex items-center justify-between flex-wrap gap-4">
|
| 318 |
+
<div className="flex items-center gap-3">
|
| 319 |
+
<span className="text-sm font-bold text-slate-400 uppercase">If buys</span>
|
| 320 |
+
<span className="bg-blue-600 text-white px-4 py-1 rounded-full font-bold flex items-center gap-2">
|
| 321 |
+
{getItemEmoji(rule.from)} {getItemDisplayName(rule.from)}
|
| 322 |
+
</span>
|
| 323 |
+
</div>
|
| 324 |
+
<div className="text-2xl text-slate-300">➜</div>
|
| 325 |
+
<div className="flex items-center gap-3">
|
| 326 |
+
<span className="text-sm font-bold text-slate-400 uppercase">Then likely buys</span>
|
| 327 |
+
<span className="bg-green-500 text-white px-4 py-1 rounded-full font-bold flex items-center gap-2">
|
| 328 |
+
{getItemEmoji(rule.to)} {getItemDisplayName(rule.to)}
|
| 329 |
+
</span>
|
| 330 |
+
</div>
|
| 331 |
+
<div className="ml-auto text-3xl font-black text-blue-600">{rule.p}%</div>
|
| 332 |
+
</div>
|
| 333 |
+
<div className="mt-4 flex items-center gap-2 text-slate-500 italic text-sm">
|
| 334 |
+
<Icon name="lightbulb" className="w-4 h-4 text-amber-500" />
|
| 335 |
+
{rule.tip}
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
))}
|
| 339 |
+
</div>
|
| 340 |
+
);
|
| 341 |
+
case 6: return (
|
| 342 |
+
<div className="text-center animate-scale-in max-w-2xl mx-auto">
|
| 343 |
+
<div className="text-8xl mb-6">🎉</div>
|
| 344 |
+
<h2 className="text-3xl font-black text-slate-800 mb-8">Sarah has a plan!</h2>
|
| 345 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
|
| 346 |
+
{[
|
| 347 |
+
{ e: "🛒", t: "Store Layout", d: "Put bread near milk and butter" },
|
| 348 |
+
{ e: "🏷️", t: "Bundle Deals", d: "Offer 'Bread + Butter' discount packs" },
|
| 349 |
+
{ e: "📱", t: "Smart Suggestions", d: "If someone has milk, suggest butter!" },
|
| 350 |
+
{ e: "📦", t: "Inventory", d: "Stock more milk when bread is on sale" },
|
| 351 |
+
].map((tip, idx) => (
|
| 352 |
+
<div key={idx} className="bg-white rounded-2xl p-5 shadow-soft border border-slate-100">
|
| 353 |
+
<div className="text-4xl mb-2">{tip.e}</div>
|
| 354 |
+
<div className="font-bold text-lg text-slate-800">{tip.t}</div>
|
| 355 |
+
<div className="text-slate-500">{tip.d}</div>
|
| 356 |
+
</div>
|
| 357 |
+
))}
|
| 358 |
+
</div>
|
| 359 |
+
<div className="mt-10 bg-blue-600 text-white rounded-3xl p-8 shadow-xl">
|
| 360 |
+
<h3 className="text-2xl font-bold mb-3">🧠 You mastered Apriori!</h3>
|
| 361 |
+
<p className="text-blue-100 text-lg leading-relaxed">
|
| 362 |
+
You just learned how to find <strong>frequent itemsets</strong> and turn them into
|
| 363 |
+
<strong> association rules</strong> to drive real business decisions!
|
| 364 |
+
</p>
|
| 365 |
+
</div>
|
| 366 |
+
</div>
|
| 367 |
+
);
|
| 368 |
+
default: return null;
|
| 369 |
+
}
|
| 370 |
+
};
|
| 371 |
+
|
| 372 |
+
return (
|
| 373 |
+
<div className="min-h-screen gradient-hero py-12 px-4">
|
| 374 |
+
<div className="max-w-4xl mx-auto">
|
| 375 |
+
{/* Header */}
|
| 376 |
+
<div className="text-center mb-10">
|
| 377 |
+
<h1 className="text-4xl md:text-5xl font-black mb-2 tracking-tight">
|
| 378 |
+
<span className="text-gradient">Apriori Algorithm</span>
|
| 379 |
+
</h1>
|
| 380 |
+
<p className="text-slate-500 text-lg font-medium">Learn how stores find shopping patterns! 🛍️</p>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
{/* Progress Bar */}
|
| 384 |
+
<div className="mb-8">
|
| 385 |
+
<div className="flex justify-between items-center mb-3">
|
| 386 |
+
<span className="text-sm font-bold text-slate-400 uppercase tracking-widest">Step {currentStep + 1} of {steps.length}</span>
|
| 387 |
+
<span className="text-sm font-bold text-blue-600">{steps[currentStep].title}</span>
|
| 388 |
+
</div>
|
| 389 |
+
<div className="h-3 bg-slate-100 rounded-full overflow-hidden shadow-inner border border-slate-50">
|
| 390 |
+
{/* Using pre-defined variable for style to avoid double-curly-brace pattern */}
|
| 391 |
+
<div
|
| 392 |
+
className="h-full gradient-primary transition-all duration-700 ease-in-out"
|
| 393 |
+
style={progressStyle}
|
| 394 |
+
/>
|
| 395 |
+
</div>
|
| 396 |
+
</div>
|
| 397 |
+
|
| 398 |
+
{/* Card Content */}
|
| 399 |
+
<div className="min-h-[500px] mb-10">
|
| 400 |
+
<div className="bg-white rounded-[2rem] p-8 shadow-card border border-blue-50">
|
| 401 |
+
<div className="text-center mb-8">
|
| 402 |
+
<h2 className="text-2xl font-black text-slate-800 mb-1">{steps[currentStep].title}</h2>
|
| 403 |
+
<p className="text-slate-400 font-medium">{steps[currentStep].subtitle}</p>
|
| 404 |
+
</div>
|
| 405 |
+
{renderStepContent()}
|
| 406 |
+
</div>
|
| 407 |
+
</div>
|
| 408 |
+
|
| 409 |
+
{/* Navigation */}
|
| 410 |
+
<div className="flex items-center justify-center gap-4">
|
| 411 |
+
<Button variant="outline" size="lg" onClick={reset} disabled={currentStep === 0}>
|
| 412 |
+
<Icon name="rotate-ccw" className="w-5 h-5 mr-2" />
|
| 413 |
+
Reset
|
| 414 |
+
</Button>
|
| 415 |
+
<Button variant="outline" size="lg" onClick={prevStep} disabled={currentStep === 0}>
|
| 416 |
+
<Icon name="chevron-left" className="w-5 h-5 mr-1" />
|
| 417 |
+
Back
|
| 418 |
+
</Button>
|
| 419 |
+
<Button variant="hero" size="lg" onClick={nextStep} className="px-12">
|
| 420 |
+
{currentStep === steps.length - 1 ? "Start Over" : "Next Step"}
|
| 421 |
+
{currentStep < steps.length - 1 && <Icon name="chevron-right" className="ml-2 w-6 h-6" />}
|
| 422 |
+
</Button>
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
{/* Centered Button (FIXED) */}
|
| 426 |
+
<div className="mt-12 flex justify-center pb-8">
|
| 427 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 428 |
+
<a
|
| 429 |
+
href="/xgboost-regression"
|
| 430 |
+
onClick={playSound}
|
| 431 |
+
className="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider cursor-pointer"
|
| 432 |
+
>
|
| 433 |
+
Back to Core
|
| 434 |
+
</a>
|
| 435 |
+
</div>
|
| 436 |
+
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
);
|
| 440 |
+
};
|
| 441 |
+
|
| 442 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 443 |
+
root.render(<AprioriSimulator />);
|
| 444 |
+
</script>
|
| 445 |
+
</body>
|
| 446 |
+
</html>
|
templates/Eclat-Algorithm-three.html
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Eclat Algorithm Simulator</title>
|
| 7 |
+
|
| 8 |
+
<!-- Tailwind CSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<script>
|
| 11 |
+
tailwind.config = {
|
| 12 |
+
darkMode: 'class',
|
| 13 |
+
theme: {
|
| 14 |
+
extend: {
|
| 15 |
+
colors: {
|
| 16 |
+
border: "hsl(var(--border))",
|
| 17 |
+
input: "hsl(var(--input))",
|
| 18 |
+
ring: "hsl(var(--ring))",
|
| 19 |
+
background: "hsl(var(--background))",
|
| 20 |
+
foreground: "hsl(var(--foreground))",
|
| 21 |
+
primary: {
|
| 22 |
+
DEFAULT: "hsl(var(--primary))",
|
| 23 |
+
foreground: "hsl(var(--primary-foreground))",
|
| 24 |
+
},
|
| 25 |
+
secondary: {
|
| 26 |
+
DEFAULT: "hsl(var(--secondary))",
|
| 27 |
+
foreground: "hsl(var(--secondary-foreground))",
|
| 28 |
+
},
|
| 29 |
+
destructive: {
|
| 30 |
+
DEFAULT: "hsl(var(--destructive))",
|
| 31 |
+
foreground: "hsl(var(--destructive-foreground))",
|
| 32 |
+
},
|
| 33 |
+
muted: {
|
| 34 |
+
DEFAULT: "hsl(var(--muted))",
|
| 35 |
+
foreground: "hsl(var(--muted-foreground))",
|
| 36 |
+
},
|
| 37 |
+
accent: {
|
| 38 |
+
DEFAULT: "hsl(var(--accent))",
|
| 39 |
+
foreground: "hsl(var(--accent-foreground))",
|
| 40 |
+
},
|
| 41 |
+
popover: {
|
| 42 |
+
DEFAULT: "hsl(var(--popover))",
|
| 43 |
+
foreground: "hsl(var(--popover-foreground))",
|
| 44 |
+
},
|
| 45 |
+
card: {
|
| 46 |
+
DEFAULT: "hsl(var(--card))",
|
| 47 |
+
foreground: "hsl(var(--card-foreground))",
|
| 48 |
+
},
|
| 49 |
+
success: {
|
| 50 |
+
DEFAULT: "#10b981",
|
| 51 |
+
foreground: "#ffffff"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
borderRadius: {
|
| 55 |
+
lg: "var(--radius)",
|
| 56 |
+
md: "calc(var(--radius) - 2px)",
|
| 57 |
+
sm: "calc(var(--radius) - 4px)",
|
| 58 |
+
},
|
| 59 |
+
animation: {
|
| 60 |
+
'pulse-glow': 'pulseGlow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 61 |
+
},
|
| 62 |
+
keyframes: {
|
| 63 |
+
pulseGlow: {
|
| 64 |
+
'0%, 100%': { opacity: 1, boxShadow: '0 0 10px hsl(var(--primary) / 0.5)' },
|
| 65 |
+
'50%': { opacity: .8, boxShadow: '0 0 20px hsl(var(--primary) / 0.8)' },
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
</script>
|
| 72 |
+
|
| 73 |
+
<!-- React & Libraries -->
|
| 74 |
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 75 |
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| 76 |
+
<script src="https://unpkg.com/framer-motion@10.16.4/dist/framer-motion.js"></script>
|
| 77 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 78 |
+
|
| 79 |
+
<style>
|
| 80 |
+
:root {
|
| 81 |
+
--background: 224 71% 4%;
|
| 82 |
+
--foreground: 213 31% 91%;
|
| 83 |
+
|
| 84 |
+
--card: 224 71% 4%;
|
| 85 |
+
--card-foreground: 213 31% 91%;
|
| 86 |
+
|
| 87 |
+
--popover: 224 71% 4%;
|
| 88 |
+
--popover-foreground: 215 20.2% 65.1%;
|
| 89 |
+
|
| 90 |
+
--primary: 263.4 70% 50.4%;
|
| 91 |
+
--primary-foreground: 210 40% 98%;
|
| 92 |
+
|
| 93 |
+
--secondary: 222.2 47.4% 11.2%;
|
| 94 |
+
--secondary-foreground: 210 40% 98%;
|
| 95 |
+
|
| 96 |
+
--muted: 217.2 32.6% 17.5%;
|
| 97 |
+
--muted-foreground: 215 20.2% 65.1%;
|
| 98 |
+
|
| 99 |
+
--accent: 45 93% 47%;
|
| 100 |
+
--accent-foreground: 210 40% 98%;
|
| 101 |
+
|
| 102 |
+
--destructive: 0 62.8% 30.6%;
|
| 103 |
+
--destructive-foreground: 210 40% 98%;
|
| 104 |
+
|
| 105 |
+
--border: 217.2 32.6% 17.5%;
|
| 106 |
+
--input: 217.2 32.6% 17.5%;
|
| 107 |
+
--ring: 224.3 76.3% 48%;
|
| 108 |
+
|
| 109 |
+
--radius: 0.5rem;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
body {
|
| 113 |
+
background-color: hsl(var(--background));
|
| 114 |
+
color: hsl(var(--foreground));
|
| 115 |
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.circuit-pattern {
|
| 119 |
+
background-image: radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
| 120 |
+
background-size: 24px 24px;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.text-glow {
|
| 124 |
+
text-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.glow-primary {
|
| 128 |
+
box-shadow: 0 0 15px -3px hsl(var(--primary) / 0.6);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.glow-accent {
|
| 132 |
+
box-shadow: 0 0 15px -3px hsl(var(--accent) / 0.4);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Custom Scrollbar */
|
| 136 |
+
::-webkit-scrollbar {
|
| 137 |
+
width: 8px;
|
| 138 |
+
}
|
| 139 |
+
::-webkit-scrollbar-track {
|
| 140 |
+
background: hsl(var(--secondary));
|
| 141 |
+
}
|
| 142 |
+
::-webkit-scrollbar-thumb {
|
| 143 |
+
background: hsl(var(--muted));
|
| 144 |
+
border-radius: 4px;
|
| 145 |
+
}
|
| 146 |
+
::-webkit-scrollbar-thumb:hover {
|
| 147 |
+
background: hsl(var(--primary));
|
| 148 |
+
}
|
| 149 |
+
</style>
|
| 150 |
+
</head>
|
| 151 |
+
<body>
|
| 152 |
+
<div id="root"></div>
|
| 153 |
+
|
| 154 |
+
<!-- We use {% raw %} to prevent Jinja2 from parsing React's curly braces -->
|
| 155 |
+
{% raw %}
|
| 156 |
+
<script type="text/babel">
|
| 157 |
+
const { useState, useEffect, useRef } = React;
|
| 158 |
+
const { motion, AnimatePresence } = window.Motion;
|
| 159 |
+
|
| 160 |
+
// --- UTILS ---
|
| 161 |
+
function cn(...classes) {
|
| 162 |
+
return classes.filter(Boolean).join(' ');
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// --- ICONS (Lucide replacement) ---
|
| 166 |
+
const Icon = ({ d, className }) => (
|
| 167 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
| 168 |
+
<path d={d} />
|
| 169 |
+
</svg>
|
| 170 |
+
);
|
| 171 |
+
|
| 172 |
+
const Icons = {
|
| 173 |
+
Play: (props) => <Icon d="M5 3l14 9-14 9V3z" {...props} />,
|
| 174 |
+
Pause: (props) => <Icon d="M6 4h4v16H6zm8 0h4v16h-4z" {...props} />,
|
| 175 |
+
RotateCcw: (props) => <Icon d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5" {...props} />,
|
| 176 |
+
ChevronRight: (props) => <Icon d="M9 18l6-6-6-6" {...props} />,
|
| 177 |
+
Database: (props) => <Icon d="M3 5c0-1.1 4.03-2 9-2s9 .9 9 2c0 1.1-4.03 2-9 2s-9-.9-9-2z M3 5v14c0 1.1 4.03 2 9 2s9-.9 9-2V5 M3 12c0 1.1 4.03 2 9 2s9-.9 9-2" {...props} />,
|
| 178 |
+
Info: (props) => <Icon d="M12 16v-4 M12 8h.01 M22 12A10 10 0 1 1 12 2a10 10 0 0 1 10 10z" {...props} />,
|
| 179 |
+
RotateCw: (props) => <Icon d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8 M21 3v5h-5" {...props} />,
|
| 180 |
+
Lightbulb: (props) => <Icon d="M9 18h6 M10 22h4 M15.09 14c.18-.9.93-1.54 1.86-1.54.96 0 1.63.74 1.93 1.54M9 14c.18-.9.93-1.54 1.86-1.54.96 0 1.63.74 1.93 1.54" {...props} />,
|
| 181 |
+
ChevronDown: (props) => <Icon d="M6 9l6 6 6-6" {...props} />,
|
| 182 |
+
ChevronUp: (props) => <Icon d="M18 15l-6-6-6 6" {...props} />,
|
| 183 |
+
ShoppingCart: (props) => <Icon d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" {...props} />,
|
| 184 |
+
Sparkles: (props) => <Icon d="M12 3l1.912 5.813a2 2 0 0 1 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 1-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 1-1.275-1.275L3 12l5.813-1.912a2 2 0 0 1 1.275-1.275z" {...props} />,
|
| 185 |
+
ArrowRight: (props) => <Icon d="M5 12h14M12 5l7 7-7 7" {...props} />,
|
| 186 |
+
BookOpen: (props) => <Icon d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" {...props} />,
|
| 187 |
+
Zap: (props) => <Icon d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" {...props} />,
|
| 188 |
+
CheckCircle: (props) => <Icon d="M22 11.08V12a10 10 0 1 1-5.93-9.14 M22 4L12 14.01l-3-3" {...props} />,
|
| 189 |
+
XCircle: (props) => <Icon d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" {...props} />,
|
| 190 |
+
Trophy: (props) => <Icon d="M8 21h8M12 17v4M7 4h10c.66 0 1.33.2 2 .59V9a8 8 0 0 1-8 8 8 8 0 0 1-8-8V4.59c.67-.39 1.34-.59 2-.59zM19 10a5 5 0 0 0 0 10M5 10a5 5 0 0 1 0 10" {...props} />,
|
| 191 |
+
TrendingUp: (props) => <Icon d="M23 6l-9.5 9.5-5-5L1 18" {...props} />,
|
| 192 |
+
HelpCircle: (props) => <Icon d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3 M12 17h.01 M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z" {...props} />,
|
| 193 |
+
X: (props) => <Icon d="M18 6L6 18M6 6l12 12" {...props} />,
|
| 194 |
+
Search: (props) => <Icon d="M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z" {...props} />,
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
// --- BASIC UI COMPONENTS ---
|
| 198 |
+
const Button = ({ children, variant = 'default', size = 'default', className, onClick, disabled }) => {
|
| 199 |
+
const variants = {
|
| 200 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
| 201 |
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 202 |
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
| 203 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 204 |
+
};
|
| 205 |
+
const sizes = {
|
| 206 |
+
default: "h-10 px-4 py-2",
|
| 207 |
+
sm: "h-9 rounded-md px-3",
|
| 208 |
+
icon: "h-10 w-10",
|
| 209 |
+
lg: "h-11 rounded-md px-8",
|
| 210 |
+
};
|
| 211 |
+
return (
|
| 212 |
+
<button
|
| 213 |
+
onClick={onClick}
|
| 214 |
+
disabled={disabled}
|
| 215 |
+
className={cn(
|
| 216 |
+
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
| 217 |
+
variants[variant],
|
| 218 |
+
sizes[size],
|
| 219 |
+
className
|
| 220 |
+
)}
|
| 221 |
+
>
|
| 222 |
+
{children}
|
| 223 |
+
</button>
|
| 224 |
+
);
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
const Slider = ({ value, onValueChange, min, max, step, className }) => {
|
| 228 |
+
return (
|
| 229 |
+
<input
|
| 230 |
+
type="range"
|
| 231 |
+
min={min}
|
| 232 |
+
max={max}
|
| 233 |
+
step={step}
|
| 234 |
+
value={value[0]}
|
| 235 |
+
onChange={(e) => onValueChange([parseFloat(e.target.value)])}
|
| 236 |
+
className={cn("w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary", className)}
|
| 237 |
+
/>
|
| 238 |
+
);
|
| 239 |
+
};
|
| 240 |
+
|
| 241 |
+
const Input = ({ className, ...props }) => (
|
| 242 |
+
<input
|
| 243 |
+
className={cn("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className)}
|
| 244 |
+
{...props}
|
| 245 |
+
/>
|
| 246 |
+
);
|
| 247 |
+
|
| 248 |
+
// --- SUB COMPONENTS ---
|
| 249 |
+
|
| 250 |
+
const BeginnerTip = ({ title, children, defaultOpen = false }) => {
|
| 251 |
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
| 252 |
+
return (
|
| 253 |
+
<motion.div layout className="rounded-lg border border-accent/30 bg-accent/5 overflow-hidden">
|
| 254 |
+
<button onClick={() => setIsOpen(!isOpen)} className="w-full p-3 flex items-center gap-2 text-left hover:bg-accent/10 transition-colors">
|
| 255 |
+
<Icons.Lightbulb className="w-4 h-4 text-accent shrink-0" />
|
| 256 |
+
<span className="text-sm font-medium text-foreground flex-1">{title}</span>
|
| 257 |
+
{isOpen ? <Icons.ChevronUp className="w-4 h-4 text-muted-foreground" /> : <Icons.ChevronDown className="w-4 h-4 text-muted-foreground" />}
|
| 258 |
+
</button>
|
| 259 |
+
{isOpen && (
|
| 260 |
+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="px-3 pb-3 text-sm text-foreground/80">
|
| 261 |
+
{children}
|
| 262 |
+
</motion.div>
|
| 263 |
+
)}
|
| 264 |
+
</motion.div>
|
| 265 |
+
);
|
| 266 |
+
};
|
| 267 |
+
|
| 268 |
+
const TransactionPanel = ({ transactions, highlightedTids, step }) => {
|
| 269 |
+
const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
|
| 270 |
+
return (
|
| 271 |
+
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
|
| 272 |
+
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
| 273 |
+
<div className="w-8 h-8 rounded-lg bg-secondary/20 flex items-center justify-center">
|
| 274 |
+
<Icons.ShoppingCart className="w-4 h-4 text-secondary" />
|
| 275 |
+
</div>
|
| 276 |
+
<div>
|
| 277 |
+
<h2 className="font-semibold text-foreground">Transaction Database</h2>
|
| 278 |
+
<p className="text-xs text-muted-foreground">Horizontal Format (Traditional)</p>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
<div className="p-4 space-y-3">
|
| 282 |
+
{transactions.map((transaction, index) => (
|
| 283 |
+
<motion.div
|
| 284 |
+
key={transaction.id}
|
| 285 |
+
initial={{ opacity: 0, x: -20 }}
|
| 286 |
+
animate={{ opacity: step >= 0 ? 1 : 0.3, x: 0, scale: highlightedTids.includes(transaction.id) ? 1.02 : 1 }}
|
| 287 |
+
transition={{ delay: index * 0.1 }}
|
| 288 |
+
className={`p-3 rounded-lg border transition-all duration-300 ${highlightedTids.includes(transaction.id) ? 'bg-primary/10 border-primary/50 glow-primary' : 'bg-muted/30 border-border/30 hover:border-border/60'}`}
|
| 289 |
+
>
|
| 290 |
+
<div className="flex items-center gap-3">
|
| 291 |
+
<span className={`font-mono text-sm font-bold w-8 h-8 rounded-full flex items-center justify-center ${highlightedTids.includes(transaction.id) ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
|
| 292 |
+
T{transaction.id}
|
| 293 |
+
</span>
|
| 294 |
+
<div className="flex flex-wrap gap-2">
|
| 295 |
+
{transaction.items.map((item, i) => (
|
| 296 |
+
<motion.span key={i} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: index * 0.1 + i * 0.05 }} className="px-2 py-1 rounded-md bg-background/50 border border-border/30 text-sm flex items-center gap-1">
|
| 297 |
+
<span>{itemEmojis[item]}</span><span className="text-foreground/80">{item}</span>
|
| 298 |
+
</motion.span>
|
| 299 |
+
))}
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
</motion.div>
|
| 303 |
+
))}
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
);
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
const TidSetPanel = ({ tidSets, step, minSupportCount, onItemHover, highlightedItem }) => {
|
| 310 |
+
const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
|
| 311 |
+
const itemColors = {
|
| 312 |
+
'Bread': 'from-amber-500/20 to-orange-500/20 border-amber-500/50',
|
| 313 |
+
'Milk': 'from-blue-500/20 to-cyan-500/20 border-blue-500/50',
|
| 314 |
+
'Eggs': 'from-yellow-500/20 to-amber-500/20 border-yellow-500/50',
|
| 315 |
+
'Butter': 'from-yellow-600/20 to-orange-400/20 border-yellow-600/50',
|
| 316 |
+
};
|
| 317 |
+
|
| 318 |
+
if (step < 1) {
|
| 319 |
+
return (
|
| 320 |
+
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
|
| 321 |
+
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
| 322 |
+
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center"><Icons.Database className="w-4 h-4 text-primary" /></div>
|
| 323 |
+
<div><h2 className="font-semibold text-foreground">TID Sets</h2><p className="text-xs text-muted-foreground">Vertical Format (Eclat)</p></div>
|
| 324 |
+
</div>
|
| 325 |
+
<div className="p-8 flex items-center justify-center">
|
| 326 |
+
<div className="text-center">
|
| 327 |
+
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mx-auto mb-4"><Icons.Database className="w-8 h-8 text-muted-foreground/50" /></div>
|
| 328 |
+
<p className="text-muted-foreground text-sm">Press <span className="text-primary font-semibold">Play</span> or <span className="text-primary font-semibold">Next Step</span> to begin</p>
|
| 329 |
+
</div>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
);
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
return (
|
| 336 |
+
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
|
| 337 |
+
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
| 338 |
+
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center animate-pulse-glow"><Icons.Database className="w-4 h-4 text-primary" /></div>
|
| 339 |
+
<div><h2 className="font-semibold text-foreground">TID Sets</h2><p className="text-xs text-muted-foreground">Vertical Format (Item → Transaction IDs)</p></div>
|
| 340 |
+
</div>
|
| 341 |
+
<div className="p-4 space-y-3">
|
| 342 |
+
{tidSets.map((tidSet, index) => {
|
| 343 |
+
const isFrequent = tidSet.tids.length >= minSupportCount;
|
| 344 |
+
const isHighlighted = highlightedItem === tidSet.item;
|
| 345 |
+
return (
|
| 346 |
+
<motion.div
|
| 347 |
+
key={tidSet.item}
|
| 348 |
+
initial={{ opacity: 0, x: 20 }}
|
| 349 |
+
animate={{ opacity: 1, x: 0, scale: isHighlighted ? 1.02 : 1 }}
|
| 350 |
+
transition={{ delay: index * 0.15 }}
|
| 351 |
+
onMouseEnter={() => onItemHover(tidSet.item, tidSet.tids)}
|
| 352 |
+
onMouseLeave={() => onItemHover(null)}
|
| 353 |
+
className={`p-3 rounded-lg border bg-gradient-to-r transition-all duration-300 cursor-pointer ${itemColors[tidSet.item]} ${isHighlighted ? 'glow-primary' : ''}`}
|
| 354 |
+
>
|
| 355 |
+
<div className="flex items-center justify-between mb-2">
|
| 356 |
+
<div className="flex items-center gap-2">
|
| 357 |
+
<span className="text-lg">{itemEmojis[tidSet.item]}</span>
|
| 358 |
+
<span className="font-semibold text-foreground">{tidSet.item}</span>
|
| 359 |
+
</div>
|
| 360 |
+
{step >= 2 && (
|
| 361 |
+
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${isFrequent ? 'bg-success/20 text-success' : 'bg-destructive/20 text-destructive'}`}>
|
| 362 |
+
{isFrequent ? <><Icons.CheckCircle className="w-3 h-3" /> Frequent</> : <><Icons.XCircle className="w-3 h-3" /> Pruned</>}
|
| 363 |
+
</motion.div>
|
| 364 |
+
)}
|
| 365 |
+
</div>
|
| 366 |
+
<div className="flex items-center gap-2">
|
| 367 |
+
<span className="text-xs text-muted-foreground font-mono">TID:</span>
|
| 368 |
+
<div className="flex flex-wrap gap-1">
|
| 369 |
+
{tidSet.tids.map((tid, i) => (
|
| 370 |
+
<motion.span key={tid} initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ delay: index * 0.15 + i * 0.05 }} className="px-2 py-0.5 rounded bg-background/50 text-xs font-mono text-foreground/80 border border-border/30">
|
| 371 |
+
{tid}
|
| 372 |
+
</motion.span>
|
| 373 |
+
))}
|
| 374 |
+
</div>
|
| 375 |
+
<span className="ml-auto text-xs font-mono text-accent font-bold">|{tidSet.tids.length}|</span>
|
| 376 |
+
</div>
|
| 377 |
+
</motion.div>
|
| 378 |
+
);
|
| 379 |
+
})}
|
| 380 |
+
</div>
|
| 381 |
+
{step >= 1 && (
|
| 382 |
+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="p-4 border-t border-border/30 bg-primary/5">
|
| 383 |
+
<p className="text-xs text-muted-foreground"><span className="text-primary font-semibold">🔄 Transformed:</span> Now each item maps to the transactions containing it.</p>
|
| 384 |
+
</motion.div>
|
| 385 |
+
)}
|
| 386 |
+
</div>
|
| 387 |
+
);
|
| 388 |
+
};
|
| 389 |
+
|
| 390 |
+
const IntersectionVisualizer = ({ tidSets, minSupportCount, step }) => {
|
| 391 |
+
const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
|
| 392 |
+
const frequentItems = tidSets.filter(t => t.tids.length >= minSupportCount);
|
| 393 |
+
const intersections = [];
|
| 394 |
+
for (let i = 0; i < frequentItems.length; i++) {
|
| 395 |
+
for (let j = i + 1; j < frequentItems.length; j++) {
|
| 396 |
+
const result = frequentItems[i].tids.filter(tid => frequentItems[j].tids.includes(tid));
|
| 397 |
+
intersections.push({
|
| 398 |
+
item1: frequentItems[i].item,
|
| 399 |
+
item2: frequentItems[j].item,
|
| 400 |
+
tids1: frequentItems[i].tids,
|
| 401 |
+
tids2: frequentItems[j].tids,
|
| 402 |
+
result,
|
| 403 |
+
isFrequent: result.length >= minSupportCount
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
return (
|
| 409 |
+
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden">
|
| 410 |
+
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
| 411 |
+
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center"><Icons.Zap className="w-4 h-4 text-accent" /></div>
|
| 412 |
+
<div><h2 className="font-semibold text-foreground">Set Intersections</h2><p className="text-xs text-muted-foreground">Finding 2-item patterns through TID overlap</p></div>
|
| 413 |
+
</div>
|
| 414 |
+
<div className="p-4">
|
| 415 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 416 |
+
{intersections.map((intersection, index) => (
|
| 417 |
+
<motion.div
|
| 418 |
+
key={`${intersection.item1}-${intersection.item2}`}
|
| 419 |
+
initial={{ opacity: 0, scale: 0.9 }}
|
| 420 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 421 |
+
transition={{ delay: index * 0.1 }}
|
| 422 |
+
className={`p-4 rounded-lg border transition-all ${intersection.isFrequent ? 'bg-success/5 border-success/30' : 'bg-muted/20 border-border/30 opacity-60'}`}
|
| 423 |
+
>
|
| 424 |
+
<div className="flex items-center justify-center gap-2 mb-3">
|
| 425 |
+
<span className="px-2 py-1 rounded bg-background/50 text-sm font-medium flex items-center gap-1">{itemEmojis[intersection.item1]} {intersection.item1}</span>
|
| 426 |
+
<span className="text-primary font-mono">∩</span>
|
| 427 |
+
<span className="px-2 py-1 rounded bg-background/50 text-sm font-medium flex items-center gap-1">{itemEmojis[intersection.item2]} {intersection.item2}</span>
|
| 428 |
+
</div>
|
| 429 |
+
<div className="flex items-center justify-center gap-2 mb-3 text-xs">
|
| 430 |
+
<div className="flex gap-0.5">
|
| 431 |
+
{intersection.tids1.map(tid => (
|
| 432 |
+
<span key={tid} className={`w-5 h-5 rounded flex items-center justify-center font-mono ${intersection.result.includes(tid) ? 'bg-success/30 text-success' : 'bg-muted/50 text-muted-foreground'}`}>{tid}</span>
|
| 433 |
+
))}
|
| 434 |
+
</div>
|
| 435 |
+
<Icons.ArrowRight className="w-3 h-3 text-muted-foreground" />
|
| 436 |
+
<div className="flex gap-0.5">
|
| 437 |
+
{intersection.result.length > 0 ? intersection.result.map(tid => (
|
| 438 |
+
<span key={tid} className="w-5 h-5 rounded flex items-center justify-center font-mono bg-success/30 text-success font-bold">{tid}</span>
|
| 439 |
+
)) : <span className="text-muted-foreground">∅</span>}
|
| 440 |
+
</div>
|
| 441 |
+
</div>
|
| 442 |
+
<div className={`text-center text-xs font-medium px-2 py-1 rounded ${intersection.isFrequent ? 'bg-success/20 text-success' : 'bg-destructive/10 text-destructive/70'}`}>
|
| 443 |
+
Support: {intersection.result.length} {intersection.isFrequent ? ' ✓ Frequent' : ' ✗ Pruned'}
|
| 444 |
+
</div>
|
| 445 |
+
</motion.div>
|
| 446 |
+
))}
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
);
|
| 451 |
+
};
|
| 452 |
+
|
| 453 |
+
const FrequentItemsetsPanel = ({ itemsets, step, onItemsetHover }) => {
|
| 454 |
+
const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
|
| 455 |
+
const oneItemsets = itemsets.filter(i => i.items.length === 1);
|
| 456 |
+
const twoItemsets = itemsets.filter(i => i.items.length === 2);
|
| 457 |
+
const threeItemsets = itemsets.filter(i => i.items.length === 3);
|
| 458 |
+
|
| 459 |
+
const ItemsetCard = ({ items, support, tids, delay }) => (
|
| 460 |
+
<motion.div
|
| 461 |
+
initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: delay }}
|
| 462 |
+
onMouseEnter={() => onItemsetHover(items.join(','), tids)} onMouseLeave={() => onItemsetHover(null)}
|
| 463 |
+
className="p-2 rounded-lg bg-muted/30 border border-border/30 hover:border-primary/50 transition-all cursor-pointer"
|
| 464 |
+
>
|
| 465 |
+
<div className="flex items-center justify-between">
|
| 466 |
+
<span className="flex items-center gap-1">
|
| 467 |
+
{items.map(item => <span key={item} className="flex items-center gap-1">{itemEmojis[item]} {item}</span>)}
|
| 468 |
+
</span>
|
| 469 |
+
<span className="text-xs font-mono text-accent">{support.toFixed(0)}%</span>
|
| 470 |
+
</div>
|
| 471 |
+
</motion.div>
|
| 472 |
+
);
|
| 473 |
+
|
| 474 |
+
return (
|
| 475 |
+
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden">
|
| 476 |
+
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
| 477 |
+
<div className="w-8 h-8 rounded-lg bg-success/20 flex items-center justify-center"><Icons.Trophy className="w-4 h-4 text-success" /></div>
|
| 478 |
+
<div><h2 className="font-semibold text-foreground">Frequent Itemsets Discovered</h2><p className="text-xs text-muted-foreground">Patterns that meet minimum support</p></div>
|
| 479 |
+
</div>
|
| 480 |
+
<div className="p-4">
|
| 481 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 482 |
+
<div>
|
| 483 |
+
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs font-mono text-primary">1</span> Single Items</h3>
|
| 484 |
+
<div className="space-y-2">{oneItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />)}</div>
|
| 485 |
+
</div>
|
| 486 |
+
<div>
|
| 487 |
+
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-secondary/20 flex items-center justify-center text-xs font-mono text-secondary">2</span> Item Pairs {step < 3 && <span className="text-xs text-muted-foreground">(next)</span>}</h3>
|
| 488 |
+
<div className="space-y-2">
|
| 489 |
+
{step >= 3 ? twoItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />) : <div className="p-4 rounded-lg border border-dashed border-border/30 text-center"><Icons.TrendingUp className="w-6 h-6 text-muted-foreground/50 mx-auto mb-2" /><p className="text-xs text-muted-foreground">Continue to find pairs</p></div>}
|
| 490 |
+
</div>
|
| 491 |
+
</div>
|
| 492 |
+
<div>
|
| 493 |
+
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-accent/20 flex items-center justify-center text-xs font-mono text-accent">3</span> Triplets {step < 4 && <span className="text-xs text-muted-foreground">(final)</span>}</h3>
|
| 494 |
+
<div className="space-y-2">
|
| 495 |
+
{step >= 4 ? (threeItemsets.length > 0 ? threeItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />) : <div className="p-4 rounded-lg border border-border/30 text-center"><p className="text-xs text-muted-foreground">No 3-itemsets found</p></div>) : <div className="p-4 rounded-lg border border-dashed border-border/30 text-center"><Icons.Trophy className="w-6 h-6 text-muted-foreground/50 mx-auto mb-2" /><p className="text-xs text-muted-foreground">Discover triplets last</p></div>}
|
| 496 |
+
</div>
|
| 497 |
+
</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
</div>
|
| 501 |
+
);
|
| 502 |
+
};
|
| 503 |
+
|
| 504 |
+
const StepExplainer = ({ step, minSupport, minSupportCount }) => {
|
| 505 |
+
const stepContent = [
|
| 506 |
+
{ title: "Step 1: Look at Receipts 🧾", simple: "Transactions", description: "Each row is one customer's shopping basket. Goal: find patterns.", color: "primary" },
|
| 507 |
+
{ title: "Step 2: Flip the View 🔄", simple: "Vertical Format", description: "Convert 'Receipt → Items' to 'Item → Receipt Numbers'. This is key to Eclat!", color: "primary" },
|
| 508 |
+
{ title: "Step 3: Check Popularity ⭐", simple: "Filter by Support", description: `Remove rare items. Must appear in at least ${minSupportCount} receipts (${minSupport}%).`, color: "secondary" },
|
| 509 |
+
{ title: "Step 4: Find Pairs 🔗", simple: "Intersections", description: "Find overlapping receipt numbers. Overlap = Bought together.", color: "accent" },
|
| 510 |
+
{ title: "Step 5: Bigger Patterns 🏆", simple: "3+ Items", description: "Intersect pairs to find triplets. Keep going until no overlaps remain.", color: "success" }
|
| 511 |
+
];
|
| 512 |
+
const content = stepContent[step] || stepContent[0];
|
| 513 |
+
|
| 514 |
+
return (
|
| 515 |
+
<AnimatePresence mode="wait">
|
| 516 |
+
<motion.div
|
| 517 |
+
key={step}
|
| 518 |
+
initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }} transition={{ duration: 0.3 }}
|
| 519 |
+
className={`p-5 rounded-xl border backdrop-blur-sm bg-${content.color}/5 border-${content.color}/30`}
|
| 520 |
+
>
|
| 521 |
+
<div className="flex items-center gap-4">
|
| 522 |
+
<div className={`w-12 h-12 rounded-xl flex items-center justify-center text-xl font-bold bg-${content.color}/20 text-${content.color}`}>{step + 1}</div>
|
| 523 |
+
<div>
|
| 524 |
+
<h3 className="font-bold text-foreground text-lg">{content.title}</h3>
|
| 525 |
+
<p className="text-sm text-foreground/80">{content.description}</p>
|
| 526 |
+
</div>
|
| 527 |
+
</div>
|
| 528 |
+
</motion.div>
|
| 529 |
+
</AnimatePresence>
|
| 530 |
+
);
|
| 531 |
+
};
|
| 532 |
+
|
| 533 |
+
const IntroTutorial = ({ onStart }) => {
|
| 534 |
+
const [page, setPage] = useState(0);
|
| 535 |
+
const pages = [
|
| 536 |
+
{ title: "Welcome to Eclat! 👋", content: <div className="space-y-4"><p>Imagine owning a store. You want to know: "If someone buys Bread, do they also buy Milk?" Eclat finds these patterns automatically using math!</p></div> },
|
| 537 |
+
{ title: "How Eclat Works 🔍", content: <div className="space-y-4"><p>Most algorithms scan receipts one by one (slow). Eclat uses a trick: it lists <strong>Transaction IDs (TIDs)</strong> for each item. Then it just compares lists of numbers!</p></div> },
|
| 538 |
+
{ title: "Let's Simluate! 🚀", content: <div className="space-y-4"><p>We have 6 receipts. You can control the speed, hover over items to see connections, and adjust the 'Support' threshold. Ready?</p></div> }
|
| 539 |
+
];
|
| 540 |
+
|
| 541 |
+
return (
|
| 542 |
+
<div className="fixed inset-0 bg-background/90 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
| 543 |
+
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="max-w-md w-full bg-card border border-border rounded-xl p-6 shadow-2xl">
|
| 544 |
+
<div className="text-center mb-6">
|
| 545 |
+
<h2 className="text-2xl font-bold text-primary mb-2">{pages[page].title}</h2>
|
| 546 |
+
<div className="text-muted-foreground">{pages[page].content}</div>
|
| 547 |
+
</div>
|
| 548 |
+
<div className="flex justify-between items-center">
|
| 549 |
+
<div className="flex gap-1">{pages.map((_, i) => <div key={i} className={`w-2 h-2 rounded-full ${i===page?'bg-primary':'bg-muted'}`} />)}</div>
|
| 550 |
+
{page < pages.length - 1 ? <Button onClick={() => setPage(p => p+1)}>Next <Icons.ChevronRight className="w-4 h-4 ml-1"/></Button> : <Button onClick={onStart}>Start <Icons.Play className="w-4 h-4 ml-1"/></Button>}
|
| 551 |
+
</div>
|
| 552 |
+
</motion.div>
|
| 553 |
+
</div>
|
| 554 |
+
);
|
| 555 |
+
};
|
| 556 |
+
|
| 557 |
+
const GlossaryPanel = ({ isOpen, onClose }) => {
|
| 558 |
+
const terms = [
|
| 559 |
+
{ t: "Transaction", d: "A single shopping receipt (e.g., T1)" },
|
| 560 |
+
{ t: "TID", d: "Transaction ID (the number of the receipt)" },
|
| 561 |
+
{ t: "Support", d: "How frequently an item appears (%)" },
|
| 562 |
+
{ t: "Itemset", d: "A collection of one or more items" },
|
| 563 |
+
{ t: "Intersection", d: "Finding common numbers in two lists" }
|
| 564 |
+
];
|
| 565 |
+
|
| 566 |
+
return (
|
| 567 |
+
<AnimatePresence>
|
| 568 |
+
{isOpen && (
|
| 569 |
+
<>
|
| 570 |
+
<div className="fixed inset-0 bg-background/50 z-40" onClick={onClose} />
|
| 571 |
+
<motion.div initial={{ x: '100%' }} animate={{ x: 0 }} exit={{ x: '100%' }} className="fixed right-0 top-0 h-full w-80 bg-card border-l border-border z-50 p-6 shadow-xl">
|
| 572 |
+
<div className="flex justify-between items-center mb-6">
|
| 573 |
+
<h2 className="font-bold text-lg">Glossary</h2>
|
| 574 |
+
<button onClick={onClose}><Icons.X className="w-5 h-5" /></button>
|
| 575 |
+
</div>
|
| 576 |
+
<div className="space-y-4">
|
| 577 |
+
{terms.map((item, i) => (
|
| 578 |
+
<div key={i} className="p-3 bg-muted/20 rounded-lg">
|
| 579 |
+
<div className="font-bold text-primary text-sm">{item.t}</div>
|
| 580 |
+
<div className="text-sm text-muted-foreground">{item.d}</div>
|
| 581 |
+
</div>
|
| 582 |
+
))}
|
| 583 |
+
</div>
|
| 584 |
+
</motion.div>
|
| 585 |
+
</>
|
| 586 |
+
)}
|
| 587 |
+
</AnimatePresence>
|
| 588 |
+
);
|
| 589 |
+
};
|
| 590 |
+
|
| 591 |
+
const TheoryPanel = () => {
|
| 592 |
+
const playSound = () => {
|
| 593 |
+
const audio = document.getElementById('clickSound');
|
| 594 |
+
if (audio) {
|
| 595 |
+
audio.currentTime = 0;
|
| 596 |
+
audio.play();
|
| 597 |
+
}
|
| 598 |
+
};
|
| 599 |
+
|
| 600 |
+
return (
|
| 601 |
+
<section className="mt-8 p-6 rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm relative">
|
| 602 |
+
<div className="flex items-center gap-3 mb-6">
|
| 603 |
+
<div className="w-10 h-10 rounded-lg bg-indigo-500/20 flex items-center justify-center">
|
| 604 |
+
<Icons.BookOpen className="w-5 h-5 text-indigo-400" />
|
| 605 |
+
</div>
|
| 606 |
+
<div>
|
| 607 |
+
<h2 className="text-xl font-bold text-foreground">Theoretical Background</h2>
|
| 608 |
+
<p className="text-sm text-muted-foreground">Understanding the core concepts behind Eclat</p>
|
| 609 |
+
</div>
|
| 610 |
+
</div>
|
| 611 |
+
|
| 612 |
+
<div className="grid md:grid-cols-2 gap-8 mb-12">
|
| 613 |
+
<div className="space-y-4">
|
| 614 |
+
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
| 615 |
+
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
| 616 |
+
1. Vertical Data Format
|
| 617 |
+
</h3>
|
| 618 |
+
<p className="text-sm text-muted-foreground leading-relaxed">
|
| 619 |
+
Traditional algorithms like Apriori use a <strong>Horizontal</strong> layout (Transaction ID → List of Items).
|
| 620 |
+
Eclat flips this to a <strong>Vertical</strong> layout (Item → List of Transaction IDs).
|
| 621 |
+
This transformation is the key to its speed, as it avoids repeatedly scanning the entire database.
|
| 622 |
+
</p>
|
| 623 |
+
</div>
|
| 624 |
+
|
| 625 |
+
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
| 626 |
+
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
| 627 |
+
2. Set Intersection
|
| 628 |
+
</h3>
|
| 629 |
+
<p className="text-sm text-muted-foreground leading-relaxed">
|
| 630 |
+
To calculate the support of a new itemset (e.g., <span className="font-mono text-accent">{"{A, B}"}</span>), Eclat simply calculates the intersection of their TID sets:
|
| 631 |
+
<br/>
|
| 632 |
+
<code className="bg-background/50 px-1 rounded text-xs mt-1 block w-fit">TID(A) ∩ TID(B) = TID(A,B)</code>
|
| 633 |
+
The size of this resulting set is the support count.
|
| 634 |
+
</p>
|
| 635 |
+
</div>
|
| 636 |
+
</div>
|
| 637 |
+
|
| 638 |
+
<div className="space-y-4">
|
| 639 |
+
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
| 640 |
+
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
| 641 |
+
3. Depth-First Search (DFS)
|
| 642 |
+
</h3>
|
| 643 |
+
<p className="text-sm text-muted-foreground leading-relaxed">
|
| 644 |
+
Unlike Apriori which uses Breadth-First Search (finding all pairs, then all triplets), Eclat uses <strong>Depth-First Search</strong>.
|
| 645 |
+
It creates long patterns quickly by extending a specific prefix (e.g., A -> AB -> ABC) before backtracking.
|
| 646 |
+
</p>
|
| 647 |
+
</div>
|
| 648 |
+
|
| 649 |
+
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
| 650 |
+
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
| 651 |
+
4. Advantages
|
| 652 |
+
</h3>
|
| 653 |
+
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
| 654 |
+
<li><strong>Speed:</strong> Generally faster than Apriori for dense datasets.</li>
|
| 655 |
+
<li><strong>Memory:</strong> Does not need to generate candidate sets explicitly.</li>
|
| 656 |
+
<li><strong>Efficiency:</strong> Intersection operations are computationally cheap on modern CPUs.</li>
|
| 657 |
+
</ul>
|
| 658 |
+
</div>
|
| 659 |
+
</div>
|
| 660 |
+
</div>
|
| 661 |
+
|
| 662 |
+
{/* Centered Button with Fixes */}
|
| 663 |
+
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center">
|
| 664 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 665 |
+
<a
|
| 666 |
+
href="/eclat"
|
| 667 |
+
onClick={playSound}
|
| 668 |
+
className="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"
|
| 669 |
+
>
|
| 670 |
+
Back to Core
|
| 671 |
+
</a>
|
| 672 |
+
</div>
|
| 673 |
+
</section>
|
| 674 |
+
);
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
// --- MAIN APP COMPONENT ---
|
| 678 |
+
|
| 679 |
+
const SAMPLE_TRANSACTIONS = [
|
| 680 |
+
{ id: 1, items: ['Bread', 'Milk', 'Eggs'] },
|
| 681 |
+
{ id: 2, items: ['Bread', 'Butter'] },
|
| 682 |
+
{ id: 3, items: ['Milk', 'Butter', 'Eggs'] },
|
| 683 |
+
{ id: 4, items: ['Bread', 'Milk', 'Butter'] },
|
| 684 |
+
{ id: 5, items: ['Bread', 'Milk', 'Eggs', 'Butter'] },
|
| 685 |
+
{ id: 6, items: ['Milk', 'Eggs'] },
|
| 686 |
+
];
|
| 687 |
+
const ITEMS = ['Bread', 'Milk', 'Eggs', 'Butter'];
|
| 688 |
+
|
| 689 |
+
const EclatSimulation = () => {
|
| 690 |
+
const [showTutorial, setShowTutorial] = useState(true);
|
| 691 |
+
const [showGlossary, setShowGlossary] = useState(false);
|
| 692 |
+
const [step, setStep] = useState(0);
|
| 693 |
+
const [isPlaying, setIsPlaying] = useState(false);
|
| 694 |
+
const [minSupport, setMinSupport] = useState(50);
|
| 695 |
+
const [tidSets, setTidSets] = useState([]);
|
| 696 |
+
const [frequentItemsets, setFrequentItemsets] = useState([]);
|
| 697 |
+
const [highlightedTids, setHighlightedTids] = useState([]);
|
| 698 |
+
const [highlightedItem, setHighlightedItem] = useState(null);
|
| 699 |
+
|
| 700 |
+
const totalTransactions = SAMPLE_TRANSACTIONS.length;
|
| 701 |
+
const minSupportCount = Math.ceil((minSupport / 100) * totalTransactions);
|
| 702 |
+
|
| 703 |
+
// Core Logic
|
| 704 |
+
const buildTidSets = () => ITEMS.map(item => ({
|
| 705 |
+
item, tids: SAMPLE_TRANSACTIONS.filter(t => t.items.includes(item)).map(t => t.id)
|
| 706 |
+
}));
|
| 707 |
+
|
| 708 |
+
const intersect = (t1, t2) => t1.filter(x => t2.includes(x));
|
| 709 |
+
|
| 710 |
+
useEffect(() => {
|
| 711 |
+
if (step >= 1) setTidSets(buildTidSets());
|
| 712 |
+
}, [step]);
|
| 713 |
+
|
| 714 |
+
useEffect(() => {
|
| 715 |
+
if (!isPlaying) return;
|
| 716 |
+
const timer = setTimeout(() => {
|
| 717 |
+
if (step < 4) setStep(s => s + 1);
|
| 718 |
+
else setIsPlaying(false);
|
| 719 |
+
}, 3000);
|
| 720 |
+
return () => clearTimeout(timer);
|
| 721 |
+
}, [isPlaying, step]);
|
| 722 |
+
|
| 723 |
+
useEffect(() => {
|
| 724 |
+
if (step >= 2) {
|
| 725 |
+
const currentTidSets = buildTidSets();
|
| 726 |
+
const frequent = [];
|
| 727 |
+
|
| 728 |
+
// 1-itemsets
|
| 729 |
+
currentTidSets.forEach(ts => {
|
| 730 |
+
if (ts.tids.length >= minSupportCount) frequent.push({ items: [ts.item], tids: ts.tids, support: (ts.tids.length / totalTransactions) * 100 });
|
| 731 |
+
});
|
| 732 |
+
|
| 733 |
+
// 2-itemsets
|
| 734 |
+
if (step >= 3) {
|
| 735 |
+
const freqItems = frequent.map(f => f.items[0]);
|
| 736 |
+
for(let i=0; i<freqItems.length; i++) {
|
| 737 |
+
for(let j=i+1; j<freqItems.length; j++) {
|
| 738 |
+
const t1 = currentTidSets.find(t=>t.item===freqItems[i]).tids;
|
| 739 |
+
const t2 = currentTidSets.find(t=>t.item===freqItems[j]).tids;
|
| 740 |
+
const inter = intersect(t1, t2);
|
| 741 |
+
if(inter.length >= minSupportCount) frequent.push({ items: [freqItems[i], freqItems[j]], tids: inter, support: (inter.length/totalTransactions)*100 });
|
| 742 |
+
}
|
| 743 |
+
}
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
// 3-itemsets
|
| 747 |
+
if (step >= 4) {
|
| 748 |
+
const pairs = frequent.filter(f => f.items.length === 2);
|
| 749 |
+
for(let i=0; i<pairs.length; i++) {
|
| 750 |
+
for(let j=i+1; j<pairs.length; j++) {
|
| 751 |
+
const combined = [...new Set([...pairs[i].items, ...pairs[j].items])];
|
| 752 |
+
if(combined.length === 3) {
|
| 753 |
+
const inter = intersect(pairs[i].tids, pairs[j].tids);
|
| 754 |
+
if(inter.length >= minSupportCount && !frequent.some(f=>f.items.length===3 && f.items.every(it=>combined.includes(it)))) {
|
| 755 |
+
frequent.push({ items: combined, tids: inter, support: (inter.length/totalTransactions)*100 });
|
| 756 |
+
}
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
}
|
| 761 |
+
setFrequentItemsets(frequent);
|
| 762 |
+
}
|
| 763 |
+
}, [step, minSupport, minSupportCount]);
|
| 764 |
+
|
| 765 |
+
const handleHover = (item, tids = []) => {
|
| 766 |
+
setHighlightedItem(item);
|
| 767 |
+
setHighlightedTids(tids);
|
| 768 |
+
};
|
| 769 |
+
|
| 770 |
+
return (
|
| 771 |
+
<div className="min-h-screen bg-background grid-bg circuit-pattern pb-20">
|
| 772 |
+
{showTutorial && <IntroTutorial onStart={() => setShowTutorial(false)} />}
|
| 773 |
+
<GlossaryPanel isOpen={showGlossary} onClose={() => setShowGlossary(false)} />
|
| 774 |
+
|
| 775 |
+
<header className="border-b border-border/50 backdrop-blur-sm bg-background/80 sticky top-0 z-30">
|
| 776 |
+
<div className="container mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-4">
|
| 777 |
+
<div className="flex items-center gap-3">
|
| 778 |
+
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center glow-primary"><Icons.Database className="w-5 h-5 text-primary"/></div>
|
| 779 |
+
<div><h1 className="text-xl font-bold text-foreground text-glow">Eclat Algorithm</h1><p className="text-xs text-muted-foreground">Interactive Learning Simulation</p></div>
|
| 780 |
+
</div>
|
| 781 |
+
<div className="flex items-center gap-3">
|
| 782 |
+
<Button variant="outline" size="sm" onClick={() => setShowGlossary(true)} className="gap-2"><Icons.HelpCircle className="w-4 h-4"/> Glossary</Button>
|
| 783 |
+
<Button variant="ghost" size="sm" onClick={() => setShowTutorial(true)} className="gap-2"><Icons.RotateCw className="w-4 h-4"/> Tutorial</Button>
|
| 784 |
+
</div>
|
| 785 |
+
</div>
|
| 786 |
+
</header>
|
| 787 |
+
|
| 788 |
+
<main className="container mx-auto px-4 py-6">
|
| 789 |
+
<motion.div initial={{opacity:0, y:20}} animate={{opacity:1, y:0}} className="mb-6 p-4 rounded-xl bg-card/50 border border-border/50 backdrop-blur-sm">
|
| 790 |
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
| 791 |
+
<div className="flex flex-wrap items-center gap-3">
|
| 792 |
+
<Button onClick={() => setIsPlaying(!isPlaying)} className="gap-2" variant={isPlaying ? "secondary" : "default"}>
|
| 793 |
+
{isPlaying ? <Icons.Pause className="w-4 h-4"/> : <Icons.Play className="w-4 h-4"/>} {isPlaying ? 'Pause' : 'Play'}
|
| 794 |
+
</Button>
|
| 795 |
+
<Button onClick={() => step < 4 && setStep(s => s+1)} variant="outline" disabled={step >= 4} className="gap-2">
|
| 796 |
+
<Icons.ChevronRight className="w-4 h-4"/> Next
|
| 797 |
+
</Button>
|
| 798 |
+
<Button onClick={() => { setStep(0); setIsPlaying(false); }} variant="ghost" className="gap-2"><Icons.RotateCcw className="w-4 h-4"/> Reset</Button>
|
| 799 |
+
</div>
|
| 800 |
+
<div className="flex items-center gap-4 w-full md:w-auto">
|
| 801 |
+
<span className="text-sm text-muted-foreground whitespace-nowrap">Support: {minSupport}%</span>
|
| 802 |
+
<Slider value={[minSupport]} onValueChange={([v]) => setMinSupport(v)} min={20} max={80} step={10} className="w-full md:w-32"/>
|
| 803 |
+
</div>
|
| 804 |
+
</div>
|
| 805 |
+
|
| 806 |
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
| 807 |
+
{['Transactions', 'Build TID Sets', 'Frequent 1-Items', '2-Item Intersections', '3-Item Patterns'].map((label, i) => (
|
| 808 |
+
<div key={i} className="flex items-center gap-2">
|
| 809 |
+
<motion.div animate={step === i ? { scale: [1, 1.1, 1] } : {}} className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ${step >= i ? 'bg-primary text-primary-foreground glow-primary' : 'bg-muted text-muted-foreground'}`}>{i + 1}</motion.div>
|
| 810 |
+
<span className={`text-xs hidden sm:block ${step >= i ? 'text-foreground' : 'text-muted-foreground'}`}>{label}</span>
|
| 811 |
+
{i < 4 && <Icons.ChevronRight className="w-4 h-4 text-muted-foreground hidden sm:block"/>}
|
| 812 |
+
</div>
|
| 813 |
+
))}
|
| 814 |
+
</div>
|
| 815 |
+
</motion.div>
|
| 816 |
+
|
| 817 |
+
<StepExplainer step={step} minSupport={minSupport} minSupportCount={minSupportCount} />
|
| 818 |
+
|
| 819 |
+
{step === 0 && <div className="mt-4"><BeginnerTip title="🆕 New to this?" defaultOpen={true}><p>Think of this like looking at <strong>grocery store receipts</strong>. Each row (T1, T2...) is one customer's shopping basket.</p></BeginnerTip></div>}
|
| 820 |
+
{step === 1 && <div className="mt-4"><BeginnerTip title="🔄 Why did we flip the data?"><p><strong>Before:</strong> "Receipt 1 has Bread" (horizontal). <strong>After:</strong> "Bread is in Receipt 1" (vertical). This makes finding pairs faster!</p></BeginnerTip></div>}
|
| 821 |
+
|
| 822 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
| 823 |
+
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
|
| 824 |
+
<TransactionPanel transactions={SAMPLE_TRANSACTIONS} highlightedTids={highlightedTids} step={step} />
|
| 825 |
+
</motion.div>
|
| 826 |
+
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
|
| 827 |
+
<TidSetPanel tidSets={tidSets} step={step} minSupportCount={minSupportCount} onItemHover={handleHover} highlightedItem={highlightedItem} />
|
| 828 |
+
</motion.div>
|
| 829 |
+
</div>
|
| 830 |
+
|
| 831 |
+
<AnimatePresence>
|
| 832 |
+
{step >= 3 && (
|
| 833 |
+
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-6">
|
| 834 |
+
<IntersectionVisualizer tidSets={tidSets} minSupportCount={minSupportCount} step={step} />
|
| 835 |
+
</motion.div>
|
| 836 |
+
)}
|
| 837 |
+
</AnimatePresence>
|
| 838 |
+
|
| 839 |
+
<AnimatePresence>
|
| 840 |
+
{step >= 2 && (
|
| 841 |
+
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-6">
|
| 842 |
+
<FrequentItemsetsPanel itemsets={frequentItemsets} step={step} onItemsetHover={handleHover} />
|
| 843 |
+
</motion.div>
|
| 844 |
+
)}
|
| 845 |
+
</AnimatePresence>
|
| 846 |
+
|
| 847 |
+
<TheoryPanel />
|
| 848 |
+
</main>
|
| 849 |
+
</div>
|
| 850 |
+
);
|
| 851 |
+
};
|
| 852 |
+
|
| 853 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 854 |
+
root.render(<EclatSimulation />);
|
| 855 |
+
</script>
|
| 856 |
+
{% endraw %}
|
| 857 |
+
</body>
|
| 858 |
+
</html>
|
templates/Gaussian-Mixture-Models.html
CHANGED
|
@@ -177,6 +177,52 @@
|
|
| 177 |
<div class="container">
|
| 178 |
<h1>🌌 Study Guide: Gaussian Mixture Models (GMM)</h1>
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
<h2>🔹 Core Concepts</h2>
|
| 181 |
<div class="story-gmm">
|
| 182 |
<p><strong>Story-style intuition: The Expert Fruit Sorter</strong></p>
|
|
|
|
| 177 |
<div class="container">
|
| 178 |
<h1>🌌 Study Guide: Gaussian Mixture Models (GMM)</h1>
|
| 179 |
|
| 180 |
+
<!-- button -->
|
| 181 |
+
<div>
|
| 182 |
+
<!-- Audio Element -->
|
| 183 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 184 |
+
but since this is triggered by a click, it should work fine. -->
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
<a
|
| 188 |
+
href="/gaussian-mixture-three"
|
| 189 |
+
target="_blank"
|
| 190 |
+
onclick="playSound()"
|
| 191 |
+
class="
|
| 192 |
+
cursor-pointer
|
| 193 |
+
inline-block
|
| 194 |
+
relative
|
| 195 |
+
bg-blue-500
|
| 196 |
+
text-white
|
| 197 |
+
font-bold
|
| 198 |
+
py-4 px-8
|
| 199 |
+
rounded-xl
|
| 200 |
+
text-2xl
|
| 201 |
+
transition-all
|
| 202 |
+
duration-150
|
| 203 |
+
|
| 204 |
+
/* 3D Effect (Hard Shadow) */
|
| 205 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 206 |
+
|
| 207 |
+
/* Pressed State (Move down & remove shadow) */
|
| 208 |
+
active:shadow-none
|
| 209 |
+
active:translate-y-[8px]
|
| 210 |
+
">
|
| 211 |
+
Tap Me!
|
| 212 |
+
</a>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<script>
|
| 216 |
+
function playSound() {
|
| 217 |
+
const audio = document.getElementById("clickSound");
|
| 218 |
+
if (audio) {
|
| 219 |
+
audio.currentTime = 0;
|
| 220 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
</script>
|
| 224 |
+
<!-- button -->
|
| 225 |
+
|
| 226 |
<h2>🔹 Core Concepts</h2>
|
| 227 |
<div class="story-gmm">
|
| 228 |
<p><strong>Story-style intuition: The Expert Fruit Sorter</strong></p>
|
templates/Gradient-Descen.html
CHANGED
|
@@ -150,6 +150,53 @@
|
|
| 150 |
<div class="container">
|
| 151 |
<h1>Gradient Descent Study Guide</h1>
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
<h2>🔹 Core Concepts</h2>
|
| 154 |
<div class="story">
|
| 155 |
<p><strong>The Story: The Lost Hiker</strong></p>
|
|
|
|
| 150 |
<div class="container">
|
| 151 |
<h1>Gradient Descent Study Guide</h1>
|
| 152 |
|
| 153 |
+
<!-- button -->
|
| 154 |
+
<div>
|
| 155 |
+
<!-- Audio Element -->
|
| 156 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 157 |
+
but since this is triggered by a click, it should work fine. -->
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
<a
|
| 161 |
+
href="/gradient-descent-three"
|
| 162 |
+
target="_blank"
|
| 163 |
+
onclick="playSound()"
|
| 164 |
+
class="
|
| 165 |
+
cursor-pointer
|
| 166 |
+
inline-block
|
| 167 |
+
relative
|
| 168 |
+
bg-blue-500
|
| 169 |
+
text-white
|
| 170 |
+
font-bold
|
| 171 |
+
py-4 px-8
|
| 172 |
+
rounded-xl
|
| 173 |
+
text-2xl
|
| 174 |
+
transition-all
|
| 175 |
+
duration-150
|
| 176 |
+
|
| 177 |
+
/* 3D Effect (Hard Shadow) */
|
| 178 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 179 |
+
|
| 180 |
+
/* Pressed State (Move down & remove shadow) */
|
| 181 |
+
active:shadow-none
|
| 182 |
+
active:translate-y-[8px]
|
| 183 |
+
">
|
| 184 |
+
Tap Me!
|
| 185 |
+
</a>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<script>
|
| 189 |
+
function playSound() {
|
| 190 |
+
const audio = document.getElementById("clickSound");
|
| 191 |
+
if (audio) {
|
| 192 |
+
audio.currentTime = 0;
|
| 193 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
</script>
|
| 197 |
+
<!-- button -->
|
| 198 |
+
|
| 199 |
+
|
| 200 |
<h2>🔹 Core Concepts</h2>
|
| 201 |
<div class="story">
|
| 202 |
<p><strong>The Story: The Lost Hiker</strong></p>
|
templates/Independent-Component-Analysis.html
CHANGED
|
@@ -192,6 +192,53 @@
|
|
| 192 |
<div class="container">
|
| 193 |
<h1>🎙️ Study Guide: Independent Component Analysis (ICA)</h1>
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
<h2>🔹 Core Concepts</h2>
|
| 196 |
<div class="story-ica">
|
| 197 |
<p><strong>Story-style intuition: The Cocktail Party Problem</strong></p>
|
|
|
|
| 192 |
<div class="container">
|
| 193 |
<h1>🎙️ Study Guide: Independent Component Analysis (ICA)</h1>
|
| 194 |
|
| 195 |
+
|
| 196 |
+
<!-- button -->
|
| 197 |
+
<div>
|
| 198 |
+
<!-- Audio Element -->
|
| 199 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 200 |
+
but since this is triggered by a click, it should work fine. -->
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
<a
|
| 204 |
+
href="/ica-three"
|
| 205 |
+
target="_blank"
|
| 206 |
+
onclick="playSound()"
|
| 207 |
+
class="
|
| 208 |
+
cursor-pointer
|
| 209 |
+
inline-block
|
| 210 |
+
relative
|
| 211 |
+
bg-blue-500
|
| 212 |
+
text-white
|
| 213 |
+
font-bold
|
| 214 |
+
py-4 px-8
|
| 215 |
+
rounded-xl
|
| 216 |
+
text-2xl
|
| 217 |
+
transition-all
|
| 218 |
+
duration-150
|
| 219 |
+
|
| 220 |
+
/* 3D Effect (Hard Shadow) */
|
| 221 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 222 |
+
|
| 223 |
+
/* Pressed State (Move down & remove shadow) */
|
| 224 |
+
active:shadow-none
|
| 225 |
+
active:translate-y-[8px]
|
| 226 |
+
">
|
| 227 |
+
Tap Me!
|
| 228 |
+
</a>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<script>
|
| 232 |
+
function playSound() {
|
| 233 |
+
const audio = document.getElementById("clickSound");
|
| 234 |
+
if (audio) {
|
| 235 |
+
audio.currentTime = 0;
|
| 236 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
</script>
|
| 240 |
+
<!-- button -->
|
| 241 |
+
|
| 242 |
<h2>🔹 Core Concepts</h2>
|
| 243 |
<div class="story-ica">
|
| 244 |
<p><strong>Story-style intuition: The Cocktail Party Problem</strong></p>
|
templates/Linear-Discriminant-Analysis.html
CHANGED
|
@@ -177,6 +177,53 @@
|
|
| 177 |
<div class="container">
|
| 178 |
<h1>🔍 Study Guide: Linear Discriminant Analysis (LDA)</h1>
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
<h2>🔹 Core Concepts</h2>
|
| 181 |
<div class="story-lda">
|
| 182 |
<p><strong>Story-style intuition: The Smart Photographer</strong></p>
|
|
|
|
| 177 |
<div class="container">
|
| 178 |
<h1>🔍 Study Guide: Linear Discriminant Analysis (LDA)</h1>
|
| 179 |
|
| 180 |
+
|
| 181 |
+
<!-- button -->
|
| 182 |
+
<div>
|
| 183 |
+
<!-- Audio Element -->
|
| 184 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 185 |
+
but since this is triggered by a click, it should work fine. -->
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
<a
|
| 189 |
+
href="/lda-three"
|
| 190 |
+
target="_blank"
|
| 191 |
+
onclick="playSound()"
|
| 192 |
+
class="
|
| 193 |
+
cursor-pointer
|
| 194 |
+
inline-block
|
| 195 |
+
relative
|
| 196 |
+
bg-blue-500
|
| 197 |
+
text-white
|
| 198 |
+
font-bold
|
| 199 |
+
py-4 px-8
|
| 200 |
+
rounded-xl
|
| 201 |
+
text-2xl
|
| 202 |
+
transition-all
|
| 203 |
+
duration-150
|
| 204 |
+
|
| 205 |
+
/* 3D Effect (Hard Shadow) */
|
| 206 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 207 |
+
|
| 208 |
+
/* Pressed State (Move down & remove shadow) */
|
| 209 |
+
active:shadow-none
|
| 210 |
+
active:translate-y-[8px]
|
| 211 |
+
">
|
| 212 |
+
Tap Me!
|
| 213 |
+
</a>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<script>
|
| 217 |
+
function playSound() {
|
| 218 |
+
const audio = document.getElementById("clickSound");
|
| 219 |
+
if (audio) {
|
| 220 |
+
audio.currentTime = 0;
|
| 221 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
</script>
|
| 225 |
+
<!-- button -->
|
| 226 |
+
|
| 227 |
<h2>🔹 Core Concepts</h2>
|
| 228 |
<div class="story-lda">
|
| 229 |
<p><strong>Story-style intuition: The Smart Photographer</strong></p>
|
templates/Naive-Bayes-Simulator.html
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>3D Naive Bayes Visualizer</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--background: #05080a;
|
| 13 |
+
--primary: #ff0055; /* Apple Red */
|
| 14 |
+
--secondary: #ffcc00; /* Orange Orange */
|
| 15 |
+
--accent: #00ffff;
|
| 16 |
+
--surface: #0a1014;
|
| 17 |
+
--border: #1e293b;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
background-color: var(--background);
|
| 22 |
+
color: #f8fafc;
|
| 23 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 24 |
+
margin: 0;
|
| 25 |
+
overflow-x: hidden;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.glass {
|
| 29 |
+
background: rgba(10, 16, 20, 0.85);
|
| 30 |
+
backdrop-filter: blur(12px);
|
| 31 |
+
border: 1px solid rgba(30, 41, 59, 0.5);
|
| 32 |
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.canvas-container {
|
| 36 |
+
width: 100%;
|
| 37 |
+
height: 50vh; /* Responsive height */
|
| 38 |
+
min-height: 400px;
|
| 39 |
+
max-height: 600px;
|
| 40 |
+
position: relative;
|
| 41 |
+
background: radial-gradient(circle at center, #111827 0%, #000000 100%);
|
| 42 |
+
border-radius: 1rem;
|
| 43 |
+
overflow: hidden;
|
| 44 |
+
border: 1px solid var(--border);
|
| 45 |
+
cursor: grab;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.canvas-container:active { cursor: grabbing; }
|
| 49 |
+
|
| 50 |
+
.learning-log {
|
| 51 |
+
height: 120px;
|
| 52 |
+
overflow-y: auto;
|
| 53 |
+
scrollbar-width: thin;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Custom Scrollbar */
|
| 57 |
+
::-webkit-scrollbar { width: 6px; }
|
| 58 |
+
::-webkit-scrollbar-track { background: #0f172a; }
|
| 59 |
+
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
| 60 |
+
|
| 61 |
+
input[type=range] {
|
| 62 |
+
-webkit-appearance: none;
|
| 63 |
+
width: 100%;
|
| 64 |
+
background: transparent;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
input[type=range]::-webkit-slider-runnable-track {
|
| 68 |
+
width: 100%;
|
| 69 |
+
height: 6px;
|
| 70 |
+
cursor: pointer;
|
| 71 |
+
background: #1e293b;
|
| 72 |
+
border-radius: 3px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
input[type=range]::-webkit-slider-thumb {
|
| 76 |
+
height: 18px;
|
| 77 |
+
width: 18px;
|
| 78 |
+
border-radius: 50%;
|
| 79 |
+
background: var(--accent);
|
| 80 |
+
cursor: pointer;
|
| 81 |
+
-webkit-appearance: none;
|
| 82 |
+
margin-top: -6px;
|
| 83 |
+
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.math-box {
|
| 87 |
+
font-family: 'JetBrains Mono', monospace;
|
| 88 |
+
font-size: 10px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.hidden { display: none; }
|
| 92 |
+
</style>
|
| 93 |
+
</head>
|
| 94 |
+
<body class="p-4 md:p-6 lg:p-8">
|
| 95 |
+
|
| 96 |
+
<div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_380px] gap-6 lg:gap-8">
|
| 97 |
+
<!-- Left Side: Visualizer -->
|
| 98 |
+
<div class="flex flex-col gap-4">
|
| 99 |
+
<header>
|
| 100 |
+
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
|
| 101 |
+
<div>
|
| 102 |
+
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2">3D Naive Bayes <span class="text-cyan-400">Viz</span></h1>
|
| 103 |
+
<p class="text-slate-400 text-sm max-w-xl">
|
| 104 |
+
A classifier that learns by shaping 3D probability clouds. It assumes dimensions (Weight, Sweetness, Color) are independent.
|
| 105 |
+
</p>
|
| 106 |
+
<!-- Centered Button --> <div class="absolute left-1/2 -translate-x-1/2 flex items-center"> <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio> <a href="/naive_bayes" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"> Back to Core </a> </div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</header>
|
| 110 |
+
|
| 111 |
+
<div class="relative">
|
| 112 |
+
<!-- 3D Canvas -->
|
| 113 |
+
<div class="canvas-container shadow-2xl" id="container">
|
| 114 |
+
<div id="three-canvas" class="w-full h-full"></div>
|
| 115 |
+
|
| 116 |
+
<!-- Top Overlays (Inside Canvas) -->
|
| 117 |
+
<div class="absolute top-4 left-4 pointer-events-none flex flex-col gap-2 z-10 max-w-[200px]">
|
| 118 |
+
<div class="glass px-3 py-2 rounded-lg">
|
| 119 |
+
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div>
|
| 120 |
+
<div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase font-bold animate-pulse">Waiting to Learn</div>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="glass px-3 py-2 rounded-lg flex flex-col gap-1">
|
| 123 |
+
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest mb-1">Legend</div>
|
| 124 |
+
<div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-[#ff0055] shadow-[0_0_8px_#ff0055]"></span> Apple Class</div>
|
| 125 |
+
<div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-[#ffcc00] shadow-[0_0_8px_#ffcc00]"></span> Orange Class</div>
|
| 126 |
+
<div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-white border border-slate-500"></span> Mystery Fruit</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<!-- Measurement Label (Inside Canvas) -->
|
| 131 |
+
<div class="absolute top-4 right-4 pointer-events-none text-right z-10">
|
| 132 |
+
<div class="glass px-3 py-2 rounded-lg">
|
| 133 |
+
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Input Features</div>
|
| 134 |
+
<div id="pos-display" class="text-xs font-mono text-cyan-400 mt-1 uppercase">W:0.0 S:0.0 C:0.0</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<!-- Instructions Overlay (Mobile/Desktop) -->
|
| 139 |
+
<div class="absolute bottom-4 left-4 pointer-events-none z-10 hidden md:block">
|
| 140 |
+
<div class="glass px-3 py-2 rounded-lg text-[10px] text-slate-400">
|
| 141 |
+
<b>Left-Click</b> Rotate | <b>Right-Click</b> Pan | <b>Shift+Drag</b> Lift Y-Axis
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<!-- Result Card: Below on Mobile, Absolute Overlay on Desktop -->
|
| 147 |
+
<div class="mt-4 md:mt-0 md:absolute md:bottom-4 md:right-4 z-20 w-full md:w-80">
|
| 148 |
+
<div class="glass p-4 rounded-xl shadow-xl border border-slate-700/50">
|
| 149 |
+
<div class="flex justify-between items-center mb-3">
|
| 150 |
+
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Prediction Engine</div>
|
| 151 |
+
<div id="uncertainty-badge" class="hidden text-[9px] bg-slate-700 text-white px-2 py-0.5 rounded font-mono">UNCERTAIN</div>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<!-- Apple Calc -->
|
| 155 |
+
<div class="space-y-1 mb-3">
|
| 156 |
+
<div class="flex justify-between text-[10px] text-[#ff0055] font-bold uppercase items-center">
|
| 157 |
+
<span>Apple Likelihood</span>
|
| 158 |
+
<span id="prob-a-val" class="text-xs">50%</span>
|
| 159 |
+
</div>
|
| 160 |
+
<div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
| 161 |
+
<div id="bar-a" class="h-full bg-[#ff0055] transition-all duration-300" style="width: 50%"></div>
|
| 162 |
+
</div>
|
| 163 |
+
<div class="math-box text-[9px] text-slate-500 flex justify-between px-1">
|
| 164 |
+
<span>Prior(<span id="prior-a">.5</span>)</span>
|
| 165 |
+
<span>×</span>
|
| 166 |
+
<span>L(<span id="total-l-a">0</span>)</span>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<!-- Orange Calc -->
|
| 171 |
+
<div class="space-y-1 mb-4">
|
| 172 |
+
<div class="flex justify-between text-[10px] text-[#ffcc00] font-bold uppercase items-center">
|
| 173 |
+
<span>Orange Likelihood</span>
|
| 174 |
+
<span id="prob-b-val" class="text-xs">50%</span>
|
| 175 |
+
</div>
|
| 176 |
+
<div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
| 177 |
+
<div id="bar-b" class="h-full bg-[#ffcc00] transition-all duration-300" style="width: 50%"></div>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="math-box text-[9px] text-slate-500 flex justify-between px-1">
|
| 180 |
+
<span>Prior(<span id="prior-b">.5</span>)</span>
|
| 181 |
+
<span>×</span>
|
| 182 |
+
<span>L(<span id="total-l-b">0</span>)</span>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<div class="pt-3 border-t border-slate-700/50 text-center">
|
| 187 |
+
<div class="text-[9px] text-slate-500 uppercase font-bold mb-1">Final Classification</div>
|
| 188 |
+
<div id="result-text" class="text-2xl font-bold text-white uppercase tracking-tighter">NEUTRAL</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<!-- The Learning Log -->
|
| 195 |
+
<div class="glass rounded-xl p-4 flex flex-col h-40">
|
| 196 |
+
<h3 class="text-xs font-bold text-white mb-2 flex items-center gap-2 shrink-0">
|
| 197 |
+
<span class="text-cyan-400">⚡</span> System Logs
|
| 198 |
+
</h3>
|
| 199 |
+
<div id="learning-log" class="learning-log text-xs font-mono text-slate-400 space-y-1.5 pr-2">
|
| 200 |
+
<div>> System initialized.</div>
|
| 201 |
+
<div>> Waiting for training data...</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<!-- Right Side: Controls -->
|
| 207 |
+
<aside class="space-y-6">
|
| 208 |
+
<div class="glass rounded-xl p-5 space-y-6">
|
| 209 |
+
<h2 class="text-lg font-bold text-white flex items-center gap-2">
|
| 210 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-cyan-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.532 1.532 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.532 1.532 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /></svg>
|
| 211 |
+
Control Panel
|
| 212 |
+
</h2>
|
| 213 |
+
|
| 214 |
+
<div class="space-y-4">
|
| 215 |
+
<div>
|
| 216 |
+
<div class="flex justify-between items-center mb-2">
|
| 217 |
+
<label class="text-xs font-bold text-slate-400 uppercase">Search Variance (Alpha)</label>
|
| 218 |
+
<span id="alpha-value" class="text-cyan-400 font-mono text-sm bg-cyan-950 px-2 rounded">1.00</span>
|
| 219 |
+
</div>
|
| 220 |
+
<input type="range" id="alpha-slider" min="0.1" max="5.0" step="0.1" value="1.0">
|
| 221 |
+
<p class="text-[10px] text-slate-500 mt-2">
|
| 222 |
+
Higher alpha = wider probability clouds (High Bias). Lower alpha = tighter clouds (High Variance).
|
| 223 |
+
</p>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<div class="grid grid-cols-1 gap-3 pt-2">
|
| 227 |
+
<button id="btn-toggle" class="group relative flex items-center justify-center gap-2 py-3 rounded-lg font-bold text-black bg-cyan-400 hover:bg-cyan-300 transition-all overflow-hidden">
|
| 228 |
+
<div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
|
| 229 |
+
<span id="toggle-icon">▶</span> <span id="toggle-text">Start Training</span>
|
| 230 |
+
</button>
|
| 231 |
+
<button id="btn-reset" class="flex items-center justify-center gap-2 py-3 rounded-lg border border-slate-700 hover:bg-slate-800 hover:border-slate-600 transition-all font-bold text-sm text-slate-300">
|
| 232 |
+
Reset Simulation
|
| 233 |
+
</button>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<!-- Scenario Selection -->
|
| 239 |
+
<div class="glass rounded-xl p-5 space-y-3">
|
| 240 |
+
<h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest mb-1">Data Scenario</h3>
|
| 241 |
+
<div class="grid grid-cols-2 gap-2">
|
| 242 |
+
<button onclick="selectScenario(0)" id="scen-0" class="text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all">Easy (Clustered)</button>
|
| 243 |
+
<button onclick="selectScenario(1)" id="scen-1" class="text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all">Hard (Scattered)</button>
|
| 244 |
+
</div>
|
| 245 |
+
<p class="text-[10px] text-slate-500 leading-relaxed pt-2 border-t border-slate-800">
|
| 246 |
+
Switching scenarios resets the robot's memory.
|
| 247 |
+
</p>
|
| 248 |
+
</div>
|
| 249 |
+
|
| 250 |
+
<!-- Learning Concept Card -->
|
| 251 |
+
<div class="glass rounded-xl p-5 border-l-2 border-cyan-400">
|
| 252 |
+
<h3 class="text-xs font-bold text-cyan-400 uppercase tracking-widest mb-2">How it works</h3>
|
| 253 |
+
<p class="text-[11px] text-slate-300 leading-relaxed">
|
| 254 |
+
The robot calculates the center (mean) and spread (variance) of the points it sees.
|
| 255 |
+
<br><br>
|
| 256 |
+
To predict the mystery fruit, it measures the distance to each cloud center relative to the cloud's size.
|
| 257 |
+
<br><br>
|
| 258 |
+
<span class="text-white font-bold">Naive Assumption:</span> It processes Width, Sweetness, and Color completely separately, then multiplies the results.
|
| 259 |
+
</p>
|
| 260 |
+
</div>
|
| 261 |
+
</aside>
|
| 262 |
+
</div>
|
| 263 |
+
|
| 264 |
+
<script>
|
| 265 |
+
// --- DATA CONFIG ---
|
| 266 |
+
const SCENARIOS = [
|
| 267 |
+
{ name: 'Clustered', points: 80, spread: 1.2 },
|
| 268 |
+
{ name: 'Scattered', points: 200, spread: 3.5 }
|
| 269 |
+
];
|
| 270 |
+
|
| 271 |
+
let currentScenario = SCENARIOS[0];
|
| 272 |
+
let dataPoints = [];
|
| 273 |
+
let processedIdx = 0;
|
| 274 |
+
let isRunning = false;
|
| 275 |
+
let loopId = null;
|
| 276 |
+
let alpha = 1.0;
|
| 277 |
+
|
| 278 |
+
// Model State
|
| 279 |
+
let model = [
|
| 280 |
+
{ name: 'Apple', color: 0xff0055, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 },
|
| 281 |
+
{ name: 'Orange', color: 0xffcc00, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 }
|
| 282 |
+
];
|
| 283 |
+
|
| 284 |
+
// --- THREE.JS SETUP ---
|
| 285 |
+
const container = document.getElementById('container');
|
| 286 |
+
const scene = new THREE.Scene();
|
| 287 |
+
// Dark gradient background effect via clear color not possible easily, handled via CSS
|
| 288 |
+
|
| 289 |
+
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
|
| 290 |
+
|
| 291 |
+
let cameraRadius = 35;
|
| 292 |
+
let cameraPhi = Math.PI / 2.5;
|
| 293 |
+
let cameraTheta = Math.PI / 4;
|
| 294 |
+
|
| 295 |
+
function updateCameraPosition() {
|
| 296 |
+
camera.position.x = cameraRadius * Math.sin(cameraPhi) * Math.sin(cameraTheta);
|
| 297 |
+
camera.position.y = cameraRadius * Math.cos(cameraPhi);
|
| 298 |
+
camera.position.z = cameraRadius * Math.sin(cameraPhi) * Math.cos(cameraTheta);
|
| 299 |
+
camera.lookAt(0, 0, 0);
|
| 300 |
+
}
|
| 301 |
+
updateCameraPosition();
|
| 302 |
+
|
| 303 |
+
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
| 304 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 305 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 306 |
+
document.getElementById('three-canvas').appendChild(renderer.domElement);
|
| 307 |
+
|
| 308 |
+
// Lighting
|
| 309 |
+
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
|
| 310 |
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| 311 |
+
dirLight.position.set(10, 20, 10);
|
| 312 |
+
scene.add(dirLight);
|
| 313 |
+
|
| 314 |
+
// Axes & Grid
|
| 315 |
+
const grid = new THREE.GridHelper(30, 30, 0x1e293b, 0x0f172a);
|
| 316 |
+
grid.position.y = -5;
|
| 317 |
+
scene.add(grid);
|
| 318 |
+
|
| 319 |
+
// Custom Axes
|
| 320 |
+
function createAxis(start, end, color) {
|
| 321 |
+
const points = [start, end];
|
| 322 |
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
| 323 |
+
const material = new THREE.LineBasicMaterial({ color: color });
|
| 324 |
+
return new THREE.Line(geometry, material);
|
| 325 |
+
}
|
| 326 |
+
// X (Red), Y (Green), Z (Blue)
|
| 327 |
+
scene.add(createAxis(new THREE.Vector3(-15, -5, 0), new THREE.Vector3(15, -5, 0), 0x334155));
|
| 328 |
+
scene.add(createAxis(new THREE.Vector3(0, -15, 0), new THREE.Vector3(0, 15, 0), 0x334155));
|
| 329 |
+
scene.add(createAxis(new THREE.Vector3(0, -5, -15), new THREE.Vector3(0, -5, 15), 0x334155));
|
| 330 |
+
|
| 331 |
+
// Data Points Group
|
| 332 |
+
const pointGroup = new THREE.Group();
|
| 333 |
+
scene.add(pointGroup);
|
| 334 |
+
|
| 335 |
+
// Mystery Fruit (Interactive)
|
| 336 |
+
const mysteryFruitMesh = new THREE.Mesh(
|
| 337 |
+
new THREE.SphereGeometry(0.7, 32, 32),
|
| 338 |
+
new THREE.MeshStandardMaterial({
|
| 339 |
+
color: 0xffffff,
|
| 340 |
+
roughness: 0.2,
|
| 341 |
+
metalness: 0.1,
|
| 342 |
+
emissive: 0xffffff,
|
| 343 |
+
emissiveIntensity: 0.2
|
| 344 |
+
})
|
| 345 |
+
);
|
| 346 |
+
mysteryFruitMesh.position.set(0, 0, 0); // Start neutral
|
| 347 |
+
scene.add(mysteryFruitMesh);
|
| 348 |
+
|
| 349 |
+
// Ring indicator for mystery fruit
|
| 350 |
+
const ringGeo = new THREE.RingGeometry(0.8, 0.9, 32);
|
| 351 |
+
const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide, transparent: true, opacity: 0.5 });
|
| 352 |
+
const ring = new THREE.Mesh(ringGeo, ringMat);
|
| 353 |
+
ring.rotation.x = -Math.PI / 2;
|
| 354 |
+
mysteryFruitMesh.add(ring);
|
| 355 |
+
|
| 356 |
+
// Measurement Lines
|
| 357 |
+
const measureLines = new THREE.LineSegments(
|
| 358 |
+
new THREE.BufferGeometry(),
|
| 359 |
+
new THREE.LineDashedMaterial({ color: 0x00ffff, dashSize: 0.4, gapSize: 0.2, opacity: 0.5, transparent: true })
|
| 360 |
+
);
|
| 361 |
+
// Fixed: Removed premature call to computeLineDistances() on empty geometry
|
| 362 |
+
scene.add(measureLines);
|
| 363 |
+
|
| 364 |
+
// Gaussian Clouds (Visualizing Variance)
|
| 365 |
+
const clouds = model.map(m => {
|
| 366 |
+
const mesh = new THREE.Mesh(
|
| 367 |
+
new THREE.SphereGeometry(1, 32, 32),
|
| 368 |
+
new THREE.MeshBasicMaterial({
|
| 369 |
+
color: m.color,
|
| 370 |
+
transparent: true,
|
| 371 |
+
opacity: 0.1,
|
| 372 |
+
wireframe: true,
|
| 373 |
+
depthWrite: false
|
| 374 |
+
})
|
| 375 |
+
);
|
| 376 |
+
mesh.visible = false; // Hide until training starts
|
| 377 |
+
scene.add(mesh);
|
| 378 |
+
return mesh;
|
| 379 |
+
});
|
| 380 |
+
|
| 381 |
+
// --- MATH & LOGIC ---
|
| 382 |
+
|
| 383 |
+
function gaussian(x, mean, variance) {
|
| 384 |
+
// Prevent division by zero or extremely small variance
|
| 385 |
+
const v = Math.max(variance, 0.1) + (alpha * 0.2);
|
| 386 |
+
// Add tiny epsilon to avoid pure 0 underflow in exp
|
| 387 |
+
return (1 / Math.sqrt(2 * Math.PI * v)) * Math.exp(-Math.pow(x - mean, 2) / (2 * v)) + 1e-9;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
function generateData() {
|
| 391 |
+
dataPoints = [];
|
| 392 |
+
pointGroup.clear();
|
| 393 |
+
|
| 394 |
+
// Offsets to make them distinct
|
| 395 |
+
const appleCenter = { x: -6, y: -2, z: -4 };
|
| 396 |
+
const orangeCenter = { x: 6, y: 3, z: 4 };
|
| 397 |
+
|
| 398 |
+
model.forEach((m, i) => {
|
| 399 |
+
const center = i === 0 ? appleCenter : orangeCenter;
|
| 400 |
+
|
| 401 |
+
for (let j = 0; j < currentScenario.points / 2; j++) {
|
| 402 |
+
const x = center.x + (Math.random() - 0.5) * currentScenario.spread * 5;
|
| 403 |
+
const y = center.y + (Math.random() - 0.5) * currentScenario.spread * 5;
|
| 404 |
+
const z = center.z + (Math.random() - 0.5) * currentScenario.spread * 5;
|
| 405 |
+
|
| 406 |
+
dataPoints.push({ x, y, z, classId: i });
|
| 407 |
+
|
| 408 |
+
const p = new THREE.Mesh(
|
| 409 |
+
new THREE.SphereGeometry(0.2, 8, 8),
|
| 410 |
+
new THREE.MeshBasicMaterial({ color: m.color, transparent: true, opacity: 0.6 })
|
| 411 |
+
);
|
| 412 |
+
p.position.set(x, y, z);
|
| 413 |
+
p.visible = false;
|
| 414 |
+
pointGroup.add(p);
|
| 415 |
+
}
|
| 416 |
+
});
|
| 417 |
+
// Shuffle
|
| 418 |
+
dataPoints.sort(() => Math.random() - 0.5);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
function trainStep() {
|
| 422 |
+
if (processedIdx >= dataPoints.length) {
|
| 423 |
+
stopTraining();
|
| 424 |
+
addLog("Training complete. Model optimized.");
|
| 425 |
+
return;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
// Process a batch for speed
|
| 429 |
+
const batchSize = 2;
|
| 430 |
+
for(let k=0; k<batchSize && processedIdx < dataPoints.length; k++) {
|
| 431 |
+
const p = dataPoints[processedIdx];
|
| 432 |
+
const m = model[p.classId];
|
| 433 |
+
|
| 434 |
+
// Online Mean/Variance Update (Welford's algorithm simplified)
|
| 435 |
+
m.count++;
|
| 436 |
+
const lr = 1.0 / (m.count + 5); // Decaying learning rate for stability
|
| 437 |
+
|
| 438 |
+
// Update Mean
|
| 439 |
+
const oldMean = { ...m.mean };
|
| 440 |
+
m.mean.x += lr * (p.x - m.mean.x);
|
| 441 |
+
m.mean.y += lr * (p.y - m.mean.y);
|
| 442 |
+
m.mean.z += lr * (p.z - m.mean.z);
|
| 443 |
+
|
| 444 |
+
// Update Variance (Approximation for visualizer)
|
| 445 |
+
const varLr = 0.1;
|
| 446 |
+
m.var.x = (1 - varLr) * m.var.x + varLr * Math.pow(p.x - m.mean.x, 2);
|
| 447 |
+
m.var.y = (1 - varLr) * m.var.y + varLr * Math.pow(p.y - m.mean.y, 2);
|
| 448 |
+
m.var.z = (1 - varLr) * m.var.z + varLr * Math.pow(p.z - m.mean.z, 2);
|
| 449 |
+
|
| 450 |
+
pointGroup.children[processedIdx].visible = true;
|
| 451 |
+
processedIdx++;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
if (processedIdx % 10 === 0) updateVisuals();
|
| 455 |
+
predict();
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
function updateVisuals() {
|
| 459 |
+
model.forEach((m, i) => {
|
| 460 |
+
clouds[i].visible = true;
|
| 461 |
+
clouds[i].position.set(m.mean.x, m.mean.y, m.mean.z);
|
| 462 |
+
|
| 463 |
+
// Visual scale based on standard deviation (sqrt of variance)
|
| 464 |
+
// Add base size so it doesn't disappear
|
| 465 |
+
const sx = Math.sqrt(m.var.x) * 2.5 + alpha;
|
| 466 |
+
const sy = Math.sqrt(m.var.y) * 2.5 + alpha;
|
| 467 |
+
const sz = Math.sqrt(m.var.z) * 2.5 + alpha;
|
| 468 |
+
|
| 469 |
+
clouds[i].scale.set(sx, sy, sz);
|
| 470 |
+
});
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
function predict() {
|
| 474 |
+
const p = mysteryFruitMesh.position;
|
| 475 |
+
const totalPoints = Math.max(1, processedIdx);
|
| 476 |
+
|
| 477 |
+
model.forEach(m => {
|
| 478 |
+
// Naive Bayes Formula: P(Class|Features) ∝ P(Class) * P(F1|Class) * P(F2|Class)...
|
| 479 |
+
|
| 480 |
+
// 1. Prior: Frequency of class (laplace smoothing)
|
| 481 |
+
const prior = (m.count + 1) / (totalPoints + 2);
|
| 482 |
+
|
| 483 |
+
// 2. Likelihoods for each dimension (Gaussian)
|
| 484 |
+
const lW = gaussian(p.x, m.mean.x, m.var.x);
|
| 485 |
+
const lC = gaussian(p.y, m.mean.y, m.var.y);
|
| 486 |
+
const lS = gaussian(p.z, m.mean.z, m.var.z);
|
| 487 |
+
|
| 488 |
+
// Store for UI
|
| 489 |
+
m.lastCalc = { prior, lW, lS, lC };
|
| 490 |
+
m.score = prior * lW * lS * lC;
|
| 491 |
+
});
|
| 492 |
+
|
| 493 |
+
// Normalize probabilities
|
| 494 |
+
const totalScore = model[0].score + model[1].score;
|
| 495 |
+
|
| 496 |
+
let probA = 0, probB = 0;
|
| 497 |
+
|
| 498 |
+
if (totalScore > 0) {
|
| 499 |
+
probA = (model[0].score / totalScore) * 100;
|
| 500 |
+
probB = (model[1].score / totalScore) * 100;
|
| 501 |
+
} else {
|
| 502 |
+
// Handle 0 score / underflow case
|
| 503 |
+
probA = 50;
|
| 504 |
+
probB = 50;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
updateUI(probA, probB);
|
| 508 |
+
updateMeasurementLines();
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
function updateUI(probA, probB) {
|
| 512 |
+
// Bars
|
| 513 |
+
document.getElementById('bar-a').style.width = `${probA}%`;
|
| 514 |
+
document.getElementById('bar-b').style.width = `${probB}%`;
|
| 515 |
+
|
| 516 |
+
// Text
|
| 517 |
+
document.getElementById('prob-a-val').innerText = probA.toFixed(1) + '%';
|
| 518 |
+
document.getElementById('prob-b-val').innerText = probB.toFixed(1) + '%';
|
| 519 |
+
|
| 520 |
+
// Details
|
| 521 |
+
document.getElementById('prior-a').innerText = model[0].lastCalc?.prior.toFixed(2) || '0.5';
|
| 522 |
+
document.getElementById('prior-b').innerText = model[1].lastCalc?.prior.toFixed(2) || '0.5';
|
| 523 |
+
|
| 524 |
+
// Just showing one likelihood sum for brevity in UI
|
| 525 |
+
const totLA = (model[0].lastCalc?.lW + model[0].lastCalc?.lC + model[0].lastCalc?.lS) || 0;
|
| 526 |
+
const totLB = (model[1].lastCalc?.lW + model[1].lastCalc?.lC + model[1].lastCalc?.lS) || 0;
|
| 527 |
+
document.getElementById('total-l-a').innerText = totLA.toFixed(2);
|
| 528 |
+
document.getElementById('total-l-b').innerText = totLB.toFixed(2);
|
| 529 |
+
|
| 530 |
+
// Winner Logic (with Neutral zone)
|
| 531 |
+
const resultText = document.getElementById('result-text');
|
| 532 |
+
const badge = document.getElementById('uncertainty-badge');
|
| 533 |
+
|
| 534 |
+
if (Math.abs(probA - probB) < 2) {
|
| 535 |
+
// Too close to call
|
| 536 |
+
resultText.innerText = "NEUTRAL";
|
| 537 |
+
resultText.style.color = "#94a3b8"; // slate-400
|
| 538 |
+
badge.classList.remove('hidden');
|
| 539 |
+
} else if (probA > probB) {
|
| 540 |
+
resultText.innerText = "APPLE";
|
| 541 |
+
resultText.style.color = "#ff0055";
|
| 542 |
+
badge.classList.add('hidden');
|
| 543 |
+
} else {
|
| 544 |
+
resultText.innerText = "ORANGE";
|
| 545 |
+
resultText.style.color = "#ffcc00";
|
| 546 |
+
badge.classList.add('hidden');
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
function updateMeasurementLines() {
|
| 551 |
+
const p = mysteryFruitMesh.position;
|
| 552 |
+
// Lines projecting to axes for visual reference
|
| 553 |
+
const points = [
|
| 554 |
+
p.x, p.y, p.z, p.x, -5, p.z, // To floor
|
| 555 |
+
p.x, -5, p.z, p.x, -5, 0, // On floor to X-axis
|
| 556 |
+
p.x, -5, p.z, 0, -5, p.z // On floor to Z-axis
|
| 557 |
+
];
|
| 558 |
+
|
| 559 |
+
const vertices = [];
|
| 560 |
+
for(let i=0; i<points.length; i+=3) {
|
| 561 |
+
vertices.push(new THREE.Vector3(points[i], points[i+1], points[i+2]));
|
| 562 |
+
}
|
| 563 |
+
measureLines.geometry.setFromPoints(vertices);
|
| 564 |
+
measureLines.computeLineDistances();
|
| 565 |
+
|
| 566 |
+
document.getElementById('pos-display').innerText =
|
| 567 |
+
`W:${p.x.toFixed(1)} S:${p.z.toFixed(1)} C:${p.y.toFixed(1)}`;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
// --- CONTROLS ---
|
| 571 |
+
|
| 572 |
+
function startTraining() {
|
| 573 |
+
if (isRunning) return;
|
| 574 |
+
isRunning = true;
|
| 575 |
+
document.getElementById('toggle-text').innerText = 'Pause';
|
| 576 |
+
document.getElementById('toggle-icon').innerText = '⏸';
|
| 577 |
+
document.getElementById('status-text').innerText = 'Training...';
|
| 578 |
+
document.getElementById('status-text').classList.remove('animate-pulse');
|
| 579 |
+
|
| 580 |
+
loopId = setInterval(trainStep, 50);
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
function stopTraining() {
|
| 584 |
+
isRunning = false;
|
| 585 |
+
document.getElementById('toggle-text').innerText = 'Resume';
|
| 586 |
+
document.getElementById('toggle-icon').innerText = '▶';
|
| 587 |
+
document.getElementById('status-text').innerText = 'Paused / Idle';
|
| 588 |
+
clearInterval(loopId);
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
function reset() {
|
| 592 |
+
stopTraining();
|
| 593 |
+
processedIdx = 0;
|
| 594 |
+
// Reset math model
|
| 595 |
+
model.forEach(m => {
|
| 596 |
+
m.count = 0;
|
| 597 |
+
m.mean = {x:0, y:0, z:0};
|
| 598 |
+
m.var = {x:1, y:1, z:1};
|
| 599 |
+
m.score = 0;
|
| 600 |
+
});
|
| 601 |
+
|
| 602 |
+
clouds.forEach(c => c.visible = false);
|
| 603 |
+
document.getElementById('toggle-text').innerText = 'Start Training';
|
| 604 |
+
document.getElementById('toggle-icon').innerText = '▶';
|
| 605 |
+
document.getElementById('status-text').innerText = 'Ready';
|
| 606 |
+
document.getElementById('status-text').classList.add('animate-pulse');
|
| 607 |
+
|
| 608 |
+
// Reset position
|
| 609 |
+
mysteryFruitMesh.position.set(0, 0, 0);
|
| 610 |
+
|
| 611 |
+
generateData();
|
| 612 |
+
addLog("Memory wiped. System reset.");
|
| 613 |
+
predict();
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
function selectScenario(idx) {
|
| 617 |
+
currentScenario = SCENARIOS[idx];
|
| 618 |
+
|
| 619 |
+
// Update UI buttons
|
| 620 |
+
document.getElementById('scen-0').className = idx === 0
|
| 621 |
+
? "text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all shadow-[0_0_10px_rgba(34,211,238,0.2)]"
|
| 622 |
+
: "text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all";
|
| 623 |
+
|
| 624 |
+
document.getElementById('scen-1').className = idx === 1
|
| 625 |
+
? "text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all shadow-[0_0_10px_rgba(34,211,238,0.2)]"
|
| 626 |
+
: "text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all";
|
| 627 |
+
|
| 628 |
+
reset();
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
function addLog(msg) {
|
| 632 |
+
const log = document.getElementById('learning-log');
|
| 633 |
+
const div = document.createElement('div');
|
| 634 |
+
const time = new Date().toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit'});
|
| 635 |
+
div.innerHTML = `<span class="text-slate-600">[${time}]</span> ${msg}`;
|
| 636 |
+
log.prepend(div);
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
// --- EVENTS ---
|
| 640 |
+
document.getElementById('btn-toggle').onclick = () => isRunning ? stopTraining() : startTraining();
|
| 641 |
+
document.getElementById('btn-reset').onclick = reset;
|
| 642 |
+
document.getElementById('alpha-slider').oninput = (e) => {
|
| 643 |
+
alpha = parseFloat(e.target.value);
|
| 644 |
+
document.getElementById('alpha-value').innerText = alpha.toFixed(2);
|
| 645 |
+
updateVisuals();
|
| 646 |
+
predict();
|
| 647 |
+
};
|
| 648 |
+
|
| 649 |
+
// 3D Interaction Logic
|
| 650 |
+
let isMouseDown = false;
|
| 651 |
+
let mouseButton = 0;
|
| 652 |
+
let prevMouse = { x: 0, y: 0 };
|
| 653 |
+
|
| 654 |
+
container.addEventListener('mousedown', (e) => {
|
| 655 |
+
isMouseDown = true;
|
| 656 |
+
mouseButton = e.button;
|
| 657 |
+
prevMouse = { x: e.clientX, y: e.clientY };
|
| 658 |
+
});
|
| 659 |
+
|
| 660 |
+
window.addEventListener('mouseup', () => isMouseDown = false);
|
| 661 |
+
container.addEventListener('contextmenu', (e) => e.preventDefault());
|
| 662 |
+
|
| 663 |
+
container.addEventListener('mousemove', (e) => {
|
| 664 |
+
if (!isMouseDown) return;
|
| 665 |
+
const dx = e.clientX - prevMouse.x;
|
| 666 |
+
const dy = e.clientY - prevMouse.y;
|
| 667 |
+
|
| 668 |
+
// Left Click (0): Rotate Camera
|
| 669 |
+
if (mouseButton === 0 && !e.shiftKey) {
|
| 670 |
+
cameraTheta -= dx * 0.01;
|
| 671 |
+
cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi - dy * 0.01));
|
| 672 |
+
updateCameraPosition();
|
| 673 |
+
}
|
| 674 |
+
// Shift + Drag: Lift Object
|
| 675 |
+
else if (e.shiftKey) {
|
| 676 |
+
mysteryFruitMesh.position.y -= dy * 0.1;
|
| 677 |
+
mysteryFruitMesh.position.y = Math.max(-10, Math.min(10, mysteryFruitMesh.position.y));
|
| 678 |
+
predict();
|
| 679 |
+
}
|
| 680 |
+
// Right Click (2): Move Object Plane
|
| 681 |
+
else if (mouseButton === 2 || (mouseButton === 0 && e.ctrlKey)) {
|
| 682 |
+
// Move relative to camera view roughly
|
| 683 |
+
const speed = 0.1;
|
| 684 |
+
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
| 685 |
+
forward.y = 0; forward.normalize();
|
| 686 |
+
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
|
| 687 |
+
right.y = 0; right.normalize();
|
| 688 |
+
|
| 689 |
+
mysteryFruitMesh.position.addScaledVector(right, dx * speed);
|
| 690 |
+
mysteryFruitMesh.position.addScaledVector(forward, dy * speed);
|
| 691 |
+
|
| 692 |
+
// Bounds
|
| 693 |
+
['x', 'y', 'z'].forEach(axis => {
|
| 694 |
+
mysteryFruitMesh.position[axis] = Math.max(-14, Math.min(14, mysteryFruitMesh.position[axis]));
|
| 695 |
+
});
|
| 696 |
+
|
| 697 |
+
predict();
|
| 698 |
+
}
|
| 699 |
+
prevMouse = { x: e.clientX, y: e.clientY };
|
| 700 |
+
});
|
| 701 |
+
|
| 702 |
+
// Zoom
|
| 703 |
+
container.addEventListener('wheel', (e) => {
|
| 704 |
+
e.preventDefault();
|
| 705 |
+
cameraRadius = Math.max(10, Math.min(60, cameraRadius + e.deltaY * 0.05));
|
| 706 |
+
updateCameraPosition();
|
| 707 |
+
}, { passive: false });
|
| 708 |
+
|
| 709 |
+
window.addEventListener('resize', () => {
|
| 710 |
+
camera.aspect = container.clientWidth / container.clientHeight;
|
| 711 |
+
camera.updateProjectionMatrix();
|
| 712 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 713 |
+
});
|
| 714 |
+
|
| 715 |
+
// Animation Loop
|
| 716 |
+
function animate() {
|
| 717 |
+
requestAnimationFrame(animate);
|
| 718 |
+
// Spin the ring
|
| 719 |
+
ring.rotation.z -= 0.02;
|
| 720 |
+
renderer.render(scene, camera);
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
// Init
|
| 724 |
+
generateData();
|
| 725 |
+
predict();
|
| 726 |
+
animate();
|
| 727 |
+
updateUI(50, 50); // Initial neutral state
|
| 728 |
+
</script>
|
| 729 |
+
</body>
|
| 730 |
+
</html>
|
templates/Neural-Networks-for-Classification-three.html
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Neural Network Playground</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<!-- Lucide Icons -->
|
| 9 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 10 |
+
|
| 11 |
+
<style>
|
| 12 |
+
:root {
|
| 13 |
+
--background: 222 47% 6%;
|
| 14 |
+
--foreground: 210 40% 96%;
|
| 15 |
+
--card: 222 47% 8%;
|
| 16 |
+
--primary: 199 89% 48%;
|
| 17 |
+
--primary-foreground: 222 47% 6%;
|
| 18 |
+
--secondary: 280 65% 55%;
|
| 19 |
+
--secondary-foreground: 210 40% 98%;
|
| 20 |
+
--muted: 217 33% 15%;
|
| 21 |
+
--muted-foreground: 215 20% 55%;
|
| 22 |
+
--accent: 142 71% 45%;
|
| 23 |
+
--destructive: 0 84% 60%;
|
| 24 |
+
--destructive-foreground: 210 40% 98%;
|
| 25 |
+
--border: 217 33% 20%;
|
| 26 |
+
|
| 27 |
+
/* Custom neural network colors */
|
| 28 |
+
--node-input: 199 89% 48%;
|
| 29 |
+
--node-hidden: 280 65% 55%;
|
| 30 |
+
--node-positive: 142 71% 45%; /* Class A (Green) */
|
| 31 |
+
--node-negative: 350 89% 60%; /* Class B (Red) */
|
| 32 |
+
|
| 33 |
+
--radius: 0.75rem;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
body {
|
| 37 |
+
background-color: hsl(var(--background));
|
| 38 |
+
color: hsl(var(--foreground));
|
| 39 |
+
font-family: system-ui, -apple-system, sans-serif;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.glass-panel {
|
| 43 |
+
background-color: hsla(var(--card), 0.6);
|
| 44 |
+
backdrop-filter: blur(16px);
|
| 45 |
+
border: 1px solid hsla(var(--border), 0.5);
|
| 46 |
+
border-radius: var(--radius);
|
| 47 |
+
box-shadow: 0 0 30px hsl(199 89% 48% / 0.1);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.gradient-text {
|
| 51 |
+
background: linear-gradient(135deg, hsl(199 89% 48%) 0%, hsl(280 65% 55%) 100%);
|
| 52 |
+
-webkit-background-clip: text;
|
| 53 |
+
-webkit-text-fill-color: transparent;
|
| 54 |
+
background-clip: text;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Animation Utilities */
|
| 58 |
+
@keyframes flow {
|
| 59 |
+
0% { stroke-dashoffset: 20; }
|
| 60 |
+
100% { stroke-dashoffset: 0; }
|
| 61 |
+
}
|
| 62 |
+
.animate-flow {
|
| 63 |
+
animation: flow 1s linear infinite;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
@keyframes pulse-glow {
|
| 67 |
+
0%, 100% { opacity: 1; filter: brightness(1); }
|
| 68 |
+
50% { opacity: 0.7; filter: brightness(1.2); }
|
| 69 |
+
}
|
| 70 |
+
.animate-node-pulse {
|
| 71 |
+
animation: pulse-glow 2s ease-in-out infinite;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Custom Scrollbar */
|
| 75 |
+
::-webkit-scrollbar { width: 8px; }
|
| 76 |
+
::-webkit-scrollbar-track { background: hsl(var(--background)); }
|
| 77 |
+
::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 4px; }
|
| 78 |
+
::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground)); }
|
| 79 |
+
|
| 80 |
+
input[type=range] {
|
| 81 |
+
-webkit-appearance: none;
|
| 82 |
+
background: transparent;
|
| 83 |
+
}
|
| 84 |
+
input[type=range]::-webkit-slider-thumb {
|
| 85 |
+
-webkit-appearance: none;
|
| 86 |
+
height: 16px;
|
| 87 |
+
width: 16px;
|
| 88 |
+
border-radius: 50%;
|
| 89 |
+
background: hsl(var(--primary));
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
margin-top: -6px;
|
| 92 |
+
}
|
| 93 |
+
input[type=range]::-webkit-slider-runnable-track {
|
| 94 |
+
width: 100%;
|
| 95 |
+
height: 4px;
|
| 96 |
+
cursor: pointer;
|
| 97 |
+
background: hsl(var(--muted));
|
| 98 |
+
border-radius: 2px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.btn {
|
| 102 |
+
display: inline-flex;
|
| 103 |
+
align-items: center;
|
| 104 |
+
justify-content: center;
|
| 105 |
+
border-radius: 0.5rem;
|
| 106 |
+
font-size: 0.875rem;
|
| 107 |
+
font-weight: 500;
|
| 108 |
+
transition-colors: 0.15s;
|
| 109 |
+
cursor: pointer;
|
| 110 |
+
}
|
| 111 |
+
.btn:disabled {
|
| 112 |
+
opacity: 0.5;
|
| 113 |
+
pointer-events: none;
|
| 114 |
+
}
|
| 115 |
+
.btn-glass { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: white; }
|
| 116 |
+
.btn-glass:hover { background: rgba(255,255,255,0.1); }
|
| 117 |
+
.btn-accent { background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
|
| 118 |
+
.btn-destructive { background: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
|
| 119 |
+
.btn-glow {
|
| 120 |
+
background: hsl(var(--primary));
|
| 121 |
+
color: white;
|
| 122 |
+
box-shadow: 0 0 15px hsl(var(--primary)/0.5);
|
| 123 |
+
}
|
| 124 |
+
.btn-glow:hover { box-shadow: 0 0 25px hsl(var(--primary)/0.6); }
|
| 125 |
+
|
| 126 |
+
.tab-btn {
|
| 127 |
+
flex: 1;
|
| 128 |
+
padding: 0.375rem;
|
| 129 |
+
font-size: 0.875rem;
|
| 130 |
+
font-weight: 500;
|
| 131 |
+
border-radius: 0.375rem;
|
| 132 |
+
transition: all 0.2s;
|
| 133 |
+
color: hsl(var(--muted-foreground));
|
| 134 |
+
}
|
| 135 |
+
.tab-btn.active {
|
| 136 |
+
background-color: hsl(var(--card));
|
| 137 |
+
color: hsl(var(--primary));
|
| 138 |
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 139 |
+
}
|
| 140 |
+
</style>
|
| 141 |
+
</head>
|
| 142 |
+
<body class="min-h-screen p-4 selection:bg-[hsl(var(--primary))] selection:text-white">
|
| 143 |
+
|
| 144 |
+
<!-- Background Ambience -->
|
| 145 |
+
<div class="fixed inset-0 pointer-events-none overflow-hidden -z-10">
|
| 146 |
+
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[hsl(var(--primary)/0.1)] rounded-full blur-[100px]"></div>
|
| 147 |
+
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[hsl(var(--secondary)/0.1)] rounded-full blur-[100px]"></div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<!-- Header -->
|
| 151 |
+
<header class="relative z-10 border-b border-white/10 bg-[hsl(var(--background)/0.8)] backdrop-blur-md sticky top-0 mb-8 rounded-xl">
|
| 152 |
+
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
| 153 |
+
<div class="flex items-center gap-3">
|
| 154 |
+
<div class="p-2 rounded-xl bg-[hsl(var(--primary)/0.2)] animate-pulse">
|
| 155 |
+
<i data-lucide="brain" class="h-6 w-6 text-[hsl(var(--primary))]"></i>
|
| 156 |
+
</div>
|
| 157 |
+
<div>
|
| 158 |
+
<h1 class="text-xl font-bold gradient-text">Neural Network Playground</h1>
|
| 159 |
+
<div class="absolute left-1/2 -translate-x-1/2 flex items-center"> <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio> <a href="/neural-network-classification" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"> Back to Core </a> </div>
|
| 160 |
+
<p class="text-xs text-[hsl(var(--muted-foreground))]">Interactive Classification Visualizer</p>
|
| 161 |
+
<p class="text-xxl p-3 text-[hsl(var(--muted-foreground))]">After training you cant train agian and cant change output so if you want add a custom data in predefine data so add before training</p>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="hidden md:flex items-center gap-4 text-sm text-[hsl(var(--muted-foreground))]">
|
| 165 |
+
<div class="flex items-center gap-2 bg-white/5 px-3 py-1.5 rounded-full">
|
| 166 |
+
<i data-lucide="layers" class="h-4 w-4"></i>
|
| 167 |
+
<span id="header-neurons-count">4 hidden neurons</span>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</header>
|
| 172 |
+
|
| 173 |
+
<!-- Main Content -->
|
| 174 |
+
<main class="relative z-10 max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-6">
|
| 175 |
+
|
| 176 |
+
<!-- Left Sidebar: Controls -->
|
| 177 |
+
<div class="lg:col-span-3 space-y-6">
|
| 178 |
+
<!-- Control Panel -->
|
| 179 |
+
<div class="glass-panel p-5 space-y-5">
|
| 180 |
+
<div>
|
| 181 |
+
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">Data Class</h3>
|
| 182 |
+
<div class="flex gap-2">
|
| 183 |
+
<button onclick="setClass(1)" id="btn-class-a" class="btn btn-accent h-9 px-3 flex-1 text-sm">
|
| 184 |
+
<div class="w-3 h-3 rounded-full bg-[hsl(var(--node-positive))] mr-2"></div> Class A
|
| 185 |
+
</button>
|
| 186 |
+
<button onclick="setClass(0)" id="btn-class-b" class="btn btn-glass h-9 px-3 flex-1 text-sm">
|
| 187 |
+
<div class="w-3 h-3 rounded-full bg-[hsl(var(--node-negative))] mr-2"></div> Class B
|
| 188 |
+
</button>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<div>
|
| 193 |
+
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">
|
| 194 |
+
Hidden Neurons: <span id="neurons-display" class="text-[hsl(var(--primary))]">4</span>
|
| 195 |
+
</h3>
|
| 196 |
+
<div class="flex items-center gap-3">
|
| 197 |
+
<button onclick="changeNeurons(-1)" class="btn btn-glass h-10 w-10 p-0"><i data-lucide="minus" class="h-4 w-4"></i></button>
|
| 198 |
+
<input type="range" min="1" max="8" value="4" class="flex-1" id="neurons-slider" oninput="changeNeuronsFromSlider(this.value)">
|
| 199 |
+
<button onclick="changeNeurons(1)" class="btn btn-glass h-10 w-10 p-0"><i data-lucide="plus" class="h-4 w-4"></i></button>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<div>
|
| 204 |
+
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">
|
| 205 |
+
Learning Rate: <span id="lr-display" class="text-[hsl(var(--secondary))]">0.50</span>
|
| 206 |
+
</h3>
|
| 207 |
+
<input type="range" min="1" max="100" value="50" class="w-full" id="lr-slider" oninput="changeLR(this.value)">
|
| 208 |
+
</div>
|
| 209 |
+
|
| 210 |
+
<div class="flex gap-2">
|
| 211 |
+
<button id="btn-train" onclick="toggleTraining()" class="btn btn-glow flex-1 h-10 px-4">
|
| 212 |
+
<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network
|
| 213 |
+
</button>
|
| 214 |
+
<button onclick="resetApp()" class="btn btn-glass h-10 w-10 p-0">
|
| 215 |
+
<i data-lucide="rotate-ccw" class="h-4 w-4"></i>
|
| 216 |
+
</button>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<div id="accuracy-panel" class="text-center py-3 rounded-lg bg-white/5 hidden">
|
| 220 |
+
<span class="text-sm text-[hsl(var(--muted-foreground))]">Accuracy: </span>
|
| 221 |
+
<span id="accuracy-display" class="text-lg font-bold">0.0%</span>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
<!-- Presets -->
|
| 226 |
+
<div class="glass-panel p-4 space-y-3">
|
| 227 |
+
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))]">Presets</h3>
|
| 228 |
+
<div class="grid grid-cols-2 gap-2">
|
| 229 |
+
<button onclick="loadPreset('Linear')" class="btn btn-glass flex flex-col h-auto py-3">
|
| 230 |
+
<i data-lucide="waves" class="h-4 w-4 mb-1"></i> <span class="text-xs">Linear</span>
|
| 231 |
+
</button>
|
| 232 |
+
<button onclick="loadPreset('XOR')" class="btn btn-glass flex flex-col h-auto py-3">
|
| 233 |
+
<i data-lucide="target" class="h-4 w-4 mb-1"></i> <span class="text-xs">XOR</span>
|
| 234 |
+
</button>
|
| 235 |
+
<button onclick="loadPreset('Circle')" class="btn btn-glass flex flex-col h-auto py-3">
|
| 236 |
+
<i data-lucide="circle" class="h-4 w-4 mb-1"></i> <span class="text-xs">Circle</span>
|
| 237 |
+
</button>
|
| 238 |
+
<button onclick="loadPreset('Spiral')" class="btn btn-glass flex flex-col h-auto py-3">
|
| 239 |
+
<i data-lucide="sparkles" class="h-4 w-4 mb-1"></i> <span class="text-xs">Spiral</span>
|
| 240 |
+
</button>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
+
<!-- Logs -->
|
| 245 |
+
<div class="glass-panel p-4 space-y-3">
|
| 246 |
+
<div class="flex justify-between items-center text-sm font-medium text-[hsl(var(--muted-foreground))]">
|
| 247 |
+
<span class="flex items-center gap-2"><i data-lucide="clock" class="h-4 w-4"></i> Training Log</span>
|
| 248 |
+
<span id="epoch-display" class="text-[hsl(var(--primary))] animate-pulse hidden">Epoch 0</span>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="w-full bg-white/10 rounded-full h-1.5 overflow-hidden">
|
| 251 |
+
<div id="progress-bar" class="h-full bg-gradient-to-r from-[hsl(var(--primary))] to-[hsl(var(--secondary))]" style="width: 0%"></div>
|
| 252 |
+
</div>
|
| 253 |
+
<div id="logs-container" class="space-y-1 max-h-32 overflow-y-auto">
|
| 254 |
+
<!-- Logs go here -->
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
<!-- Center: Visualizations -->
|
| 260 |
+
<div class="lg:col-span-6 space-y-6">
|
| 261 |
+
<!-- Network Vis -->
|
| 262 |
+
<div class="glass-panel p-6">
|
| 263 |
+
<div class="flex justify-between items-center mb-4">
|
| 264 |
+
<h2 class="text-lg font-semibold flex items-center gap-2">
|
| 265 |
+
<i data-lucide="brain" class="h-5 w-5 text-[hsl(var(--primary))]"></i> Network Architecture
|
| 266 |
+
</h2>
|
| 267 |
+
</div>
|
| 268 |
+
<div class="flex justify-center" id="network-container">
|
| 269 |
+
<!-- SVG Network goes here -->
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
|
| 273 |
+
<!-- Data Canvas -->
|
| 274 |
+
<div class="glass-panel p-6">
|
| 275 |
+
<div class="flex justify-between items-center mb-4">
|
| 276 |
+
<h2 class="text-lg font-semibold">Data & Decision Boundary</h2>
|
| 277 |
+
<span class="text-xs px-2 py-1 bg-white/10 rounded text-[hsl(var(--primary))] font-mono">Points: <span id="points-count">0</span></span>
|
| 278 |
+
</div>
|
| 279 |
+
<div class="flex justify-center relative">
|
| 280 |
+
<div class="relative">
|
| 281 |
+
<canvas id="main-canvas" width="300" height="300" class="rounded-lg border border-white/10 cursor-crosshair shadow-2xl bg-black"></canvas>
|
| 282 |
+
<div class="absolute -bottom-6 left-0 right-0 text-center text-xs text-muted-foreground text-[hsl(var(--muted-foreground))]">X Coordinate</div>
|
| 283 |
+
<div class="absolute -left-6 top-1/2 -translate-y-1/2 -rotate-90 text-xs text-muted-foreground text-[hsl(var(--muted-foreground))]">Y Coordinate</div>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
<div class="mt-4 flex flex-wrap justify-center gap-4 text-xs text-[hsl(var(--muted-foreground))]">
|
| 287 |
+
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-[hsl(var(--node-positive))]"></div> Class A</div>
|
| 288 |
+
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-[hsl(var(--node-negative))]"></div> Class B</div>
|
| 289 |
+
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[hsl(var(--node-positive))/0.3]"></div> Prediction A</div>
|
| 290 |
+
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[hsl(var(--node-negative))/0.3]"></div> Prediction B</div>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<!-- Right Sidebar: Explainers -->
|
| 296 |
+
<div class="lg:col-span-3 space-y-6">
|
| 297 |
+
<div class="w-full">
|
| 298 |
+
<div class="flex bg-white/5 p-1 rounded-lg mb-4">
|
| 299 |
+
<button onclick="switchTab('howItWorks')" id="tab-howItWorks" class="tab-btn active"><i data-lucide="lightbulb" class="h-3 w-3 mr-1 inline"></i> How It Works</button>
|
| 300 |
+
<button onclick="switchTab('learn')" id="tab-learn" class="tab-btn"><i data-lucide="sparkles" class="h-3 w-3 mr-1 inline"></i> Learn</button>
|
| 301 |
+
</div>
|
| 302 |
+
|
| 303 |
+
<div id="content-howItWorks" class="glass-panel p-5 space-y-4">
|
| 304 |
+
<h3 class="text-lg font-semibold gradient-text">Live Prediction (Hover)</h3>
|
| 305 |
+
<div class="space-y-4 text-sm">
|
| 306 |
+
<div>
|
| 307 |
+
<div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--node-input))]">
|
| 308 |
+
<span class="w-5 h-5 rounded-full bg-[hsl(var(--node-input))/0.2] flex items-center justify-center text-xs">1</span> Input
|
| 309 |
+
</div>
|
| 310 |
+
<div class="bg-white/5 p-3 rounded-lg border border-white/10 font-mono text-xs">
|
| 311 |
+
X: <span id="val-x">0.00</span><br>
|
| 312 |
+
Y: <span id="val-y">0.00</span>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
<div>
|
| 316 |
+
<div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--node-hidden))]">
|
| 317 |
+
<span class="w-5 h-5 rounded-full bg-[hsl(var(--node-hidden))/0.2] flex items-center justify-center text-xs">2</span> Hidden Layer
|
| 318 |
+
</div>
|
| 319 |
+
<div id="val-hidden" class="bg-white/5 p-3 rounded-lg border border-white/10 font-mono text-xs grid grid-cols-4 gap-1">
|
| 320 |
+
<!-- Hidden values -->
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
<div>
|
| 324 |
+
<div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--accent))]">
|
| 325 |
+
<span class="w-5 h-5 rounded-full bg-[hsl(var(--accent))/0.2] flex items-center justify-center text-xs">3</span> Output
|
| 326 |
+
</div>
|
| 327 |
+
<div class="bg-white/5 p-3 rounded-lg border border-white/10">
|
| 328 |
+
<div class="flex justify-between items-center">
|
| 329 |
+
<span class="text-xs text-gray-400">Raw: <span id="val-raw">0.0000</span></span>
|
| 330 |
+
<span id="val-class" class="font-bold text-gray-500">-</span>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
|
| 337 |
+
<div id="content-learn" class="glass-panel p-5 space-y-4 hidden">
|
| 338 |
+
<div class="flex items-center gap-3">
|
| 339 |
+
<div class="p-2 rounded-lg bg-[hsl(var(--primary))/0.2]"><i data-lucide="brain" class="h-5 w-5 text-[hsl(var(--primary))]"></i></div>
|
| 340 |
+
<h3 class="font-semibold gradient-text">Training Process</h3>
|
| 341 |
+
</div>
|
| 342 |
+
<p class="text-sm text-gray-300 leading-relaxed">
|
| 343 |
+
The network learns by "Backpropagation". It compares its guess to the real label, finds the error, and adjusts the weights backwards from output to input.
|
| 344 |
+
</p>
|
| 345 |
+
<div class="p-3 rounded-lg bg-white/5 text-xs text-gray-400 border border-white/10">
|
| 346 |
+
💡 <strong>Tip:</strong> If the network gets stuck, try increasing neurons or clicking "Reset" to randomize weights.
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
</main>
|
| 352 |
+
|
| 353 |
+
<script>
|
| 354 |
+
// --- Neural Network Logic ---
|
| 355 |
+
class SimpleNeuralNetwork {
|
| 356 |
+
constructor(inputSize, hiddenSize, outputSize, learningRate) {
|
| 357 |
+
this.inputSize = inputSize;
|
| 358 |
+
this.hiddenSize = hiddenSize;
|
| 359 |
+
this.outputSize = outputSize;
|
| 360 |
+
this.learningRate = learningRate;
|
| 361 |
+
|
| 362 |
+
// Xavier initialization
|
| 363 |
+
const scale1 = Math.sqrt(2 / (this.inputSize + this.hiddenSize));
|
| 364 |
+
this.w1 = Array(this.hiddenSize).fill(0).map(() =>
|
| 365 |
+
Array(this.inputSize).fill(0).map(() => (Math.random() * 2 - 1) * scale1)
|
| 366 |
+
);
|
| 367 |
+
this.b1 = Array(this.hiddenSize).fill(0);
|
| 368 |
+
|
| 369 |
+
const scale2 = Math.sqrt(2 / (this.hiddenSize + this.outputSize));
|
| 370 |
+
this.w2 = Array(this.outputSize).fill(0).map(() =>
|
| 371 |
+
Array(this.hiddenSize).fill(0).map(() => (Math.random() * 2 - 1) * scale2)
|
| 372 |
+
);
|
| 373 |
+
this.b2 = Array(this.outputSize).fill(0);
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
|
| 377 |
+
sigmoidDeriv(y) { return y * (1 - y); }
|
| 378 |
+
|
| 379 |
+
forward(inputs) {
|
| 380 |
+
const hActivations = this.w1.map((weights, i) =>
|
| 381 |
+
this.sigmoid(weights.reduce((acc, w, j) => acc + w * inputs[j], 0) + this.b1[i])
|
| 382 |
+
);
|
| 383 |
+
const outputs = this.w2.map((weights, i) =>
|
| 384 |
+
this.sigmoid(weights.reduce((acc, w, j) => acc + w * hActivations[j], 0) + this.b2[i])
|
| 385 |
+
);
|
| 386 |
+
return { activations: [[...inputs], hActivations, outputs], output: outputs[0] };
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
predict(x, y) { return this.forward([x, y]).output; }
|
| 390 |
+
|
| 391 |
+
train(data, batchSize) {
|
| 392 |
+
for(let k = 0; k < batchSize * 5; k++) {
|
| 393 |
+
const point = data[Math.floor(Math.random() * data.length)];
|
| 394 |
+
const inputs = [point.x, point.y];
|
| 395 |
+
const target = [point.label];
|
| 396 |
+
|
| 397 |
+
const { activations } = this.forward(inputs);
|
| 398 |
+
const hActivations = activations[1];
|
| 399 |
+
const outputs = activations[2];
|
| 400 |
+
|
| 401 |
+
const outputErrors = outputs.map((o, i) => target[i] - o);
|
| 402 |
+
const outputGradients = outputs.map((o, i) => outputErrors[i] * this.sigmoidDeriv(o));
|
| 403 |
+
|
| 404 |
+
const hiddenErrors = this.w1.map((_, i) =>
|
| 405 |
+
this.w2.reduce((acc, weights, j) => acc + weights[i] * outputGradients[j], 0)
|
| 406 |
+
);
|
| 407 |
+
const hiddenGradients = hActivations.map((h, i) => hiddenErrors[i] * this.sigmoidDeriv(h));
|
| 408 |
+
|
| 409 |
+
for(let i=0; i<this.outputSize; i++) {
|
| 410 |
+
for(let j=0; j<this.hiddenSize; j++) {
|
| 411 |
+
this.w2[i][j] += this.learningRate * outputGradients[i] * hActivations[j];
|
| 412 |
+
}
|
| 413 |
+
this.b2[i] += this.learningRate * outputGradients[i];
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
for(let i=0; i<this.hiddenSize; i++) {
|
| 417 |
+
for(let j=0; j<this.inputSize; j++) {
|
| 418 |
+
this.w1[i][j] += this.learningRate * hiddenGradients[i] * inputs[j];
|
| 419 |
+
}
|
| 420 |
+
this.b1[i] += this.learningRate * hiddenGradients[i];
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
getWeights() { return [this.w1, this.w2]; }
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
// --- Application State ---
|
| 428 |
+
let state = {
|
| 429 |
+
dataPoints: [],
|
| 430 |
+
currentClass: 1,
|
| 431 |
+
hiddenNeurons: 4,
|
| 432 |
+
learningRate: 0.5,
|
| 433 |
+
isTraining: false,
|
| 434 |
+
network: null,
|
| 435 |
+
epoch: 0,
|
| 436 |
+
accuracy: 0,
|
| 437 |
+
activations: null,
|
| 438 |
+
predictions: [], // Store heatmap data here
|
| 439 |
+
logs: [],
|
| 440 |
+
lastProbe: { x: 0, y: 0 } // Track last cursor position for live updates
|
| 441 |
+
};
|
| 442 |
+
|
| 443 |
+
// --- Helper: Generate Grid Predictions ---
|
| 444 |
+
function generatePredictions() {
|
| 445 |
+
const gridSize = 30;
|
| 446 |
+
const grid = [];
|
| 447 |
+
for (let i = 0; i < gridSize; i++) {
|
| 448 |
+
const row = [];
|
| 449 |
+
for (let j = 0; j < gridSize; j++) {
|
| 450 |
+
const x = (j / gridSize) * 2 - 1;
|
| 451 |
+
const y = 1 - (i / gridSize) * 2;
|
| 452 |
+
row.push(state.network.predict(x, y));
|
| 453 |
+
}
|
| 454 |
+
grid.push(row);
|
| 455 |
+
}
|
| 456 |
+
return grid;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
// --- Initialization ---
|
| 460 |
+
function init() {
|
| 461 |
+
lucide.createIcons();
|
| 462 |
+
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
|
| 463 |
+
state.predictions = generatePredictions(); // Initial heatmap
|
| 464 |
+
setupCanvas();
|
| 465 |
+
renderUI();
|
| 466 |
+
|
| 467 |
+
// Set initial dummy activations
|
| 468 |
+
state.activations = [[0,0], Array(state.hiddenNeurons).fill(0), [0]];
|
| 469 |
+
updateExplainers();
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
// --- UI Updates ---
|
| 473 |
+
function setClass(c) {
|
| 474 |
+
state.currentClass = c;
|
| 475 |
+
const btnA = document.getElementById('btn-class-a');
|
| 476 |
+
const btnB = document.getElementById('btn-class-b');
|
| 477 |
+
|
| 478 |
+
if(c === 1) {
|
| 479 |
+
btnA.classList.remove('btn-glass'); btnA.classList.add('btn-accent');
|
| 480 |
+
btnB.classList.add('btn-glass'); btnB.classList.remove('btn-destructive');
|
| 481 |
+
} else {
|
| 482 |
+
btnA.classList.add('btn-glass'); btnA.classList.remove('btn-accent');
|
| 483 |
+
btnB.classList.remove('btn-glass'); btnB.classList.add('btn-destructive');
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
function changeNeurons(delta) {
|
| 488 |
+
const newVal = Math.max(1, Math.min(8, state.hiddenNeurons + delta));
|
| 489 |
+
state.hiddenNeurons = newVal;
|
| 490 |
+
document.getElementById('neurons-slider').value = newVal;
|
| 491 |
+
updateNeuronsUI();
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
function changeNeuronsFromSlider(val) {
|
| 495 |
+
state.hiddenNeurons = parseInt(val);
|
| 496 |
+
updateNeuronsUI();
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
function updateNeuronsUI() {
|
| 500 |
+
document.getElementById('neurons-display').innerText = state.hiddenNeurons;
|
| 501 |
+
document.getElementById('header-neurons-count').innerText = state.hiddenNeurons + " hidden neurons";
|
| 502 |
+
resetApp(); // Rebuild network on architecture change
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
function changeLR(val) {
|
| 506 |
+
state.learningRate = val / 100;
|
| 507 |
+
document.getElementById('lr-display').innerText = state.learningRate.toFixed(2);
|
| 508 |
+
if(state.network) state.network.learningRate = state.learningRate;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
function resetApp() {
|
| 512 |
+
state.dataPoints = [];
|
| 513 |
+
state.isTraining = false;
|
| 514 |
+
state.epoch = 0;
|
| 515 |
+
state.accuracy = 0;
|
| 516 |
+
state.logs = [];
|
| 517 |
+
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
|
| 518 |
+
state.predictions = generatePredictions(); // Generate initial random boundary
|
| 519 |
+
state.lastProbe = { x: 0, y: 0 };
|
| 520 |
+
|
| 521 |
+
document.getElementById('points-count').innerText = "0";
|
| 522 |
+
document.getElementById('accuracy-panel').classList.add('hidden');
|
| 523 |
+
document.getElementById('epoch-display').classList.add('hidden');
|
| 524 |
+
document.getElementById('progress-bar').style.width = '0%';
|
| 525 |
+
document.getElementById('logs-container').innerHTML = '';
|
| 526 |
+
|
| 527 |
+
const btnTrain = document.getElementById('btn-train');
|
| 528 |
+
btnTrain.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
|
| 529 |
+
lucide.createIcons();
|
| 530 |
+
|
| 531 |
+
renderCanvas();
|
| 532 |
+
renderNetwork();
|
| 533 |
+
|
| 534 |
+
// Recalc activations for default probe
|
| 535 |
+
state.activations = state.network.forward([0, 0]).activations;
|
| 536 |
+
updateExplainers();
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
function switchTab(tab) {
|
| 540 |
+
document.getElementById('tab-howItWorks').classList.remove('active');
|
| 541 |
+
document.getElementById('tab-learn').classList.remove('active');
|
| 542 |
+
document.getElementById('content-howItWorks').classList.add('hidden');
|
| 543 |
+
document.getElementById('content-learn').classList.add('hidden');
|
| 544 |
+
|
| 545 |
+
document.getElementById('tab-' + tab).classList.add('active');
|
| 546 |
+
document.getElementById('content-' + tab).classList.remove('hidden');
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
// --- Canvas Logic ---
|
| 550 |
+
const canvas = document.getElementById('main-canvas');
|
| 551 |
+
const ctx = canvas.getContext('2d');
|
| 552 |
+
const canvasSize = 300;
|
| 553 |
+
const gridSize = 30;
|
| 554 |
+
|
| 555 |
+
function setupCanvas() {
|
| 556 |
+
canvas.addEventListener('mousedown', handleCanvasClick);
|
| 557 |
+
canvas.addEventListener('mousemove', handleCanvasHover);
|
| 558 |
+
canvas.addEventListener('mouseleave', () => {
|
| 559 |
+
renderCanvas(); // clear hover
|
| 560 |
+
});
|
| 561 |
+
renderCanvas();
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
function handleCanvasClick(e) {
|
| 565 |
+
const rect = canvas.getBoundingClientRect();
|
| 566 |
+
const scaleX = canvas.width / rect.width;
|
| 567 |
+
const scaleY = canvas.height / rect.height;
|
| 568 |
+
const clickX = (e.clientX - rect.left) * scaleX;
|
| 569 |
+
const clickY = (e.clientY - rect.top) * scaleY;
|
| 570 |
+
|
| 571 |
+
const x = (clickX / (canvasSize / 2)) - 1;
|
| 572 |
+
const y = 1 - (clickY / (canvasSize / 2));
|
| 573 |
+
|
| 574 |
+
const point = {
|
| 575 |
+
x: Math.max(-1, Math.min(1, x)),
|
| 576 |
+
y: Math.max(-1, Math.min(1, y)),
|
| 577 |
+
label: state.currentClass
|
| 578 |
+
};
|
| 579 |
+
|
| 580 |
+
state.dataPoints.push(point);
|
| 581 |
+
state.lastProbe = { x, y }; // Update probe
|
| 582 |
+
document.getElementById('points-count').innerText = state.dataPoints.length;
|
| 583 |
+
|
| 584 |
+
// Forward pass for viz
|
| 585 |
+
state.activations = state.network.forward([point.x, point.y]).activations;
|
| 586 |
+
updateExplainers();
|
| 587 |
+
|
| 588 |
+
renderCanvas();
|
| 589 |
+
renderNetwork();
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
function handleCanvasHover(e) {
|
| 593 |
+
const rect = canvas.getBoundingClientRect();
|
| 594 |
+
const scaleX = canvas.width / rect.width;
|
| 595 |
+
const scaleY = canvas.height / rect.height;
|
| 596 |
+
const clickX = (e.clientX - rect.left) * scaleX;
|
| 597 |
+
const clickY = (e.clientY - rect.top) * scaleY;
|
| 598 |
+
|
| 599 |
+
renderCanvas();
|
| 600 |
+
|
| 601 |
+
// Draw hover cursor
|
| 602 |
+
ctx.beginPath();
|
| 603 |
+
ctx.arc(clickX, clickY, 8, 0, Math.PI * 2);
|
| 604 |
+
ctx.strokeStyle = state.currentClass === 1 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
|
| 605 |
+
ctx.setLineDash([4, 4]);
|
| 606 |
+
ctx.stroke();
|
| 607 |
+
ctx.setLineDash([]);
|
| 608 |
+
|
| 609 |
+
// LIVE UPDATE: Calculate network output for current mouse position
|
| 610 |
+
const x = (clickX / (canvasSize / 2)) - 1;
|
| 611 |
+
const y = 1 - (clickY / (canvasSize / 2));
|
| 612 |
+
state.lastProbe = { x, y }; // Update probe tracker
|
| 613 |
+
|
| 614 |
+
if (state.network) {
|
| 615 |
+
state.activations = state.network.forward([x, y]).activations;
|
| 616 |
+
updateExplainers(); // Update the "Output" panel text
|
| 617 |
+
renderNetwork(); // Update the node visualizations/colors
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
function renderCanvas() {
|
| 622 |
+
// Background
|
| 623 |
+
ctx.fillStyle = 'hsl(222, 47%, 8%)';
|
| 624 |
+
ctx.fillRect(0, 0, canvasSize, canvasSize);
|
| 625 |
+
|
| 626 |
+
// Heatmap - Draw if predictions exist
|
| 627 |
+
if (state.predictions && state.predictions.length > 0) {
|
| 628 |
+
const cellSize = canvasSize / gridSize;
|
| 629 |
+
for (let i = 0; i < gridSize; i++) {
|
| 630 |
+
for (let j = 0; j < gridSize; j++) {
|
| 631 |
+
const pred = state.predictions[i][j];
|
| 632 |
+
|
| 633 |
+
const hue = pred > 0.5 ? 142 : 350;
|
| 634 |
+
const lightness = 20 + Math.abs(pred - 0.5) * 40;
|
| 635 |
+
const alpha = 0.3 + Math.abs(pred - 0.5) * 0.4;
|
| 636 |
+
ctx.fillStyle = `hsla(${hue}, 70%, ${lightness}%, ${alpha})`;
|
| 637 |
+
ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize);
|
| 638 |
+
}
|
| 639 |
+
}
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
// Grid
|
| 643 |
+
ctx.strokeStyle = 'hsla(217, 33%, 40%, 0.2)';
|
| 644 |
+
ctx.lineWidth = 1;
|
| 645 |
+
for (let i = 0; i <= canvasSize; i += 30) {
|
| 646 |
+
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvasSize); ctx.stroke();
|
| 647 |
+
ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvasSize, i); ctx.stroke();
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
// Axes
|
| 651 |
+
ctx.strokeStyle = 'hsla(217, 33%, 50%, 0.5)';
|
| 652 |
+
ctx.lineWidth = 2;
|
| 653 |
+
ctx.beginPath(); ctx.moveTo(canvasSize / 2, 0); ctx.lineTo(canvasSize / 2, canvasSize); ctx.stroke();
|
| 654 |
+
ctx.beginPath(); ctx.moveTo(0, canvasSize / 2); ctx.lineTo(canvasSize, canvasSize / 2); ctx.stroke();
|
| 655 |
+
|
| 656 |
+
// Points
|
| 657 |
+
state.dataPoints.forEach(point => {
|
| 658 |
+
const drawX = (point.x + 1) * (canvasSize / 2);
|
| 659 |
+
const drawY = (1 - point.y) * (canvasSize / 2);
|
| 660 |
+
|
| 661 |
+
ctx.beginPath();
|
| 662 |
+
ctx.arc(drawX, drawY, 6, 0, Math.PI * 2);
|
| 663 |
+
ctx.fillStyle = point.label === 1 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
|
| 664 |
+
ctx.fill();
|
| 665 |
+
ctx.strokeStyle = 'white';
|
| 666 |
+
ctx.lineWidth = 2;
|
| 667 |
+
ctx.stroke();
|
| 668 |
+
});
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
// --- Network Visualizer (SVG) ---
|
| 672 |
+
function renderNetwork() {
|
| 673 |
+
const container = document.getElementById('network-container');
|
| 674 |
+
const width = 500;
|
| 675 |
+
const height = 300;
|
| 676 |
+
const layers = [2, state.hiddenNeurons, 1];
|
| 677 |
+
const layerSpacing = (width - 100) / (layers.length - 1);
|
| 678 |
+
|
| 679 |
+
let svgHtml = `<svg width="${width}" height="${height}" style="overflow: visible;">`;
|
| 680 |
+
|
| 681 |
+
// Calculate positions
|
| 682 |
+
const nodePositions = [];
|
| 683 |
+
layers.forEach((count, layerIdx) => {
|
| 684 |
+
const x = 50 + layerIdx * layerSpacing;
|
| 685 |
+
const maxNodes = Math.max(...layers);
|
| 686 |
+
const vSpacing = (height - 100) / (maxNodes + 1);
|
| 687 |
+
const offset = ((maxNodes - count) * vSpacing) / 2;
|
| 688 |
+
for(let i=0; i<count; i++) {
|
| 689 |
+
nodePositions.push({
|
| 690 |
+
x: x,
|
| 691 |
+
y: 50 + offset + (i+1) * vSpacing,
|
| 692 |
+
layer: layerIdx,
|
| 693 |
+
index: i
|
| 694 |
+
});
|
| 695 |
+
}
|
| 696 |
+
});
|
| 697 |
+
|
| 698 |
+
// Draw Connections
|
| 699 |
+
const weights = state.network.getWeights();
|
| 700 |
+
let fromIndex = 0;
|
| 701 |
+
for(let l=0; l<layers.length-1; l++) {
|
| 702 |
+
const fromCount = layers[l];
|
| 703 |
+
const toCount = layers[l+1];
|
| 704 |
+
const toStartIndex = fromIndex + fromCount;
|
| 705 |
+
|
| 706 |
+
for(let i=0; i<fromCount; i++) {
|
| 707 |
+
for(let j=0; j<toCount; j++) {
|
| 708 |
+
const w = weights[l][j][i];
|
| 709 |
+
const fromNode = nodePositions[fromIndex + i];
|
| 710 |
+
const toNode = nodePositions[toStartIndex + j];
|
| 711 |
+
const opacity = Math.min(0.8, 0.1 + Math.abs(w) * 0.3);
|
| 712 |
+
const color = w > 0 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
|
| 713 |
+
const dash = state.isTraining ? 'stroke-dasharray="4 4" class="animate-flow"' : '';
|
| 714 |
+
|
| 715 |
+
svgHtml += `<line x1="${fromNode.x}" y1="${fromNode.y}" x2="${toNode.x}" y2="${toNode.y}" stroke="${color}" stroke-width="${1 + Math.abs(w)}" stroke-opacity="${opacity}" ${dash} />`;
|
| 716 |
+
}
|
| 717 |
+
}
|
| 718 |
+
fromIndex += fromCount;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
// Draw Nodes
|
| 722 |
+
nodePositions.forEach(node => {
|
| 723 |
+
let activation = 0;
|
| 724 |
+
if(state.activations) {
|
| 725 |
+
activation = state.activations[node.layer][node.index];
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
let color = 'hsl(280, 65%, 55%)'; // hidden
|
| 729 |
+
if(node.layer === 0) color = 'hsl(199, 89%, 48%)'; // input
|
| 730 |
+
if(node.layer === layers.length - 1) {
|
| 731 |
+
color = activation > 0.5 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
const r = 10 + (activation * 5);
|
| 735 |
+
const pulseClass = state.isTraining ? 'class="animate-node-pulse"' : '';
|
| 736 |
+
|
| 737 |
+
svgHtml += `<circle cx="${node.x}" cy="${node.y}" r="${r+4}" fill="none" stroke="${color}" stroke-opacity="0.3" ${pulseClass} />`;
|
| 738 |
+
svgHtml += `<circle cx="${node.x}" cy="${node.y}" r="${r}" fill="${color}" />`;
|
| 739 |
+
svgHtml += `<text x="${node.x}" y="${node.y - r - 5}" text-anchor="middle" fill="white" font-size="9">${activation.toFixed(2)}</text>`;
|
| 740 |
+
});
|
| 741 |
+
|
| 742 |
+
svgHtml += `</svg>`;
|
| 743 |
+
container.innerHTML = svgHtml;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
// --- Explainers ---
|
| 747 |
+
function updateExplainers() {
|
| 748 |
+
if(!state.activations) return;
|
| 749 |
+
|
| 750 |
+
// Input
|
| 751 |
+
document.getElementById('val-x').innerText = state.activations[0][0].toFixed(2);
|
| 752 |
+
document.getElementById('val-y').innerText = state.activations[0][1].toFixed(2);
|
| 753 |
+
|
| 754 |
+
// Hidden
|
| 755 |
+
const hiddenContainer = document.getElementById('val-hidden');
|
| 756 |
+
let hiddenHtml = '';
|
| 757 |
+
state.activations[1].forEach(v => {
|
| 758 |
+
const cls = v > 0.5 ? 'bg-white/10 text-white' : 'text-gray-500';
|
| 759 |
+
hiddenHtml += `<div class="text-center p-1 rounded ${cls}">${v.toFixed(1)}</div>`;
|
| 760 |
+
});
|
| 761 |
+
hiddenContainer.innerHTML = hiddenHtml;
|
| 762 |
+
|
| 763 |
+
// Output
|
| 764 |
+
const outVal = state.activations[2][0];
|
| 765 |
+
document.getElementById('val-raw').innerText = outVal.toFixed(4);
|
| 766 |
+
const classEl = document.getElementById('val-class');
|
| 767 |
+
|
| 768 |
+
// Direct style application
|
| 769 |
+
if(outVal > 0.5) {
|
| 770 |
+
classEl.innerText = "Class A";
|
| 771 |
+
classEl.style.color = "hsl(142, 71%, 45%)"; // Green
|
| 772 |
+
} else {
|
| 773 |
+
classEl.innerText = "Class B";
|
| 774 |
+
classEl.style.color = "hsl(350, 89%, 60%)"; // Red
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
// --- Training Loop ---
|
| 779 |
+
function toggleTraining() {
|
| 780 |
+
if(state.dataPoints.length < 2) {
|
| 781 |
+
alert("Please add at least 2 data points first!");
|
| 782 |
+
return;
|
| 783 |
+
}
|
| 784 |
+
state.isTraining = !state.isTraining;
|
| 785 |
+
const btn = document.getElementById('btn-train');
|
| 786 |
+
|
| 787 |
+
if(state.isTraining) {
|
| 788 |
+
btn.innerHTML = '<i data-lucide="zap" class="h-4 w-4 animate-pulse mr-2"></i> Stop';
|
| 789 |
+
document.getElementById('epoch-display').classList.remove('hidden');
|
| 790 |
+
document.getElementById('accuracy-panel').classList.remove('hidden');
|
| 791 |
+
trainStep();
|
| 792 |
+
} else {
|
| 793 |
+
btn.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
|
| 794 |
+
}
|
| 795 |
+
lucide.createIcons();
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
function trainStep() {
|
| 799 |
+
if(!state.isTraining) return;
|
| 800 |
+
if(state.epoch >= 100) {
|
| 801 |
+
toggleTraining();
|
| 802 |
+
return;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
state.network.train(state.dataPoints, 10);
|
| 806 |
+
state.epoch++;
|
| 807 |
+
|
| 808 |
+
// Recalculate predictions for the current cursor position ("lastProbe")
|
| 809 |
+
// This ensures the "Live Prediction" text updates instantly as the network learns
|
| 810 |
+
state.activations = state.network.forward([state.lastProbe.x, state.lastProbe.y]).activations;
|
| 811 |
+
updateExplainers();
|
| 812 |
+
renderNetwork(); // Update the nodes/weights visual
|
| 813 |
+
|
| 814 |
+
if(state.epoch % 5 === 0) {
|
| 815 |
+
// Update predictions every 5 epochs
|
| 816 |
+
state.predictions = generatePredictions();
|
| 817 |
+
|
| 818 |
+
// Calc accuracy
|
| 819 |
+
let correct = 0;
|
| 820 |
+
state.dataPoints.forEach(p => {
|
| 821 |
+
if ((state.network.predict(p.x, p.y) > 0.5 ? 1 : 0) === p.label) correct++;
|
| 822 |
+
});
|
| 823 |
+
state.accuracy = correct / state.dataPoints.length;
|
| 824 |
+
|
| 825 |
+
// Update UI
|
| 826 |
+
document.getElementById('epoch-display').innerText = "Epoch " + state.epoch;
|
| 827 |
+
document.getElementById('accuracy-display').innerText = (state.accuracy * 100).toFixed(1) + "%";
|
| 828 |
+
document.getElementById('accuracy-display').className = "text-lg font-bold " + (state.accuracy > 0.8 ? 'text-[hsl(var(--accent))]' : state.accuracy > 0.5 ? 'text-[hsl(var(--secondary))]' : 'text-[hsl(var(--destructive))]');
|
| 829 |
+
|
| 830 |
+
document.getElementById('progress-bar').style.width = state.epoch + "%";
|
| 831 |
+
|
| 832 |
+
// Add log
|
| 833 |
+
const logItem = `<div class="flex justify-between text-xs py-1 border-b border-white/5 last:border-0">
|
| 834 |
+
<span class="text-[hsl(var(--muted-foreground))]">Epoch ${state.epoch}</span>
|
| 835 |
+
<span class="${state.accuracy > 0.8 ? 'text-[hsl(var(--accent))]' : 'text-white'}">${(state.accuracy * 100).toFixed(1)}%</span>
|
| 836 |
+
</div>`;
|
| 837 |
+
document.getElementById('logs-container').insertAdjacentHTML('afterbegin', logItem);
|
| 838 |
+
|
| 839 |
+
renderCanvas();
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
requestAnimationFrame(trainStep);
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
// --- Presets ---
|
| 846 |
+
function loadPreset(type) {
|
| 847 |
+
let points = [];
|
| 848 |
+
if (type === 'XOR') {
|
| 849 |
+
for(let i=0; i<20; i++) {
|
| 850 |
+
points.push({ x: -0.5 + Math.random()*0.3, y: 0.5 + Math.random()*0.3, label: 1 });
|
| 851 |
+
points.push({ x: 0.5 + Math.random()*0.3, y: -0.5 - Math.random()*0.3, label: 1 });
|
| 852 |
+
points.push({ x: 0.5 + Math.random()*0.3, y: 0.5 + Math.random()*0.3, label: 0 });
|
| 853 |
+
points.push({ x: -0.5 + Math.random()*0.3, y: -0.5 - Math.random()*0.3, label: 0 });
|
| 854 |
+
}
|
| 855 |
+
} else if (type === 'Circle') {
|
| 856 |
+
for(let i=0; i<40; i++) {
|
| 857 |
+
const angle = Math.random() * Math.PI * 2;
|
| 858 |
+
const r1 = Math.random() * 0.4;
|
| 859 |
+
points.push({ x: Math.cos(angle)*r1, y: Math.sin(angle)*r1, label: 1 });
|
| 860 |
+
const r2 = 0.6 + Math.random() * 0.3;
|
| 861 |
+
points.push({ x: Math.cos(angle)*r2, y: Math.sin(angle)*r2, label: 0 });
|
| 862 |
+
}
|
| 863 |
+
} else if (type === 'Linear') {
|
| 864 |
+
for(let i=0; i<30; i++) {
|
| 865 |
+
points.push({ x: -0.4 - Math.random()*0.4, y: Math.random()*1.6 - 0.8, label: 1 });
|
| 866 |
+
points.push({ x: 0.4 + Math.random()*0.4, y: Math.random()*1.6 - 0.8, label: 0 });
|
| 867 |
+
}
|
| 868 |
+
} else if (type === 'Spiral') {
|
| 869 |
+
for (let i = 0; i < 60; i++) {
|
| 870 |
+
const r = i / 60;
|
| 871 |
+
const t = 1.75 * i / 60 * 2 * Math.PI;
|
| 872 |
+
points.push({ x: r * Math.sin(t), y: r * Math.cos(t), label: 1 });
|
| 873 |
+
points.push({ x: r * Math.sin(t + Math.PI), y: r * Math.cos(t + Math.PI), label: 0 });
|
| 874 |
+
}
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
// Reset but keep new points and generate initial map
|
| 878 |
+
state.dataPoints = points;
|
| 879 |
+
state.isTraining = false;
|
| 880 |
+
state.epoch = 0;
|
| 881 |
+
state.accuracy = 0;
|
| 882 |
+
state.logs = [];
|
| 883 |
+
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
|
| 884 |
+
state.predictions = generatePredictions(); // Generate immediate prediction map
|
| 885 |
+
state.lastProbe = { x: 0, y: 0 };
|
| 886 |
+
|
| 887 |
+
document.getElementById('points-count').innerText = points.length;
|
| 888 |
+
document.getElementById('accuracy-panel').classList.add('hidden');
|
| 889 |
+
document.getElementById('epoch-display').classList.add('hidden');
|
| 890 |
+
document.getElementById('progress-bar').style.width = '0%';
|
| 891 |
+
document.getElementById('logs-container').innerHTML = '';
|
| 892 |
+
|
| 893 |
+
const btnTrain = document.getElementById('btn-train');
|
| 894 |
+
btnTrain.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
|
| 895 |
+
lucide.createIcons();
|
| 896 |
+
|
| 897 |
+
renderCanvas();
|
| 898 |
+
renderNetwork();
|
| 899 |
+
|
| 900 |
+
if(points.length) {
|
| 901 |
+
// Set probe to first point to avoid 0,0 default
|
| 902 |
+
state.lastProbe = { x: points[0].x, y: points[0].y };
|
| 903 |
+
state.activations = state.network.forward([points[0].x, points[0].y]).activations;
|
| 904 |
+
updateExplainers();
|
| 905 |
+
}
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
function renderUI() {
|
| 909 |
+
renderNetwork();
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
// Start
|
| 913 |
+
window.onload = init;
|
| 914 |
+
</script>
|
| 915 |
+
</body>
|
| 916 |
+
</html>
|
templates/Neural-Networks-for-Classification.html
CHANGED
|
@@ -150,6 +150,54 @@
|
|
| 150 |
<div class="container">
|
| 151 |
<h1>🧠 Study Guide: Neural Networks for Classification</h1>
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
<h2>🔹 Core Concepts</h2>
|
| 154 |
<div class="story">
|
| 155 |
<p><strong>Story-style intuition: The Corporate Hierarchy</strong></p>
|
|
|
|
| 150 |
<div class="container">
|
| 151 |
<h1>🧠 Study Guide: Neural Networks for Classification</h1>
|
| 152 |
|
| 153 |
+
|
| 154 |
+
<!-- button -->
|
| 155 |
+
<div>
|
| 156 |
+
<!-- Audio Element -->
|
| 157 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 158 |
+
but since this is triggered by a click, it should work fine. -->
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
<a
|
| 162 |
+
href="/Neural-Networks-for-Classification-three"
|
| 163 |
+
target="_blank"
|
| 164 |
+
onclick="playSound()"
|
| 165 |
+
class="
|
| 166 |
+
cursor-pointer
|
| 167 |
+
inline-block
|
| 168 |
+
relative
|
| 169 |
+
bg-blue-500
|
| 170 |
+
text-white
|
| 171 |
+
font-bold
|
| 172 |
+
py-4 px-8
|
| 173 |
+
rounded-xl
|
| 174 |
+
text-2xl
|
| 175 |
+
transition-all
|
| 176 |
+
duration-150
|
| 177 |
+
|
| 178 |
+
/* 3D Effect (Hard Shadow) */
|
| 179 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 180 |
+
|
| 181 |
+
/* Pressed State (Move down & remove shadow) */
|
| 182 |
+
active:shadow-none
|
| 183 |
+
active:translate-y-[8px]
|
| 184 |
+
">
|
| 185 |
+
Tap Me!
|
| 186 |
+
</a>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<script>
|
| 190 |
+
function playSound() {
|
| 191 |
+
const audio = document.getElementById("clickSound");
|
| 192 |
+
if (audio) {
|
| 193 |
+
audio.currentTime = 0;
|
| 194 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
</script>
|
| 198 |
+
<!-- button -->
|
| 199 |
+
|
| 200 |
+
|
| 201 |
<h2>🔹 Core Concepts</h2>
|
| 202 |
<div class="story">
|
| 203 |
<p><strong>Story-style intuition: The Corporate Hierarchy</strong></p>
|
templates/Optimization.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "layout.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<h1 class="text-4xl font-bold text-gray-800 mb-6">Optimization Method</h1>
|
| 5 |
+
<p class="text-gray-600 text-lg mb-8">Optimization (Gradient Descent): Gradient descent is an optimization algorithm used to minimize the error (loss function) of a model during the training process. It is the underlying mechanism that powers the "gradient" aspect of gradient boosting, allowing the models to iteratively improve their performance.</p>
|
| 6 |
+
<div class="flex flex-col gap-6">
|
| 7 |
+
<div class="card p-6">
|
| 8 |
+
<h2 class="text-2xl font-semibold text-gray-800 mb-4"></h2>
|
| 9 |
+
<a href="/gradient-descent" class="algorithm-box">Gradient Descent</a>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
{% endblock %}
|
templates/XGBoost-Regression.html
CHANGED
|
@@ -151,6 +151,100 @@
|
|
| 151 |
<div class="container">
|
| 152 |
<h1>🚀 Study Guide: XGBoost Regression</h1>
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
<h2>🔹 Core Concepts</h2>
|
| 155 |
<div class="story">
|
| 156 |
<p><strong>Story-style intuition: The Master Craftsman</strong></p>
|
|
|
|
| 151 |
<div class="container">
|
| 152 |
<h1>🚀 Study Guide: XGBoost Regression</h1>
|
| 153 |
|
| 154 |
+
|
| 155 |
+
<!-- button -->
|
| 156 |
+
<div>
|
| 157 |
+
<!-- Audio Element -->
|
| 158 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 159 |
+
but since this is triggered by a click, it should work fine. -->
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
<a
|
| 163 |
+
href="/xgboost-tree-three"
|
| 164 |
+
target="_blank"
|
| 165 |
+
onclick="playSound()"
|
| 166 |
+
class="
|
| 167 |
+
cursor-pointer
|
| 168 |
+
inline-block
|
| 169 |
+
relative
|
| 170 |
+
bg-blue-500
|
| 171 |
+
text-white
|
| 172 |
+
font-bold
|
| 173 |
+
py-4 px-8
|
| 174 |
+
rounded-xl
|
| 175 |
+
text-2xl
|
| 176 |
+
transition-all
|
| 177 |
+
duration-150
|
| 178 |
+
|
| 179 |
+
/* 3D Effect (Hard Shadow) */
|
| 180 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 181 |
+
|
| 182 |
+
/* Pressed State (Move down & remove shadow) */
|
| 183 |
+
active:shadow-none
|
| 184 |
+
active:translate-y-[8px]
|
| 185 |
+
">
|
| 186 |
+
Tap Me!
|
| 187 |
+
</a>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<script>
|
| 191 |
+
function playSound() {
|
| 192 |
+
const audio = document.getElementById("clickSound");
|
| 193 |
+
if (audio) {
|
| 194 |
+
audio.currentTime = 0;
|
| 195 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
</script>
|
| 199 |
+
<!-- button -->
|
| 200 |
+
|
| 201 |
+
<!-- button -->
|
| 202 |
+
<div>
|
| 203 |
+
<!-- Audio Element -->
|
| 204 |
+
<!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
|
| 205 |
+
but since this is triggered by a click, it should work fine. -->
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
<a
|
| 209 |
+
href="/xgboost-graph-three2"
|
| 210 |
+
target="_blank"
|
| 211 |
+
onclick="playSound()"
|
| 212 |
+
class="
|
| 213 |
+
cursor-pointer
|
| 214 |
+
inline-block
|
| 215 |
+
relative
|
| 216 |
+
bg-blue-500
|
| 217 |
+
text-white
|
| 218 |
+
font-bold
|
| 219 |
+
py-4 px-8
|
| 220 |
+
rounded-xl
|
| 221 |
+
text-2xl
|
| 222 |
+
transition-all
|
| 223 |
+
duration-150
|
| 224 |
+
|
| 225 |
+
/* 3D Effect (Hard Shadow) */
|
| 226 |
+
shadow-[0_8px_0_rgb(29,78,216)]
|
| 227 |
+
|
| 228 |
+
/* Pressed State (Move down & remove shadow) */
|
| 229 |
+
active:shadow-none
|
| 230 |
+
active:translate-y-[8px]
|
| 231 |
+
">
|
| 232 |
+
Tap Me!
|
| 233 |
+
</a>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<script>
|
| 237 |
+
function playSound() {
|
| 238 |
+
const audio = document.getElementById("clickSound");
|
| 239 |
+
if (audio) {
|
| 240 |
+
audio.currentTime = 0;
|
| 241 |
+
audio.play().catch(e => console.log("Audio play failed:", e));
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
</script>
|
| 245 |
+
<!-- button -->
|
| 246 |
+
|
| 247 |
+
|
| 248 |
<h2>🔹 Core Concepts</h2>
|
| 249 |
<div class="story">
|
| 250 |
<p><strong>Story-style intuition: The Master Craftsman</strong></p>
|
templates/gmm-threejs.html
ADDED
|
@@ -0,0 +1,977 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>GMM Simulator - Learn Gaussian Mixture Models Visually</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 9 |
+
<style>
|
| 10 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap');
|
| 11 |
+
|
| 12 |
+
:root {
|
| 13 |
+
--primary: 221.2 83.2% 53.3%;
|
| 14 |
+
--accent: 142.1 76.2% 36.3%;
|
| 15 |
+
--cluster-1: 15 85% 60%;
|
| 16 |
+
--cluster-2: 195 80% 50%;
|
| 17 |
+
--cluster-3: 45 90% 55%;
|
| 18 |
+
--cluster-4: 280 65% 60%;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
body {
|
| 22 |
+
font-family: 'Inter', sans-serif;
|
| 23 |
+
background-color: #f8fafc;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.font-mono { font-family: 'JetBrains Mono', monospace; }
|
| 27 |
+
|
| 28 |
+
.shadow-soft {
|
| 29 |
+
box-shadow: 0 2px 15px -3px rgba(0,0,0,0.07), 0 4px 6px -2px rgba(0,0,0,0.05);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.animate-pulse-soft {
|
| 33 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@keyframes pulse {
|
| 37 |
+
0%, 100% { opacity: 1; }
|
| 38 |
+
50% { opacity: .7; }
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.tab-content { display: none; }
|
| 42 |
+
.tab-content.active { display: block; }
|
| 43 |
+
|
| 44 |
+
.tab-trigger.active {
|
| 45 |
+
background: white;
|
| 46 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 47 |
+
color: black;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.step-node.active { background-color: rgb(59 130 246); color: white; }
|
| 51 |
+
.step-node.done { background-color: #f1f5f9; color: #64748b; }
|
| 52 |
+
.step-node.converged { background-color: #10b981; color: white; }
|
| 53 |
+
|
| 54 |
+
.modal-overlay {
|
| 55 |
+
background: rgba(15, 23, 42, 0.6);
|
| 56 |
+
backdrop-filter: blur(4px);
|
| 57 |
+
}
|
| 58 |
+
</style>
|
| 59 |
+
</head>
|
| 60 |
+
<body class="text-slate-900">
|
| 61 |
+
|
| 62 |
+
<div id="app" class="min-h-screen p-4 md:p-6">
|
| 63 |
+
<div class="max-w-7xl mx-auto space-y-6">
|
| 64 |
+
|
| 65 |
+
<!-- Header -->
|
| 66 |
+
<header class="text-center space-y-2">
|
| 67 |
+
<h1 class="text-2xl md:text-4xl font-extrabold tracking-tight">
|
| 68 |
+
EM Algorithm & <span class="text-blue-600">Gaussian Mixture Models</span>
|
| 69 |
+
</h1>
|
| 70 |
+
<p class="text-slate-500 text-sm md:text-base max-w-2xl mx-auto">
|
| 71 |
+
Learn how machines find hidden groups in data. Watch points get assigned and clusters adapt in real-time!
|
| 72 |
+
</p>
|
| 73 |
+
</header>
|
| 74 |
+
|
| 75 |
+
<!-- Tabs Navigation -->
|
| 76 |
+
<div class="flex justify-center">
|
| 77 |
+
<div class="bg-slate-200/50 p-1 rounded-lg inline-flex w-full max-w-md">
|
| 78 |
+
<button onclick="switchTab('simulator')" id="btn-tab-simulator" class="tab-trigger flex-1 py-1.5 px-3 rounded-md text-sm font-medium transition-all active flex items-center justify-center gap-2">
|
| 79 |
+
<i data-lucide="play" class="w-4 h-4"></i> Interactive Simulator
|
| 80 |
+
</button>
|
| 81 |
+
<button onclick="switchTab('learn')" id="btn-tab-learn" class="tab-trigger flex-1 py-1.5 px-3 rounded-md text-sm font-medium transition-all flex items-center justify-center gap-2 text-slate-600">
|
| 82 |
+
<i data-lucide="book-open" class="w-4 h-4"></i> Learn EM Algorithm
|
| 83 |
+
</button>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<!-- Simulator Tab Content -->
|
| 88 |
+
<div id="tab-simulator" class="tab-content active space-y-6">
|
| 89 |
+
|
| 90 |
+
<!-- Quick Facts Grid -->
|
| 91 |
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
| 92 |
+
<div class="bg-white rounded-xl border p-4 shadow-soft">
|
| 93 |
+
<i data-lucide="book-open" class="w-6 h-6 mb-2 text-orange-500"></i>
|
| 94 |
+
<h4 class="font-bold text-sm mb-1">GMM = Soft Clustering</h4>
|
| 95 |
+
<p class="text-xs text-slate-500">Points can partially belong to multiple clusters.</p>
|
| 96 |
+
</div>
|
| 97 |
+
<div class="bg-white rounded-xl border p-4 shadow-soft">
|
| 98 |
+
<i data-lucide="zap" class="w-6 h-6 mb-2 text-blue-500"></i>
|
| 99 |
+
<h4 class="font-bold text-sm mb-1">EM is Iterative</h4>
|
| 100 |
+
<p class="text-xs text-slate-500">Alternates between E-step and M-step.</p>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="bg-white rounded-xl border p-4 shadow-soft">
|
| 103 |
+
<i data-lucide="eye" class="w-6 h-6 mb-2 text-emerald-500"></i>
|
| 104 |
+
<h4 class="font-bold text-sm mb-1">Watch the Ellipses</h4>
|
| 105 |
+
<p class="text-xs text-slate-500">Ellipses show the statistical shape of clusters.</p>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="bg-white rounded-xl border p-4 shadow-soft">
|
| 108 |
+
<i data-lucide="brain" class="w-6 h-6 mb-2 text-indigo-500"></i>
|
| 109 |
+
<h4 class="font-bold text-sm mb-1">Convergence</h4>
|
| 110 |
+
<p class="text-xs text-slate-500">Algorithm stops when centers stabilize.</p>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<div class="grid lg:grid-cols-3 gap-6">
|
| 115 |
+
<!-- Left: Canvas and Main Controls -->
|
| 116 |
+
<div class="lg:col-span-2 space-y-4">
|
| 117 |
+
|
| 118 |
+
<!-- Algorithm Flow Indicator -->
|
| 119 |
+
<div class="bg-white rounded-xl border p-4 shadow-soft">
|
| 120 |
+
<h4 class="font-bold text-sm mb-4 text-center">EM Algorithm Flow</h4>
|
| 121 |
+
<div class="flex items-center justify-between">
|
| 122 |
+
<div class="flex flex-col items-center flex-1 text-center">
|
| 123 |
+
<div id="step-init" class="step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all">
|
| 124 |
+
<i data-lucide="circle" class="w-5 h-5"></i>
|
| 125 |
+
</div>
|
| 126 |
+
<span class="text-xs font-medium mt-2">Initialize</span>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="h-0.5 flex-1 mx-2 bg-slate-200" id="line-1"></div>
|
| 129 |
+
<div class="flex flex-col items-center flex-1 text-center">
|
| 130 |
+
<div id="step-e" class="step-node w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center font-bold text-sm transition-all">
|
| 131 |
+
<i data-lucide="circle" class="w-5 h-5"></i>
|
| 132 |
+
</div>
|
| 133 |
+
<span class="text-xs font-medium mt-2">E-Step</span>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="h-0.5 flex-1 mx-2 bg-slate-200" id="line-2"></div>
|
| 136 |
+
<div class="flex flex-col items-center flex-1 text-center">
|
| 137 |
+
<div id="step-m" class="step-node w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center font-bold text-sm transition-all">
|
| 138 |
+
<i data-lucide="refresh-cw" class="w-5 h-5"></i>
|
| 139 |
+
</div>
|
| 140 |
+
<span class="text-xs font-medium mt-2">M-Step</span>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<!-- Canvas Area -->
|
| 146 |
+
<div class="bg-white rounded-2xl shadow-soft border overflow-hidden">
|
| 147 |
+
<div class="p-3 border-b bg-slate-50 flex flex-wrap gap-4" id="legend">
|
| 148 |
+
<!-- Legend items generated via JS -->
|
| 149 |
+
</div>
|
| 150 |
+
<div class="relative p-2 flex justify-center items-center overflow-hidden" style="height: 400px;" id="canvas-container">
|
| 151 |
+
<svg id="gmm-svg" width="100%" height="100%" class="rounded-lg">
|
| 152 |
+
<defs>
|
| 153 |
+
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
| 154 |
+
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#e2e8f0" stroke-width="0.5" />
|
| 155 |
+
</pattern>
|
| 156 |
+
</defs>
|
| 157 |
+
<rect width="100%" height="100%" fill="url(#grid)" />
|
| 158 |
+
<g id="ellipses-group"></g>
|
| 159 |
+
<g id="points-group"></g>
|
| 160 |
+
</svg>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<!-- Main Controls -->
|
| 165 |
+
<div class="bg-white rounded-xl p-5 border shadow-soft space-y-6">
|
| 166 |
+
<div class="flex flex-wrap items-center gap-3">
|
| 167 |
+
<button id="btn-play" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| 168 |
+
<i data-lucide="play" class="w-5 h-5"></i> <span id="btn-play-text">Auto Play</span>
|
| 169 |
+
</button>
|
| 170 |
+
<button id="btn-step" class="bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| 171 |
+
<i data-lucide="skip-forward" class="w-5 h-5"></i> Next Step
|
| 172 |
+
</button>
|
| 173 |
+
<button id="btn-reset" class="border border-slate-200 hover:bg-slate-50 text-slate-800 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| 174 |
+
<i data-lucide="rotate-ccw" class="w-5 h-5"></i> Reset
|
| 175 |
+
</button>
|
| 176 |
+
<button id="btn-generate" class="border border-emerald-200 text-emerald-600 hover:bg-emerald-50 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| 177 |
+
<i data-lucide="sparkles" class="w-5 h-5"></i> New Data
|
| 178 |
+
</button>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<div class="flex flex-wrap items-center justify-between gap-6 pt-2 border-t border-slate-100">
|
| 182 |
+
<div class="flex items-center gap-4">
|
| 183 |
+
<span class="text-sm font-medium text-slate-500 whitespace-nowrap">Speed:</span>
|
| 184 |
+
<input type="range" id="speed-slider" min="500" max="3000" step="100" value="1500" class="w-32 accent-blue-600">
|
| 185 |
+
<span id="speed-text" class="text-xs text-slate-400 w-12 text-center">Medium</span>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<div class="flex items-center gap-4">
|
| 189 |
+
<div class="bg-slate-100 px-4 py-2 rounded-lg flex items-center gap-2">
|
| 190 |
+
<span class="text-sm font-medium text-slate-500">Iteration:</span>
|
| 191 |
+
<span id="counter-text" class="font-mono text-lg font-bold">0 / 20</span>
|
| 192 |
+
</div>
|
| 193 |
+
<div id="status-badge" class="hidden px-4 py-2 rounded-lg font-semibold text-sm"></div>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
<!-- Right: Info Panels -->
|
| 200 |
+
<div class="space-y-4">
|
| 201 |
+
<!-- Dynamic Explanation -->
|
| 202 |
+
<div id="explanation-card" class="p-5 rounded-xl border-2 bg-blue-50/50 border-blue-200 shadow-soft transition-all">
|
| 203 |
+
<div class="flex items-start gap-4">
|
| 204 |
+
<div class="p-3 rounded-lg bg-white text-blue-600" id="expl-icon">
|
| 205 |
+
<i data-lucide="lightbulb" class="w-6 h-6"></i>
|
| 206 |
+
</div>
|
| 207 |
+
<div class="flex-1">
|
| 208 |
+
<h3 class="font-bold text-lg mb-2" id="expl-title">Ready to Start! 🚀</h3>
|
| 209 |
+
<ul class="space-y-2" id="expl-content">
|
| 210 |
+
<li class="flex items-start gap-2 text-sm text-slate-600">
|
| 211 |
+
<i data-lucide="arrow-right" class="w-4 h-4 mt-0.5 text-emerald-500 flex-shrink-0"></i>
|
| 212 |
+
<span>Press 'Next Step' to see how the EM algorithm works.</span>
|
| 213 |
+
</li>
|
| 214 |
+
<li class="flex items-start gap-2 text-sm text-slate-600">
|
| 215 |
+
<i data-lucide="arrow-right" class="w-4 h-4 mt-0.5 text-emerald-500 flex-shrink-0"></i>
|
| 216 |
+
<span>Watch points get assigned to the most likely cluster center.</span>
|
| 217 |
+
</li>
|
| 218 |
+
</ul>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<!-- Log-Likelihood Chart -->
|
| 224 |
+
<div class="bg-white rounded-xl border p-4 shadow-soft">
|
| 225 |
+
<div class="flex items-center justify-between mb-3">
|
| 226 |
+
<div class="flex items-center gap-2">
|
| 227 |
+
<i data-lucide="trending-up" class="w-5 h-5 text-blue-600"></i>
|
| 228 |
+
<h4 class="font-bold">Log-Likelihood</h4>
|
| 229 |
+
</div>
|
| 230 |
+
<div id="converged-badge" class="hidden items-center gap-1 text-emerald-600 text-sm font-medium">
|
| 231 |
+
<i data-lucide="check-circle-2" class="w-4 h-4"></i> Converged!
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
<p class="text-xs text-slate-500 mb-4">
|
| 235 |
+
Measures model fit. Higher values = better grouping.
|
| 236 |
+
</p>
|
| 237 |
+
<div class="h-32 w-full flex items-end gap-1 px-1" id="chart-container">
|
| 238 |
+
<!-- Chart bars generated via JS -->
|
| 239 |
+
<div class="flex-1 h-full flex items-center justify-center text-slate-300 text-xs text-center px-4">
|
| 240 |
+
Start simulation to see progress...
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<!-- Key Terms -->
|
| 246 |
+
<div class="bg-white rounded-xl p-5 border shadow-soft">
|
| 247 |
+
<h4 class="font-bold mb-3">Key Terms 📚</h4>
|
| 248 |
+
<div class="space-y-4 text-sm">
|
| 249 |
+
<div>
|
| 250 |
+
<span class="font-semibold text-orange-500">E-Step:</span>
|
| 251 |
+
<p class="text-slate-500 mt-0.5">Calculating the "responsibility" (probability) that each point belongs to each cluster center.</p>
|
| 252 |
+
</div>
|
| 253 |
+
<div>
|
| 254 |
+
<span class="font-semibold text-blue-500">M-Step:</span>
|
| 255 |
+
<p class="text-slate-500 mt-0.5">Moving and stretching clusters to better fit the points assigned to them.</p>
|
| 256 |
+
</div>
|
| 257 |
+
<div>
|
| 258 |
+
<span class="font-semibold text-emerald-500">Convergence:</span>
|
| 259 |
+
<p class="text-slate-500 mt-0.5">The point where the clusters stop moving because they've found the optimal mathematical fit.</p>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
<!-- Learn Tab Content -->
|
| 268 |
+
<div id="tab-learn" class="tab-content space-y-8">
|
| 269 |
+
<!-- Introduction Section -->
|
| 270 |
+
<section class="bg-white rounded-2xl border p-6 shadow-soft">
|
| 271 |
+
<div class="flex items-center gap-3 mb-4">
|
| 272 |
+
<div class="p-3 rounded-xl bg-blue-50 text-blue-600">
|
| 273 |
+
<i data-lucide="book-open" class="w-6 h-6"></i>
|
| 274 |
+
</div>
|
| 275 |
+
<h2 class="text-xl font-bold">What is the EM Algorithm?</h2>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="space-y-4 text-slate-600">
|
| 278 |
+
<p>
|
| 279 |
+
<strong class="text-slate-900">EM (Expectation-Maximization)</strong> is a powerful iterative method used to find maximum likelihood estimates of parameters in statistical models, where the model depends on unobserved latent variables.
|
| 280 |
+
</p>
|
| 281 |
+
<div class="bg-slate-50 rounded-xl p-4 border border-slate-100">
|
| 282 |
+
<h4 class="font-semibold text-slate-900 mb-2">🎯 Real-Life Analogy</h4>
|
| 283 |
+
<p class="text-sm">
|
| 284 |
+
Imagine you have a bag of colored marbles, but you're colorblind! You can feel their sizes and weights, but you can't see the colors. EM helps you figure out which marbles are likely the same color based on their shared physical properties.
|
| 285 |
+
</p>
|
| 286 |
+
</div>
|
| 287 |
+
<p>The algorithm works by alternating between two main steps until it finds the best mathematical grouping.</p>
|
| 288 |
+
</div>
|
| 289 |
+
</section>
|
| 290 |
+
|
| 291 |
+
<!-- Detailed Steps Grid -->
|
| 292 |
+
<div class="grid md:grid-cols-2 gap-6">
|
| 293 |
+
<!-- E-Step Details -->
|
| 294 |
+
<section class="bg-gradient-to-br from-orange-50/50 to-white rounded-2xl border border-orange-100 p-6 shadow-sm">
|
| 295 |
+
<div class="flex items-center gap-3 mb-4">
|
| 296 |
+
<div class="p-3 rounded-xl bg-orange-100 text-orange-600">
|
| 297 |
+
<i data-lucide="target" class="w-6 h-6"></i>
|
| 298 |
+
</div>
|
| 299 |
+
<div>
|
| 300 |
+
<h3 class="text-lg font-bold">E-Step (Expectation)</h3>
|
| 301 |
+
<p class="text-xs text-orange-500 font-medium">Assignment Phase</p>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
<div class="space-y-4">
|
| 305 |
+
<p class="text-sm text-slate-600"><strong>Goal:</strong> Calculate the probability (responsibility) of each cluster for each data point.</p>
|
| 306 |
+
<div class="space-y-2">
|
| 307 |
+
<h4 class="font-semibold text-sm">Process:</h4>
|
| 308 |
+
<ul class="space-y-2 text-sm text-slate-500">
|
| 309 |
+
<li class="flex items-start gap-2"><span class="text-orange-600">•</span> Each point "looks" at all current cluster curves.</li>
|
| 310 |
+
<li class="flex items-start gap-2"><span class="text-orange-600">•</span> It asks: "Given my location, which cluster would likely have produced me?"</li>
|
| 311 |
+
<li class="flex items-start gap-2"><span class="text-orange-600">•</span> Points get colored by their most likely cluster.</li>
|
| 312 |
+
</ul>
|
| 313 |
+
</div>
|
| 314 |
+
<div class="bg-white/80 rounded-lg p-3 border border-orange-100">
|
| 315 |
+
<p class="text-xs text-slate-500 font-mono">responsibility = (fit to cluster) / (total fit to all clusters)</p>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
</section>
|
| 319 |
+
|
| 320 |
+
<!-- M-Step Details -->
|
| 321 |
+
<section class="bg-gradient-to-br from-blue-50/50 to-white rounded-2xl border border-blue-100 p-6 shadow-sm">
|
| 322 |
+
<div class="flex items-center gap-3 mb-4">
|
| 323 |
+
<div class="p-3 rounded-xl bg-blue-100 text-blue-600">
|
| 324 |
+
<i data-lucide="bar-chart-3" class="w-6 h-6"></i>
|
| 325 |
+
</div>
|
| 326 |
+
<div>
|
| 327 |
+
<h3 class="text-lg font-bold">M-Step (Maximization)</h3>
|
| 328 |
+
<p class="text-xs text-blue-500 font-medium">Update Phase</p>
|
| 329 |
+
</div>
|
| 330 |
+
</div>
|
| 331 |
+
<div class="space-y-4">
|
| 332 |
+
<p class="text-sm text-slate-600"><strong>Goal:</strong> Update cluster parameters (center, shape, weight) based on assigned points.</p>
|
| 333 |
+
<div class="space-y-2">
|
| 334 |
+
<h4 class="font-semibold text-sm">Process:</h4>
|
| 335 |
+
<ul class="space-y-2 text-sm text-slate-500">
|
| 336 |
+
<li class="flex items-start gap-2"><span class="text-blue-600">•</span> Cluster centers move to the weighted average of points.</li>
|
| 337 |
+
<li class="flex items-start gap-2"><span class="text-blue-600">•</span> The ellipse stretches/shrinks to cover its assigned points better.</li>
|
| 338 |
+
<li class="flex items-start gap-2"><span class="text-blue-600">•</span> Cluster "popularity" (weight) is updated.</li>
|
| 339 |
+
</ul>
|
| 340 |
+
</div>
|
| 341 |
+
<div class="bg-white/80 rounded-lg p-3 border border-blue-100">
|
| 342 |
+
<p class="text-xs text-slate-500 font-mono">new_mean = average(points × their_responsibilities)</p>
|
| 343 |
+
</div>
|
| 344 |
+
</div>
|
| 345 |
+
</section>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<!-- Algorithm Flow Visual -->
|
| 349 |
+
<section class="bg-white rounded-2xl border p-6 shadow-soft">
|
| 350 |
+
<div class="flex items-center gap-3 mb-6">
|
| 351 |
+
<div class="p-3 rounded-xl bg-indigo-50 text-indigo-600">
|
| 352 |
+
<i data-lucide="refresh-cw" class="w-6 h-6"></i>
|
| 353 |
+
</div>
|
| 354 |
+
<h2 class="text-xl font-bold">The Complete Cycle</h2>
|
| 355 |
+
</div>
|
| 356 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 relative">
|
| 357 |
+
<div class="bg-slate-50 p-4 rounded-xl text-center border">
|
| 358 |
+
<i data-lucide="shuffle" class="w-8 h-8 mx-auto mb-2 text-slate-400"></i>
|
| 359 |
+
<h4 class="font-bold text-sm">1. Initialize</h4>
|
| 360 |
+
<p class="text-xs text-slate-500 mt-1">Random centers and circular shapes</p>
|
| 361 |
+
</div>
|
| 362 |
+
<div class="bg-orange-50 p-4 rounded-xl text-center border border-orange-100">
|
| 363 |
+
<i data-lucide="target" class="w-8 h-8 mx-auto mb-2 text-orange-500"></i>
|
| 364 |
+
<h4 class="font-bold text-sm">2. E-Step</h4>
|
| 365 |
+
<p class="text-xs text-slate-500 mt-1">Assign data points to clusters</p>
|
| 366 |
+
</div>
|
| 367 |
+
<div class="bg-blue-50 p-4 rounded-xl text-center border border-blue-100">
|
| 368 |
+
<i data-lucide="bar-chart-3" class="w-8 h-8 mx-auto mb-2 text-blue-500"></i>
|
| 369 |
+
<h4 class="font-bold text-sm">3. M-Step</h4>
|
| 370 |
+
<p class="text-xs text-slate-500 mt-1">Update cluster parameters</p>
|
| 371 |
+
</div>
|
| 372 |
+
<div class="bg-emerald-50 p-4 rounded-xl text-center border border-emerald-100">
|
| 373 |
+
<i data-lucide="check-circle-2" class="w-8 h-8 mx-auto mb-2 text-emerald-500"></i>
|
| 374 |
+
<h4 class="font-bold text-sm">4. Converged?</h4>
|
| 375 |
+
<p class="text-xs text-slate-500 mt-1">Repeat 2 & 3 until stable</p>
|
| 376 |
+
</div>
|
| 377 |
+
</div>
|
| 378 |
+
</section>
|
| 379 |
+
|
| 380 |
+
<!-- Why it Works Section -->
|
| 381 |
+
<section class="bg-white rounded-2xl border p-6 shadow-soft">
|
| 382 |
+
<div class="flex items-center gap-3 mb-6">
|
| 383 |
+
<div class="p-3 rounded-xl bg-amber-50 text-amber-600">
|
| 384 |
+
<i data-lucide="lightbulb" class="w-6 h-6"></i>
|
| 385 |
+
</div>
|
| 386 |
+
<h2 class="text-xl font-bold">Why Does EM Work?</h2>
|
| 387 |
+
</div>
|
| 388 |
+
<div class="grid md:grid-cols-2 gap-8">
|
| 389 |
+
<div class="space-y-4">
|
| 390 |
+
<h4 class="font-bold text-slate-900">The Chicken-and-Egg Problem 🐔🥚</h4>
|
| 391 |
+
<p class="text-sm text-slate-600 leading-relaxed">
|
| 392 |
+
We have a classic loop: To find the centers, we need to know point assignments. To find point assignments, we need to know the centers.
|
| 393 |
+
EM solves this by starting with a "best guess" and iteratively refining it.
|
| 394 |
+
</p>
|
| 395 |
+
</div>
|
| 396 |
+
<div class="space-y-4">
|
| 397 |
+
<h4 class="font-bold text-slate-900">Guaranteed Improvement 📈</h4>
|
| 398 |
+
<p class="text-sm text-slate-600 leading-relaxed">
|
| 399 |
+
Mathematically, each iteration of EM is guaranteed to increase the <strong>Log-Likelihood</strong> of the model (or leave it unchanged). This means the model always gets better at explaining the data until it hits a maximum.
|
| 400 |
+
</p>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
</section>
|
| 404 |
+
|
| 405 |
+
<!-- Comparison Table -->
|
| 406 |
+
<section class="bg-white rounded-2xl border p-6 shadow-soft overflow-hidden">
|
| 407 |
+
<h2 class="text-xl font-bold mb-6">GMM vs K-Means: The Difference</h2>
|
| 408 |
+
<div class="overflow-x-auto">
|
| 409 |
+
<table class="w-full text-sm">
|
| 410 |
+
<thead class="bg-slate-50">
|
| 411 |
+
<tr class="text-left">
|
| 412 |
+
<th class="py-3 px-4 font-bold border-b">Feature</th>
|
| 413 |
+
<th class="py-3 px-4 font-bold border-b text-blue-600">K-Means</th>
|
| 414 |
+
<th class="py-3 px-4 font-bold border-b text-orange-600">GMM (EM)</th>
|
| 415 |
+
</tr>
|
| 416 |
+
</thead>
|
| 417 |
+
<tbody class="divide-y text-slate-600">
|
| 418 |
+
<tr>
|
| 419 |
+
<td class="py-4 px-4 font-semibold text-slate-900">Assignment</td>
|
| 420 |
+
<td class="py-4 px-4">Hard (0 or 1)</td>
|
| 421 |
+
<td class="py-4 px-4">Soft (Probabilities)</td>
|
| 422 |
+
</tr>
|
| 423 |
+
<tr>
|
| 424 |
+
<td class="py-4 px-4 font-semibold text-slate-900">Cluster Shape</td>
|
| 425 |
+
<td class="py-4 px-4">Always circular/spherical</td>
|
| 426 |
+
<td class="py-4 px-4">Flexible ellipses (any orientation)</td>
|
| 427 |
+
</tr>
|
| 428 |
+
<tr>
|
| 429 |
+
<td class="py-4 px-4 font-semibold text-slate-900">Model Type</td>
|
| 430 |
+
<td class="py-4 px-4">Distance-based</td>
|
| 431 |
+
<td class="py-4 px-4">Distribution-based</td>
|
| 432 |
+
</tr>
|
| 433 |
+
<tr>
|
| 434 |
+
<td class="py-4 px-4 font-semibold text-slate-900">Use Case</td>
|
| 435 |
+
<td class="py-4 px-4">Simple, distinct groups</td>
|
| 436 |
+
<td class="py-4 px-4">Overlapping, varied group shapes</td>
|
| 437 |
+
</tr>
|
| 438 |
+
</tbody>
|
| 439 |
+
</table>
|
| 440 |
+
</div>
|
| 441 |
+
</section>
|
| 442 |
+
|
| 443 |
+
<!-- Real World Applications Grid -->
|
| 444 |
+
<section class="bg-slate-900 text-white rounded-2xl p-8 shadow-xl">
|
| 445 |
+
<h2 class="text-2xl font-bold mb-6">🌍 Real-World Applications</h2>
|
| 446 |
+
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-6">
|
| 447 |
+
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| 448 |
+
<div class="text-2xl mb-2">🖼️</div>
|
| 449 |
+
<h4 class="font-bold text-sm">Image Segmentation</h4>
|
| 450 |
+
<p class="text-xs text-slate-400 mt-1">Grouping pixels by color/texture to separate objects in photos.</p>
|
| 451 |
+
</div>
|
| 452 |
+
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| 453 |
+
<div class="text-2xl mb-2">🔊</div>
|
| 454 |
+
<h4 class="font-bold text-sm">Speech Recognition</h4>
|
| 455 |
+
<p class="text-xs text-slate-400 mt-1">Identifying different speakers in an audio stream using voice patterns.</p>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| 458 |
+
<div class="text-2xl mb-2">📊</div>
|
| 459 |
+
<h4 class="font-bold text-sm">Customer Segmentation</h4>
|
| 460 |
+
<p class="text-xs text-slate-400 mt-1">Finding groups of customers with similar shopping behaviors.</p>
|
| 461 |
+
</div>
|
| 462 |
+
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| 463 |
+
<div class="text-2xl mb-2">🧬</div>
|
| 464 |
+
<h4 class="font-bold text-sm">Genetics</h4>
|
| 465 |
+
<p class="text-xs text-slate-400 mt-1">Clustering gene expression data to find functional biological groups.</p>
|
| 466 |
+
</div>
|
| 467 |
+
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| 468 |
+
<div class="text-2xl mb-2">🌤️</div>
|
| 469 |
+
<h4 class="font-bold text-sm">Meteorology</h4>
|
| 470 |
+
<p class="text-xs text-slate-400 mt-1">Classifying climate zones based on temperature and humidity data.</p>
|
| 471 |
+
</div>
|
| 472 |
+
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| 473 |
+
<div class="text-2xl mb-2">📧</div>
|
| 474 |
+
<h4 class="font-bold text-sm">Spam Detection</h4>
|
| 475 |
+
<p class="text-xs text-slate-400 mt-1">Clustering emails into 'Ham' and 'Spam' based on content features.</p>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
</section>
|
| 479 |
+
</div>
|
| 480 |
+
</div>
|
| 481 |
+
</div>
|
| 482 |
+
|
| 483 |
+
<!-- Intro Modal -->
|
| 484 |
+
<div id="intro-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 modal-overlay">
|
| 485 |
+
<div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full p-6 md:p-8 relative">
|
| 486 |
+
<button onclick="closeModal()" class="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-100 transition-colors">
|
| 487 |
+
<i data-lucide="x" class="w-5 h-5"></i>
|
| 488 |
+
</button>
|
| 489 |
+
<div class="text-center mb-8">
|
| 490 |
+
<h2 class="text-2xl md:text-3xl font-extrabold mb-2">Welcome to the <span class="text-blue-600">EM Simulator</span></h2>
|
| 491 |
+
<p class="text-slate-500">Discover how AI learns hidden patterns in data</p>
|
| 492 |
+
</div>
|
| 493 |
+
<div class="space-y-4 mb-8">
|
| 494 |
+
<div class="flex items-start gap-4 p-4 rounded-xl bg-slate-50">
|
| 495 |
+
<div class="p-3 rounded-lg bg-white text-orange-500 shadow-sm"><i data-lucide="sparkles" class="w-6 h-6"></i></div>
|
| 496 |
+
<div><h3 class="font-bold">What is GMM?</h3><p class="text-sm text-slate-500">A statistical model that groups data points into distinct probability curves (clusters).</p></div>
|
| 497 |
+
</div>
|
| 498 |
+
<div class="flex items-start gap-4 p-4 rounded-xl bg-slate-50">
|
| 499 |
+
<div class="p-3 rounded-lg bg-white text-blue-500 shadow-sm"><i data-lucide="zap" class="w-6 h-6"></i></div>
|
| 500 |
+
<div><h3 class="font-bold">How it learns</h3><p class="text-sm text-slate-500">It "cycles" between assigning points (E-Step) and updating groups (M-Step) until stable.</p></div>
|
| 501 |
+
</div>
|
| 502 |
+
</div>
|
| 503 |
+
<button onclick="closeModal()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl transition-all shadow-lg flex items-center justify-center gap-2">
|
| 504 |
+
Start Learning <i data-lucide="arrow-right" class="w-5 h-5"></i>
|
| 505 |
+
</button>
|
| 506 |
+
</div>
|
| 507 |
+
<!-- Centered Button -->
|
| 508 |
+
<div class="absolute left-1/2 -translate-x-1/2 flex items-center">
|
| 509 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 510 |
+
<a href="/gaussian-mixture-models" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider">
|
| 511 |
+
Back to Core
|
| 512 |
+
</a>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<script>
|
| 517 |
+
// --- CONSTANTS & DATA ---
|
| 518 |
+
const CLUSTER_COLORS = [
|
| 519 |
+
{ fill: 'rgba(239, 115, 87, 0.15)', stroke: 'hsl(15, 85%, 60%)', label: 'Coral' },
|
| 520 |
+
{ fill: 'rgba(50, 180, 205, 0.15)', stroke: 'hsl(195, 80%, 50%)', label: 'Teal' },
|
| 521 |
+
{ fill: 'rgba(245, 185, 66, 0.15)', stroke: 'hsl(45, 90%, 55%)', label: 'Amber' },
|
| 522 |
+
{ fill: 'rgba(175, 100, 200, 0.15)', stroke: 'hsl(280, 65%, 60%)', label: 'Violet' }
|
| 523 |
+
];
|
| 524 |
+
const MAX_ITERATIONS = 20;
|
| 525 |
+
const NUM_POINTS = 100;
|
| 526 |
+
|
| 527 |
+
// --- STATE ---
|
| 528 |
+
let state = {
|
| 529 |
+
points: [],
|
| 530 |
+
clusters: [],
|
| 531 |
+
iteration: 0,
|
| 532 |
+
stepType: 'none', // 'none', 'e-step', 'm-step'
|
| 533 |
+
isPlaying: false,
|
| 534 |
+
converged: false,
|
| 535 |
+
logLikelihoods: [],
|
| 536 |
+
speed: 1500,
|
| 537 |
+
timer: null
|
| 538 |
+
};
|
| 539 |
+
|
| 540 |
+
// --- MATH UTILS ---
|
| 541 |
+
function multivariateNormalPDF(x, y, mean, cov) {
|
| 542 |
+
const dx = x - mean.x;
|
| 543 |
+
const dy = y - mean.y;
|
| 544 |
+
const det = cov.xx * cov.yy - cov.xy * cov.xy;
|
| 545 |
+
const inv = {
|
| 546 |
+
xx: cov.yy / det,
|
| 547 |
+
xy: -cov.xy / det,
|
| 548 |
+
yy: cov.xx / det
|
| 549 |
+
};
|
| 550 |
+
const exponent = -0.5 * (dx * (dx * inv.xx + dy * inv.xy) + dy * (dx * inv.xy + dy * inv.yy));
|
| 551 |
+
return (1 / (2 * Math.PI * Math.sqrt(Math.abs(det)))) * Math.exp(exponent);
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
function getEllipseParams(cov) {
|
| 555 |
+
const trace = cov.xx + cov.yy;
|
| 556 |
+
const det = cov.xx * cov.yy - cov.xy * cov.xy;
|
| 557 |
+
const sqrtDisc = Math.sqrt(Math.pow(trace / 2, 2) - det);
|
| 558 |
+
const lambda1 = trace / 2 + sqrtDisc;
|
| 559 |
+
const lambda2 = trace / 2 - sqrtDisc;
|
| 560 |
+
|
| 561 |
+
const a = Math.sqrt(Math.max(0, lambda1)) * 2;
|
| 562 |
+
const b = Math.sqrt(Math.max(0, lambda2)) * 2;
|
| 563 |
+
const angle = 0.5 * Math.atan2(2 * cov.xy, cov.xx - cov.yy) * (180 / Math.PI);
|
| 564 |
+
|
| 565 |
+
return { a, b, angle };
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
// --- CORE LOGIC (EM ALGORITHM) ---
|
| 569 |
+
function generateData() {
|
| 570 |
+
const points = [];
|
| 571 |
+
const centers = [
|
| 572 |
+
{ x: 150, y: 150 },
|
| 573 |
+
{ x: 450, y: 250 },
|
| 574 |
+
{ x: 250, y: 350 }
|
| 575 |
+
];
|
| 576 |
+
|
| 577 |
+
for (let i = 0; i < NUM_POINTS; i++) {
|
| 578 |
+
const center = centers[Math.floor(Math.random() * centers.length)];
|
| 579 |
+
points.push({
|
| 580 |
+
x: center.x + (Math.random() - 0.5) * 150,
|
| 581 |
+
y: center.y + (Math.random() - 0.5) * 150,
|
| 582 |
+
responsibilities: []
|
| 583 |
+
});
|
| 584 |
+
}
|
| 585 |
+
state.points = points;
|
| 586 |
+
initClusters();
|
| 587 |
+
resetSimState();
|
| 588 |
+
render();
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
function initClusters() {
|
| 592 |
+
state.clusters = [];
|
| 593 |
+
for (let i = 0; i < 3; i++) {
|
| 594 |
+
state.clusters.push({
|
| 595 |
+
mean: { x: Math.random() * 600, y: Math.random() * 400 },
|
| 596 |
+
covariance: { xx: 2500, xy: 0, yy: 2500 },
|
| 597 |
+
weight: 1 / 3,
|
| 598 |
+
color: CLUSTER_COLORS[i]
|
| 599 |
+
});
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
function resetSimState() {
|
| 604 |
+
state.iteration = 0;
|
| 605 |
+
state.stepType = 'none';
|
| 606 |
+
state.converged = false;
|
| 607 |
+
state.logLikelihoods = [];
|
| 608 |
+
state.points.forEach(p => delete p.clusterIndex);
|
| 609 |
+
stopAutoPlay();
|
| 610 |
+
updateUI();
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
function eStep() {
|
| 614 |
+
let totalLogLikelihood = 0;
|
| 615 |
+
|
| 616 |
+
state.points.forEach(p => {
|
| 617 |
+
let responsibilities = state.clusters.map(c => {
|
| 618 |
+
const prob = multivariateNormalPDF(p.x, p.y, c.mean, c.covariance);
|
| 619 |
+
return c.weight * prob;
|
| 620 |
+
});
|
| 621 |
+
|
| 622 |
+
const sum = responsibilities.reduce((a, b) => a + b, 0);
|
| 623 |
+
totalLogLikelihood += Math.log(sum || 1e-10);
|
| 624 |
+
|
| 625 |
+
if (sum > 0) {
|
| 626 |
+
responsibilities = responsibilities.map(r => r / sum);
|
| 627 |
+
} else {
|
| 628 |
+
responsibilities = state.clusters.map(() => 1 / state.clusters.length);
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
p.responsibilities = responsibilities;
|
| 632 |
+
|
| 633 |
+
// For visualization
|
| 634 |
+
let maxIdx = 0;
|
| 635 |
+
responsibilities.forEach((r, idx) => { if (r > responsibilities[maxIdx]) maxIdx = idx; });
|
| 636 |
+
p.clusterIndex = maxIdx;
|
| 637 |
+
});
|
| 638 |
+
|
| 639 |
+
state.logLikelihoods.push(totalLogLikelihood);
|
| 640 |
+
state.stepType = 'e-step';
|
| 641 |
+
|
| 642 |
+
// Check convergence
|
| 643 |
+
if (state.logLikelihoods.length > 2) {
|
| 644 |
+
const diff = Math.abs(state.logLikelihoods[state.logLikelihoods.length - 1] - state.logLikelihoods[state.logLikelihoods.length - 2]);
|
| 645 |
+
if (diff < 0.1) state.converged = true;
|
| 646 |
+
}
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
function mStep() {
|
| 650 |
+
const N = state.points.length;
|
| 651 |
+
|
| 652 |
+
state.clusters.forEach((c, j) => {
|
| 653 |
+
const sumResp = state.points.reduce((acc, p) => acc + p.responsibilities[j], 0);
|
| 654 |
+
|
| 655 |
+
// Update Weight
|
| 656 |
+
c.weight = sumResp / N;
|
| 657 |
+
|
| 658 |
+
// Update Mean
|
| 659 |
+
if (sumResp > 0) {
|
| 660 |
+
const newMean = state.points.reduce((acc, p) => {
|
| 661 |
+
acc.x += p.responsibilities[j] * p.x;
|
| 662 |
+
acc.y += p.responsibilities[j] * p.y;
|
| 663 |
+
return acc;
|
| 664 |
+
}, { x: 0, y: 0 });
|
| 665 |
+
c.mean.x = newMean.x / sumResp;
|
| 666 |
+
c.mean.y = newMean.y / sumResp;
|
| 667 |
+
|
| 668 |
+
// Update Covariance
|
| 669 |
+
const newCov = state.points.reduce((acc, p) => {
|
| 670 |
+
const dx = p.x - c.mean.x;
|
| 671 |
+
const dy = p.y - c.mean.y;
|
| 672 |
+
acc.xx += p.responsibilities[j] * dx * dx;
|
| 673 |
+
acc.xy += p.responsibilities[j] * dx * dy;
|
| 674 |
+
acc.yy += p.responsibilities[j] * dy * dy;
|
| 675 |
+
return acc;
|
| 676 |
+
}, { xx: 0, xy: 0, yy: 0 });
|
| 677 |
+
|
| 678 |
+
// Add a small value to diagonal for stability
|
| 679 |
+
c.covariance.xx = newCov.xx / sumResp + 10;
|
| 680 |
+
c.covariance.xy = newCov.xy / sumResp;
|
| 681 |
+
c.covariance.yy = newCov.yy / sumResp + 10;
|
| 682 |
+
}
|
| 683 |
+
});
|
| 684 |
+
|
| 685 |
+
state.iteration++;
|
| 686 |
+
state.stepType = 'm-step';
|
| 687 |
+
if (state.iteration >= MAX_ITERATIONS) state.converged = true;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
// --- RENDER & UI ---
|
| 691 |
+
function render() {
|
| 692 |
+
const pointsGroup = document.getElementById('points-group');
|
| 693 |
+
const ellipsesGroup = document.getElementById('ellipses-group');
|
| 694 |
+
|
| 695 |
+
// Clear
|
| 696 |
+
pointsGroup.innerHTML = '';
|
| 697 |
+
ellipsesGroup.innerHTML = '';
|
| 698 |
+
|
| 699 |
+
// Draw Clusters
|
| 700 |
+
state.clusters.forEach((c, i) => {
|
| 701 |
+
const params = getEllipseParams(c.covariance);
|
| 702 |
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
| 703 |
+
|
| 704 |
+
const ellipse = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
|
| 705 |
+
ellipse.setAttribute('cx', c.mean.x);
|
| 706 |
+
ellipse.setAttribute('cy', c.mean.y);
|
| 707 |
+
ellipse.setAttribute('rx', params.a);
|
| 708 |
+
ellipse.setAttribute('ry', params.b);
|
| 709 |
+
ellipse.setAttribute('fill', c.color.fill);
|
| 710 |
+
ellipse.setAttribute('stroke', c.color.stroke);
|
| 711 |
+
ellipse.setAttribute('stroke-width', '2');
|
| 712 |
+
ellipse.setAttribute('transform', `rotate(${params.angle}, ${c.mean.x}, ${c.mean.y})`);
|
| 713 |
+
if (state.isPlaying) ellipse.classList.add('animate-pulse-soft');
|
| 714 |
+
|
| 715 |
+
const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
| 716 |
+
center.setAttribute('cx', c.mean.x);
|
| 717 |
+
center.setAttribute('cy', c.mean.y);
|
| 718 |
+
center.setAttribute('r', '6');
|
| 719 |
+
center.setAttribute('fill', c.color.stroke);
|
| 720 |
+
center.setAttribute('stroke', 'white');
|
| 721 |
+
center.setAttribute('stroke-width', '2');
|
| 722 |
+
|
| 723 |
+
g.appendChild(ellipse);
|
| 724 |
+
g.appendChild(center);
|
| 725 |
+
ellipsesGroup.appendChild(g);
|
| 726 |
+
});
|
| 727 |
+
|
| 728 |
+
// Draw Points
|
| 729 |
+
state.points.forEach((p, i) => {
|
| 730 |
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
| 731 |
+
circle.setAttribute('cx', p.x);
|
| 732 |
+
circle.setAttribute('cy', p.y);
|
| 733 |
+
circle.setAttribute('r', '5');
|
| 734 |
+
circle.setAttribute('stroke', 'white');
|
| 735 |
+
circle.setAttribute('stroke-width', '1');
|
| 736 |
+
|
| 737 |
+
const color = p.clusterIndex !== undefined ? CLUSTER_COLORS[p.clusterIndex % CLUSTER_COLORS.length].stroke : '#94a3b8';
|
| 738 |
+
circle.setAttribute('fill', color);
|
| 739 |
+
pointsGroup.appendChild(circle);
|
| 740 |
+
});
|
| 741 |
+
|
| 742 |
+
updateUI();
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
function updateUI() {
|
| 746 |
+
// Update Legend
|
| 747 |
+
const legend = document.getElementById('legend');
|
| 748 |
+
if (legend) {
|
| 749 |
+
legend.innerHTML = state.clusters.map((c, i) => `
|
| 750 |
+
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white border shadow-sm">
|
| 751 |
+
<div class="w-3 h-3 rounded-full" style="background: ${c.color.stroke}"></div>
|
| 752 |
+
<span class="font-medium text-xs">${c.color.label}</span>
|
| 753 |
+
<span class="text-[10px] text-slate-400 font-mono">(${(c.weight * 100).toFixed(0)}%)</span>
|
| 754 |
+
</div>
|
| 755 |
+
`).join('');
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// Steps logic
|
| 759 |
+
const sInit = document.getElementById('step-init');
|
| 760 |
+
const sE = document.getElementById('step-e');
|
| 761 |
+
const sM = document.getElementById('step-m');
|
| 762 |
+
const l1 = document.getElementById('line-1');
|
| 763 |
+
const l2 = document.getElementById('line-2');
|
| 764 |
+
|
| 765 |
+
if (sInit && sE && sM) {
|
| 766 |
+
[sInit, sE, sM].forEach(el => el.className = 'step-node w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all bg-slate-100 text-slate-400');
|
| 767 |
+
if (l1 && l2) [l1, l2].forEach(el => el.className = 'h-0.5 flex-1 mx-2 bg-slate-200');
|
| 768 |
+
|
| 769 |
+
if (state.converged) {
|
| 770 |
+
[sInit, sE, sM].forEach(el => el.classList.add('converged'));
|
| 771 |
+
if (l1 && l2) [l1, l2].forEach(el => el.classList.add('bg-emerald-500'));
|
| 772 |
+
} else if (state.stepType === 'e-step') {
|
| 773 |
+
sE.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
|
| 774 |
+
sInit.className = 'step-node done w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-slate-200 text-slate-500';
|
| 775 |
+
if (l1) l1.className = 'h-0.5 flex-1 mx-2 bg-blue-200';
|
| 776 |
+
} else if (state.stepType === 'm-step') {
|
| 777 |
+
sM.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
|
| 778 |
+
sE.className = 'step-node done w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-slate-200 text-slate-500';
|
| 779 |
+
if (l2) l2.className = 'h-0.5 flex-1 mx-2 bg-blue-200';
|
| 780 |
+
} else {
|
| 781 |
+
sInit.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
|
| 782 |
+
}
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// Status Badge
|
| 786 |
+
const badge = document.getElementById('status-badge');
|
| 787 |
+
if (badge) {
|
| 788 |
+
if (state.converged) {
|
| 789 |
+
badge.className = 'px-4 py-2 rounded-lg font-semibold text-sm bg-emerald-100 text-emerald-700 block';
|
| 790 |
+
badge.innerText = 'Converged!';
|
| 791 |
+
} else if (state.stepType !== 'none') {
|
| 792 |
+
badge.className = `px-4 py-2 rounded-lg font-semibold text-sm block ${state.stepType === 'e-step' ? 'bg-orange-100 text-orange-700' : 'bg-blue-100 text-blue-700'}`;
|
| 793 |
+
badge.innerText = state.stepType === 'e-step' ? 'E-Step' : 'M-Step';
|
| 794 |
+
} else {
|
| 795 |
+
badge.className = 'hidden';
|
| 796 |
+
}
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
// Explanation Card
|
| 800 |
+
const explCard = document.getElementById('explanation-card');
|
| 801 |
+
const explTitle = document.getElementById('expl-title');
|
| 802 |
+
const explContent = document.getElementById('expl-content');
|
| 803 |
+
const explIcon = document.getElementById('expl-icon');
|
| 804 |
+
|
| 805 |
+
if (explCard && explTitle && explContent && explIcon) {
|
| 806 |
+
if (state.converged) {
|
| 807 |
+
explCard.className = 'p-5 rounded-xl border-2 bg-emerald-50 border-emerald-200 shadow-soft';
|
| 808 |
+
explTitle.innerText = "Converged! 🎉";
|
| 809 |
+
explIcon.className = "p-3 rounded-lg bg-white text-emerald-600";
|
| 810 |
+
explIcon.innerHTML = '<i data-lucide="check-circle-2" class="w-6 h-6"></i>';
|
| 811 |
+
explContent.innerHTML = `
|
| 812 |
+
<li class="flex items-start gap-2 text-sm text-emerald-800 font-medium">Clusters have stopped moving. Optimal fit found!</li>
|
| 813 |
+
<li class="flex items-start gap-2 text-sm text-slate-600">Total iterations: ${state.iteration}</li>
|
| 814 |
+
`;
|
| 815 |
+
} else if (state.stepType === 'e-step') {
|
| 816 |
+
explCard.className = 'p-5 rounded-xl border-2 bg-orange-50 border-orange-200 shadow-soft';
|
| 817 |
+
explTitle.innerText = "E-Step: Expectations 🤔";
|
| 818 |
+
explIcon.className = "p-3 rounded-lg bg-white text-orange-600";
|
| 819 |
+
explIcon.innerHTML = '<i data-lucide="target" class="w-6 h-6"></i>';
|
| 820 |
+
explContent.innerHTML = `
|
| 821 |
+
<li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-orange-500"></i> Each point calculates probabilities for all clusters.</li>
|
| 822 |
+
<li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-orange-500"></i> Points change color based on the highest probability.</li>
|
| 823 |
+
`;
|
| 824 |
+
} else if (state.stepType === 'm-step') {
|
| 825 |
+
explCard.className = 'p-5 rounded-xl border-2 bg-blue-50 border-blue-200 shadow-soft';
|
| 826 |
+
explTitle.innerText = "M-Step: Update 📊";
|
| 827 |
+
explIcon.className = "p-3 rounded-lg bg-white text-blue-600";
|
| 828 |
+
explIcon.innerHTML = '<i data-lucide="bar-chart-3" class="w-6 h-6"></i>';
|
| 829 |
+
explContent.innerHTML = `
|
| 830 |
+
<li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-blue-500"></i> Centers move to the heart of their assigned points.</li>
|
| 831 |
+
<li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-blue-500"></i> Ellipses stretch to cover the point spread.</li>
|
| 832 |
+
`;
|
| 833 |
+
} else {
|
| 834 |
+
explCard.className = 'p-5 rounded-xl border-2 bg-slate-50 border-slate-200 shadow-soft';
|
| 835 |
+
explTitle.innerText = "Ready to Start! 🚀";
|
| 836 |
+
explIcon.className = "p-3 rounded-lg bg-white text-blue-600";
|
| 837 |
+
explIcon.innerHTML = '<i data-lucide="lightbulb" class="w-6 h-6"></i>';
|
| 838 |
+
explContent.innerHTML = `
|
| 839 |
+
<li class="flex items-start gap-2 text-sm text-slate-600">Press 'Next Step' or 'Auto Play' to begin.</li>
|
| 840 |
+
`;
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
// Counters
|
| 845 |
+
const counterText = document.getElementById('counter-text');
|
| 846 |
+
if (counterText) counterText.innerText = `${state.iteration} / ${MAX_ITERATIONS}`;
|
| 847 |
+
|
| 848 |
+
const btnPlayText = document.getElementById('btn-play-text');
|
| 849 |
+
if (btnPlayText) btnPlayText.innerText = state.isPlaying ? 'Pause' : 'Auto Play';
|
| 850 |
+
|
| 851 |
+
const btnPlay = document.getElementById('btn-play');
|
| 852 |
+
if (btnPlay) {
|
| 853 |
+
const icon = btnPlay.querySelector('i, svg');
|
| 854 |
+
if (icon) icon.setAttribute('data-lucide', state.isPlaying ? 'pause' : 'play');
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
// Chart
|
| 858 |
+
renderChart();
|
| 859 |
+
if (window.lucide) lucide.createIcons();
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
function renderChart() {
|
| 863 |
+
const container = document.getElementById('chart-container');
|
| 864 |
+
if (!container) return;
|
| 865 |
+
if (state.logLikelihoods.length === 0) return;
|
| 866 |
+
|
| 867 |
+
const min = Math.min(...state.logLikelihoods);
|
| 868 |
+
const max = Math.max(...state.logLikelihoods);
|
| 869 |
+
const range = max - min || 10;
|
| 870 |
+
|
| 871 |
+
container.innerHTML = state.logLikelihoods.map((val, i) => {
|
| 872 |
+
const height = ((val - min) / range * 80) + 10;
|
| 873 |
+
return `<div class="bg-blue-500 rounded-t-sm flex-1 transition-all duration-300" style="height: ${height}%" title="Iter ${i}: ${val.toFixed(2)}"></div>`;
|
| 874 |
+
}).join('');
|
| 875 |
+
|
| 876 |
+
const convergedBadge = document.getElementById('converged-badge');
|
| 877 |
+
if (convergedBadge) {
|
| 878 |
+
if (state.converged) {
|
| 879 |
+
convergedBadge.classList.replace('hidden', 'flex');
|
| 880 |
+
} else {
|
| 881 |
+
convergedBadge.classList.replace('flex', 'hidden');
|
| 882 |
+
}
|
| 883 |
+
}
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
// --- HANDLERS ---
|
| 887 |
+
function performStep() {
|
| 888 |
+
if (state.converged) return;
|
| 889 |
+
if (state.stepType === 'none' || state.stepType === 'm-step') {
|
| 890 |
+
eStep();
|
| 891 |
+
} else {
|
| 892 |
+
mStep();
|
| 893 |
+
}
|
| 894 |
+
render();
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
function toggleAutoPlay() {
|
| 898 |
+
if (state.isPlaying) {
|
| 899 |
+
stopAutoPlay();
|
| 900 |
+
} else {
|
| 901 |
+
if (state.converged) return;
|
| 902 |
+
state.isPlaying = true;
|
| 903 |
+
state.timer = setInterval(() => {
|
| 904 |
+
performStep();
|
| 905 |
+
if (state.converged) stopAutoPlay();
|
| 906 |
+
}, state.speed);
|
| 907 |
+
updateUI();
|
| 908 |
+
}
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
function stopAutoPlay() {
|
| 912 |
+
state.isPlaying = false;
|
| 913 |
+
clearInterval(state.timer);
|
| 914 |
+
updateUI();
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
function switchTab(tabId) {
|
| 918 |
+
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
| 919 |
+
document.querySelectorAll('.tab-trigger').forEach(el => el.classList.remove('active', 'text-slate-900'));
|
| 920 |
+
document.querySelectorAll('.tab-trigger').forEach(el => el.classList.add('text-slate-600'));
|
| 921 |
+
|
| 922 |
+
const tab = document.getElementById(`tab-${tabId}`);
|
| 923 |
+
if (tab) tab.classList.add('active');
|
| 924 |
+
|
| 925 |
+
const btn = document.getElementById(`btn-tab-${tabId}`);
|
| 926 |
+
if (btn) {
|
| 927 |
+
btn.classList.add('active', 'text-slate-900');
|
| 928 |
+
btn.classList.remove('text-slate-600');
|
| 929 |
+
}
|
| 930 |
+
if (window.lucide) lucide.createIcons();
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
function closeModal() {
|
| 934 |
+
const modal = document.getElementById('intro-modal');
|
| 935 |
+
if (modal) modal.classList.add('hidden');
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
// --- INITIALIZATION ---
|
| 939 |
+
window.onload = () => {
|
| 940 |
+
generateData();
|
| 941 |
+
|
| 942 |
+
// Event Listeners
|
| 943 |
+
const btnPlay = document.getElementById('btn-play');
|
| 944 |
+
if (btnPlay) btnPlay.addEventListener('click', toggleAutoPlay);
|
| 945 |
+
|
| 946 |
+
const btnStep = document.getElementById('btn-step');
|
| 947 |
+
if (btnStep) btnStep.addEventListener('click', performStep);
|
| 948 |
+
|
| 949 |
+
const btnReset = document.getElementById('btn-reset');
|
| 950 |
+
if (btnReset) btnReset.addEventListener('click', () => {
|
| 951 |
+
initClusters();
|
| 952 |
+
resetSimState();
|
| 953 |
+
render();
|
| 954 |
+
});
|
| 955 |
+
|
| 956 |
+
const btnGenerate = document.getElementById('btn-generate');
|
| 957 |
+
if (btnGenerate) btnGenerate.addEventListener('click', generateData);
|
| 958 |
+
|
| 959 |
+
const speedSlider = document.getElementById('speed-slider');
|
| 960 |
+
if (speedSlider) {
|
| 961 |
+
speedSlider.addEventListener('input', (e) => {
|
| 962 |
+
state.speed = 3500 - parseInt(e.target.value);
|
| 963 |
+
const text = state.speed > 2000 ? 'Slow' : state.speed > 1000 ? 'Medium' : 'Fast';
|
| 964 |
+
const speedText = document.getElementById('speed-text');
|
| 965 |
+
if (speedText) speedText.innerText = text;
|
| 966 |
+
if (state.isPlaying) {
|
| 967 |
+
stopAutoPlay();
|
| 968 |
+
toggleAutoPlay();
|
| 969 |
+
}
|
| 970 |
+
});
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
if (window.lucide) lucide.createIcons();
|
| 974 |
+
};
|
| 975 |
+
</script>
|
| 976 |
+
</body>
|
| 977 |
+
</html>
|
templates/gradient-descent-three.html
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Gradient Descent Simulator</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--background: #05080a;
|
| 13 |
+
--primary: #00ffff;
|
| 14 |
+
--secondary: #a855f7;
|
| 15 |
+
--accent: #ec4899;
|
| 16 |
+
--surface: #0a1014;
|
| 17 |
+
--border: #1e293b;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
background-color: var(--background);
|
| 22 |
+
color: #f8fafc;
|
| 23 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 24 |
+
margin: 0;
|
| 25 |
+
overflow-x: hidden;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.gradient-text {
|
| 29 |
+
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
| 30 |
+
-webkit-background-clip: text;
|
| 31 |
+
-webkit-text-fill-color: transparent;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.glass {
|
| 35 |
+
background: rgba(10, 16, 20, 0.8);
|
| 36 |
+
backdrop-filter: blur(12px);
|
| 37 |
+
border: 1px solid rgba(30, 41, 59, 0.5);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.glow-cyan {
|
| 41 |
+
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.canvas-container {
|
| 45 |
+
width: 100%;
|
| 46 |
+
height: 350px; /* Default for mobile */
|
| 47 |
+
position: relative;
|
| 48 |
+
background: #000;
|
| 49 |
+
border-radius: 1rem;
|
| 50 |
+
overflow: hidden;
|
| 51 |
+
border: 1px solid var(--border);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* Desktop override */
|
| 55 |
+
@media (min-width: 768px) {
|
| 56 |
+
.canvas-container {
|
| 57 |
+
height: 500px;
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
input[type=range] {
|
| 62 |
+
-webkit-appearance: none;
|
| 63 |
+
width: 100%;
|
| 64 |
+
background: transparent;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
input[type=range]::-webkit-slider-runnable-track {
|
| 68 |
+
width: 100%;
|
| 69 |
+
height: 6px;
|
| 70 |
+
cursor: pointer;
|
| 71 |
+
background: #1e293b;
|
| 72 |
+
border-radius: 3px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
input[type=range]::-webkit-slider-thumb {
|
| 76 |
+
height: 18px;
|
| 77 |
+
width: 18px;
|
| 78 |
+
border-radius: 50%;
|
| 79 |
+
background: var(--primary);
|
| 80 |
+
cursor: pointer;
|
| 81 |
+
-webkit-appearance: none;
|
| 82 |
+
margin-top: -6px;
|
| 83 |
+
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.hidden { display: none; }
|
| 87 |
+
|
| 88 |
+
@keyframes float {
|
| 89 |
+
0%, 100% { transform: translateY(0px); }
|
| 90 |
+
50% { transform: translateY(-10px); }
|
| 91 |
+
}
|
| 92 |
+
.animate-float { animation: float 3s ease-in-out infinite; }
|
| 93 |
+
</style>
|
| 94 |
+
</head>
|
| 95 |
+
<body class="p-4 md:p-8">
|
| 96 |
+
|
| 97 |
+
<div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_350px] gap-8">
|
| 98 |
+
<!-- Left Side: Header & Viewport -->
|
| 99 |
+
<div class="space-y-6">
|
| 100 |
+
<header class="relative flex flex-col md:block">
|
| 101 |
+
<h1 class="text-4xl md:text-5xl font-bold gradient-text text-center md:text-left">Gradient Descent</h1>
|
| 102 |
+
|
| 103 |
+
<!-- Centered Button (Responsive Position) -->
|
| 104 |
+
<div class="order-last md:order-none mt-4 md:mt-0 md:absolute md:left-1/2 md:-translate-x-1/2 md:top-1 flex items-center justify-center">
|
| 105 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 106 |
+
<a href="/gradient-descent" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider">
|
| 107 |
+
Back to Core
|
| 108 |
+
</a>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<p class="text-slate-400 text-sm max-w-md mt-2 text-center md:text-left mx-auto md:mx-0">
|
| 112 |
+
Optimize parameters by descending the loss landscape. Now with <b>Adaptive Learning Rate</b> logic!
|
| 113 |
+
</p>
|
| 114 |
+
</header>
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
<div class="canvas-container" id="container">
|
| 118 |
+
<div id="three-canvas"></div>
|
| 119 |
+
|
| 120 |
+
<!-- Viewport Overlays -->
|
| 121 |
+
<div class="absolute top-4 left-4 pointer-events-none">
|
| 122 |
+
<div class="glass px-3 py-2 rounded-lg">
|
| 123 |
+
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div>
|
| 124 |
+
<div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase">Ready</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div class="absolute top-4 right-4 pointer-events-none text-right">
|
| 129 |
+
<div class="glass px-3 py-2 rounded-lg">
|
| 130 |
+
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Position</div>
|
| 131 |
+
<div id="pos-display" class="text-xs font-mono text-cyan-400 mt-1">X: 3.00 Y: 3.00</div>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<div class="absolute bottom-4 left-4 glass px-3 py-1 rounded-full text-[10px] text-slate-400 font-bold border border-slate-800">
|
| 136 |
+
GOAL AT <span id="goal-coords" class="text-green-400">0, 0</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<!-- Educational Info -->
|
| 141 |
+
<div class="glass rounded-xl overflow-hidden">
|
| 142 |
+
<button onclick="toggleInfo()" class="w-full flex items-center justify-between p-4 hover:bg-slate-800/30 transition-colors">
|
| 143 |
+
<div class="flex items-center gap-2">
|
| 144 |
+
<svg class="w-5 h-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
|
| 145 |
+
<span class="font-bold">Optimizer Secrets</span>
|
| 146 |
+
</div>
|
| 147 |
+
<svg id="info-arrow" class="w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
| 148 |
+
</button>
|
| 149 |
+
<div id="info-content" class="p-4 pt-0 text-sm text-slate-400 leading-relaxed grid md:grid-cols-2 gap-6 border-t border-slate-800 hidden">
|
| 150 |
+
<div class="space-y-2 pt-4">
|
| 151 |
+
<p><strong class="text-white">Momentum (β):</strong> Acts like a physical ball with weight. It keeps moving in the same direction, helping it "roll" through flat valleys and over small local pits.</p>
|
| 152 |
+
</div>
|
| 153 |
+
<div class="space-y-2 pt-4">
|
| 154 |
+
<p><strong class="text-white">The Challenge:</strong> Rosenbrock and Rastrigin are "non-convex" or have "vanishing gradients." Vanilla GD is often too weak to reach the center without help!</p>
|
| 155 |
+
</div>
|
| 156 |
+
<!-- Pro Tip Note -->
|
| 157 |
+
<div class="col-span-full pt-4 border-t border-slate-800/50 mt-2">
|
| 158 |
+
<p class="text-yellow-400 font-medium">
|
| 159 |
+
<span class="mr-2">💡</span> <b>Pro Tip:</b> If the ball gets stuck or vibrates wildly, you must <b>adjust the learning rate</b> or <b>momentum</b>. Or turn on <b>Adaptive Rate</b> to let the algorithm handle it!
|
| 160 |
+
</p>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<!-- Right Side: Controls -->
|
| 167 |
+
<aside class="space-y-6">
|
| 168 |
+
<div class="glass rounded-xl p-6 space-y-6">
|
| 169 |
+
<!-- Learning Rate Slider -->
|
| 170 |
+
<div class="space-y-4">
|
| 171 |
+
<div class="flex justify-between items-center">
|
| 172 |
+
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider">Learning Rate (α)</label>
|
| 173 |
+
<span id="lr-value" class="text-cyan-400 font-mono bg-cyan-400/10 px-2 py-0.5 rounded border border-cyan-400/20 text-sm">0.050</span>
|
| 174 |
+
</div>
|
| 175 |
+
<input type="range" id="lr-slider" min="0.001" max="0.5" step="0.001" value="0.05">
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<!-- Adaptive Toggle -->
|
| 179 |
+
<div onclick="toggleAdaptive()" class="flex items-center justify-between p-3 rounded-lg bg-slate-800/40 border border-slate-700/50 cursor-pointer hover:bg-slate-800 transition-colors">
|
| 180 |
+
<div class="flex flex-col">
|
| 181 |
+
<span class="text-xs font-bold text-slate-300">Adaptive Rate</span>
|
| 182 |
+
<span class="text-[10px] text-slate-500">Auto-tune α during descent</span>
|
| 183 |
+
</div>
|
| 184 |
+
<div id="adaptive-toggle-bg" class="w-10 h-5 rounded-full bg-slate-700 relative transition-colors">
|
| 185 |
+
<div id="adaptive-toggle-dot" class="absolute left-1 top-1 w-3 h-3 rounded-full bg-white transition-all"></div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<!-- Momentum Slider -->
|
| 190 |
+
<div class="space-y-4">
|
| 191 |
+
<div class="flex justify-between items-center">
|
| 192 |
+
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider">Momentum (β)</label>
|
| 193 |
+
<span id="mom-value" class="text-purple-400 font-mono bg-purple-400/10 px-2 py-0.5 rounded border border-purple-400/20 text-sm">0.10</span>
|
| 194 |
+
</div>
|
| 195 |
+
<input type="range" id="mom-slider" min="0" max="0.99" step="0.01" value="0.1">
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<!-- Playback Buttons -->
|
| 199 |
+
<div class="grid grid-cols-2 gap-3">
|
| 200 |
+
<button id="btn-toggle" class="flex items-center justify-center gap-2 py-3 rounded-lg font-bold transition-all bg-cyan-400 text-black glow-cyan hover:brightness-110">
|
| 201 |
+
<span id="toggle-icon">▶</span> <span id="toggle-text">Start</span>
|
| 202 |
+
</button>
|
| 203 |
+
<button id="btn-step" class="flex items-center justify-center gap-2 py-3 rounded-lg border border-slate-700 hover:bg-slate-800 transition-all font-bold">
|
| 204 |
+
Step ➜
|
| 205 |
+
</button>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<button id="btn-reset" class="w-full flex items-center justify-center gap-2 py-2 text-slate-400 hover:text-white text-sm transition-colors font-medium">
|
| 209 |
+
Reset Position ↺
|
| 210 |
+
</button>
|
| 211 |
+
|
| 212 |
+
<!-- Stats -->
|
| 213 |
+
<div class="grid grid-cols-2 gap-4 pt-4 border-t border-slate-800">
|
| 214 |
+
<div class="text-center">
|
| 215 |
+
<div id="steps-val" class="text-2xl font-mono font-bold text-cyan-400">0</div>
|
| 216 |
+
<div class="text-[10px] text-slate-500 uppercase tracking-tighter">Steps</div>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="text-center">
|
| 219 |
+
<div id="loss-val" class="text-2xl font-mono font-bold text-pink-500">9.00</div>
|
| 220 |
+
<div class="text-[10px] text-slate-500 uppercase tracking-tighter">Loss</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<!-- Progress -->
|
| 225 |
+
<div class="p-4 bg-slate-900/50 rounded-lg border border-slate-800">
|
| 226 |
+
<div class="flex justify-between text-xs mb-2">
|
| 227 |
+
<span class="text-slate-500">Distance to Target</span>
|
| 228 |
+
<span id="dist-val" class="font-mono text-slate-300">4.242</span>
|
| 229 |
+
</div>
|
| 230 |
+
<div class="h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
| 231 |
+
<div id="progress-bar" class="h-full bg-cyan-400 transition-all duration-300" style="width: 10%"></div>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<!-- Level Selector -->
|
| 237 |
+
<div class="glass rounded-xl p-4 space-y-3">
|
| 238 |
+
<h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest px-2">Function Landscape</h3>
|
| 239 |
+
<div id="level-list" class="space-y-1">
|
| 240 |
+
<!-- Levels injected here -->
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</aside>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<!-- Victory Modal -->
|
| 247 |
+
<div id="victory-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm hidden">
|
| 248 |
+
<div class="glass relative max-w-sm w-full rounded-2xl p-8 text-center animate-float">
|
| 249 |
+
<!-- Close Button (Cross) -->
|
| 250 |
+
<button onclick="closeModal()" class="absolute top-4 right-4 text-slate-500 hover:text-white transition-colors">
|
| 251 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
| 252 |
+
</button>
|
| 253 |
+
|
| 254 |
+
<div class="w-16 h-16 bg-cyan-400/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 255 |
+
<span class="text-3xl text-cyan-400">🏆</span>
|
| 256 |
+
</div>
|
| 257 |
+
<h2 class="text-2xl font-bold gradient-text mb-1">Convergence!</h2>
|
| 258 |
+
<p id="modal-level-name" class="text-slate-400 text-sm mb-6">Level Complete</p>
|
| 259 |
+
|
| 260 |
+
<div class="bg-slate-900/80 rounded-xl p-4 mb-6">
|
| 261 |
+
<div id="modal-steps" class="text-3xl font-mono text-cyan-400 font-bold">0</div>
|
| 262 |
+
<div class="text-xs text-slate-500 uppercase tracking-widest">Total Steps</div>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
+
<div class="flex gap-3">
|
| 266 |
+
<button onclick="closeModal()" class="flex-1 py-2 rounded-lg border border-slate-700 hover:bg-slate-800 transition-colors text-sm font-semibold">Retry</button>
|
| 267 |
+
<button onclick="nextLevel()" class="flex-1 py-2 rounded-lg bg-cyan-400 text-black font-bold text-sm">Next Level</button>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<script>
|
| 273 |
+
// --- LEVEL DEFINITIONS ---
|
| 274 |
+
// Added 'presets' to automatically tune parameters for each landscape
|
| 275 |
+
const LEVELS = [
|
| 276 |
+
{
|
| 277 |
+
id: 1,
|
| 278 |
+
name: 'The Bowl',
|
| 279 |
+
difficulty: 'easy',
|
| 280 |
+
loss: (x, y) => x*x + y*y,
|
| 281 |
+
min: {x: 0, y: 0},
|
| 282 |
+
winRadius: 0.15,
|
| 283 |
+
presets: { lr: 0.05, mom: 0.1 } // Slow & Steady
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
id: 2,
|
| 287 |
+
name: 'The Ellipse',
|
| 288 |
+
difficulty: 'easy',
|
| 289 |
+
loss: (x, y) => x*x + 10*y*y,
|
| 290 |
+
min: {x: 0, y: 0},
|
| 291 |
+
winRadius: 0.2,
|
| 292 |
+
presets: { lr: 0.05, mom: 0.1 }
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
id: 3,
|
| 296 |
+
name: 'Rosenbrock Valley',
|
| 297 |
+
difficulty: 'medium',
|
| 298 |
+
loss: (x, y) => Math.pow(1-x, 2) + 100*Math.pow(y-x*x, 2),
|
| 299 |
+
min: {x: 1, y: 1},
|
| 300 |
+
winRadius: 0.25,
|
| 301 |
+
presets: { lr: 0.002, mom: 0.85 } // Needs momentum to traverse the valley
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
id: 4,
|
| 305 |
+
name: 'Beale Function',
|
| 306 |
+
difficulty: 'medium',
|
| 307 |
+
loss: (x, y) => Math.pow(1.5-x+x*y, 2) + Math.pow(2.25-x+x*y*y, 2) + Math.pow(2.625-x+x*y*y*y, 2),
|
| 308 |
+
min: {x: 3, y: 0.5},
|
| 309 |
+
winRadius: 0.25,
|
| 310 |
+
presets: { lr: 0.01, mom: 0.5 }
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
id: 5,
|
| 314 |
+
name: 'Rastrigin',
|
| 315 |
+
difficulty: 'hard',
|
| 316 |
+
loss: (x, y) => 20 + x*x - 10*Math.cos(2*Math.PI*x) + y*y - 10*Math.cos(2*Math.PI*y),
|
| 317 |
+
min: {x: 0, y: 0},
|
| 318 |
+
winRadius: 0.3,
|
| 319 |
+
presets: { lr: 0.005, mom: 0.95 } // HIGH MOMENTUM to escape local minima
|
| 320 |
+
}
|
| 321 |
+
];
|
| 322 |
+
|
| 323 |
+
// Constants for 3D Visuals
|
| 324 |
+
const SURFACE_OFFSET = -0.5;
|
| 325 |
+
const Z_SCALE = 0.8;
|
| 326 |
+
const BALL_FLOAT = 0.1;
|
| 327 |
+
|
| 328 |
+
// --- APP STATE ---
|
| 329 |
+
let currentLevel = LEVELS[0];
|
| 330 |
+
let pos = { x: 3, y: 3 };
|
| 331 |
+
let velocity = { x: 0, y: 0 };
|
| 332 |
+
let path = [];
|
| 333 |
+
let learningRate = 0.05;
|
| 334 |
+
let momentum = 0.1; // Default starting momentum
|
| 335 |
+
let isAdaptive = false;
|
| 336 |
+
let isRunning = false;
|
| 337 |
+
let steps = 0;
|
| 338 |
+
let loopId = null;
|
| 339 |
+
let prevLoss = Infinity;
|
| 340 |
+
|
| 341 |
+
// --- THREE.JS SETUP ---
|
| 342 |
+
const container = document.getElementById('container');
|
| 343 |
+
const scene = new THREE.Scene();
|
| 344 |
+
scene.background = new THREE.Color(0x05080a);
|
| 345 |
+
|
| 346 |
+
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
|
| 347 |
+
camera.position.set(8, 8, 8);
|
| 348 |
+
camera.lookAt(0, 0, 0);
|
| 349 |
+
|
| 350 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 351 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 352 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
| 353 |
+
document.getElementById('three-canvas').appendChild(renderer.domElement);
|
| 354 |
+
|
| 355 |
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
| 356 |
+
scene.add(ambientLight);
|
| 357 |
+
const pointLight = new THREE.PointLight(0x00ffff, 1);
|
| 358 |
+
pointLight.position.set(10, 10, 10);
|
| 359 |
+
scene.add(pointLight);
|
| 360 |
+
|
| 361 |
+
function calculateVizHeight(x, y) {
|
| 362 |
+
let z = currentLevel.loss(x, y);
|
| 363 |
+
z = Math.min(z, 50);
|
| 364 |
+
return (Math.log(z + 1) * Z_SCALE) + SURFACE_OFFSET;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// Surface
|
| 368 |
+
let surfaceMesh;
|
| 369 |
+
function updateSurface() {
|
| 370 |
+
if (surfaceMesh) scene.remove(surfaceMesh);
|
| 371 |
+
|
| 372 |
+
const size = 64;
|
| 373 |
+
const geo = new THREE.PlaneBufferGeometry(10, 10, size, size);
|
| 374 |
+
const posAttr = geo.attributes.position;
|
| 375 |
+
const colorAttr = new THREE.BufferAttribute(new Float32Array(posAttr.count * 3), 3);
|
| 376 |
+
|
| 377 |
+
let minVal = Infinity, maxVal = -Infinity;
|
| 378 |
+
const vals = [];
|
| 379 |
+
|
| 380 |
+
for (let i = 0; i < posAttr.count; i++) {
|
| 381 |
+
const x = posAttr.getX(i);
|
| 382 |
+
const y = posAttr.getY(i);
|
| 383 |
+
let z = currentLevel.loss(x, y);
|
| 384 |
+
z = Math.min(z, 50);
|
| 385 |
+
z = Math.log(z + 1) * Z_SCALE;
|
| 386 |
+
vals.push(z);
|
| 387 |
+
minVal = Math.min(minVal, z);
|
| 388 |
+
maxVal = Math.max(maxVal, z);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
for (let i = 0; i < posAttr.count; i++) {
|
| 392 |
+
const z = vals[i];
|
| 393 |
+
posAttr.setZ(i, z);
|
| 394 |
+
const t = (z - minVal) / (maxVal - minVal || 1);
|
| 395 |
+
colorAttr.setXYZ(i, 0.1 + t * 0.8, 0.8 - t * 0.6, 0.8 + t * 0.2);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
geo.setAttribute('color', colorAttr);
|
| 399 |
+
geo.computeVertexNormals();
|
| 400 |
+
|
| 401 |
+
const mat = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide, transparent: true, opacity: 0.8, roughness: 0.5 });
|
| 402 |
+
surfaceMesh = new THREE.Mesh(geo, mat);
|
| 403 |
+
surfaceMesh.rotation.x = -Math.PI / 2;
|
| 404 |
+
surfaceMesh.position.y = SURFACE_OFFSET;
|
| 405 |
+
scene.add(surfaceMesh);
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
const ball = new THREE.Mesh(
|
| 409 |
+
new THREE.SphereGeometry(0.18, 24, 24),
|
| 410 |
+
new THREE.MeshStandardMaterial({ color: 0xff66cc, emissive: 0xff0088, emissiveIntensity: 0.5 })
|
| 411 |
+
);
|
| 412 |
+
scene.add(ball);
|
| 413 |
+
|
| 414 |
+
let pathLine;
|
| 415 |
+
function updatePath() {
|
| 416 |
+
if (pathLine) scene.remove(pathLine);
|
| 417 |
+
if (path.length < 2) return;
|
| 418 |
+
|
| 419 |
+
const points = path.map(p => new THREE.Vector3(p.x, calculateVizHeight(p.x, p.y) + 0.05, p.y));
|
| 420 |
+
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
| 421 |
+
const mat = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8 });
|
| 422 |
+
pathLine = new THREE.Line(geo, mat);
|
| 423 |
+
scene.add(pathLine);
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
const goalMarker = new THREE.Group();
|
| 427 |
+
const torus = new THREE.Mesh(new THREE.TorusGeometry(0.3, 0.02, 16, 32), new THREE.MeshBasicMaterial({ color: 0x00ff66, transparent: true, opacity: 0.5 }));
|
| 428 |
+
torus.rotation.x = Math.PI/2;
|
| 429 |
+
const star = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), new THREE.MeshBasicMaterial({ color: 0x00ff66 }));
|
| 430 |
+
goalMarker.add(torus);
|
| 431 |
+
goalMarker.add(star);
|
| 432 |
+
scene.add(goalMarker);
|
| 433 |
+
|
| 434 |
+
function updateGoalMarker() {
|
| 435 |
+
const gx = currentLevel.min.x;
|
| 436 |
+
const gy = currentLevel.min.y;
|
| 437 |
+
const gz = calculateVizHeight(gx, gy);
|
| 438 |
+
goalMarker.position.set(gx, gz + 0.05, gy);
|
| 439 |
+
document.getElementById('goal-coords').innerText = `[${gx}, ${gy}]`;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
scene.add(new THREE.GridHelper(10, 20, 0x1a3a4a, 0x0a1a2a));
|
| 443 |
+
|
| 444 |
+
// --- OPTIMIZER LOGIC ---
|
| 445 |
+
function calculateGradient(x, y) {
|
| 446 |
+
const h = 0.0001;
|
| 447 |
+
const dx = (currentLevel.loss(x + h, y) - currentLevel.loss(x - h, y)) / (2 * h);
|
| 448 |
+
const dy = (currentLevel.loss(x, y + h) - currentLevel.loss(x, y - h)) / (2 * h);
|
| 449 |
+
|
| 450 |
+
const magnitude = Math.sqrt(dx*dx + dy*dy);
|
| 451 |
+
const limit = 50;
|
| 452 |
+
if (magnitude > limit) {
|
| 453 |
+
return { x: (dx / magnitude) * limit, y: (dy / magnitude) * limit, magnitude: limit };
|
| 454 |
+
}
|
| 455 |
+
return { x: dx, y: dy, magnitude };
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
function step() {
|
| 459 |
+
const currentLoss = currentLevel.loss(pos.x, pos.y);
|
| 460 |
+
const grad = calculateGradient(pos.x, pos.y);
|
| 461 |
+
|
| 462 |
+
// --- ADAPTIVE LOGIC ---
|
| 463 |
+
if (isAdaptive) {
|
| 464 |
+
if (currentLoss > prevLoss * 1.05) {
|
| 465 |
+
learningRate *= 0.5;
|
| 466 |
+
}
|
| 467 |
+
else if (Math.abs(currentLoss - prevLoss) < 0.00001 && grad.magnitude < 0.01) {
|
| 468 |
+
learningRate *= 1.2;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
learningRate = Math.max(0.001, Math.min(0.5, learningRate));
|
| 472 |
+
updateLRElement();
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// Momentum Logic
|
| 476 |
+
velocity.x = momentum * velocity.x - learningRate * grad.x;
|
| 477 |
+
velocity.y = momentum * velocity.y - learningRate * grad.y;
|
| 478 |
+
|
| 479 |
+
pos.x = Math.max(-5, Math.min(5, pos.x + velocity.x));
|
| 480 |
+
pos.y = Math.max(-5, Math.min(5, pos.y + velocity.y));
|
| 481 |
+
|
| 482 |
+
path.push({...pos});
|
| 483 |
+
steps++;
|
| 484 |
+
prevLoss = currentLoss;
|
| 485 |
+
updateUI();
|
| 486 |
+
|
| 487 |
+
const dist = Math.sqrt(Math.pow(pos.x - currentLevel.min.x, 2) + Math.pow(pos.y - currentLevel.min.y, 2));
|
| 488 |
+
if (dist < currentLevel.winRadius) {
|
| 489 |
+
stopDescent();
|
| 490 |
+
showVictory();
|
| 491 |
+
} else if (steps > 3000) {
|
| 492 |
+
stopDescent();
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
function updateLRElement() {
|
| 497 |
+
document.getElementById('lr-slider').value = learningRate;
|
| 498 |
+
document.getElementById('lr-value').innerText = learningRate.toFixed(3);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
function updateUI() {
|
| 502 |
+
document.getElementById('pos-display').innerText = `X: ${pos.x.toFixed(2)} Y: ${pos.y.toFixed(2)}`;
|
| 503 |
+
document.getElementById('steps-val').innerText = steps;
|
| 504 |
+
const lossVal = currentLevel.loss(pos.x, pos.y);
|
| 505 |
+
document.getElementById('loss-val').innerText = lossVal.toFixed(2);
|
| 506 |
+
|
| 507 |
+
const dist = Math.sqrt(Math.pow(pos.x - currentLevel.min.x, 2) + Math.pow(pos.y - currentLevel.min.y, 2));
|
| 508 |
+
document.getElementById('dist-val').innerText = dist.toFixed(3);
|
| 509 |
+
document.getElementById('progress-bar').style.width = `${Math.max(5, Math.min(100, (1 - dist / 7) * 100))}%`;
|
| 510 |
+
|
| 511 |
+
const ballZ = calculateVizHeight(pos.x, pos.y);
|
| 512 |
+
ball.position.set(pos.x, ballZ + BALL_FLOAT, pos.y);
|
| 513 |
+
updatePath();
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
function toggleAdaptive() {
|
| 517 |
+
isAdaptive = !isAdaptive;
|
| 518 |
+
const bg = document.getElementById('adaptive-toggle-bg');
|
| 519 |
+
const dot = document.getElementById('adaptive-toggle-dot');
|
| 520 |
+
if (isAdaptive) {
|
| 521 |
+
bg.classList.replace('bg-slate-700', 'bg-cyan-400');
|
| 522 |
+
dot.style.left = '22px';
|
| 523 |
+
} else {
|
| 524 |
+
bg.classList.replace('bg-cyan-400', 'bg-slate-700');
|
| 525 |
+
dot.style.left = '4px';
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
function startDescent() {
|
| 530 |
+
isRunning = true;
|
| 531 |
+
document.getElementById('toggle-text').innerText = 'Pause';
|
| 532 |
+
document.getElementById('status-text').innerText = 'Descending...';
|
| 533 |
+
document.getElementById('status-text').className = 'text-xs font-mono text-yellow-400 mt-1 uppercase';
|
| 534 |
+
prevLoss = currentLevel.loss(pos.x, pos.y);
|
| 535 |
+
loopId = setInterval(step, 50);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
function stopDescent() {
|
| 539 |
+
isRunning = false;
|
| 540 |
+
document.getElementById('toggle-text').innerText = 'Start';
|
| 541 |
+
document.getElementById('status-text').innerText = 'Paused';
|
| 542 |
+
document.getElementById('status-text').className = 'text-xs font-mono text-cyan-400 mt-1 uppercase';
|
| 543 |
+
clearInterval(loopId);
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
function showVictory() {
|
| 547 |
+
document.getElementById('modal-level-name').innerText = currentLevel.name;
|
| 548 |
+
document.getElementById('modal-steps').innerText = steps;
|
| 549 |
+
document.getElementById('victory-modal').classList.remove('hidden');
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
function reset() {
|
| 553 |
+
stopDescent();
|
| 554 |
+
if(currentLevel.id === 3) pos = { x: -3, y: -3 };
|
| 555 |
+
else if(currentLevel.id === 4) pos = { x: 1, y: 1 };
|
| 556 |
+
else pos = { x: (Math.random() - 0.5) * 8, y: (Math.random() - 0.5) * 8 };
|
| 557 |
+
|
| 558 |
+
velocity = { x: 0, y: 0 };
|
| 559 |
+
path = [{...pos}];
|
| 560 |
+
steps = 0;
|
| 561 |
+
updateUI();
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
// --- EVENT HANDLERS ---
|
| 565 |
+
document.getElementById('btn-toggle').onclick = () => isRunning ? stopDescent() : startDescent();
|
| 566 |
+
document.getElementById('btn-step').onclick = step;
|
| 567 |
+
document.getElementById('btn-reset').onclick = reset;
|
| 568 |
+
document.getElementById('lr-slider').oninput = (e) => {
|
| 569 |
+
learningRate = parseFloat(e.target.value);
|
| 570 |
+
document.getElementById('lr-value').innerText = learningRate.toFixed(3);
|
| 571 |
+
};
|
| 572 |
+
document.getElementById('mom-slider').oninput = (e) => {
|
| 573 |
+
momentum = parseFloat(e.target.value);
|
| 574 |
+
document.getElementById('mom-value').innerText = momentum.toFixed(2);
|
| 575 |
+
};
|
| 576 |
+
|
| 577 |
+
function toggleInfo() {
|
| 578 |
+
const content = document.getElementById('info-content');
|
| 579 |
+
content.classList.toggle('hidden');
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
function closeModal() {
|
| 583 |
+
document.getElementById('victory-modal').classList.add('hidden');
|
| 584 |
+
reset();
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
function nextLevel() {
|
| 588 |
+
const idx = LEVELS.findIndex(l => l.id === currentLevel.id);
|
| 589 |
+
const next = LEVELS[(idx + 1) % LEVELS.length];
|
| 590 |
+
selectLevel(next.id);
|
| 591 |
+
document.getElementById('victory-modal').classList.add('hidden');
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
function selectLevel(id) {
|
| 595 |
+
currentLevel = LEVELS.find(l => l.id === id);
|
| 596 |
+
|
| 597 |
+
// Apply Presets if they exist
|
| 598 |
+
if (currentLevel.presets) {
|
| 599 |
+
learningRate = currentLevel.presets.lr;
|
| 600 |
+
momentum = currentLevel.presets.mom;
|
| 601 |
+
|
| 602 |
+
// Update UI Controls
|
| 603 |
+
document.getElementById('lr-slider').value = learningRate;
|
| 604 |
+
document.getElementById('lr-value').innerText = learningRate.toFixed(3);
|
| 605 |
+
document.getElementById('mom-slider').value = momentum;
|
| 606 |
+
document.getElementById('mom-value').innerText = momentum.toFixed(2);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
renderLevels();
|
| 610 |
+
updateSurface();
|
| 611 |
+
updateGoalMarker();
|
| 612 |
+
reset();
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function renderLevels() {
|
| 616 |
+
const list = document.getElementById('level-list');
|
| 617 |
+
list.innerHTML = LEVELS.map(l => `
|
| 618 |
+
<button onclick="selectLevel(${l.id})" class="w-full text-left p-3 rounded-lg transition-all border ${currentLevel.id === l.id ? 'bg-cyan-400/10 border-cyan-400/50' : 'border-transparent hover:bg-slate-800'}">
|
| 619 |
+
<div class="flex justify-between items-center mb-1">
|
| 620 |
+
<span class="font-bold text-sm ${currentLevel.id === l.id ? 'text-cyan-400' : ''}">${l.name}</span>
|
| 621 |
+
<span class="text-[9px] px-1.5 py-0.5 rounded border ${l.difficulty === 'easy' ? 'border-green-500/30 text-green-400' : l.difficulty === 'medium' ? 'border-yellow-500/30 text-yellow-400' : 'border-red-500/30 text-red-400'} uppercase">${l.difficulty}</span>
|
| 622 |
+
</div>
|
| 623 |
+
</button>
|
| 624 |
+
`).join('');
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
function animate() {
|
| 628 |
+
requestAnimationFrame(animate);
|
| 629 |
+
if (isRunning) {
|
| 630 |
+
const s = 1 + Math.sin(Date.now() * 0.01) * 0.15;
|
| 631 |
+
ball.scale.setScalar(s);
|
| 632 |
+
}
|
| 633 |
+
renderer.render(scene, camera);
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
window.addEventListener('resize', () => {
|
| 637 |
+
camera.aspect = container.clientWidth / container.clientHeight;
|
| 638 |
+
camera.updateProjectionMatrix();
|
| 639 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 640 |
+
});
|
| 641 |
+
|
| 642 |
+
renderLevels();
|
| 643 |
+
updateSurface();
|
| 644 |
+
updateGoalMarker();
|
| 645 |
+
updateUI();
|
| 646 |
+
animate();
|
| 647 |
+
|
| 648 |
+
// --- MOUSE & TOUCH CONTROLS ---
|
| 649 |
+
let isMouseDown = false;
|
| 650 |
+
let prevMouse = { x: 0, y: 0 };
|
| 651 |
+
|
| 652 |
+
// Mouse Events
|
| 653 |
+
container.addEventListener('mousedown', (e) => {
|
| 654 |
+
isMouseDown = true;
|
| 655 |
+
prevMouse = { x: e.clientX, y: e.clientY };
|
| 656 |
+
});
|
| 657 |
+
window.addEventListener('mouseup', () => isMouseDown = false);
|
| 658 |
+
window.addEventListener('mousemove', (e) => handleCameraRotate(e.clientX, e.clientY));
|
| 659 |
+
|
| 660 |
+
// Touch Events (Mobile)
|
| 661 |
+
container.addEventListener('touchstart', (e) => {
|
| 662 |
+
if(e.touches.length === 1) {
|
| 663 |
+
isMouseDown = true;
|
| 664 |
+
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
| 665 |
+
}
|
| 666 |
+
}, { passive: false });
|
| 667 |
+
|
| 668 |
+
window.addEventListener('touchend', () => isMouseDown = false);
|
| 669 |
+
|
| 670 |
+
window.addEventListener('touchmove', (e) => {
|
| 671 |
+
if(e.touches.length === 1 && isMouseDown) {
|
| 672 |
+
handleCameraRotate(e.touches[0].clientX, e.touches[0].clientY);
|
| 673 |
+
}
|
| 674 |
+
}, { passive: false });
|
| 675 |
+
|
| 676 |
+
// Common Rotation Logic
|
| 677 |
+
function handleCameraRotate(clientX, clientY) {
|
| 678 |
+
if (!isMouseDown) return;
|
| 679 |
+
const dx = (clientX - prevMouse.x) * 0.005;
|
| 680 |
+
const dy = (clientY - prevMouse.y) * 0.005;
|
| 681 |
+
const radius = camera.position.length();
|
| 682 |
+
let phi = Math.atan2(camera.position.x, camera.position.z);
|
| 683 |
+
let theta = Math.acos(camera.position.y / radius);
|
| 684 |
+
phi -= dx;
|
| 685 |
+
theta = Math.max(0.1, Math.min(Math.PI / 2.1, theta - dy));
|
| 686 |
+
camera.position.x = radius * Math.sin(theta) * Math.sin(phi);
|
| 687 |
+
camera.position.y = radius * Math.cos(theta);
|
| 688 |
+
camera.position.z = radius * Math.sin(theta) * Math.cos(phi);
|
| 689 |
+
camera.lookAt(0, 0, 0);
|
| 690 |
+
prevMouse = { x: clientX, y: clientY };
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
</script>
|
| 694 |
+
</body>
|
| 695 |
+
</html>
|
templates/ica-threejs.html
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>ICA Simulator - Independent Component Analysis</title>
|
| 7 |
+
|
| 8 |
+
<!-- Tailwind CSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
|
| 11 |
+
<!-- Babel for JSX (Standalone) -->
|
| 12 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 13 |
+
|
| 14 |
+
<!-- Google Fonts -->
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 16 |
+
|
| 17 |
+
<!-- Import Map -->
|
| 18 |
+
<script type="importmap">
|
| 19 |
+
{
|
| 20 |
+
"imports": {
|
| 21 |
+
"react": "https://esm.sh/react@18.2.0",
|
| 22 |
+
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
{% raw %}
|
| 28 |
+
<style>
|
| 29 |
+
:root {
|
| 30 |
+
--background: 40 30% 98%;
|
| 31 |
+
--foreground: 220 20% 20%;
|
| 32 |
+
--card: 0 0% 100%;
|
| 33 |
+
--card-foreground: 220 20% 20%;
|
| 34 |
+
--popover: 0 0% 100%;
|
| 35 |
+
--popover-foreground: 220 20% 20%;
|
| 36 |
+
--primary: 12 80% 60%;
|
| 37 |
+
--primary-foreground: 0 0% 100%;
|
| 38 |
+
--secondary: 180 50% 45%;
|
| 39 |
+
--secondary-foreground: 0 0% 100%;
|
| 40 |
+
--muted: 40 20% 92%;
|
| 41 |
+
--muted-foreground: 220 10% 45%;
|
| 42 |
+
--accent: 260 60% 60%;
|
| 43 |
+
--accent-foreground: 0 0% 100%;
|
| 44 |
+
--destructive: 0 84.2% 60.2%;
|
| 45 |
+
--destructive-foreground: 210 40% 98%;
|
| 46 |
+
--border: 40 20% 88%;
|
| 47 |
+
--input: 40 20% 88%;
|
| 48 |
+
--ring: 12 80% 60%;
|
| 49 |
+
--radius: 1rem;
|
| 50 |
+
|
| 51 |
+
/* Signal Colors */
|
| 52 |
+
--signal-1: 12 85% 62%;
|
| 53 |
+
--signal-2: 175 65% 45%;
|
| 54 |
+
--signal-mixed: 280 60% 55%;
|
| 55 |
+
|
| 56 |
+
--shadow-soft: 0 4px 20px -4px hsl(220 20% 20% / 0.1);
|
| 57 |
+
--shadow-glow: 0 0 30px hsl(12 80% 60% / 0.2);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
body {
|
| 61 |
+
background-color: hsl(var(--background));
|
| 62 |
+
color: hsl(var(--foreground));
|
| 63 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 64 |
+
margin: 0;
|
| 65 |
+
overflow-x: hidden;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
code, .mono { font-family: 'JetBrains Mono', monospace; }
|
| 69 |
+
|
| 70 |
+
.text-gradient {
|
| 71 |
+
background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
|
| 72 |
+
-webkit-background-clip: text;
|
| 73 |
+
-webkit-text-fill-color: transparent;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.shadow-soft { box-shadow: var(--shadow-soft); }
|
| 77 |
+
.shadow-glow { box-shadow: var(--shadow-glow); }
|
| 78 |
+
|
| 79 |
+
@keyframes float {
|
| 80 |
+
0%, 100% { transform: translateY(0); }
|
| 81 |
+
50% { transform: translateY(-10px); }
|
| 82 |
+
}
|
| 83 |
+
.animate-float { animation: float 3s ease-in-out infinite; }
|
| 84 |
+
|
| 85 |
+
@keyframes fadeIn {
|
| 86 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 87 |
+
to { opacity: 1; transform: translateY(0); }
|
| 88 |
+
}
|
| 89 |
+
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
| 90 |
+
|
| 91 |
+
.glass-card {
|
| 92 |
+
background: rgba(255, 255, 255, 0.7);
|
| 93 |
+
backdrop-filter: blur(10px);
|
| 94 |
+
border: 1px solid hsl(var(--border));
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
input[type=range] {
|
| 98 |
+
accent-color: hsl(var(--primary));
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
canvas {
|
| 102 |
+
width: 100% !important;
|
| 103 |
+
height: auto !important;
|
| 104 |
+
display: block;
|
| 105 |
+
}
|
| 106 |
+
</style>
|
| 107 |
+
{% endraw %}
|
| 108 |
+
</head>
|
| 109 |
+
<body>
|
| 110 |
+
<div id="root"></div>
|
| 111 |
+
|
| 112 |
+
{% raw %}
|
| 113 |
+
<script type="text/babel" data-type="module">
|
| 114 |
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
| 115 |
+
import { createRoot } from 'react-dom/client';
|
| 116 |
+
|
| 117 |
+
/** --- Icons --- */
|
| 118 |
+
const Icons = {
|
| 119 |
+
Play: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>,
|
| 120 |
+
RotateCcw: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74-2.74L3 12"/><path d="M3 3v9h9"/></svg>,
|
| 121 |
+
Sparkles: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/></svg>,
|
| 122 |
+
Music: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>,
|
| 123 |
+
Radio: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 1 0-8.48 0"/><path d="M19.07 4.93a10 10 0 1 0-14.14 0"/></svg>,
|
| 124 |
+
Zap: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
|
| 125 |
+
ArrowRight: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>,
|
| 126 |
+
Lightbulb: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18h6m-3 4h.01M12 2a7 7 0 0 0-7 7c0 2.32 1.35 4.31 3.31 5.31l.69 1.69h6l.69-1.69C17.65 13.31 19 11.32 19 9a7 7 0 0 0-7-7z"/></svg>,
|
| 127 |
+
PartyPopper: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5.8 11.3 2 22l10.7-3.8"/><path d="M4 3h.01"/><path d="M22 8h.01"/><path d="M15 2h.01"/><path d="M22 20h.01"/><path d="m22 2-2.24.75a2.9 2.9 0 0 0-1.96 2.4l-.15 1.41a2.9 2.9 0 0 1-2.4 2.6l-1.4.15a2.9 2.9 0 0 0-2.4 1.96L11 13"/><path d="m18 13 2.24-.75a2.9 2.9 0 0 0 1.96-2.4l.15-1.41a2.9 2.9 0 0 1 2.4-2.6l1.4-.15a2.9 2.9 0 0 0 2.4-1.96L29 1.5"/></svg>,
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
/** --- Math Utility --- */
|
| 131 |
+
const generateSignal = (type, frequency, samples) => {
|
| 132 |
+
const signal = [];
|
| 133 |
+
for (let i = 0; i < samples; i++) {
|
| 134 |
+
const t = (i / samples) * 4 * Math.PI * frequency;
|
| 135 |
+
switch (type) {
|
| 136 |
+
case "sine": signal.push(Math.sin(t)); break;
|
| 137 |
+
case "square": signal.push(Math.sin(t) > 0 ? 0.8 : -0.8); break;
|
| 138 |
+
case "sawtooth": signal.push(((t % (2 * Math.PI)) / Math.PI - 1) * 0.8); break;
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
return signal;
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
/** --- UI Sub-Components --- */
|
| 145 |
+
const Button = ({ children, className = '', variant = 'default', size = 'default', ...props }) => {
|
| 146 |
+
const variants = {
|
| 147 |
+
default: "bg-primary text-primary-foreground hover:opacity-90 shadow-soft",
|
| 148 |
+
outline: "border border-input bg-background hover:bg-slate-50",
|
| 149 |
+
ghost: "hover:bg-slate-100",
|
| 150 |
+
};
|
| 151 |
+
const sizes = { default: "h-10 px-4 py-2", lg: "h-12 px-6 text-lg", sm: "h-9 rounded-md px-3" };
|
| 152 |
+
return <button className={`inline-flex items-center justify-center rounded-xl font-medium transition-all active:scale-95 disabled:opacity-50 whitespace-nowrap ${variants[variant] || variants.default} ${sizes[size] || sizes.default} ${className}`} {...props}>{children}</button>;
|
| 153 |
+
};
|
| 154 |
+
|
| 155 |
+
const WaveCanvas = ({ signals, colors, width = 400, height = 120, animated = true, showGrid = true }) => {
|
| 156 |
+
const canvasRef = useRef(null);
|
| 157 |
+
const offsetRef = useRef(0);
|
| 158 |
+
|
| 159 |
+
useEffect(() => {
|
| 160 |
+
const canvas = canvasRef.current; if (!canvas) return;
|
| 161 |
+
const ctx = canvas.getContext("2d"); if (!ctx) return;
|
| 162 |
+
|
| 163 |
+
const dpr = window.devicePixelRatio || 1;
|
| 164 |
+
canvas.width = width * dpr; canvas.height = height * dpr;
|
| 165 |
+
ctx.scale(dpr, dpr);
|
| 166 |
+
|
| 167 |
+
let req;
|
| 168 |
+
const draw = () => {
|
| 169 |
+
ctx.clearRect(0, 0, width, height);
|
| 170 |
+
if (showGrid) {
|
| 171 |
+
ctx.strokeStyle = "hsl(220 15% 88% / 0.5)"; ctx.lineWidth = 1;
|
| 172 |
+
ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke();
|
| 173 |
+
}
|
| 174 |
+
signals.forEach((signal, idx) => {
|
| 175 |
+
ctx.strokeStyle = colors[idx] || "#666"; ctx.lineWidth = 2.5; ctx.lineCap = "round";
|
| 176 |
+
ctx.beginPath();
|
| 177 |
+
const step = width / (signal.length - 1);
|
| 178 |
+
signal.forEach((val, i) => {
|
| 179 |
+
const x = i * step;
|
| 180 |
+
const drift = animated ? Math.sin((i * 0.05) + offsetRef.current * 0.02) * 0.1 : 0;
|
| 181 |
+
const y = height / 2 - (val + drift) * (height / 2 * 0.8);
|
| 182 |
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
| 183 |
+
});
|
| 184 |
+
ctx.stroke();
|
| 185 |
+
});
|
| 186 |
+
if (animated) { offsetRef.current += 1; req = requestAnimationFrame(draw); }
|
| 187 |
+
};
|
| 188 |
+
draw();
|
| 189 |
+
return () => cancelAnimationFrame(req);
|
| 190 |
+
}, [signals, colors, width, height, animated, showGrid]);
|
| 191 |
+
|
| 192 |
+
return <canvas ref={canvasRef} className="rounded-lg w-full h-auto" />;
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
const MixingSlider = ({ label, value, onChange, color }) => (
|
| 196 |
+
<div className="space-y-2">
|
| 197 |
+
<div className="flex items-center justify-between">
|
| 198 |
+
<span className="text-sm font-medium text-foreground">{label}</span>
|
| 199 |
+
<span className="text-sm font-mono px-2 py-0.5 rounded bg-slate-100" style={{ color }}>{value.toFixed(2)}</span>
|
| 200 |
+
</div>
|
| 201 |
+
<input
|
| 202 |
+
type="range" min="0" max="1" step="0.01" value={value}
|
| 203 |
+
onChange={(e) => onChange(parseFloat(e.target.value))}
|
| 204 |
+
className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer"
|
| 205 |
+
/>
|
| 206 |
+
</div>
|
| 207 |
+
);
|
| 208 |
+
|
| 209 |
+
const SignalCard = ({ title, description, signals, colors, icon, badge, badgeColor = "bg-primary" }) => (
|
| 210 |
+
<div className="glass-card rounded-xl p-5 shadow-soft hover:shadow-lg transition-all group">
|
| 211 |
+
<div className="flex items-start justify-between mb-3">
|
| 212 |
+
<div className="flex items-center gap-3">
|
| 213 |
+
{icon && <div className="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-slate-600 group-hover:scale-110 transition-transform">{icon}</div>}
|
| 214 |
+
<div><h3 className="font-semibold text-foreground text-sm sm:text-base">{title}</h3>{description && <p className="text-xs text-muted-foreground">{description}</p>}</div>
|
| 215 |
+
</div>
|
| 216 |
+
{badge && <span className={`text-[10px] font-bold px-2 py-1 rounded-full ${badgeColor} text-white uppercase`}>{badge}</span>}
|
| 217 |
+
</div>
|
| 218 |
+
<div className="bg-slate-50/50 rounded-lg p-3 overflow-hidden">
|
| 219 |
+
<WaveCanvas signals={signals} colors={colors} width={400} height={100} />
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
);
|
| 223 |
+
|
| 224 |
+
const StepIndicator = ({ currentStep, totalSteps, labels }) => (
|
| 225 |
+
<div className="flex items-center justify-center gap-2 sm:gap-4 mb-8 sm:mb-12 overflow-x-auto pb-2">
|
| 226 |
+
{labels.map((label, i) => (
|
| 227 |
+
<React.Fragment key={i}>
|
| 228 |
+
<div className="flex flex-col items-center flex-shrink-0">
|
| 229 |
+
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center font-bold text-xs sm:text-sm transition-all ${i < currentStep ? "bg-secondary text-white" : i === currentStep ? "bg-primary text-white shadow-glow" : "bg-slate-200 text-slate-400"}`}>{i + 1}</div>
|
| 230 |
+
<span className={`mt-2 text-[8px] sm:text-[10px] font-bold uppercase tracking-wider ${i <= currentStep ? "text-foreground" : "text-slate-400"}`}>{label}</span>
|
| 231 |
+
</div>
|
| 232 |
+
{i < totalSteps - 1 && <div className={`w-8 sm:w-12 h-0.5 mt-[-16px] sm:mt-[-20px] ${i < currentStep ? "bg-secondary" : "bg-slate-200"}`} />}
|
| 233 |
+
</React.Fragment>
|
| 234 |
+
))}
|
| 235 |
+
</div>
|
| 236 |
+
);
|
| 237 |
+
|
| 238 |
+
/** --- Main ICA Simulator --- */
|
| 239 |
+
const App = () => {
|
| 240 |
+
const [step, setStep] = useState(0);
|
| 241 |
+
const [mixingMatrix, setMixingMatrix] = useState({ a11: 0.7, a12: 0.3, a21: 0.4, a22: 0.6 });
|
| 242 |
+
const [isUnmixing, setIsUnmixing] = useState(false);
|
| 243 |
+
const [unmixProgress, setUnmixProgress] = useState(0);
|
| 244 |
+
|
| 245 |
+
const SAMPLES = 200;
|
| 246 |
+
const originalSignals = useMemo(() => ({
|
| 247 |
+
signal1: generateSignal("sine", 1, SAMPLES),
|
| 248 |
+
signal2: generateSignal("square", 2, SAMPLES),
|
| 249 |
+
}), []);
|
| 250 |
+
|
| 251 |
+
const mixedSignals = useMemo(() => {
|
| 252 |
+
const { a11, a12, a21, a22 } = mixingMatrix;
|
| 253 |
+
const mixed1 = originalSignals.signal1.map((s1, i) => a11 * s1 + a12 * originalSignals.signal2[i]);
|
| 254 |
+
const mixed2 = originalSignals.signal1.map((s1, i) => a21 * s1 + a22 * originalSignals.signal2[i]);
|
| 255 |
+
return { mixed1, mixed2 };
|
| 256 |
+
}, [originalSignals, mixingMatrix]);
|
| 257 |
+
|
| 258 |
+
const recoveredSignals = useMemo(() => {
|
| 259 |
+
const { a11, a12, a21, a22 } = mixingMatrix;
|
| 260 |
+
const det = a11 * a22 - a12 * a21;
|
| 261 |
+
if (Math.abs(det) < 0.01) return { recovered1: mixedSignals.mixed1, recovered2: mixedSignals.mixed2 };
|
| 262 |
+
const invA11 = a22 / det; const invA12 = -a12 / det; const invA21 = -a21 / det; const invA22 = a11 / det;
|
| 263 |
+
const progress = unmixProgress / 100;
|
| 264 |
+
const recovered1 = mixedSignals.mixed1.map((m1, i) => {
|
| 265 |
+
const target = invA11 * m1 + invA12 * mixedSignals.mixed2[i];
|
| 266 |
+
return m1 + (target - m1) * progress;
|
| 267 |
+
});
|
| 268 |
+
const recovered2 = mixedSignals.mixed1.map((m1, i) => {
|
| 269 |
+
const target = invA21 * m1 + invA22 * mixedSignals.mixed2[i];
|
| 270 |
+
return mixedSignals.mixed2[i] + (target - mixedSignals.mixed2[i]) * progress;
|
| 271 |
+
});
|
| 272 |
+
return { recovered1, recovered2 };
|
| 273 |
+
}, [mixedSignals, mixingMatrix, unmixProgress]);
|
| 274 |
+
|
| 275 |
+
const runICA = useCallback(() => {
|
| 276 |
+
setIsUnmixing(true); setUnmixProgress(0);
|
| 277 |
+
const interval = setInterval(() => {
|
| 278 |
+
setUnmixProgress(prev => {
|
| 279 |
+
if (prev >= 100) { clearInterval(interval); setIsUnmixing(false); return 100; }
|
| 280 |
+
return prev + 2;
|
| 281 |
+
});
|
| 282 |
+
}, 30);
|
| 283 |
+
}, []);
|
| 284 |
+
|
| 285 |
+
const reset = () => { setStep(0); setUnmixProgress(0); setIsUnmixing(false); setMixingMatrix({ a11: 0.7, a12: 0.3, a21: 0.4, a22: 0.6 }); };
|
| 286 |
+
|
| 287 |
+
const signalColors = { signal1: "hsl(12 85% 62%)", signal2: "hsl(175 65% 45%)", mixed: "hsl(280 60% 55%)" };
|
| 288 |
+
|
| 289 |
+
return (
|
| 290 |
+
<div className="min-h-screen bg-background pb-12">
|
| 291 |
+
<header className="relative overflow-hidden pt-12 pb-6 sm:pt-16 sm:pb-8">
|
| 292 |
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-accent/5 to-secondary/5" />
|
| 293 |
+
<div className="max-w-4xl mx-auto px-6 relative text-center">
|
| 294 |
+
<div className="inline-flex items-center gap-2 bg-primary/10 text-primary px-4 py-2 rounded-full text-[10px] sm:text-xs font-bold uppercase tracking-wider mb-6 animate-fade-in">
|
| 295 |
+
<Icons.Sparkles className="w-3 h-3 sm:w-4 sm:h-4" /> Interactive Lab
|
| 296 |
+
</div>
|
| 297 |
+
<h1 className="text-3xl md:text-6xl font-bold tracking-tight text-foreground mb-4 animate-fade-in">Understanding <span className="text-gradient">ICA</span></h1>
|
| 298 |
+
<p className="text-muted-foreground text-sm sm:text-lg max-w-2xl mx-auto animate-fade-in leading-relaxed">Independent Component Analysis made simple. Learn to untangle mixed signals like conversations at a party!</p>
|
| 299 |
+
</div>
|
| 300 |
+
</header>
|
| 301 |
+
|
| 302 |
+
<main className="max-w-6xl mx-auto px-4 sm:px-6">
|
| 303 |
+
<StepIndicator currentStep={step} totalSteps={4} labels={["Sources", "Mix", "Observe", "Separate"]} />
|
| 304 |
+
|
| 305 |
+
{step === 0 && (
|
| 306 |
+
<div className="animate-fade-in space-y-6">
|
| 307 |
+
<div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft">
|
| 308 |
+
<div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-accent/10 flex items-center justify-center shrink-0"><Icons.Lightbulb className="w-6 h-6 sm:w-8 sm:h-8 text-accent" /></div>
|
| 309 |
+
<div><h2 className="text-xl sm:text-2xl font-bold mb-2">Step 1: The Original Sources</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">Imagine two people talking. Each voice is an <strong>independent source</strong>. In real-world data science, these could be audio recordings, brain waves, or even economic trends.</p></div>
|
| 310 |
+
</div>
|
| 311 |
+
<div className="grid md:grid-cols-2 gap-4 sm:gap-6">
|
| 312 |
+
<SignalCard title="Voice 1 (Sine Wave)" signals={[originalSignals.signal1]} colors={[signalColors.signal1]} icon={<Icons.Music className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Source A" />
|
| 313 |
+
<SignalCard title="Voice 2 (Square Wave)" signals={[originalSignals.signal2]} colors={[signalColors.signal2]} icon={<Icons.Radio className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Source B" />
|
| 314 |
+
</div>
|
| 315 |
+
<div className="flex justify-center"><Button size="lg" onClick={() => setStep(1)} className="gap-2">Next: Mix the Signals <Icons.ArrowRight className="w-4 h-4"/></Button></div>
|
| 316 |
+
</div>
|
| 317 |
+
)}
|
| 318 |
+
|
| 319 |
+
{step === 1 && (
|
| 320 |
+
<div className="animate-fade-in space-y-6">
|
| 321 |
+
<div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft">
|
| 322 |
+
<div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-secondary/10 flex items-center justify-center shrink-0"><Icons.PartyPopper className="w-6 h-6 sm:w-8 sm:h-8 text-secondary" /></div>
|
| 323 |
+
<div><h2 className="text-xl sm:text-2xl font-bold mb-2">Step 2: Mixing the Signals</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">At a party, microphones don't record one voice perfectly. They pick up a <strong>blend</strong> of everyone. Adjust the matrix sliders below to change how the signals are mixed!</p></div>
|
| 324 |
+
</div>
|
| 325 |
+
<div className="grid lg:grid-cols-2 gap-6">
|
| 326 |
+
<div className="glass-card rounded-2xl p-6 sm:p-8 space-y-6">
|
| 327 |
+
<h3 className="font-bold text-base sm:text-lg">Mixing Parameters</h3>
|
| 328 |
+
<div className="space-y-4 pt-4 border-t border-slate-100">
|
| 329 |
+
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Mic 1 Weights</p>
|
| 330 |
+
<MixingSlider label="Voice A" value={mixingMatrix.a11} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a11: v })} color={signalColors.signal1} />
|
| 331 |
+
<MixingSlider label="Voice B" value={mixingMatrix.a12} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a12: v })} color={signalColors.signal2} />
|
| 332 |
+
</div>
|
| 333 |
+
<div className="space-y-4 pt-4 border-t border-slate-100">
|
| 334 |
+
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Mic 2 Weights</p>
|
| 335 |
+
<MixingSlider label="Voice A" value={mixingMatrix.a21} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a21: v })} color={signalColors.signal1} />
|
| 336 |
+
<MixingSlider label="Voice B" value={mixingMatrix.a22} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a22: v })} color={signalColors.signal2} />
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
<div className="space-y-4">
|
| 340 |
+
<SignalCard title="Microphone 1 Input" signals={[mixedSignals.mixed1]} colors={[signalColors.mixed]} icon={<Icons.Zap className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Mixed" />
|
| 341 |
+
<SignalCard title="Microphone 2 Input" signals={[mixedSignals.mixed2]} colors={[signalColors.mixed]} icon={<Icons.Zap className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Mixed" />
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
| 345 |
+
<Button variant="outline" onClick={() => setStep(0)}>Back</Button>
|
| 346 |
+
<Button size="lg" onClick={() => setStep(2)}>Next: Observe Challenge <Icons.ArrowRight className="ml-2 w-4 h-4"/></Button>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
)}
|
| 350 |
+
|
| 351 |
+
{step === 2 && (
|
| 352 |
+
<div className="animate-fade-in space-y-6">
|
| 353 |
+
<div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft border-destructive/20">
|
| 354 |
+
<div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-destructive/10 flex items-center justify-center shrink-0"><Icons.Zap className="w-6 h-6 sm:w-8 sm:h-8 text-destructive" /></div>
|
| 355 |
+
<div><h2 className="text-xl sm:text-2xl font-bold mb-2 text-destructive">Step 3: The ICA Challenge</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">We only have the <strong>mixed recordings</strong>. We don't know the original voices or how they were combined. This is a classic Blind Source Separation problem.</p></div>
|
| 356 |
+
</div>
|
| 357 |
+
<div className="glass-card rounded-2xl p-4 sm:p-8 space-y-6">
|
| 358 |
+
<h3 className="font-bold text-lg text-center">Visualizing the Entanglement</h3>
|
| 359 |
+
<div className="bg-white rounded-xl p-3 sm:p-6 border border-slate-100 overflow-hidden">
|
| 360 |
+
<WaveCanvas
|
| 361 |
+
signals={[originalSignals.signal1, originalSignals.signal2, mixedSignals.mixed1, mixedSignals.mixed2]}
|
| 362 |
+
colors={[signalColors.signal1, signalColors.signal2, signalColors.mixed, "hsl(280 40% 45%)"]}
|
| 363 |
+
width={600}
|
| 364 |
+
height={200}
|
| 365 |
+
/>
|
| 366 |
+
</div>
|
| 367 |
+
<div className="flex flex-wrap justify-center gap-4 sm:gap-6 text-[8px] sm:text-[10px] font-bold uppercase tracking-widest text-slate-400 pt-2 sm:pt-4">
|
| 368 |
+
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{ backgroundColor: signalColors.signal1 }} /> Original A</div>
|
| 369 |
+
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{ backgroundColor: signalColors.signal2 }} /> Original B</div>
|
| 370 |
+
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{ backgroundColor: signalColors.mixed }} /> Mixed Result</div>
|
| 371 |
+
</div>
|
| 372 |
+
</div>
|
| 373 |
+
<div className="flex flex-col sm:flex-row justify-center gap-4 px-4">
|
| 374 |
+
<Button variant="outline" onClick={() => setStep(1)} className="w-full sm:w-auto">Back</Button>
|
| 375 |
+
<Button
|
| 376 |
+
size="lg"
|
| 377 |
+
onClick={() => setStep(3)}
|
| 378 |
+
className="w-full sm:w-auto gap-2 text-sm sm:text-lg"
|
| 379 |
+
>
|
| 380 |
+
Next: Separate with ICA <Icons.ArrowRight className="w-4 h-4"/>
|
| 381 |
+
</Button>
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
)}
|
| 385 |
+
|
| 386 |
+
{step === 3 && (
|
| 387 |
+
<div className="animate-fade-in space-y-6">
|
| 388 |
+
<div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft border-secondary/20">
|
| 389 |
+
<div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-secondary/10 flex items-center justify-center shrink-0 animate-float"><Icons.Sparkles className="w-6 h-6 sm:w-8 sm:h-8 text-secondary" /></div>
|
| 390 |
+
<div><h2 className="text-xl sm:text-2xl font-bold mb-2 text-secondary">Step 4: ICA Magic</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">ICA assumes the sources are independent and non-Gaussian. By maximizing statistical independence, it finds the inverse mixing matrix automatically.</p></div>
|
| 391 |
+
</div>
|
| 392 |
+
<div className="grid lg:grid-cols-2 gap-6 sm:gap-8">
|
| 393 |
+
<div className="space-y-4">
|
| 394 |
+
<h3 className="font-bold text-center text-slate-400 text-sm sm:text-base">Input: Mixed recordings</h3>
|
| 395 |
+
<SignalCard title="Recorded Mic 1" signals={[mixedSignals.mixed1]} colors={[signalColors.mixed]} />
|
| 396 |
+
<SignalCard title="Recorded Mic 2" signals={[mixedSignals.mixed2]} colors={[signalColors.mixed]} />
|
| 397 |
+
</div>
|
| 398 |
+
<div className="space-y-4">
|
| 399 |
+
<h3 className="font-bold text-center text-secondary text-sm sm:text-base">Output: Recovered Sources</h3>
|
| 400 |
+
<SignalCard title="Recovered A" signals={[recoveredSignals.recovered1]} colors={[signalColors.signal1]} badge={unmixProgress === 100 ? "Success" : `${unmixProgress}%`} badgeColor="bg-secondary" />
|
| 401 |
+
<SignalCard title="Recovered B" signals={[recoveredSignals.recovered2]} colors={[signalColors.signal2]} badge={unmixProgress === 100 ? "Success" : `${unmixProgress}%`} badgeColor="bg-secondary" />
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
<div className="flex flex-col items-center space-y-6">
|
| 405 |
+
{unmixProgress === 0 && <Button size="lg" onClick={runICA} className="bg-secondary hover:bg-secondary/90 gap-2"><Icons.Play className="w-4 h-4" /> Run ICA Algorithm</Button>}
|
| 406 |
+
{isUnmixing && <div className="w-full max-w-md px-4"><div className="h-2 bg-slate-100 rounded-full overflow-hidden"><div className="h-full bg-secondary transition-all" style={{ width: `${unmixProgress}%` }} /></div><p className="text-center text-[10px] font-bold mt-2 text-secondary uppercase tracking-widest">Optimizing Independence...</p></div>}
|
| 407 |
+
{unmixProgress === 100 && <div className="text-center space-y-4 animate-scale-in"><div className="bg-secondary/10 text-secondary px-6 sm:px-8 py-3 sm:py-4 rounded-2xl border border-secondary/20 inline-block font-bold text-sm sm:text-base">🎉 Signals successfully separated!</div></div>}
|
| 408 |
+
</div>
|
| 409 |
+
<div className="flex flex-col sm:flex-row justify-center gap-4 px-4 mt-6">
|
| 410 |
+
<Button variant="outline" onClick={() => setStep(2)} className="w-full sm:w-auto">Back</Button>
|
| 411 |
+
<Button variant="outline" onClick={reset} className="w-full sm:w-auto"><Icons.RotateCcw className="w-4 h-4 mr-2" /> Start Over</Button>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
)}
|
| 415 |
+
</main>
|
| 416 |
+
|
| 417 |
+
{/* Info Footer Section */}
|
| 418 |
+
<section className="mt-12 sm:mt-16 max-w-3xl mx-auto px-4">
|
| 419 |
+
<div className="glass-card p-6 sm:p-8 bg-gradient-to-br from-primary/5 to-accent/5 border-primary/10 rounded-3xl">
|
| 420 |
+
<h2 className="text-xl sm:text-2xl font-bold text-foreground mb-6 text-center">How does ICA work? 🧠</h2>
|
| 421 |
+
<div className="grid md:grid-cols-3 gap-8 text-center">
|
| 422 |
+
<div>
|
| 423 |
+
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-primary/10 flex items-center justify-center mx-auto mb-3 text-lg sm:text-xl">1️⃣</div>
|
| 424 |
+
<h3 className="font-semibold text-foreground mb-1 text-sm sm:text-base">Assumption</h3>
|
| 425 |
+
<p className="text-xs sm:text-sm text-muted-foreground">Original sources are statistically independent.</p>
|
| 426 |
+
</div>
|
| 427 |
+
<div>
|
| 428 |
+
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-accent/10 flex items-center justify-center mx-auto mb-3 text-lg sm:text-xl">2️⃣</div>
|
| 429 |
+
<h3 className="font-semibold text-foreground mb-1 text-sm sm:text-base">Goal</h3>
|
| 430 |
+
<p className="text-xs sm:text-sm text-muted-foreground">Find a transformation that maximizes output independence.</p>
|
| 431 |
+
</div>
|
| 432 |
+
<div>
|
| 433 |
+
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-secondary/10 flex items-center justify-center mx-auto mb-3 text-lg sm:text-xl">3️⃣</div>
|
| 434 |
+
<h3 className="font-semibold text-foreground mb-1 text-sm sm:text-base">Result</h3>
|
| 435 |
+
<p className="text-xs sm:text-sm text-muted-foreground">Recover original hidden sources from mixed records.</p>
|
| 436 |
+
</div>
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
|
| 440 |
+
<div class="absolute left-1/2 -translate-x-1/2 flex items-center">
|
| 441 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 442 |
+
<a href="/ica" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider">
|
| 443 |
+
Back to Core
|
| 444 |
+
</a>
|
| 445 |
+
</div>
|
| 446 |
+
</section>
|
| 447 |
+
</div>
|
| 448 |
+
);
|
| 449 |
+
};
|
| 450 |
+
|
| 451 |
+
const root = createRoot(document.getElementById('root'));
|
| 452 |
+
root.render(<App />);
|
| 453 |
+
</script>
|
| 454 |
+
{% endraw %}
|
| 455 |
+
</body>
|
| 456 |
+
</html>
|
templates/lda-three.html
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>LDA Simulator</title>
|
| 7 |
+
|
| 8 |
+
<!-- Tailwind CSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<script>
|
| 11 |
+
tailwind.config = {
|
| 12 |
+
theme: {
|
| 13 |
+
extend: {
|
| 14 |
+
colors: {
|
| 15 |
+
border: "hsl(var(--border))",
|
| 16 |
+
input: "hsl(var(--input))",
|
| 17 |
+
ring: "hsl(var(--ring))",
|
| 18 |
+
background: "hsl(var(--background))",
|
| 19 |
+
foreground: "hsl(var(--foreground))",
|
| 20 |
+
primary: {
|
| 21 |
+
DEFAULT: "hsl(var(--primary))",
|
| 22 |
+
foreground: "hsl(var(--primary-foreground))",
|
| 23 |
+
},
|
| 24 |
+
secondary: {
|
| 25 |
+
DEFAULT: "hsl(var(--secondary))",
|
| 26 |
+
foreground: "hsl(var(--secondary-foreground))",
|
| 27 |
+
},
|
| 28 |
+
destructive: {
|
| 29 |
+
DEFAULT: "hsl(var(--destructive))",
|
| 30 |
+
foreground: "hsl(var(--destructive-foreground))",
|
| 31 |
+
},
|
| 32 |
+
muted: {
|
| 33 |
+
DEFAULT: "hsl(var(--muted))",
|
| 34 |
+
foreground: "hsl(var(--muted-foreground))",
|
| 35 |
+
},
|
| 36 |
+
accent: {
|
| 37 |
+
DEFAULT: "hsl(var(--accent))",
|
| 38 |
+
foreground: "hsl(var(--accent-foreground))",
|
| 39 |
+
},
|
| 40 |
+
card: {
|
| 41 |
+
DEFAULT: "hsl(var(--card))",
|
| 42 |
+
foreground: "hsl(var(--card-foreground))",
|
| 43 |
+
},
|
| 44 |
+
// Custom LDA Class Colors
|
| 45 |
+
'class-a': '#06b6d4', // Cyan-500
|
| 46 |
+
'class-b': '#d946ef', // Fuchsia-500
|
| 47 |
+
'centroid': '#f59e0b', // Amber-500
|
| 48 |
+
},
|
| 49 |
+
animation: {
|
| 50 |
+
'fade-in': 'fadeIn 0.5s ease-out',
|
| 51 |
+
},
|
| 52 |
+
keyframes: {
|
| 53 |
+
fadeIn: {
|
| 54 |
+
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
| 55 |
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
</script>
|
| 62 |
+
|
| 63 |
+
<!-- React & ReactDOM -->
|
| 64 |
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 65 |
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| 66 |
+
|
| 67 |
+
<!-- Babel for JSX -->
|
| 68 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 69 |
+
|
| 70 |
+
<style>
|
| 71 |
+
:root {
|
| 72 |
+
--background: 222.2 84% 4.9%;
|
| 73 |
+
--foreground: 210 40% 98%;
|
| 74 |
+
--card: 222.2 84% 4.9%;
|
| 75 |
+
--card-foreground: 210 40% 98%;
|
| 76 |
+
--popover: 222.2 84% 4.9%;
|
| 77 |
+
--popover-foreground: 210 40% 98%;
|
| 78 |
+
--primary: 217.2 91.2% 59.8%;
|
| 79 |
+
--primary-foreground: 222.2 47.4% 11.2%;
|
| 80 |
+
--secondary: 217.2 32.6% 17.5%;
|
| 81 |
+
--secondary-foreground: 210 40% 98%;
|
| 82 |
+
--muted: 217.2 32.6% 17.5%;
|
| 83 |
+
--muted-foreground: 215 20.2% 65.1%;
|
| 84 |
+
--accent: 217.2 32.6% 17.5%;
|
| 85 |
+
--accent-foreground: 210 40% 98%;
|
| 86 |
+
--destructive: 0 62.8% 30.6%;
|
| 87 |
+
--destructive-foreground: 210 40% 98%;
|
| 88 |
+
--border: 217.2 32.6% 17.5%;
|
| 89 |
+
--input: 217.2 32.6% 17.5%;
|
| 90 |
+
--ring: 212.7 26.8% 83.9%;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
body {
|
| 94 |
+
background-color: hsl(222.2, 84%, 4.9%);
|
| 95 |
+
color: hsl(210, 40%, 98%);
|
| 96 |
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.glow-primary { box-shadow: 0 0 15px hsl(217.2, 91.2%, 59.8%, 0.3); }
|
| 100 |
+
.glow-accent { box-shadow: 0 0 15px hsl(189, 94%, 43%, 0.3); }
|
| 101 |
+
.glow-class-b { box-shadow: 0 0 15px hsl(300, 76%, 60%, 0.3); }
|
| 102 |
+
</style>
|
| 103 |
+
</head>
|
| 104 |
+
<body>
|
| 105 |
+
<div id="root"></div>
|
| 106 |
+
|
| 107 |
+
<script type="text/babel">
|
| 108 |
+
const { useState, useEffect, useRef, useCallback, useMemo } = React;
|
| 109 |
+
|
| 110 |
+
// --- UTILS ---
|
| 111 |
+
function cn(...classes) {
|
| 112 |
+
return classes.filter(Boolean).join(' ');
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// --- MATH ENGINE (LDA) ---
|
| 116 |
+
// Simplified Linear Discriminant Analysis for 2D data
|
| 117 |
+
const calculateLDA = (points, canvasWidth, canvasHeight) => {
|
| 118 |
+
const classA = points.filter(p => p.classLabel === 'A');
|
| 119 |
+
const classB = points.filter(p => p.classLabel === 'B');
|
| 120 |
+
|
| 121 |
+
if (classA.length < 2 || classB.length < 2) return null;
|
| 122 |
+
|
| 123 |
+
// 1. Calculate Mean Vectors (Centroids)
|
| 124 |
+
const getMean = (pts) => {
|
| 125 |
+
const sum = pts.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
|
| 126 |
+
return { x: sum.x / pts.length, y: sum.y / pts.length };
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
const meanA = getMean(classA);
|
| 130 |
+
const meanB = getMean(classB);
|
| 131 |
+
|
| 132 |
+
// 2. Calculate Within-Class Scatter Matrix (Sw)
|
| 133 |
+
// Sw = sum((x - mu)(x - mu)T) for all classes
|
| 134 |
+
let swxx = 0, swxy = 0, swyy = 0;
|
| 135 |
+
|
| 136 |
+
const addToScatter = (pts, mean) => {
|
| 137 |
+
pts.forEach(p => {
|
| 138 |
+
const dx = p.x - mean.x;
|
| 139 |
+
const dy = p.y - mean.y;
|
| 140 |
+
swxx += dx * dx;
|
| 141 |
+
swxy += dx * dy;
|
| 142 |
+
swyy += dy * dy;
|
| 143 |
+
});
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
addToScatter(classA, meanA);
|
| 147 |
+
addToScatter(classB, meanB);
|
| 148 |
+
|
| 149 |
+
// 3. Calculate Inverse of Sw (Sw^-1)
|
| 150 |
+
const det = swxx * swyy - swxy * swxy;
|
| 151 |
+
if (Math.abs(det) < 1e-10) return null; // Singular matrix
|
| 152 |
+
|
| 153 |
+
const invSwxx = swyy / det;
|
| 154 |
+
const invSwxy = -swxy / det;
|
| 155 |
+
const invSwyy = swxx / det;
|
| 156 |
+
|
| 157 |
+
// 4. Calculate Vector w = Sw^-1 * (meanB - meanA)
|
| 158 |
+
// We want direction that separates means
|
| 159 |
+
const diffMeanX = meanB.x - meanA.x;
|
| 160 |
+
const diffMeanY = meanB.y - meanA.y;
|
| 161 |
+
|
| 162 |
+
const wx = invSwxx * diffMeanX + invSwxy * diffMeanY;
|
| 163 |
+
const wy = invSwxy * diffMeanX + invSwyy * diffMeanY;
|
| 164 |
+
|
| 165 |
+
// Normalize w
|
| 166 |
+
const mag = Math.sqrt(wx * wx + wy * wy);
|
| 167 |
+
const w = { x: wx / mag, y: wy / mag };
|
| 168 |
+
|
| 169 |
+
// 5. Project Points onto Line defined by w passing through Global Mean
|
| 170 |
+
const allPoints = [...classA, ...classB];
|
| 171 |
+
const globalMean = getMean(allPoints);
|
| 172 |
+
|
| 173 |
+
const projectedPoints = allPoints.map(p => {
|
| 174 |
+
// Vector from mean to point
|
| 175 |
+
const vmx = p.x - globalMean.x;
|
| 176 |
+
const vmy = p.y - globalMean.y;
|
| 177 |
+
|
| 178 |
+
// Scalar projection length
|
| 179 |
+
const dot = vmx * w.x + vmy * w.y;
|
| 180 |
+
|
| 181 |
+
// Projected point coordinates
|
| 182 |
+
return {
|
| 183 |
+
x: globalMean.x + dot * w.x,
|
| 184 |
+
y: globalMean.y + dot * w.y,
|
| 185 |
+
classLabel: p.classLabel
|
| 186 |
+
};
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
return {
|
| 190 |
+
projectionVector: w,
|
| 191 |
+
centroidA: meanA,
|
| 192 |
+
centroidB: meanB,
|
| 193 |
+
projectedPoints
|
| 194 |
+
};
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
// --- UI COMPONENTS ---
|
| 199 |
+
|
| 200 |
+
const Button = ({ children, className, variant = "default", ...props }) => {
|
| 201 |
+
const base = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2";
|
| 202 |
+
const variants = {
|
| 203 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
| 204 |
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
| 205 |
+
ghost: "hover:bg-accent hover:text-accent-foreground"
|
| 206 |
+
};
|
| 207 |
+
return (
|
| 208 |
+
<button className={cn(base, variants[variant], className)} {...props}>
|
| 209 |
+
{children}
|
| 210 |
+
</button>
|
| 211 |
+
);
|
| 212 |
+
};
|
| 213 |
+
|
| 214 |
+
const Toast = ({ message, type, onClose }) => {
|
| 215 |
+
useEffect(() => {
|
| 216 |
+
const timer = setTimeout(onClose, 3000);
|
| 217 |
+
return () => clearTimeout(timer);
|
| 218 |
+
}, [onClose]);
|
| 219 |
+
|
| 220 |
+
return (
|
| 221 |
+
<div className={cn(
|
| 222 |
+
"fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white font-medium animate-fade-in z-50",
|
| 223 |
+
type === 'error' ? "bg-red-500" : "bg-emerald-600"
|
| 224 |
+
)}>
|
| 225 |
+
{message}
|
| 226 |
+
</div>
|
| 227 |
+
);
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
// --- ICONS ---
|
| 231 |
+
const IconWrapper = ({ children, className }) => (
|
| 232 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
| 233 |
+
{children}
|
| 234 |
+
</svg>
|
| 235 |
+
);
|
| 236 |
+
|
| 237 |
+
const Icons = {
|
| 238 |
+
CircleDot: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></IconWrapper>,
|
| 239 |
+
Trash2: (props) => <IconWrapper {...props}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></IconWrapper>,
|
| 240 |
+
Play: (props) => <IconWrapper {...props}><polygon points="5 3 19 12 5 21 5 3"/></IconWrapper>,
|
| 241 |
+
RotateCcw: (props) => <IconWrapper {...props}><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></IconWrapper>,
|
| 242 |
+
Sparkles: (props) => <IconWrapper {...props}><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M9 3v4"/><path d="M3 9h4"/><path d="M3 5h4"/></IconWrapper>,
|
| 243 |
+
Info: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></IconWrapper>,
|
| 244 |
+
Target: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></IconWrapper>,
|
| 245 |
+
Layers: (props) => <IconWrapper {...props}><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></IconWrapper>,
|
| 246 |
+
TrendingUp: (props) => <IconWrapper {...props}><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></IconWrapper>,
|
| 247 |
+
BookOpen: (props) => <IconWrapper {...props}><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></IconWrapper>,
|
| 248 |
+
Check: (props) => <IconWrapper {...props}><polyline points="20 6 9 17 4 12"/></IconWrapper>,
|
| 249 |
+
Circle: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/></IconWrapper>,
|
| 250 |
+
ArrowRight: (props) => <IconWrapper {...props}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></IconWrapper>,
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
// --- SUB-COMPONENTS ---
|
| 254 |
+
|
| 255 |
+
const LDACanvas = ({ points, onAddPoint, selectedClass, showProjection, ldaResult }) => {
|
| 256 |
+
const canvasRef = useRef(null);
|
| 257 |
+
const containerRef = useRef(null);
|
| 258 |
+
const [canvasSize, setCanvasSize] = useState({ width: 600, height: 400 });
|
| 259 |
+
|
| 260 |
+
// Handle adding points
|
| 261 |
+
const handleCanvasClick = useCallback((e) => {
|
| 262 |
+
const canvas = canvasRef.current;
|
| 263 |
+
if (!canvas) return;
|
| 264 |
+
const rect = canvas.getBoundingClientRect();
|
| 265 |
+
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
|
| 266 |
+
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
|
| 267 |
+
onAddPoint({ x, y, classLabel: selectedClass });
|
| 268 |
+
}, [onAddPoint, selectedClass]);
|
| 269 |
+
|
| 270 |
+
// Resize observer
|
| 271 |
+
useEffect(() => {
|
| 272 |
+
const handleResize = () => {
|
| 273 |
+
const container = containerRef.current;
|
| 274 |
+
if (container) {
|
| 275 |
+
setCanvasSize({
|
| 276 |
+
width: container.clientWidth,
|
| 277 |
+
height: Math.min(400, container.clientWidth * 0.75)
|
| 278 |
+
});
|
| 279 |
+
}
|
| 280 |
+
};
|
| 281 |
+
window.addEventListener('resize', handleResize);
|
| 282 |
+
handleResize();
|
| 283 |
+
return () => window.removeEventListener('resize', handleResize);
|
| 284 |
+
}, []);
|
| 285 |
+
|
| 286 |
+
// Draw Loop
|
| 287 |
+
useEffect(() => {
|
| 288 |
+
const canvas = canvasRef.current;
|
| 289 |
+
if (!canvas) return;
|
| 290 |
+
const ctx = canvas.getContext("2d");
|
| 291 |
+
|
| 292 |
+
// Clear
|
| 293 |
+
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
|
| 294 |
+
|
| 295 |
+
// Grid
|
| 296 |
+
ctx.strokeStyle = "rgba(100, 116, 139, 0.1)";
|
| 297 |
+
ctx.lineWidth = 1;
|
| 298 |
+
const gridSize = 30;
|
| 299 |
+
for (let x = 0; x < canvasSize.width; x += gridSize) {
|
| 300 |
+
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvasSize.height); ctx.stroke();
|
| 301 |
+
}
|
| 302 |
+
for (let y = 0; y < canvasSize.height; y += gridSize) {
|
| 303 |
+
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvasSize.width, y); ctx.stroke();
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
// Projection
|
| 307 |
+
if (showProjection && ldaResult) {
|
| 308 |
+
const { projectionVector, centroidA, centroidB, projectedPoints } = ldaResult;
|
| 309 |
+
const allPts = [...points];
|
| 310 |
+
// Global Mean approx
|
| 311 |
+
const gMean = {
|
| 312 |
+
x: allPts.reduce((s, p) => s + p.x, 0) / allPts.length,
|
| 313 |
+
y: allPts.reduce((s, p) => s + p.y, 0) / allPts.length
|
| 314 |
+
};
|
| 315 |
+
|
| 316 |
+
// Draw Line
|
| 317 |
+
const lineLen = 1000;
|
| 318 |
+
ctx.beginPath();
|
| 319 |
+
ctx.strokeStyle = "hsl(45, 93%, 58%)"; // Amber
|
| 320 |
+
ctx.lineWidth = 2;
|
| 321 |
+
ctx.setLineDash([5, 5]);
|
| 322 |
+
ctx.moveTo(gMean.x - projectionVector.x * lineLen, gMean.y - projectionVector.y * lineLen);
|
| 323 |
+
ctx.lineTo(gMean.x + projectionVector.x * lineLen, gMean.y + projectionVector.y * lineLen);
|
| 324 |
+
ctx.stroke();
|
| 325 |
+
ctx.setLineDash([]);
|
| 326 |
+
|
| 327 |
+
// Draw Projected Points
|
| 328 |
+
projectedPoints.forEach(pp => {
|
| 329 |
+
ctx.beginPath();
|
| 330 |
+
ctx.fillStyle = pp.classLabel === 'A' ? "hsl(189, 94%, 43%)" : "hsl(300, 76%, 60%)";
|
| 331 |
+
ctx.globalAlpha = 0.5;
|
| 332 |
+
ctx.arc(pp.x, pp.y, 4, 0, Math.PI * 2);
|
| 333 |
+
ctx.fill();
|
| 334 |
+
|
| 335 |
+
// Connecting line (optional, purely for visual connection)
|
| 336 |
+
// This would need original point ref, skipping for simplicity
|
| 337 |
+
});
|
| 338 |
+
ctx.globalAlpha = 1;
|
| 339 |
+
|
| 340 |
+
// Centroids
|
| 341 |
+
[centroidA, centroidB].forEach((c, idx) => {
|
| 342 |
+
ctx.beginPath();
|
| 343 |
+
ctx.strokeStyle = idx === 0 ? "hsl(189, 94%, 43%)" : "hsl(300, 76%, 60%)";
|
| 344 |
+
ctx.lineWidth = 2;
|
| 345 |
+
ctx.arc(c.x, c.y, 12, 0, Math.PI * 2);
|
| 346 |
+
ctx.stroke();
|
| 347 |
+
// Cross
|
| 348 |
+
ctx.beginPath();
|
| 349 |
+
ctx.moveTo(c.x - 5, c.y); ctx.lineTo(c.x + 5, c.y);
|
| 350 |
+
ctx.moveTo(c.x, c.y - 5); ctx.lineTo(c.x, c.y + 5);
|
| 351 |
+
ctx.stroke();
|
| 352 |
+
});
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// Points
|
| 356 |
+
points.forEach(p => {
|
| 357 |
+
ctx.beginPath();
|
| 358 |
+
const color = p.classLabel === 'A' ? "hsl(189, 94%, 43%)" : "hsl(300, 76%, 60%)";
|
| 359 |
+
|
| 360 |
+
// Glow
|
| 361 |
+
const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 15);
|
| 362 |
+
grad.addColorStop(0, color);
|
| 363 |
+
grad.addColorStop(1, "transparent");
|
| 364 |
+
ctx.fillStyle = grad;
|
| 365 |
+
ctx.globalAlpha = 0.3;
|
| 366 |
+
ctx.arc(p.x, p.y, 15, 0, Math.PI * 2);
|
| 367 |
+
ctx.fill();
|
| 368 |
+
|
| 369 |
+
// Core
|
| 370 |
+
ctx.globalAlpha = 1;
|
| 371 |
+
ctx.fillStyle = color;
|
| 372 |
+
ctx.beginPath();
|
| 373 |
+
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
|
| 374 |
+
ctx.fill();
|
| 375 |
+
ctx.strokeStyle = "white";
|
| 376 |
+
ctx.lineWidth = 1.5;
|
| 377 |
+
ctx.stroke();
|
| 378 |
+
});
|
| 379 |
+
|
| 380 |
+
}, [points, canvasSize, showProjection, ldaResult]);
|
| 381 |
+
|
| 382 |
+
return (
|
| 383 |
+
<div ref={containerRef} className="relative w-full rounded-xl overflow-hidden border border-border bg-card/50 backdrop-blur-sm shadow-sm">
|
| 384 |
+
<canvas
|
| 385 |
+
ref={canvasRef}
|
| 386 |
+
width={canvasSize.width}
|
| 387 |
+
height={canvasSize.height}
|
| 388 |
+
onClick={handleCanvasClick}
|
| 389 |
+
className="cursor-crosshair block w-full h-auto"
|
| 390 |
+
/>
|
| 391 |
+
<div className="absolute bottom-3 left-3 flex gap-2 text-[10px] sm:text-xs font-mono">
|
| 392 |
+
<span className="px-2 py-1 rounded bg-secondary/90 text-class-a border border-class-a/20">
|
| 393 |
+
Class A: {points.filter((p) => p.classLabel === "A").length}
|
| 394 |
+
</span>
|
| 395 |
+
<span className="px-2 py-1 rounded bg-secondary/90 text-class-b border border-class-b/20">
|
| 396 |
+
Class B: {points.filter((p) => p.classLabel === "B").length}
|
| 397 |
+
</span>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
);
|
| 401 |
+
};
|
| 402 |
+
|
| 403 |
+
const ControlPanel = ({ selectedClass, onSelectClass, onClear, onRunLDA, onLoadExample, canRunLDA, showProjection }) => (
|
| 404 |
+
<div className="flex flex-col sm:flex-row flex-wrap gap-3 items-center justify-between p-4 rounded-xl bg-card border border-border shadow-sm">
|
| 405 |
+
<div className="flex gap-2 w-full sm:w-auto justify-center">
|
| 406 |
+
<Button
|
| 407 |
+
variant={selectedClass === "A" ? "default" : "outline"}
|
| 408 |
+
onClick={() => onSelectClass("A")}
|
| 409 |
+
className={`gap-2 flex-1 sm:flex-none ${selectedClass === "A" ? "bg-class-a hover:bg-class-a/90 text-white glow-accent" : "border-class-a/50 text-class-a hover:bg-class-a/10"}`}
|
| 410 |
+
>
|
| 411 |
+
<Icons.CircleDot className="w-4 h-4" /> Class A
|
| 412 |
+
</Button>
|
| 413 |
+
<Button
|
| 414 |
+
variant={selectedClass === "B" ? "default" : "outline"}
|
| 415 |
+
onClick={() => onSelectClass("B")}
|
| 416 |
+
className={`gap-2 flex-1 sm:flex-none ${selectedClass === "B" ? "bg-class-b hover:bg-class-b/90 text-white glow-class-b" : "border-class-b/50 text-class-b hover:bg-class-b/10"}`}
|
| 417 |
+
>
|
| 418 |
+
<Icons.CircleDot className="w-4 h-4" /> Class B
|
| 419 |
+
</Button>
|
| 420 |
+
</div>
|
| 421 |
+
|
| 422 |
+
<div className="flex gap-2 w-full sm:w-auto justify-center">
|
| 423 |
+
<Button variant="outline" onClick={onLoadExample} className="gap-2 text-xs sm:text-sm">
|
| 424 |
+
<Icons.Sparkles className="w-4 h-4" /> Example
|
| 425 |
+
</Button>
|
| 426 |
+
<Button variant="outline" onClick={onClear} className="gap-2 border-destructive/50 text-destructive hover:bg-destructive/10 text-xs sm:text-sm">
|
| 427 |
+
<Icons.Trash2 className="w-4 h-4" /> Clear
|
| 428 |
+
</Button>
|
| 429 |
+
<Button
|
| 430 |
+
onClick={onRunLDA}
|
| 431 |
+
disabled={!canRunLDA}
|
| 432 |
+
className={`gap-2 flex-1 sm:flex-none min-w-[120px] ${showProjection ? "bg-primary/80" : "glow-primary"}`}
|
| 433 |
+
>
|
| 434 |
+
{showProjection ? <><Icons.RotateCcw className="w-4 h-4" /> Reset View</> : <><Icons.Play className="w-4 h-4" /> Run LDA</>}
|
| 435 |
+
</Button>
|
| 436 |
+
</div>
|
| 437 |
+
</div>
|
| 438 |
+
);
|
| 439 |
+
|
| 440 |
+
const StepIndicator = ({ currentStep, showProjection }) => {
|
| 441 |
+
const steps = ["Add A", "Add B", "Run LDA", "Project"];
|
| 442 |
+
const activeStep = showProjection ? 4 : currentStep;
|
| 443 |
+
|
| 444 |
+
return (
|
| 445 |
+
<div className="flex items-center justify-between sm:justify-center gap-2 sm:gap-4 p-4 rounded-xl bg-card/50 border border-border/50 overflow-x-auto">
|
| 446 |
+
{steps.map((label, index) => {
|
| 447 |
+
const stepNum = index + 1;
|
| 448 |
+
const isCompleted = stepNum < activeStep;
|
| 449 |
+
const isActive = stepNum === activeStep;
|
| 450 |
+
|
| 451 |
+
return (
|
| 452 |
+
<div key={index} className="flex items-center gap-2 min-w-fit">
|
| 453 |
+
<div className={cn("w-6 h-6 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-xs sm:text-sm font-mono transition-all",
|
| 454 |
+
isCompleted ? "bg-accent text-white" : isActive ? "bg-primary text-primary-foreground glow-primary" : "bg-secondary text-muted-foreground"
|
| 455 |
+
)}>
|
| 456 |
+
{isCompleted ? <Icons.Check className="w-3 h-3 sm:w-4 sm:h-4" /> : isActive ? <Icons.CircleDot className="w-3 h-3 sm:w-4 sm:h-4" /> : stepNum}
|
| 457 |
+
</div>
|
| 458 |
+
<span className={cn("text-xs hidden sm:block", isActive ? "text-foreground font-medium" : "text-muted-foreground")}>{label}</span>
|
| 459 |
+
{index < steps.length - 1 && <Icons.ArrowRight className="w-3 h-3 text-muted-foreground/30" />}
|
| 460 |
+
</div>
|
| 461 |
+
)
|
| 462 |
+
})}
|
| 463 |
+
</div>
|
| 464 |
+
);
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
const ExplanationPanel = ({ step, showProjection }) => {
|
| 468 |
+
const currentStep = showProjection ? "result" : step;
|
| 469 |
+
const content = {
|
| 470 |
+
intro: { icon: Icons.Info, title: "Welcome!", desc: "LDA finds the best way to separate groups.", details: ["Click canvas to add points", "Switch classes with buttons"] },
|
| 471 |
+
addPoints: { icon: Icons.Target, title: "Add Points", desc: "Create two groups of data.", details: ["Add at least 3 points per class", "Try overlapping groups to see LDA magic"] },
|
| 472 |
+
centroids: { icon: Icons.Layers, title: "Ready to Run", desc: "We have enough data.", details: ["LDA will calculate centroids (averages)", "It finds a line to separate them"] },
|
| 473 |
+
projection: { icon: Icons.TrendingUp, title: "Projection", desc: "LDA finds the optimal separation line.", details: ["Golden line = Projection direction", "Notice how classes separate on the line"] },
|
| 474 |
+
result: { icon: Icons.TrendingUp, title: "Result", desc: "Data reduced from 2D to 1D.", details: ["Maximizes between-class distance", "Minimizes within-class scatter"] }
|
| 475 |
+
}[currentStep];
|
| 476 |
+
|
| 477 |
+
const Icon = content.icon;
|
| 478 |
+
|
| 479 |
+
return (
|
| 480 |
+
<div className="p-5 rounded-xl bg-card border border-border animate-fade-in shadow-sm">
|
| 481 |
+
<div className="flex items-start gap-3 mb-4">
|
| 482 |
+
<div className="p-2 rounded-lg bg-primary/20 text-primary"><Icon className="w-5 h-5" /></div>
|
| 483 |
+
<div>
|
| 484 |
+
<h3 className="font-semibold text-lg text-foreground">{content.title}</h3>
|
| 485 |
+
<p className="text-muted-foreground text-sm mt-1">{content.desc}</p>
|
| 486 |
+
</div>
|
| 487 |
+
</div>
|
| 488 |
+
<ul className="space-y-2 ml-10">
|
| 489 |
+
{content.details.map((d, i) => (
|
| 490 |
+
<li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
|
| 491 |
+
<span className="text-primary mt-1">•</span> {d}
|
| 492 |
+
</li>
|
| 493 |
+
))}
|
| 494 |
+
</ul>
|
| 495 |
+
</div>
|
| 496 |
+
);
|
| 497 |
+
};
|
| 498 |
+
|
| 499 |
+
const FormulaCard = ({ showProjection }) => (
|
| 500 |
+
<div className="p-5 rounded-xl bg-card border border-border shadow-sm mt-4 lg:mt-0">
|
| 501 |
+
<div className="flex items-center gap-2 mb-4">
|
| 502 |
+
<Icons.BookOpen className="w-5 h-5 text-primary" />
|
| 503 |
+
<h3 className="font-semibold text-foreground">The Math</h3>
|
| 504 |
+
</div>
|
| 505 |
+
<div className="space-y-4 text-sm">
|
| 506 |
+
<div className="p-3 rounded-lg bg-secondary/50 font-mono text-center text-xs sm:text-base">
|
| 507 |
+
<span className="text-primary">w</span>
|
| 508 |
+
<span className="text-muted-foreground"> = </span>
|
| 509 |
+
<span className="text-accent">S<sub>w</sub><sup>-1</sup></span>
|
| 510 |
+
<span className="text-muted-foreground"> (</span>
|
| 511 |
+
<span className="text-class-b">μ<sub>B</sub></span>
|
| 512 |
+
<span className="text-muted-foreground"> - </span>
|
| 513 |
+
<span className="text-class-a">μ<sub>A</sub></span>
|
| 514 |
+
<span className="text-muted-foreground">)</span>
|
| 515 |
+
</div>
|
| 516 |
+
<p className="text-xs text-muted-foreground text-center">
|
| 517 |
+
We inverse the Scatter Matrix (<span className="text-accent">S<sub>w</sub></span>) and multiply by the difference of Means.
|
| 518 |
+
</p>
|
| 519 |
+
</div>
|
| 520 |
+
</div>
|
| 521 |
+
);
|
| 522 |
+
|
| 523 |
+
const TheorySection = () => (
|
| 524 |
+
<div className="mt-8 grid md:grid-cols-2 gap-6 animate-fade-in">
|
| 525 |
+
<div className="p-6 rounded-xl bg-card border border-border">
|
| 526 |
+
<div className="flex items-center gap-2 mb-4">
|
| 527 |
+
<Icons.BookOpen className="w-5 h-5 text-primary" />
|
| 528 |
+
<h2 className="text-xl font-bold">The Intuition</h2>
|
| 529 |
+
</div>
|
| 530 |
+
<p className="text-muted-foreground mb-4">
|
| 531 |
+
Imagine you have two bags of mixed candy (classes) spilled on a table. You want to take a photo (project to 2D -> 1D) such that the two types of candy look as separate as possible in the picture.
|
| 532 |
+
</p>
|
| 533 |
+
<p className="text-muted-foreground">
|
| 534 |
+
LDA tries to find the camera angle that:
|
| 535 |
+
</p>
|
| 536 |
+
<ul className="list-disc ml-5 mt-2 space-y-1 text-muted-foreground">
|
| 537 |
+
<li>Keeps candies of the same type close together (<strong>Minimize Within-Class Variance</strong>)</li>
|
| 538 |
+
<li>Pushes the centers of the two groups far apart (<strong>Maximize Between-Class Variance</strong>)</li>
|
| 539 |
+
</ul>
|
| 540 |
+
</div>
|
| 541 |
+
|
| 542 |
+
<div className="p-6 rounded-xl bg-card border border-border">
|
| 543 |
+
<div className="flex items-center gap-2 mb-4">
|
| 544 |
+
<Icons.TrendingUp className="w-5 h-5 text-accent" />
|
| 545 |
+
<h2 className="text-xl font-bold">Fisher's Criterion</h2>
|
| 546 |
+
</div>
|
| 547 |
+
<p className="text-muted-foreground mb-4">
|
| 548 |
+
Mathematically, LDA solves for a vector <span className="font-mono text-primary">w</span> that maximizes the ratio <span className="font-mono">J(w)</span>:
|
| 549 |
+
</p>
|
| 550 |
+
<div className="p-4 bg-secondary/30 rounded-lg text-center font-mono my-4">
|
| 551 |
+
J(w) = <span className="text-accent">Between-Class Variance</span> / <span className="text-primary">Within-Class Variance</span>
|
| 552 |
+
</div>
|
| 553 |
+
<p className="text-sm text-muted-foreground">
|
| 554 |
+
If we just maximized the distance between means, we might pick an angle where the classes are spread out and overlap. By dividing by the variance (spread), we ensure the clusters are tight <i>and</i> separated.
|
| 555 |
+
</p>
|
| 556 |
+
</div>
|
| 557 |
+
</div>
|
| 558 |
+
);
|
| 559 |
+
|
| 560 |
+
const App = () => {
|
| 561 |
+
const [points, setPoints] = useState([]);
|
| 562 |
+
const [selectedClass, setSelectedClass] = useState("A");
|
| 563 |
+
const [showProjection, setShowProjection] = useState(false);
|
| 564 |
+
const [ldaResult, setLdaResult] = useState(null);
|
| 565 |
+
const [toastMsg, setToastMsg] = useState(null);
|
| 566 |
+
|
| 567 |
+
const showToast = (msg, type = 'info') => setToastMsg({ msg, type, id: Date.now() });
|
| 568 |
+
|
| 569 |
+
// Example Data
|
| 570 |
+
const handleLoadExample = () => {
|
| 571 |
+
setPoints([
|
| 572 |
+
{ x: 120, y: 150, classLabel: "A" }, { x: 150, y: 180, classLabel: "A" },
|
| 573 |
+
{ x: 130, y: 200, classLabel: "A" }, { x: 160, y: 160, classLabel: "A" },
|
| 574 |
+
{ x: 140, y: 220, classLabel: "A" }, { x: 180, y: 190, classLabel: "A" },
|
| 575 |
+
{ x: 450, y: 200, classLabel: "B" }, { x: 480, y: 230, classLabel: "B" },
|
| 576 |
+
{ x: 460, y: 260, classLabel: "B" }, { x: 490, y: 180, classLabel: "B" },
|
| 577 |
+
{ x: 520, y: 220, classLabel: "B" }, { x: 470, y: 280, classLabel: "B" },
|
| 578 |
+
]);
|
| 579 |
+
setShowProjection(false);
|
| 580 |
+
setLdaResult(null);
|
| 581 |
+
showToast("Example data loaded!");
|
| 582 |
+
};
|
| 583 |
+
|
| 584 |
+
const handleAddPoint = (point) => {
|
| 585 |
+
if (showProjection) return;
|
| 586 |
+
setPoints(prev => [...prev, point]);
|
| 587 |
+
};
|
| 588 |
+
|
| 589 |
+
const handleClear = () => {
|
| 590 |
+
setPoints([]);
|
| 591 |
+
setShowProjection(false);
|
| 592 |
+
setLdaResult(null);
|
| 593 |
+
showToast("Canvas cleared");
|
| 594 |
+
};
|
| 595 |
+
|
| 596 |
+
const handleRunLDA = () => {
|
| 597 |
+
if (showProjection) {
|
| 598 |
+
setShowProjection(false);
|
| 599 |
+
setLdaResult(null);
|
| 600 |
+
return;
|
| 601 |
+
}
|
| 602 |
+
const result = calculateLDA(points);
|
| 603 |
+
if (result) {
|
| 604 |
+
setLdaResult(result);
|
| 605 |
+
setShowProjection(true);
|
| 606 |
+
showToast("LDA Projection calculated!", "success");
|
| 607 |
+
} else {
|
| 608 |
+
showToast("Need more points!", "error");
|
| 609 |
+
}
|
| 610 |
+
};
|
| 611 |
+
|
| 612 |
+
const classACount = points.filter(p => p.classLabel === 'A').length;
|
| 613 |
+
const classBCount = points.filter(p => p.classLabel === 'B').length;
|
| 614 |
+
const canRunLDA = classACount >= 2 && classBCount >= 2;
|
| 615 |
+
|
| 616 |
+
const getCurrentStep = () => {
|
| 617 |
+
if (classACount === 0) return 1;
|
| 618 |
+
if (classBCount === 0) return 2;
|
| 619 |
+
if (!showProjection) return 3;
|
| 620 |
+
return 4;
|
| 621 |
+
};
|
| 622 |
+
|
| 623 |
+
const getExplanationStep = () => {
|
| 624 |
+
if (points.length === 0) return "intro";
|
| 625 |
+
if (classACount < 2 || classBCount < 2) return "addPoints";
|
| 626 |
+
if (!showProjection) return "centroids";
|
| 627 |
+
return "projection";
|
| 628 |
+
};
|
| 629 |
+
|
| 630 |
+
return (
|
| 631 |
+
<div className="min-h-screen pb-10">
|
| 632 |
+
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
|
| 633 |
+
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
| 634 |
+
<div>
|
| 635 |
+
<h1 className="text-xl sm:text-2xl font-bold text-foreground">
|
| 636 |
+
LDA <span className="text-primary">Simulator</span>
|
| 637 |
+
</h1>
|
| 638 |
+
<p className="text-xs sm:text-sm text-muted-foreground hidden sm:block">Linear Discriminant Analysis — Interactive Learning</p>
|
| 639 |
+
</div>
|
| 640 |
+
</div>
|
| 641 |
+
</header>
|
| 642 |
+
|
| 643 |
+
<div class="absolute left-1/2 -translate-x-1/2 flex items-center">
|
| 644 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 645 |
+
<a href="/lda" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider">
|
| 646 |
+
Back to Core
|
| 647 |
+
</a>
|
| 648 |
+
</div>
|
| 649 |
+
<main className="container mx-auto px-4 py-6 space-y-6">
|
| 650 |
+
<StepIndicator currentStep={getCurrentStep()} showProjection={showProjection} />
|
| 651 |
+
|
| 652 |
+
<div className="grid lg:grid-cols-3 gap-6">
|
| 653 |
+
<div className="lg:col-span-2 space-y-4">
|
| 654 |
+
<ControlPanel
|
| 655 |
+
selectedClass={selectedClass} onSelectClass={setSelectedClass}
|
| 656 |
+
onClear={handleClear} onRunLDA={handleRunLDA} onLoadExample={handleLoadExample}
|
| 657 |
+
canRunLDA={canRunLDA} showProjection={showProjection}
|
| 658 |
+
/>
|
| 659 |
+
<LDACanvas
|
| 660 |
+
points={points} onAddPoint={handleAddPoint}
|
| 661 |
+
selectedClass={selectedClass} showProjection={showProjection} ldaResult={ldaResult}
|
| 662 |
+
/>
|
| 663 |
+
<div className="flex flex-wrap gap-2 text-[10px] sm:text-xs">
|
| 664 |
+
<span className="px-3 py-1.5 rounded-full bg-class-a/20 text-class-a border border-class-a/30">● Class A (Cyan)</span>
|
| 665 |
+
<span className="px-3 py-1.5 rounded-full bg-class-b/20 text-class-b border border-class-b/30">● Class B (Magenta)</span>
|
| 666 |
+
{showProjection && <span className="px-3 py-1.5 rounded-full bg-primary/20 text-primary border border-primary/30">─ ─ Projection</span>}
|
| 667 |
+
</div>
|
| 668 |
+
</div>
|
| 669 |
+
|
| 670 |
+
<div className="space-y-4">
|
| 671 |
+
<ExplanationPanel step={getExplanationStep()} showProjection={showProjection} />
|
| 672 |
+
<FormulaCard showProjection={showProjection} />
|
| 673 |
+
</div>
|
| 674 |
+
</div>
|
| 675 |
+
|
| 676 |
+
<TheorySection />
|
| 677 |
+
</main>
|
| 678 |
+
|
| 679 |
+
{toastMsg && <Toast message={toastMsg.msg} type={toastMsg.type} onClose={() => setToastMsg(null)} />}
|
| 680 |
+
</div>
|
| 681 |
+
);
|
| 682 |
+
};
|
| 683 |
+
|
| 684 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 685 |
+
root.render(<App />);
|
| 686 |
+
</script>
|
| 687 |
+
</body>
|
| 688 |
+
</html>
|
templates/pca-threejs.html
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>PCA Playground - Interactive Tool</title>
|
| 7 |
+
|
| 8 |
+
<!-- Tailwind CSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
|
| 11 |
+
<!-- Babel for JSX (Standalone) -->
|
| 12 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 13 |
+
|
| 14 |
+
<!-- Google Fonts -->
|
| 15 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 16 |
+
|
| 17 |
+
<!-- Import Map -->
|
| 18 |
+
<script type="importmap">
|
| 19 |
+
{
|
| 20 |
+
"imports": {
|
| 21 |
+
"react": "https://esm.sh/react@18.2.0",
|
| 22 |
+
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
</script>
|
| 26 |
+
|
| 27 |
+
<style>
|
| 28 |
+
:root {
|
| 29 |
+
--background: 40 30% 98%;
|
| 30 |
+
--foreground: 220 20% 20%;
|
| 31 |
+
--card: 0 0% 100%;
|
| 32 |
+
--card-foreground: 220 20% 20%;
|
| 33 |
+
--popover: 0 0% 100%;
|
| 34 |
+
--popover-foreground: 220 20% 20%;
|
| 35 |
+
--primary: 12 80% 60%;
|
| 36 |
+
--primary-foreground: 0 0% 100%;
|
| 37 |
+
--secondary: 180 50% 45%;
|
| 38 |
+
--secondary-foreground: 0 0% 100%;
|
| 39 |
+
--muted: 40 20% 92%;
|
| 40 |
+
--muted-foreground: 220 10% 45%;
|
| 41 |
+
--accent: 260 60% 60%;
|
| 42 |
+
--accent-foreground: 0 0% 100%;
|
| 43 |
+
--destructive: 0 84.2% 60.2%;
|
| 44 |
+
--destructive-foreground: 210 40% 98%;
|
| 45 |
+
--border: 40 20% 88%;
|
| 46 |
+
--input: 40 20% 88%;
|
| 47 |
+
--ring: 12 80% 60%;
|
| 48 |
+
--radius: 1rem;
|
| 49 |
+
|
| 50 |
+
--pc1-color: 180 70% 45%;
|
| 51 |
+
--pc2-color: 260 60% 60%;
|
| 52 |
+
--point-color: 12 80% 60%;
|
| 53 |
+
--grid-color: 220 20% 90%;
|
| 54 |
+
|
| 55 |
+
--gradient-warm: linear-gradient(135deg, hsl(12 80% 60%) 0%, hsl(30 90% 65%) 100%);
|
| 56 |
+
--gradient-cool: linear-gradient(135deg, hsl(180 50% 45%) 0%, hsl(200 60% 50%) 100%);
|
| 57 |
+
--gradient-accent: linear-gradient(135deg, hsl(260 60% 60%) 0%, hsl(280 70% 65%) 100%);
|
| 58 |
+
--gradient-bg: linear-gradient(180deg, hsl(40 30% 98%) 0%, hsl(40 25% 95%) 100%);
|
| 59 |
+
|
| 60 |
+
--shadow-soft: 0 4px 20px -4px hsl(220 20% 20% / 0.1);
|
| 61 |
+
--shadow-glow: 0 0 30px hsl(12 80% 60% / 0.2);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
body {
|
| 65 |
+
background-color: hsl(var(--background));
|
| 66 |
+
color: hsl(var(--foreground));
|
| 67 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 68 |
+
margin: 0;
|
| 69 |
+
overflow-x: hidden;
|
| 70 |
+
-webkit-tap-highlight-color: transparent;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
code, .mono { font-family: 'JetBrains Mono', monospace; }
|
| 74 |
+
.shadow-soft { box-shadow: var(--shadow-soft); }
|
| 75 |
+
.text-pc1 { color: hsl(var(--pc1-color)); }
|
| 76 |
+
.bg-pc1 { background-color: hsl(var(--pc1-color)); }
|
| 77 |
+
.text-pc2 { color: hsl(var(--pc2-color)); }
|
| 78 |
+
.bg-pc2 { background-color: hsl(var(--pc2-color)); }
|
| 79 |
+
|
| 80 |
+
@keyframes fadeIn {
|
| 81 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 82 |
+
to { opacity: 1; transform: translateY(0); }
|
| 83 |
+
}
|
| 84 |
+
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
| 85 |
+
|
| 86 |
+
canvas { touch-action: none; }
|
| 87 |
+
|
| 88 |
+
.glass-btn {
|
| 89 |
+
background: white;
|
| 90 |
+
border: 1px solid hsl(var(--border));
|
| 91 |
+
transition: all 0.2s;
|
| 92 |
+
}
|
| 93 |
+
.glass-btn:hover { background: hsl(var(--muted)); }
|
| 94 |
+
|
| 95 |
+
.glass-btn-active {
|
| 96 |
+
background: hsl(var(--primary));
|
| 97 |
+
color: white;
|
| 98 |
+
border-color: hsl(var(--primary));
|
| 99 |
+
}
|
| 100 |
+
</style>
|
| 101 |
+
</head>
|
| 102 |
+
<body>
|
| 103 |
+
<div id="root"></div>
|
| 104 |
+
|
| 105 |
+
<script type="text/babel" data-type="module">
|
| 106 |
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
| 107 |
+
import { createRoot } from 'react-dom/client';
|
| 108 |
+
|
| 109 |
+
/** --- Utils: Icons --- */
|
| 110 |
+
const Icons = {
|
| 111 |
+
Trash: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2m-6 5v6m4-6v6"/></svg>,
|
| 112 |
+
Eye: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>,
|
| 113 |
+
EyeOff: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" y1="2" x2="22" y2="22"/></svg>,
|
| 114 |
+
Tags: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 5H2v7l6.29 6.29c.94.94 2.48.94 3.42 0l3.58-3.58M13 13l3.58 3.58a2.41 2.41 0 0 0 3.42 0l3.58-3.58a2.41 2.41 0 0 0 0-3.42L13 2H6"/></svg>,
|
| 115 |
+
Tag: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>,
|
| 116 |
+
Sparkles: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/></svg>,
|
| 117 |
+
ChevronDown: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>,
|
| 118 |
+
ChevronUp: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg>,
|
| 119 |
+
Camera: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="13" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><circle cx="12" cy="13" r="3"/></svg>,
|
| 120 |
+
Chart: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>,
|
| 121 |
+
Dna: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m8 18.8 8.9-8.9M12 12l3.5-3.5M15.5 15.4 12 12M19 11.9l-3.5-3.5M19 19 10.1 10.1M5 12.1 8.4 8.7M8.5 15.4 12 12M5 5l8.9 8.9"/></svg>,
|
| 122 |
+
Music: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>,
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
/** --- Math Logic --- */
|
| 126 |
+
const calculatePCA = (points) => {
|
| 127 |
+
if (points.length < 2) return null;
|
| 128 |
+
const n = points.length;
|
| 129 |
+
const mean = { x: points.reduce((s, p) => s + p.x, 0) / n, y: points.reduce((s, p) => s + p.y, 0) / n };
|
| 130 |
+
|
| 131 |
+
let covXX = 0, covYY = 0, covXY = 0;
|
| 132 |
+
points.forEach(p => {
|
| 133 |
+
const dx = p.x - mean.x;
|
| 134 |
+
const dy = p.y - mean.y;
|
| 135 |
+
covXX += dx * dx; covYY += dy * dy; covXY += dx * dy;
|
| 136 |
+
});
|
| 137 |
+
covXX /= (n - 1); covYY /= (n - 1); covXY /= (n - 1);
|
| 138 |
+
|
| 139 |
+
const trace = covXX + covYY;
|
| 140 |
+
const det = covXX * covYY - covXY * covXY;
|
| 141 |
+
const gap = Math.sqrt(Math.max(0, trace * trace - 4 * det));
|
| 142 |
+
const lambda1 = (trace + gap) / 2;
|
| 143 |
+
const lambda2 = (trace - gap) / 2;
|
| 144 |
+
|
| 145 |
+
const getEigenvector = (lambda) => {
|
| 146 |
+
if (Math.abs(covXY) > 1e-9) {
|
| 147 |
+
const x = covXY, y = lambda - covXX;
|
| 148 |
+
const mag = Math.sqrt(x*x + y*y);
|
| 149 |
+
return { x: x/mag, y: y/mag };
|
| 150 |
+
} else {
|
| 151 |
+
return covXX >= covYY
|
| 152 |
+
? (lambda === lambda1 ? { x: 1, y: 0 } : { x: 0, y: 1 })
|
| 153 |
+
: (lambda === lambda1 ? { x: 0, y: 1 } : { x: 1, y: 0 });
|
| 154 |
+
}
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
const pc1 = getEigenvector(lambda1);
|
| 158 |
+
const pc2 = getEigenvector(lambda2);
|
| 159 |
+
const v1 = (lambda1 + lambda2 === 0) ? 50 : (lambda1 / (lambda1 + lambda2)) * 100;
|
| 160 |
+
const v2 = (lambda1 + lambda2 === 0) ? 50 : (lambda2 / (lambda1 + lambda2)) * 100;
|
| 161 |
+
|
| 162 |
+
const projections = points.map(p => {
|
| 163 |
+
const dot = (p.x - mean.x) * pc1.x + (p.y - mean.y) * pc1.y;
|
| 164 |
+
return { x: mean.x + pc1.x * dot, y: mean.y + pc1.y * dot };
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
return { mean, pc1, pc2, variance1: v1, variance2: v2, projections };
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
const generateSamplePoints = (pattern) => {
|
| 171 |
+
const pts = [];
|
| 172 |
+
const cx = 250, cy = 200;
|
| 173 |
+
for (let i = 0; i < 8; i++) {
|
| 174 |
+
let x, y;
|
| 175 |
+
if (pattern === 'diagonal') { x = cx + (i-4)*40 + (Math.random()-0.5)*20; y = cy + (i-4)*40 + (Math.random()-0.5)*20; }
|
| 176 |
+
else if (pattern === 'cluster') { x = cx + (Math.random()-0.5)*100; y = cy + (Math.random()-0.5)*100; }
|
| 177 |
+
else { x = Math.random()*400+50; y = Math.random()*300+50; }
|
| 178 |
+
pts.push({ x, y, id: `p_${Date.now()}_${i}` });
|
| 179 |
+
}
|
| 180 |
+
return pts;
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
/** --- Sub-Components --- */
|
| 184 |
+
const Button = ({ children, className = '', variant = 'default', size = 'default', ...props }) => {
|
| 185 |
+
const variants = {
|
| 186 |
+
default: "bg-primary text-primary-foreground hover:opacity-90 shadow-soft",
|
| 187 |
+
outline: "border border-input bg-background hover:bg-slate-50",
|
| 188 |
+
ghost: "hover:bg-slate-100",
|
| 189 |
+
glass: "glass-btn",
|
| 190 |
+
secondary: "bg-secondary text-secondary-foreground hover:opacity-90 shadow-soft",
|
| 191 |
+
pc2: "bg-[hsl(var(--pc2-color))] text-white shadow-soft",
|
| 192 |
+
};
|
| 193 |
+
const sizes = { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", icon: "h-10 w-10" };
|
| 194 |
+
const vClass = variants[variant] || variants.default;
|
| 195 |
+
const sClass = sizes[size] || sizes.default;
|
| 196 |
+
return <button className={`inline-flex items-center justify-center rounded-xl text-sm font-medium transition-all focus:outline-none active:scale-95 disabled:opacity-50 ${vClass} ${sClass} ${className}`} {...props}>{children}</button>;
|
| 197 |
+
};
|
| 198 |
+
|
| 199 |
+
const ExplanationPanel = () => {
|
| 200 |
+
const [expanded, setExpanded] = useState(0);
|
| 201 |
+
const steps = [
|
| 202 |
+
{ title: "What is PCA?", emoji: "🎯", content: "PCA finds the directions where your data varies the most. Think of it like finding the 'spine' of your data cloud!" },
|
| 203 |
+
{ title: "Step 1: Find the Center", emoji: "⭕", content: "First, we find the average position of all points. This is shown as the red hollow circle. Everything rotates around this center." },
|
| 204 |
+
{ title: "Step 2: Find PC1", emoji: "📏", content: "The teal arrow (PC1) points in the direction of most spread. Points would be as far apart as possible!" },
|
| 205 |
+
{ title: "Step 3: Find PC2", emoji: "↔️", content: "The purple arrow (PC2) is always perpendicular to PC1. It captures the remaining variation." },
|
| 206 |
+
{ title: "Projections", emoji: "📐", content: "The dotted lines show how each point 'projects' onto PC1. This is how PCA compresses 2D data into 1D!" },
|
| 207 |
+
];
|
| 208 |
+
return (
|
| 209 |
+
<div className="space-y-2">
|
| 210 |
+
{steps.map((s, i) => (
|
| 211 |
+
<div key={i} className="border border-border bg-card rounded-xl overflow-hidden shadow-soft transition-all duration-300">
|
| 212 |
+
<button className="w-full flex items-center justify-between p-3 transition-colors hover:bg-muted/50" onClick={() => setExpanded(expanded === i ? -1 : i)}>
|
| 213 |
+
<span className="text-sm font-medium flex items-center gap-2"><span>{s.emoji}</span> {s.title}</span>
|
| 214 |
+
{expanded === i ? <Icons.ChevronUp /> : <Icons.ChevronDown />}
|
| 215 |
+
</button>
|
| 216 |
+
{expanded === i && <p className="px-3 pb-3 text-xs text-muted-foreground leading-relaxed pl-9 animate-fade-in">{s.content}</p>}
|
| 217 |
+
</div>
|
| 218 |
+
))}
|
| 219 |
+
</div>
|
| 220 |
+
);
|
| 221 |
+
};
|
| 222 |
+
|
| 223 |
+
const RealWorldExamples = () => (
|
| 224 |
+
<div className="bg-card rounded-xl p-4 border border-border shadow-soft space-y-3">
|
| 225 |
+
<h3 className="text-sm font-semibold flex items-center gap-2">🌍 Real-World Uses of PCA</h3>
|
| 226 |
+
{[
|
| 227 |
+
{ Icon: Icons.Camera, title: "Face Recognition", desc: "Compress pixels into key features" },
|
| 228 |
+
{ Icon: Icons.Chart, title: "Stock Analysis", desc: "Find patterns in market movements" },
|
| 229 |
+
{ Icon: Icons.Dna, title: "Genetics", desc: "Visualize population spread" },
|
| 230 |
+
{ Icon: Icons.Music, title: "Recommendation", desc: "Group songs by audio vibes" }
|
| 231 |
+
].map((e, i) => (
|
| 232 |
+
<div key={i} className="flex gap-3 items-center group cursor-default">
|
| 233 |
+
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors">
|
| 234 |
+
<e.Icon className="text-primary" />
|
| 235 |
+
</div>
|
| 236 |
+
<div>
|
| 237 |
+
<p className="text-xs font-semibold">{e.title}</p>
|
| 238 |
+
<p className="text-[10px] text-muted-foreground leading-tight">{e.desc}</p>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
))}
|
| 242 |
+
</div>
|
| 243 |
+
);
|
| 244 |
+
|
| 245 |
+
const PCACanvas = ({ points, onAddPoint, onMovePoint, showProjections, showPC2, showLabels }) => {
|
| 246 |
+
const canvasRef = useRef(null);
|
| 247 |
+
const [dragging, setDragging] = useState(null);
|
| 248 |
+
const [hovered, setHovered] = useState(null);
|
| 249 |
+
const pca = useMemo(() => calculatePCA(points), [points]);
|
| 250 |
+
|
| 251 |
+
const draw = useCallback(() => {
|
| 252 |
+
const canvas = canvasRef.current; if (!canvas) return;
|
| 253 |
+
const ctx = canvas.getContext('2d');
|
| 254 |
+
const { width, height } = canvas;
|
| 255 |
+
|
| 256 |
+
ctx.fillStyle = 'hsl(40, 30%, 98%)'; ctx.fillRect(0, 0, width, height);
|
| 257 |
+
ctx.strokeStyle = 'hsl(220, 20%, 90%)'; ctx.lineWidth = 1;
|
| 258 |
+
for (let x = 0; x <= width; x += 50) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); }
|
| 259 |
+
for (let y = 0; y <= height; y += 50) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); }
|
| 260 |
+
|
| 261 |
+
if (pca && points.length >= 2) {
|
| 262 |
+
const scale = 150;
|
| 263 |
+
if (showProjections) {
|
| 264 |
+
ctx.strokeStyle = 'rgba(12, 80, 60, 0.2)'; ctx.setLineDash([5, 5]);
|
| 265 |
+
points.forEach((p, i) => {
|
| 266 |
+
const proj = pca.projections[i];
|
| 267 |
+
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(proj.x, proj.y); ctx.stroke();
|
| 268 |
+
});
|
| 269 |
+
ctx.setLineDash([]); ctx.fillStyle = 'hsl(var(--pc1-color))';
|
| 270 |
+
pca.projections.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2); ctx.fill(); });
|
| 271 |
+
}
|
| 272 |
+
if (showPC2) {
|
| 273 |
+
ctx.strokeStyle = 'hsl(var(--pc2-color))'; ctx.lineWidth = 3; ctx.beginPath();
|
| 274 |
+
ctx.moveTo(pca.mean.x - pca.pc2.x * scale, pca.mean.y - pca.pc2.y * scale);
|
| 275 |
+
ctx.lineTo(pca.mean.x + pca.pc2.x * scale, pca.mean.y + pca.pc2.y * scale); ctx.stroke();
|
| 276 |
+
}
|
| 277 |
+
ctx.strokeStyle = 'hsl(var(--pc1-color))'; ctx.lineWidth = 4; ctx.beginPath();
|
| 278 |
+
ctx.moveTo(pca.mean.x - pca.pc1.x * scale, pca.mean.y - pca.pc1.y * scale);
|
| 279 |
+
ctx.lineTo(pca.mean.x + pca.pc1.x * scale, pca.mean.y + pca.pc1.y * scale); ctx.stroke();
|
| 280 |
+
|
| 281 |
+
// --- BOLD RED MEAN POINT ---
|
| 282 |
+
ctx.strokeStyle = '#000000'; ctx.lineWidth = 4;
|
| 283 |
+
ctx.beginPath(); ctx.arc(pca.mean.x, pca.mean.y, 11, 0, Math.PI * 2); ctx.stroke();
|
| 284 |
+
ctx.fillStyle = '#ff3b30'; // Bold Red
|
| 285 |
+
ctx.beginPath(); ctx.arc(pca.mean.x, pca.mean.y, 7, 0, Math.PI * 2); ctx.fill();
|
| 286 |
+
|
| 287 |
+
if (showLabels) {
|
| 288 |
+
ctx.fillStyle = '#ff3b30';
|
| 289 |
+
ctx.font = 'bold 15px "Space Grotesk"';
|
| 290 |
+
ctx.textAlign = 'left';
|
| 291 |
+
ctx.fillText('MEAN', pca.mean.x + 18, pca.mean.y - 4);
|
| 292 |
+
ctx.font = 'bold 10px "Space Grotesk"';
|
| 293 |
+
ctx.fillStyle = '#000000';
|
| 294 |
+
ctx.fillText('(center)', pca.mean.x + 18, pca.mean.y + 10);
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// --- BOLD DATA POINTS (3D Look) ---
|
| 299 |
+
points.forEach((p, i) => {
|
| 300 |
+
const isH = hovered === p.id;
|
| 301 |
+
const radius = isH ? 14 : 11; // Smaller radius
|
| 302 |
+
const hue = (i * 137.5) % 360;
|
| 303 |
+
|
| 304 |
+
// 3D Gradient
|
| 305 |
+
const grad = ctx.createRadialGradient(
|
| 306 |
+
p.x - radius/3, p.y - radius/3, radius/5,
|
| 307 |
+
p.x, p.y, radius
|
| 308 |
+
);
|
| 309 |
+
grad.addColorStop(0, `hsl(${hue}, 100%, 80%)`); // Highlight
|
| 310 |
+
grad.addColorStop(0.4, `hsl(${hue}, 85%, 55%)`); // Body
|
| 311 |
+
grad.addColorStop(1, `hsl(${hue}, 90%, 30%)`); // Shadow
|
| 312 |
+
|
| 313 |
+
ctx.shadowBlur = isH ? 8 : 4;
|
| 314 |
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
| 315 |
+
ctx.fillStyle = grad;
|
| 316 |
+
|
| 317 |
+
ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.fill();
|
| 318 |
+
|
| 319 |
+
// Subtle border
|
| 320 |
+
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
|
| 321 |
+
ctx.lineWidth = 1;
|
| 322 |
+
ctx.stroke();
|
| 323 |
+
|
| 324 |
+
// Reset shadow for text
|
| 325 |
+
ctx.shadowBlur = 0;
|
| 326 |
+
ctx.fillStyle = 'white';
|
| 327 |
+
ctx.font = 'bold 10px "Space Grotesk"';
|
| 328 |
+
ctx.textAlign = 'center';
|
| 329 |
+
ctx.textBaseline = 'middle';
|
| 330 |
+
ctx.fillText(i + 1, p.x, p.y);
|
| 331 |
+
|
| 332 |
+
if (showLabels) {
|
| 333 |
+
ctx.textBaseline = 'alphabetic';
|
| 334 |
+
ctx.fillStyle = `hsl(${hue}, 85%, 45%)`;
|
| 335 |
+
ctx.font = 'bold 11px "Space Grotesk"';
|
| 336 |
+
ctx.textAlign = 'center';
|
| 337 |
+
ctx.fillText(`Point ${i + 1}`, p.x, p.y + (isH ? 30 : 26));
|
| 338 |
+
}
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
if (points.length === 0) {
|
| 342 |
+
ctx.fillStyle = 'hsl(var(--muted-foreground))';
|
| 343 |
+
ctx.font = 'bold 16px "Space Grotesk"';
|
| 344 |
+
ctx.textAlign = 'center';
|
| 345 |
+
ctx.fillText('Click/Touch to add your first data point!', width/2, height/2);
|
| 346 |
+
}
|
| 347 |
+
}, [points, pca, hovered, dragging, showProjections, showPC2, showLabels]);
|
| 348 |
+
|
| 349 |
+
useEffect(() => draw(), [draw]);
|
| 350 |
+
|
| 351 |
+
// --- Mobile Touch & Mouse Unified Handler ---
|
| 352 |
+
const getPos = (e) => {
|
| 353 |
+
const canvas = canvasRef.current;
|
| 354 |
+
const rect = canvas.getBoundingClientRect();
|
| 355 |
+
|
| 356 |
+
// Handle both touch and mouse events
|
| 357 |
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
| 358 |
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
| 359 |
+
|
| 360 |
+
const scaleX = canvas.width / rect.width;
|
| 361 |
+
const scaleY = canvas.height / rect.height;
|
| 362 |
+
return {
|
| 363 |
+
x: (clientX - rect.left) * scaleX,
|
| 364 |
+
y: (clientY - rect.top) * scaleY
|
| 365 |
+
};
|
| 366 |
+
};
|
| 367 |
+
|
| 368 |
+
const handleStart = (e) => {
|
| 369 |
+
e.preventDefault(); // Prevent scrolling on touch
|
| 370 |
+
const { x, y } = getPos(e);
|
| 371 |
+
const target = points.find(p => Math.sqrt((p.x - x)**2 + (p.y - y)**2) < 25);
|
| 372 |
+
if (target) setDragging(target.id);
|
| 373 |
+
else onAddPoint({ x, y, id: `p_${Date.now()}` });
|
| 374 |
+
};
|
| 375 |
+
|
| 376 |
+
const handleMove = (e) => {
|
| 377 |
+
e.preventDefault();
|
| 378 |
+
const { x, y } = getPos(e);
|
| 379 |
+
if (dragging) {
|
| 380 |
+
onMovePoint(dragging, x, y);
|
| 381 |
+
} else {
|
| 382 |
+
// Only hover effects for mouse, not touch (usually)
|
| 383 |
+
if (!e.touches) {
|
| 384 |
+
setHovered(points.find(p => Math.sqrt((p.x - x)**2 + (p.y - y)**2) < 25)?.id || null);
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
};
|
| 388 |
+
|
| 389 |
+
const handleEnd = (e) => {
|
| 390 |
+
// For touchEnd, we don't need to preventDefault usually, but it's good practice for games
|
| 391 |
+
if(e.cancelable) e.preventDefault();
|
| 392 |
+
setDragging(null);
|
| 393 |
+
};
|
| 394 |
+
|
| 395 |
+
return (
|
| 396 |
+
<canvas
|
| 397 |
+
ref={canvasRef} width={500} height={400}
|
| 398 |
+
className="w-full h-auto bg-white rounded-xl border-2 border-border cursor-crosshair shadow-soft"
|
| 399 |
+
|
| 400 |
+
// Mouse Events
|
| 401 |
+
onMouseDown={handleStart}
|
| 402 |
+
onMouseMove={handleMove}
|
| 403 |
+
onMouseUp={handleEnd}
|
| 404 |
+
onMouseLeave={() => { setDragging(null); setHovered(null); }}
|
| 405 |
+
|
| 406 |
+
// Touch Events (Mobile)
|
| 407 |
+
onTouchStart={handleStart}
|
| 408 |
+
onTouchMove={handleMove}
|
| 409 |
+
onTouchEnd={handleEnd}
|
| 410 |
+
onTouchCancel={handleEnd}
|
| 411 |
+
/>
|
| 412 |
+
);
|
| 413 |
+
};
|
| 414 |
+
|
| 415 |
+
const ControlPanel = ({ onClear, onGenerate, showProjections, onToggleProjections, showPC2, onTogglePC2, showLabels, onToggleLabels, count }) => (
|
| 416 |
+
<div className="bg-card rounded-xl p-4 border border-border shadow-soft space-y-4">
|
| 417 |
+
<h3 className="text-sm font-semibold">Toggle Features</h3>
|
| 418 |
+
<div className="grid grid-cols-1 gap-2">
|
| 419 |
+
<Button variant={showProjections ? "default" : "glass"} size="sm" onClick={onToggleProjections} className={`justify-start gap-2 ${showProjections ? 'glass-btn-active' : ''}`}>
|
| 420 |
+
{showProjections ? <Icons.EyeOff /> : <Icons.Eye />} Projections
|
| 421 |
+
</Button>
|
| 422 |
+
<Button variant={showPC2 ? "pc2" : "glass"} size="sm" onClick={onTogglePC2} className={`justify-start gap-2 ${showPC2 ? 'glass-btn-active' : ''}`}>
|
| 423 |
+
{showPC2 ? <Icons.EyeOff /> : <Icons.Eye />} PC2 Arrow
|
| 424 |
+
</Button>
|
| 425 |
+
<Button variant={showLabels ? "secondary" : "glass"} size="sm" onClick={onToggleLabels} className={`justify-start gap-2 ${showLabels ? 'glass-btn-active' : ''}`}>
|
| 426 |
+
{showLabels ? <Icons.Tags /> : <Icons.Tag />} Labels
|
| 427 |
+
</Button>
|
| 428 |
+
</div>
|
| 429 |
+
<div className="pt-2 border-t border-border">
|
| 430 |
+
<h4 className="text-xs font-semibold mb-2">Try Patterns</h4>
|
| 431 |
+
<div className="grid grid-cols-3 gap-2">
|
| 432 |
+
<Button variant="outline" size="sm" onClick={() => onGenerate('diagonal')}>Linear</Button>
|
| 433 |
+
<Button variant="outline" size="sm" onClick={() => onGenerate('cluster')}>Cluster</Button>
|
| 434 |
+
<Button variant="outline" size="sm" onClick={() => onGenerate('spread')}>Random</Button>
|
| 435 |
+
</div>
|
| 436 |
+
</div>
|
| 437 |
+
<div className="flex items-center justify-between pt-2 border-t border-border">
|
| 438 |
+
<span className="text-xs text-muted-foreground font-medium">{count} points added</span>
|
| 439 |
+
<Button variant="ghost" size="sm" onClick={onClear} className="text-destructive h-auto p-1 px-2"><Icons.Trash /> Clear All</Button>
|
| 440 |
+
</div>
|
| 441 |
+
</div>
|
| 442 |
+
);
|
| 443 |
+
|
| 444 |
+
const App = () => {
|
| 445 |
+
const [points, setPoints] = useState([]);
|
| 446 |
+
const [showProjections, setShowProjections] = useState(false);
|
| 447 |
+
const [showPC2, setShowPC2] = useState(false);
|
| 448 |
+
const [showLabels, setShowLabels] = useState(true);
|
| 449 |
+
const [variance, setVariance] = useState({ v1: 50, v2: 50 });
|
| 450 |
+
|
| 451 |
+
useEffect(() => {
|
| 452 |
+
const res = calculatePCA(points);
|
| 453 |
+
setVariance(res ? { v1: res.variance1, v2: res.variance2 } : { v1: 50, v2: 50 });
|
| 454 |
+
}, [points]);
|
| 455 |
+
|
| 456 |
+
// Styles defined here to avoid Jinja2 conflict with double curly braces
|
| 457 |
+
const pc1BarStyle = { width: `${variance.v1}%` };
|
| 458 |
+
const pc2BarStyle = { width: `${variance.v2}%` };
|
| 459 |
+
|
| 460 |
+
return (
|
| 461 |
+
<div className="min-h-screen bg-background p-4 md:p-8 animate-fade-in">
|
| 462 |
+
<div className="max-w-6xl mx-auto space-y-8">
|
| 463 |
+
<header className="text-center space-y-3">
|
| 464 |
+
<div className="inline-flex items-center gap-2 bg-primary/10 text-primary px-4 py-2 rounded-full text-xs font-semibold tracking-wide uppercase"><Icons.Sparkles /> Interactive Lab</div>
|
| 465 |
+
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-foreground">PCA Playground</h1>
|
| 466 |
+
<p className="text-muted-foreground max-w-xl mx-auto text-base">Explore Principal Component Analysis (PCA) by adding points, dragging them, and discovering hidden patterns.</p>
|
| 467 |
+
</header>
|
| 468 |
+
|
| 469 |
+
<div className="grid lg:grid-cols-[1fr_340px] gap-8">
|
| 470 |
+
<div className="space-y-6">
|
| 471 |
+
<PCACanvas points={points} onAddPoint={p => setPoints(prev => [...prev, p])} onMovePoint={(id, x, y) => setPoints(prev => prev.map(p => p.id === id ? {...p, x, y} : p))} showProjections={showProjections} showPC2={showPC2} showLabels={showLabels} />
|
| 472 |
+
<div className="flex flex-wrap gap-6 justify-center bg-card p-3 rounded-xl border border-border text-[11px] font-bold text-muted-foreground shadow-sm uppercase tracking-wider">
|
| 473 |
+
<div className="flex items-center gap-2"><div className="w-4 h-4 rounded-full bg-point"/> Data Balls</div>
|
| 474 |
+
<div className="flex items-center gap-2"><div className="w-5 h-1.5 bg-pc1 rounded"/> PC1 (Main)</div>
|
| 475 |
+
<div className="flex items-center gap-2"><div className="w-5 h-1.5 bg-pc2 rounded"/> PC2 (Secondary)</div>
|
| 476 |
+
<div className="flex items-center gap-2"><div className="w-5 h-5 rounded-full border-2 border-foreground bg-[#ff3b30]"/> Mean Center</div>
|
| 477 |
+
</div>
|
| 478 |
+
</div>
|
| 479 |
+
<div className="space-y-4">
|
| 480 |
+
<ControlPanel onClear={() => { setPoints([]); setShowProjections(false); setShowPC2(false); }} onGenerate={pattern => setPoints(generateSamplePoints(pattern))} showProjections={showProjections} onToggleProjections={() => setShowProjections(prev => !prev)} showPC2={showPC2} onTogglePC2={() => setShowPC2(prev => !prev)} showLabels={showLabels} onToggleLabels={() => setShowLabels(prev => !prev)} count={points.length} />
|
| 481 |
+
{points.length >= 2 && (
|
| 482 |
+
<div className="bg-card rounded-xl p-4 border border-border shadow-soft space-y-3">
|
| 483 |
+
<h3 className="text-sm font-semibold">Variance Explained</h3>
|
| 484 |
+
<div className="space-y-4">
|
| 485 |
+
<div>
|
| 486 |
+
<div className="flex justify-between text-xs font-medium mb-1 text-pc1"><span>PC1</span><span>{variance.v1.toFixed(1)}%</span></div>
|
| 487 |
+
<div className="h-3 bg-muted rounded-full overflow-hidden"><div className="h-full bg-pc1 transition-all duration-500" style={pc1BarStyle} /></div>
|
| 488 |
+
</div>
|
| 489 |
+
<div>
|
| 490 |
+
<div className="flex justify-between text-xs font-medium mb-1 text-pc2"><span>PC2</span><span>{variance.v2.toFixed(1)}%</span></div>
|
| 491 |
+
<div className="h-3 bg-muted rounded-full overflow-hidden"><div className="h-full bg-pc2 transition-all duration-500" style={pc2BarStyle} /></div>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
)}
|
| 496 |
+
<ExplanationPanel />
|
| 497 |
+
<RealWorldExamples />
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
{/* Centered Back Button */}
|
| 502 |
+
<div className="mt-12 flex justify-center pb-8 relative">
|
| 503 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 504 |
+
<a
|
| 505 |
+
href="/hierarchical-clustering"
|
| 506 |
+
onClick={(e) => {
|
| 507 |
+
const audio = document.getElementById("clickSound");
|
| 508 |
+
if(audio) audio.play().catch(err => console.log(err));
|
| 509 |
+
}}
|
| 510 |
+
className="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"
|
| 511 |
+
>
|
| 512 |
+
Back to Core
|
| 513 |
+
</a>
|
| 514 |
+
</div>
|
| 515 |
+
</div>
|
| 516 |
+
</div>
|
| 517 |
+
);
|
| 518 |
+
};
|
| 519 |
+
|
| 520 |
+
const root = createRoot(document.getElementById('root'));
|
| 521 |
+
root.render(<App />);
|
| 522 |
+
</script>
|
| 523 |
+
</body>
|
| 524 |
+
</html>
|
templates/xboost-tree-three.html
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>XGBoost 3D Simulator</title>
|
| 7 |
+
<!-- Fonts -->
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 9 |
+
<!-- Tailwind CSS -->
|
| 10 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 11 |
+
<!-- Lucide Icons -->
|
| 12 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 13 |
+
<!-- Three.js -->
|
| 14 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 15 |
+
<!-- OrbitControls -->
|
| 16 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
| 17 |
+
|
| 18 |
+
<script>
|
| 19 |
+
tailwind.config = {
|
| 20 |
+
theme: {
|
| 21 |
+
extend: {
|
| 22 |
+
colors: {
|
| 23 |
+
background: 'hsl(222 47% 6%)',
|
| 24 |
+
foreground: 'hsl(210 40% 98%)',
|
| 25 |
+
primary: 'hsl(187 100% 50%)',
|
| 26 |
+
secondary: 'hsl(32 100% 55%)',
|
| 27 |
+
accent: 'hsl(270 80% 60%)',
|
| 28 |
+
success: 'hsl(142 76% 50%)',
|
| 29 |
+
muted: 'hsl(222 30% 15%)',
|
| 30 |
+
'muted-foreground': 'hsl(215 20% 65%)',
|
| 31 |
+
border: 'hsl(222 30% 18%)',
|
| 32 |
+
},
|
| 33 |
+
fontFamily: {
|
| 34 |
+
sans: ['Outfit', 'sans-serif'],
|
| 35 |
+
mono: ['JetBrains Mono', 'monospace'],
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
</script>
|
| 41 |
+
|
| 42 |
+
<style type="text/css">
|
| 43 |
+
body {
|
| 44 |
+
background-color: hsl(222 47% 6%);
|
| 45 |
+
color: hsl(210 40% 98%);
|
| 46 |
+
font-family: 'Outfit', sans-serif;
|
| 47 |
+
margin: 0;
|
| 48 |
+
overflow-x: hidden;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.gradient-hero {
|
| 52 |
+
background: linear-gradient(180deg, hsl(222 47% 8%) 0%, hsl(222 47% 4%) 100%);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.gradient-card {
|
| 56 |
+
background: linear-gradient(145deg, hsl(222 47% 10%) 0%, hsl(222 47% 6%) 100%);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.text-gradient-primary {
|
| 60 |
+
background: linear-gradient(135deg, hsl(187 100% 50%) 0%, hsl(200 100% 60%) 100%);
|
| 61 |
+
-webkit-background-clip: text;
|
| 62 |
+
-webkit-text-fill-color: transparent;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.glow-primary {
|
| 66 |
+
box-shadow: 0 0 30px hsla(187, 100%, 50%, 0.3);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
canvas {
|
| 70 |
+
display: block;
|
| 71 |
+
width: 100%;
|
| 72 |
+
height: 100%;
|
| 73 |
+
touch-action: none; /* Prevents scrolling while rotating model */
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.glass-btn {
|
| 77 |
+
background: rgba(255, 255, 255, 0.05);
|
| 78 |
+
backdrop-filter: blur(8px);
|
| 79 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 80 |
+
transition: all 0.2s ease;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.glass-btn:hover:not(:disabled) {
|
| 84 |
+
background: rgba(255, 255, 255, 0.1);
|
| 85 |
+
border-color: rgba(255, 255, 255, 0.2);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.primary-btn {
|
| 89 |
+
background: linear-gradient(135deg, hsl(187 100% 50%) 0%, hsl(200 100% 60%) 100%);
|
| 90 |
+
color: hsl(222 47% 6%);
|
| 91 |
+
font-weight: 600;
|
| 92 |
+
transition: all 0.2s ease;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.primary-btn:hover {
|
| 96 |
+
transform: translateY(-1px);
|
| 97 |
+
box-shadow: 0 4px 20px hsla(187, 100%, 50%, 0.4);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
input[type=range] {
|
| 101 |
+
-webkit-appearance: none;
|
| 102 |
+
width: 100%;
|
| 103 |
+
background: transparent;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
input[type=range]:focus { outline: none; }
|
| 107 |
+
input[type=range]::-webkit-slider-runnable-track {
|
| 108 |
+
width: 100%;
|
| 109 |
+
height: 6px;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
background: hsl(222 30% 18%);
|
| 112 |
+
border-radius: 3px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
input[type=range]::-webkit-slider-thumb {
|
| 116 |
+
height: 18px;
|
| 117 |
+
width: 18px;
|
| 118 |
+
border-radius: 50%;
|
| 119 |
+
background: hsl(187 100% 50%);
|
| 120 |
+
cursor: pointer;
|
| 121 |
+
-webkit-appearance: none;
|
| 122 |
+
margin-top: -6px;
|
| 123 |
+
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.chart-path {
|
| 127 |
+
stroke-dasharray: 1000;
|
| 128 |
+
stroke-dashoffset: 1000;
|
| 129 |
+
animation: dash 2s ease-out forwards;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
@keyframes dash { to { stroke-dashoffset: 0; } }
|
| 133 |
+
|
| 134 |
+
select {
|
| 135 |
+
background: hsl(222 30% 12%);
|
| 136 |
+
border: 1px solid hsl(222 30% 20%);
|
| 137 |
+
color: white;
|
| 138 |
+
padding: 4px 8px;
|
| 139 |
+
border-radius: 4px;
|
| 140 |
+
font-size: 14px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.logic-step {
|
| 144 |
+
position: relative;
|
| 145 |
+
padding-left: 2rem;
|
| 146 |
+
}
|
| 147 |
+
.logic-step::before {
|
| 148 |
+
content: '';
|
| 149 |
+
position: absolute;
|
| 150 |
+
left: 0.75rem;
|
| 151 |
+
top: 0;
|
| 152 |
+
bottom: 0;
|
| 153 |
+
width: 2px;
|
| 154 |
+
background: hsl(222 30% 20%);
|
| 155 |
+
}
|
| 156 |
+
.logic-step:last-child::before {
|
| 157 |
+
display: none;
|
| 158 |
+
}
|
| 159 |
+
</style>
|
| 160 |
+
</head>
|
| 161 |
+
<body class="gradient-hero min-h-screen">
|
| 162 |
+
|
| 163 |
+
<!-- Header -->
|
| 164 |
+
<!-- Changed: Added flex-col for mobile, relative positioning -->
|
| 165 |
+
<header class="border-b border-border/50 backdrop-blur-sm sticky top-0 z-50">
|
| 166 |
+
<div class="container mx-auto px-4 md:px-6 py-4 flex flex-col md:flex-row items-center justify-between gap-4 md:gap-0 relative">
|
| 167 |
+
|
| 168 |
+
<!-- Logo Area -->
|
| 169 |
+
<div class="flex items-center gap-4 w-full md:w-auto justify-start">
|
| 170 |
+
<div class="p-2 rounded-lg bg-primary/20 glow-primary shrink-0">
|
| 171 |
+
<i data-lucide="trees" class="h-6 w-6 text-primary"></i>
|
| 172 |
+
</div>
|
| 173 |
+
<div>
|
| 174 |
+
<h1 class="text-xl md:text-2xl font-bold text-gradient-primary leading-tight">XGBoost Simulator</h1>
|
| 175 |
+
<p class="text-xs md:text-sm text-muted-foreground hidden sm:block">Interactive 3D Visualization</p>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<!-- Centered Button -->
|
| 180 |
+
<!-- Changed: Relative on mobile (order-3), Absolute centered on desktop -->
|
| 181 |
+
<div class="order-3 md:order-none w-full md:w-auto md:absolute md:left-1/2 md:-translate-x-1/2 flex items-center justify-center">
|
| 182 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 183 |
+
<a href="/xgboost-regression" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider w-full md:w-auto">
|
| 184 |
+
Back to Core
|
| 185 |
+
</a>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<!-- Dataset Selector -->
|
| 189 |
+
<!-- Changed: Full width on mobile -->
|
| 190 |
+
<div class="flex items-center justify-between md:justify-start gap-3 glass-btn px-4 py-2 rounded-xl w-full md:w-auto">
|
| 191 |
+
<span class="text-xs text-muted-foreground font-medium uppercase tracking-wider text-white">Dataset:</span>
|
| 192 |
+
<select id="dataset-selector" class="bg-transparent border-none outline-none text-right md:text-left">
|
| 193 |
+
<option value="sine">Sine Wave</option>
|
| 194 |
+
<option value="step">Step Function</option>
|
| 195 |
+
<option value="linear">Linear Trend</option>
|
| 196 |
+
<option value="random">Random Noise</option>
|
| 197 |
+
</select>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</header>
|
| 201 |
+
|
| 202 |
+
<main class="container mx-auto px-4 md:px-6 py-4 md:py-8 space-y-6 md:space-y-8">
|
| 203 |
+
<!-- Stats Section -->
|
| 204 |
+
<div id="stats-container" class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4"></div>
|
| 205 |
+
|
| 206 |
+
<!-- Simulator Layout -->
|
| 207 |
+
<!-- Changed: Stacked columns on mobile -->
|
| 208 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8">
|
| 209 |
+
<div class="lg:col-span-2 space-y-6">
|
| 210 |
+
<!-- 3D Scene -->
|
| 211 |
+
<div class="relative aspect-[16/12] md:aspect-[16/10] rounded-xl border border-border overflow-hidden gradient-card shadow-2xl touch-none">
|
| 212 |
+
<div id="canvas-container" class="w-full h-full"></div>
|
| 213 |
+
<div id="labels-container" class="absolute inset-0 pointer-events-none"></div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<!-- Dual Charts Row -->
|
| 217 |
+
<!-- Changed: Stacked on small mobile, grid on md -->
|
| 218 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 219 |
+
<!-- Training Progress -->
|
| 220 |
+
<div class="gradient-card border border-border rounded-xl p-4 md:p-6 shadow-lg">
|
| 221 |
+
<div class="flex items-center justify-between mb-4">
|
| 222 |
+
<div class="flex items-center gap-2">
|
| 223 |
+
<i data-lucide="line-chart" class="h-5 w-5 text-success"></i>
|
| 224 |
+
<h3 class="text-lg font-semibold text-foreground">Loss Decay</h3>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="h-28 w-full relative">
|
| 228 |
+
<svg id="progress-chart" viewBox="0 0 400 100" class="w-full h-full overflow-visible">
|
| 229 |
+
<path id="loss-path" d="" fill="none" stroke="hsl(187 100% 50%)" stroke-width="2" class="chart-path" />
|
| 230 |
+
<path id="loss-area" d="" fill="url(#chart-gradient)" opacity="0.1" />
|
| 231 |
+
<g id="chart-dots"></g>
|
| 232 |
+
<defs>
|
| 233 |
+
<linearGradient id="chart-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
| 234 |
+
<stop offset="0%" style="stop-color:hsl(187 100% 50%);stop-opacity:1" />
|
| 235 |
+
<stop offset="100%" style="stop-color:hsl(187 100% 50%);stop-opacity:0" />
|
| 236 |
+
</linearGradient>
|
| 237 |
+
</defs>
|
| 238 |
+
</svg>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<!-- Feature Importance -->
|
| 243 |
+
<div class="gradient-card border border-border rounded-xl p-4 md:p-6 shadow-lg">
|
| 244 |
+
<div class="flex items-center justify-between mb-4">
|
| 245 |
+
<div class="flex items-center gap-2">
|
| 246 |
+
<i data-lucide="bar-chart-3" class="h-5 w-5 text-accent"></i>
|
| 247 |
+
<h3 class="text-lg font-semibold text-foreground">Feature Importance</h3>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
<div id="importance-container" class="space-y-3 pt-2">
|
| 251 |
+
<!-- Bars injected by JS -->
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
<!-- Decision Path Explorer -->
|
| 257 |
+
<div class="gradient-card border border-border rounded-xl p-4 md:p-6 shadow-lg">
|
| 258 |
+
<div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
|
| 259 |
+
<div class="flex items-center gap-2">
|
| 260 |
+
<i data-lucide="route" class="h-5 w-5 text-primary"></i>
|
| 261 |
+
<h3 class="text-lg font-semibold text-foreground">Decision Path Explorer</h3>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="flex items-center justify-between sm:justify-end gap-2 w-full sm:w-auto bg-black/20 p-2 rounded-lg sm:bg-transparent sm:p-0">
|
| 264 |
+
<span class="text-[10px] text-muted-foreground uppercase font-mono tracking-widest">Tracking Point:</span>
|
| 265 |
+
<select id="point-selector" class="text-[10px] py-1 max-w-[120px]">
|
| 266 |
+
<!-- Options injected by JS -->
|
| 267 |
+
</select>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
<div id="decision-path-container" class="space-y-4">
|
| 271 |
+
<!-- Iteration steps injected here -->
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
</div>
|
| 275 |
+
|
| 276 |
+
<!-- Side Panels -->
|
| 277 |
+
<div class="space-y-6">
|
| 278 |
+
<!-- Configuration Panel -->
|
| 279 |
+
<div class="gradient-card border border-border rounded-xl p-4 md:p-6 space-y-6 shadow-lg">
|
| 280 |
+
<div class="flex items-center justify-between">
|
| 281 |
+
<h3 class="text-lg font-semibold text-foreground">Configuration</h3>
|
| 282 |
+
<span id="iteration-count" class="text-sm text-muted-foreground font-mono">Iter 0/5</span>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<!-- Playback Controls -->
|
| 286 |
+
<div class="flex items-center justify-between sm:justify-center gap-2">
|
| 287 |
+
<button id="prev-btn" class="glass-btn p-2 rounded-lg disabled:opacity-30 flex-1 sm:flex-none justify-center flex"><i data-lucide="chevron-left" class="h-5 w-5"></i></button>
|
| 288 |
+
<button id="play-pause-btn" class="primary-btn px-4 sm:px-6 py-2 rounded-lg flex items-center gap-2 min-w-[100px] sm:min-w-[120px] justify-center flex-2">
|
| 289 |
+
<i id="play-icon" data-lucide="play" class="h-5 w-5"></i>
|
| 290 |
+
<span id="play-text">Play</span>
|
| 291 |
+
</button>
|
| 292 |
+
<button id="next-btn" class="glass-btn p-2 rounded-lg disabled:opacity-30 flex-1 sm:flex-none justify-center flex"><i data-lucide="chevron-right" class="h-5 w-5"></i></button>
|
| 293 |
+
<button id="reset-btn" class="glass-btn p-2 rounded-lg flex-1 sm:flex-none justify-center flex"><i data-lucide="rotate-ccw" class="h-5 w-5"></i></button>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<div class="space-y-4 pt-4 border-t border-border/50">
|
| 297 |
+
<div class="space-y-2">
|
| 298 |
+
<div class="flex justify-between text-xs text-muted-foreground"><span>Iteration</span><span id="boost-val">0</span></div>
|
| 299 |
+
<input id="iteration-slider" type="range" min="0" max="5" value="0" step="1">
|
| 300 |
+
</div>
|
| 301 |
+
<div class="space-y-2">
|
| 302 |
+
<div class="flex justify-between text-xs text-muted-foreground"><span>Learning Rate (η)</span><span id="lr-value" class="text-primary font-mono">0.30</span></div>
|
| 303 |
+
<input id="lr-slider" type="range" min="1" max="100" value="30">
|
| 304 |
+
</div>
|
| 305 |
+
<div class="space-y-2">
|
| 306 |
+
<div class="flex justify-between text-xs text-muted-foreground"><span>Max Tree Depth</span><span id="depth-val" class="text-secondary font-mono">2</span></div>
|
| 307 |
+
<input id="depth-slider" type="range" min="1" max="4" value="2">
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<!-- Explanation Panel -->
|
| 313 |
+
<div id="explanation-panel" class="gradient-card border border-border rounded-xl p-4 md:p-6 space-y-4 shadow-lg">
|
| 314 |
+
<div class="flex items-center gap-3">
|
| 315 |
+
<div class="p-2 rounded-lg bg-primary/20"><i data-lucide="book-open" class="h-5 w-5 text-primary"></i></div>
|
| 316 |
+
<h3 id="exp-title" class="text-lg font-semibold text-foreground">Status</h3>
|
| 317 |
+
</div>
|
| 318 |
+
<p id="exp-desc" class="text-muted-foreground text-sm leading-relaxed">Starting simulation...</p>
|
| 319 |
+
<div class="bg-muted/50 rounded-lg p-3 border border-border/50 overflow-x-auto">
|
| 320 |
+
<code id="exp-formula" class="text-sm font-mono text-gradient-primary whitespace-nowrap"></code>
|
| 321 |
+
</div>
|
| 322 |
+
<div class="pt-4 mt-4 border-t border-border/50 space-y-3">
|
| 323 |
+
<h4 class="text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
| 324 |
+
<i data-lucide="brain-circuit" class="h-3 w-3"></i> The Objective Function
|
| 325 |
+
</h4>
|
| 326 |
+
<p class="text-[10px] text-muted-foreground leading-relaxed">
|
| 327 |
+
XGBoost minimizes $L(\phi) = \sum l(y_i, \hat{y}_i) + \sum \Omega(f_k)$.
|
| 328 |
+
It uses a 2nd order Taylor expansion for fast optimization.
|
| 329 |
+
</p>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
</main>
|
| 335 |
+
|
| 336 |
+
<script>
|
| 337 |
+
// --- STATE & CONSTANTS ---
|
| 338 |
+
let currentIteration = 0, maxIterations = 5, learningRate = 0.3, maxDepth = 2, datasetType = 'sine', selectedPointIdx = 0;
|
| 339 |
+
let isPlaying = false, playInterval = null;
|
| 340 |
+
let scene, camera, renderer, controls, treeGroup, pointsGroup, initialGroup;
|
| 341 |
+
let dataPoints = [], iterationsData = [];
|
| 342 |
+
|
| 343 |
+
// --- SIMULATION LOGIC ---
|
| 344 |
+
function generateData(type) {
|
| 345 |
+
const points = [];
|
| 346 |
+
for (let i = 0; i < 12; i++) {
|
| 347 |
+
const x = (i / 11) * 10;
|
| 348 |
+
let actual = 0;
|
| 349 |
+
if (type === 'sine') actual = Math.sin(x * 0.6) * 2;
|
| 350 |
+
else if (type === 'step') actual = x < 5 ? -1.5 : 1.5;
|
| 351 |
+
else if (type === 'linear') actual = (x - 5) * 0.4;
|
| 352 |
+
else actual = (Math.random() - 0.5) * 4;
|
| 353 |
+
points.push({ id: i, x, actual: actual + (Math.random() * 0.2) });
|
| 354 |
+
}
|
| 355 |
+
return points;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
function createTree(depthLimit, iteration, currentDepth = 0, x = 0, y = 0, z = 0, width = 4) {
|
| 359 |
+
const isLeaf = currentDepth >= depthLimit;
|
| 360 |
+
const threshold = Math.random() * 10;
|
| 361 |
+
const node = {
|
| 362 |
+
id: Math.random().toString(36),
|
| 363 |
+
feature: "X",
|
| 364 |
+
threshold: threshold,
|
| 365 |
+
isLeaf,
|
| 366 |
+
value: isLeaf ? (Math.random() * 1.5 - 0.75) : undefined,
|
| 367 |
+
position: [x, y, z]
|
| 368 |
+
};
|
| 369 |
+
if (!isLeaf) {
|
| 370 |
+
const nextWidth = width / 2;
|
| 371 |
+
node.left = createTree(depthLimit, iteration, currentDepth + 1, x - nextWidth, y - 1.5, z, nextWidth);
|
| 372 |
+
node.right = createTree(depthLimit, iteration, currentDepth + 1, x + nextWidth, y - 1.5, z, nextWidth);
|
| 373 |
+
}
|
| 374 |
+
return node;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
function simulate(points, treeCount, lr, depth) {
|
| 378 |
+
const results = [];
|
| 379 |
+
let currentPreds = Array(points.length).fill(points.reduce((a,b)=>a+b.actual,0)/points.length);
|
| 380 |
+
for (let i = 1; i <= treeCount; i++) {
|
| 381 |
+
const tree = createTree(depth, i);
|
| 382 |
+
const residuals = points.map((p, idx) => p.actual - currentPreds[idx]);
|
| 383 |
+
|
| 384 |
+
// Track path for each point
|
| 385 |
+
const treeOutputs = points.map(p => {
|
| 386 |
+
let curr = tree;
|
| 387 |
+
const path = [];
|
| 388 |
+
while(!curr.isLeaf) {
|
| 389 |
+
const direction = p.x < curr.threshold ? "left" : "right";
|
| 390 |
+
path.push({ node: curr, direction });
|
| 391 |
+
curr = curr[direction];
|
| 392 |
+
}
|
| 393 |
+
path.push({ node: curr, direction: "leaf" });
|
| 394 |
+
return { value: curr.value, path };
|
| 395 |
+
});
|
| 396 |
+
|
| 397 |
+
currentPreds = currentPreds.map((p, idx) => p + lr * treeOutputs[idx].value);
|
| 398 |
+
results.push({
|
| 399 |
+
iteration: i,
|
| 400 |
+
tree,
|
| 401 |
+
predictions: [...currentPreds],
|
| 402 |
+
residuals: points.map((p, idx) => p.actual - currentPreds[idx]),
|
| 403 |
+
outputs: treeOutputs
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
return results;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
// --- RENDERING ---
|
| 410 |
+
function initThree() {
|
| 411 |
+
const canvasContainer = document.getElementById('canvas-container');
|
| 412 |
+
scene = new THREE.Scene();
|
| 413 |
+
scene.fog = new THREE.Fog(0x0a0f1a, 10, 40);
|
| 414 |
+
camera = new THREE.PerspectiveCamera(50, canvasContainer.clientWidth/canvasContainer.clientHeight, 0.1, 1000);
|
| 415 |
+
camera.position.set(0, 2, 12);
|
| 416 |
+
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
| 417 |
+
renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
|
| 418 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
| 419 |
+
canvasContainer.appendChild(renderer.domElement);
|
| 420 |
+
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
| 421 |
+
controls.enableDamping = true;
|
| 422 |
+
|
| 423 |
+
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
|
| 424 |
+
const p1 = new THREE.PointLight(0x00d4ff, 1); p1.position.set(10,10,10); scene.add(p1);
|
| 425 |
+
const grid = new THREE.GridHelper(40, 40, 0x1e3a5f, 0x0f172a); grid.position.y = -5; scene.add(grid);
|
| 426 |
+
|
| 427 |
+
treeGroup = new THREE.Group(); pointsGroup = new THREE.Group(); initialGroup = new THREE.Group();
|
| 428 |
+
scene.add(treeGroup); scene.add(pointsGroup); scene.add(initialGroup);
|
| 429 |
+
|
| 430 |
+
window.addEventListener('resize', () => {
|
| 431 |
+
camera.aspect = canvasContainer.clientWidth/canvasContainer.clientHeight; camera.updateProjectionMatrix();
|
| 432 |
+
renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
|
| 433 |
+
});
|
| 434 |
+
|
| 435 |
+
const animate = () => { requestAnimationFrame(animate); controls.update(); updateLabels(); renderer.render(scene, camera); };
|
| 436 |
+
animate();
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
function drawNode(node, parentPos = null) {
|
| 440 |
+
const group = new THREE.Group(); group.position.set(...node.position);
|
| 441 |
+
const nodeColor = node.isLeaf ? 0x22c55e : 0x00d4ff;
|
| 442 |
+
if (parentPos) {
|
| 443 |
+
const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0,0), new THREE.Vector3(parentPos[0]-node.position[0], parentPos[1]-node.position[1], parentPos[2]-node.position[2])]), new THREE.LineBasicMaterial({ color: 0x334155, transparent: true, opacity: 0.6 }));
|
| 444 |
+
group.add(line);
|
| 445 |
+
}
|
| 446 |
+
const mesh = new THREE.Mesh(new THREE.SphereGeometry(node.isLeaf?0.25:0.35, 32, 32), new THREE.MeshStandardMaterial({ color: nodeColor, emissive: nodeColor, emissiveIntensity: 0.4 }));
|
| 447 |
+
mesh.userData = { label: node.isLeaf ? node.value.toFixed(2) : `X < ${node.threshold.toFixed(1)}`, isNode: true };
|
| 448 |
+
group.add(mesh); treeGroup.add(group);
|
| 449 |
+
if (node.left) drawNode(node.left, node.position);
|
| 450 |
+
if (node.right) drawNode(node.right, node.position);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function drawPoints(data) {
|
| 454 |
+
pointsGroup.clear();
|
| 455 |
+
const startX = -((data.predictions.length-1) * 0.8) / 2;
|
| 456 |
+
data.predictions.forEach((pred, i) => {
|
| 457 |
+
const g = new THREE.Group(); g.position.set(startX + i * 0.8, -4, 0);
|
| 458 |
+
const barH = Math.max(0.1, Math.abs(pred)*1.5);
|
| 459 |
+
const bar = new THREE.Mesh(new THREE.BoxGeometry(0.3, barH, 0.3), new THREE.MeshStandardMaterial({ color: i === selectedPointIdx ? 0xffff00 : 0x00d4ff, emissive: i === selectedPointIdx ? 0xffff00 : 0x000000, emissiveIntensity: 0.5 }));
|
| 460 |
+
bar.position.y = barH/2; g.add(bar);
|
| 461 |
+
const res = new THREE.Mesh(new THREE.SphereGeometry(0.15), new THREE.MeshStandardMaterial({ color: data.residuals[i]>0?0x22c55e:0xef4444 }));
|
| 462 |
+
res.position.y = -0.5; g.add(res); pointsGroup.add(g);
|
| 463 |
+
});
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
function updateLabels() {
|
| 467 |
+
const labelsContainer = document.getElementById('labels-container');
|
| 468 |
+
if (!labelsContainer) return;
|
| 469 |
+
labelsContainer.innerHTML = '';
|
| 470 |
+
if (currentIteration === 0) {
|
| 471 |
+
createLabel("Base Prediction: Mean(y)", new THREE.Vector3(0,2,0), "text-white font-semibold text-xs");
|
| 472 |
+
} else {
|
| 473 |
+
treeGroup.children.forEach(g => {
|
| 474 |
+
const m = g.children.find(c=>c.userData.isNode);
|
| 475 |
+
if (m) {
|
| 476 |
+
const v = new THREE.Vector3(); m.getWorldPosition(v);
|
| 477 |
+
createLabel(m.userData.label, v.add(new THREE.Vector3(0,0.6,0)), "text-[9px] text-muted-foreground bg-background/80 px-1 rounded shadow-sm");
|
| 478 |
+
}
|
| 479 |
+
});
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
function createLabel(txt, pos, cls) {
|
| 484 |
+
const v = pos.project(camera); if (v.z > 1) return;
|
| 485 |
+
const x = (v.x*0.5+0.5)*renderer.domElement.clientWidth, y = (v.y*-0.5+0.5)*renderer.domElement.clientHeight;
|
| 486 |
+
const d = document.createElement('div'); d.className = `absolute transform -translate-x-1/2 pointer-events-none ${cls}`;
|
| 487 |
+
d.style.left = `${x}px`; d.style.top = `${y}px`; d.textContent = txt;
|
| 488 |
+
const labelsContainer = document.getElementById('labels-container');
|
| 489 |
+
if (labelsContainer) labelsContainer.appendChild(d);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
// --- UI UPDATES ---
|
| 493 |
+
function refresh() {
|
| 494 |
+
dataPoints = generateData(datasetType);
|
| 495 |
+
iterationsData = simulate(dataPoints, maxIterations, learningRate, maxDepth);
|
| 496 |
+
|
| 497 |
+
// Re-populate point selector
|
| 498 |
+
const sel = document.getElementById('point-selector');
|
| 499 |
+
sel.innerHTML = '';
|
| 500 |
+
dataPoints.forEach((p, i) => {
|
| 501 |
+
const opt = document.createElement('option');
|
| 502 |
+
opt.value = i;
|
| 503 |
+
opt.textContent = `Point #${i} (X: ${p.x.toFixed(1)})`;
|
| 504 |
+
sel.appendChild(opt);
|
| 505 |
+
});
|
| 506 |
+
sel.value = selectedPointIdx;
|
| 507 |
+
|
| 508 |
+
render();
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
function render() {
|
| 512 |
+
document.getElementById('iteration-count').textContent = `Iter ${currentIteration}/${maxIterations}`;
|
| 513 |
+
document.getElementById('boost-val').textContent = currentIteration;
|
| 514 |
+
document.getElementById('depth-val').textContent = maxDepth;
|
| 515 |
+
document.getElementById('iteration-slider').value = currentIteration;
|
| 516 |
+
|
| 517 |
+
const baseMse = 1.0;
|
| 518 |
+
const currentMse = currentIteration === 0 ? baseMse : (iterationsData[currentIteration-1].residuals.reduce((a,b)=>a+b*b,0)/dataPoints.length);
|
| 519 |
+
|
| 520 |
+
updateStats(currentMse);
|
| 521 |
+
updateCharts(currentMse);
|
| 522 |
+
updateDecisionPath();
|
| 523 |
+
|
| 524 |
+
treeGroup.clear(); pointsGroup.clear(); initialGroup.clear();
|
| 525 |
+
if (currentIteration === 0) {
|
| 526 |
+
const g = new THREE.SphereGeometry(1,32,32);
|
| 527 |
+
initialGroup.add(new THREE.Mesh(g, new THREE.MeshStandardMaterial({color:0x00d4ff, transparent:true, opacity:0.8})));
|
| 528 |
+
document.getElementById('exp-title').textContent = "Initialization";
|
| 529 |
+
document.getElementById('exp-desc').textContent = "Model starts with the global mean. Initial residuals are high.";
|
| 530 |
+
document.getElementById('exp-formula').textContent = "F₀(x) = average(targets)";
|
| 531 |
+
} else {
|
| 532 |
+
const d = iterationsData[currentIteration-1];
|
| 533 |
+
drawNode(d.tree); drawPoints(d);
|
| 534 |
+
document.getElementById('exp-title').textContent = `Tree ${currentIteration}`;
|
| 535 |
+
document.getElementById('exp-desc').textContent = `Building weak learner ${currentIteration} to minimize residuals using the learning rate.`;
|
| 536 |
+
document.getElementById('exp-formula').textContent = `Fₘ = Fₘ₋₁ + η · Treeₘ(x)`;
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
function updateStats(mse) {
|
| 541 |
+
const container = document.getElementById('stats-container'); container.innerHTML = '';
|
| 542 |
+
const stats = [
|
| 543 |
+
{ l: "MSE Loss", v: mse.toFixed(4), i: "target", c: "text-secondary" },
|
| 544 |
+
{ l: "Complexity", v: currentIteration*maxDepth, i: "layers", c: "text-primary" },
|
| 545 |
+
{ l: "Learning Rate", v: learningRate.toFixed(2), i: "zap", c: "text-accent" },
|
| 546 |
+
{ l: "Progress", v: ((currentIteration/maxIterations)*100).toFixed(0)+"%", i: "trending-down", c: "text-success" }
|
| 547 |
+
];
|
| 548 |
+
stats.forEach(s => {
|
| 549 |
+
const d = document.createElement('div'); d.className="gradient-card border border-border rounded-xl p-4";
|
| 550 |
+
d.innerHTML = `<div class='flex gap-2 mb-1'><i data-lucide='${s.i}' class='h-4 w-4 ${s.c}'></i><span class='text-xs text-muted-foreground'>${s.l}</span></div><div class='text-xl font-mono font-bold ${s.c}'>${s.v}</div>`;
|
| 551 |
+
container.appendChild(d);
|
| 552 |
+
});
|
| 553 |
+
lucide.createIcons();
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
function updateCharts(mse) {
|
| 557 |
+
const path = document.getElementById('loss-path'), area = document.getElementById('loss-area'), dots = document.getElementById('chart-dots');
|
| 558 |
+
const pts = [{x:0, y:1}];
|
| 559 |
+
iterationsData.slice(0, currentIteration).forEach((it, i) => {
|
| 560 |
+
const m = it.residuals.reduce((a,b)=>a+b*b,0)/dataPoints.length;
|
| 561 |
+
pts.push({x:(i+1)*(400/maxIterations), y:m});
|
| 562 |
+
});
|
| 563 |
+
let d = `M ${pts[0].x} ${100-pts[0].y*100}`;
|
| 564 |
+
let dotH = `<circle cx="${pts[0].x}" cy="${100-pts[0].y*100}" r="3" fill="hsl(187 100% 50%)" />`;
|
| 565 |
+
for(let i=1;i<pts.length;i++){ d += ` L ${pts[i].x} ${100-pts[i].y*100}`; dotH += `<circle cx="${pts[i].x}" cy="${100-pts[i].y*100}" r="3" fill="hsl(187 100% 50%)" />`; }
|
| 566 |
+
path.setAttribute('d', d); area.setAttribute('d', `${d} L ${pts[pts.length-1].x} 100 L 0 100 Z`); dots.innerHTML = dotH;
|
| 567 |
+
|
| 568 |
+
const impC = document.getElementById('importance-container'); impC.innerHTML = '';
|
| 569 |
+
const feats = [ {n: "Feature X", v: Math.min(100, currentIteration*18 + 5)}, {n: "Interaction", v: Math.min(100, currentIteration*8)}, {n: "Bias", v: 10} ];
|
| 570 |
+
feats.forEach(f => {
|
| 571 |
+
const row = document.createElement('div'); row.className = "space-y-1";
|
| 572 |
+
row.innerHTML = `<div class='flex justify-between text-[10px] uppercase font-bold text-muted-foreground'><span>${f.n}</span><span>${f.v}%</span></div><div class='h-1.5 w-full bg-border rounded-full overflow-hidden'><div class='h-full bg-accent transition-all duration-500' style='width:${f.v}%'></div></div>`;
|
| 573 |
+
impC.appendChild(row);
|
| 574 |
+
});
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
function updateDecisionPath() {
|
| 578 |
+
const container = document.getElementById('decision-path-container');
|
| 579 |
+
container.innerHTML = '';
|
| 580 |
+
|
| 581 |
+
if (currentIteration === 0) {
|
| 582 |
+
container.innerHTML = '<div class="text-center py-8 text-muted-foreground text-xs italic">Step into the boosting process to see point #0 trace through the ensemble.</div>';
|
| 583 |
+
return;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
const p = dataPoints[selectedPointIdx];
|
| 587 |
+
|
| 588 |
+
iterationsData.slice(0, currentIteration).forEach((iter, idx) => {
|
| 589 |
+
const output = iter.outputs[selectedPointIdx];
|
| 590 |
+
const card = document.createElement('div');
|
| 591 |
+
card.className = "p-4 border border-border/50 rounded-lg bg-white/5 space-y-3";
|
| 592 |
+
|
| 593 |
+
let stepsHtml = '';
|
| 594 |
+
output.path.forEach((step, sIdx) => {
|
| 595 |
+
const isLast = sIdx === output.path.length - 1;
|
| 596 |
+
const nodeTxt = isLast ? `Leaf Weight: ${step.node.value.toFixed(3)}` : `Split: X (${p.x.toFixed(1)}) < ${step.node.threshold.toFixed(1)}?`;
|
| 597 |
+
const resTxt = isLast ? '' : `<span class="px-2 py-0.5 rounded text-[10px] font-bold ${step.direction === 'left' ? 'bg-success/20 text-success' : 'bg-secondary/20 text-secondary'}">${step.direction === 'left' ? 'YES' : 'NO'}</span>`;
|
| 598 |
+
|
| 599 |
+
stepsHtml += `
|
| 600 |
+
<div class="logic-step flex items-center justify-between text-[11px]">
|
| 601 |
+
<div class="flex items-center gap-2">
|
| 602 |
+
<div class="w-1.5 h-1.5 rounded-full ${isLast ? 'bg-success' : 'bg-primary'}"></div>
|
| 603 |
+
<span class="${isLast ? 'text-white font-bold' : 'text-muted-foreground'}">${nodeTxt}</span>
|
| 604 |
+
</div>
|
| 605 |
+
${resTxt}
|
| 606 |
+
</div>
|
| 607 |
+
`;
|
| 608 |
+
});
|
| 609 |
+
|
| 610 |
+
card.innerHTML = `
|
| 611 |
+
<div class="flex items-center justify-between">
|
| 612 |
+
<span class="text-[10px] font-bold text-primary uppercase">Iteration ${iter.iteration}</span>
|
| 613 |
+
<span class="text-[10px] font-mono text-muted-foreground">contrib: η × ${output.value.toFixed(2)}</span>
|
| 614 |
+
</div>
|
| 615 |
+
<div class="space-y-2 relative">${stepsHtml}</div>
|
| 616 |
+
`;
|
| 617 |
+
container.appendChild(card);
|
| 618 |
+
});
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
window.onload = () => {
|
| 622 |
+
initThree(); refresh();
|
| 623 |
+
document.getElementById('dataset-selector').onchange = (e) => { datasetType = e.target.value; currentIteration = 0; refresh(); };
|
| 624 |
+
document.getElementById('depth-slider').oninput = (e) => { maxDepth = parseInt(e.target.value); refresh(); };
|
| 625 |
+
document.getElementById('lr-slider').oninput = (e) => { learningRate = parseInt(e.target.value)/100; document.getElementById('lr-value').textContent = learningRate.toFixed(2); refresh(); };
|
| 626 |
+
document.getElementById('iteration-slider').oninput = (e) => { currentIteration = parseInt(e.target.value); render(); };
|
| 627 |
+
document.getElementById('point-selector').onchange = (e) => { selectedPointIdx = parseInt(e.target.value); render(); };
|
| 628 |
+
|
| 629 |
+
document.getElementById('play-pause-btn').onclick = () => {
|
| 630 |
+
isPlaying = !isPlaying;
|
| 631 |
+
if(isPlaying) {
|
| 632 |
+
document.getElementById('play-text').textContent = "Pause";
|
| 633 |
+
document.getElementById('play-icon').setAttribute('data-lucide', 'pause');
|
| 634 |
+
playInterval = setInterval(() => { if(currentIteration < maxIterations){ currentIteration++; render(); } else { isPlaying=false; clearInterval(playInterval); document.getElementById('play-text').textContent = "Play"; document.getElementById('play-icon').setAttribute('data-lucide', 'play'); } }, 1500);
|
| 635 |
+
} else { clearInterval(playInterval); document.getElementById('play-text').textContent = "Play"; document.getElementById('play-icon').setAttribute('data-lucide', 'play'); }
|
| 636 |
+
lucide.createIcons();
|
| 637 |
+
};
|
| 638 |
+
document.getElementById('next-btn').onclick = () => { if(currentIteration < maxIterations) { currentIteration++; render(); } };
|
| 639 |
+
document.getElementById('prev-btn').onclick = () => { if(currentIteration > 0) { currentIteration--; render(); } };
|
| 640 |
+
document.getElementById('reset-btn').onclick = () => { currentIteration = 0; render(); };
|
| 641 |
+
lucide.createIcons();
|
| 642 |
+
};
|
| 643 |
+
</script>
|
| 644 |
+
</body>
|
| 645 |
+
</html>
|
templates/xbost-graph-three.html
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
+
<title>XGB-CORE // Gradient Boosting Simulator</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 9 |
+
<!-- Added OrbitControls for user interaction -->
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 12 |
+
<style>
|
| 13 |
+
:root {
|
| 14 |
+
--bg-color: #05080a;
|
| 15 |
+
--accent-cyan: #00f3ff;
|
| 16 |
+
--accent-pink: #ff00ff;
|
| 17 |
+
--accent-purple: #7000ff;
|
| 18 |
+
--glass: rgba(10, 15, 20, 0.7);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
body {
|
| 22 |
+
background-color: var(--bg-color);
|
| 23 |
+
color: #e2e8f0;
|
| 24 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 25 |
+
overflow: hidden; /* Main body hidden, scroll happens in panels */
|
| 26 |
+
margin: 0;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.glass-panel {
|
| 30 |
+
background: var(--glass);
|
| 31 |
+
backdrop-filter: blur(12px);
|
| 32 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 33 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.8);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.glow-cyan { text-shadow: 0 0 10px var(--accent-cyan); }
|
| 37 |
+
.glow-pink { text-shadow: 0 0 10px var(--accent-pink); }
|
| 38 |
+
|
| 39 |
+
.border-glow-cyan { border-color: var(--accent-cyan); box-shadow: 0 0 10px rgba(0, 243, 255, 0.3); }
|
| 40 |
+
|
| 41 |
+
input[type="range"] {
|
| 42 |
+
-webkit-appearance: none;
|
| 43 |
+
background: rgba(255, 255, 255, 0.1);
|
| 44 |
+
height: 4px;
|
| 45 |
+
border-radius: 2px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 49 |
+
-webkit-appearance: none;
|
| 50 |
+
width: 14px;
|
| 51 |
+
height: 14px;
|
| 52 |
+
background: var(--accent-cyan);
|
| 53 |
+
border-radius: 50%;
|
| 54 |
+
cursor: pointer;
|
| 55 |
+
box-shadow: 0 0 10px var(--accent-cyan);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.gradient-text {
|
| 59 |
+
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple), var(--accent-pink));
|
| 60 |
+
-webkit-background-clip: text;
|
| 61 |
+
-webkit-text-fill-color: transparent;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.btn-action {
|
| 65 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 66 |
+
position: relative;
|
| 67 |
+
overflow: hidden;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.btn-action:hover {
|
| 71 |
+
transform: translateY(-2px);
|
| 72 |
+
filter: brightness(1.2);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
canvas { display: block; touch-action: none; }
|
| 76 |
+
|
| 77 |
+
@keyframes pulse {
|
| 78 |
+
0%, 100% { opacity: 1; }
|
| 79 |
+
50% { opacity: 0.5; }
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.status-pulse { animation: pulse 2s infinite; }
|
| 83 |
+
|
| 84 |
+
/* Scrollbar */
|
| 85 |
+
::-webkit-scrollbar { width: 5px; }
|
| 86 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 87 |
+
::-webkit-scrollbar-thumb { background: #1a1a1a; border-radius: 10px; }
|
| 88 |
+
</style>
|
| 89 |
+
</head>
|
| 90 |
+
<body class="h-screen w-screen flex flex-col supports-[height:100dvh]:h-[100dvh]">
|
| 91 |
+
|
| 92 |
+
<!-- Header -->
|
| 93 |
+
<header class="h-16 shrink-0 flex items-center justify-between px-4 lg:px-8 border-b border-white/10 z-50 glass-panel relative">
|
| 94 |
+
<div class="flex items-center gap-2 lg:gap-4 z-10">
|
| 95 |
+
<div class="w-8 h-8 bg-gradient-to-br from-cyan-400 to-purple-600 rounded-lg flex items-center justify-center font-bold text-black shadow-lg shadow-cyan-500/20 shrink-0">X</div>
|
| 96 |
+
<div>
|
| 97 |
+
<h1 class="text-lg lg:text-xl font-bold gradient-text tracking-tighter leading-none">XGB-CORE <span class="hidden sm:inline">//</span> <span class="text-white/80 block sm:inline text-xs sm:text-lg">REGR-01</span></h1>
|
| 98 |
+
<p class="hidden sm:block text-[10px] text-white/40 uppercase tracking-[0.2em]">Gradient Boosting Machine Visualizer</p>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<!-- Centered Button - Position adjusted for mobile -->
|
| 103 |
+
<div class="absolute left-1/2 -translate-x-1/2 flex items-center top-1/2 -translate-y-1/2">
|
| 104 |
+
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
| 105 |
+
<a href="/xgboost-regression" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-1.5 px-4 lg:py-2 lg:px-6 rounded-xl text-xs lg:text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider whitespace-nowrap">
|
| 106 |
+
Back to Core
|
| 107 |
+
</a>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<div class="hidden md:flex items-center gap-6 text-xs uppercase tracking-widest text-white/60">
|
| 111 |
+
<div class="flex gap-2 items-center">
|
| 112 |
+
<span class="w-2 h-2 rounded-full bg-cyan-500 status-pulse"></span>
|
| 113 |
+
<span>System Ready</span>
|
| 114 |
+
</div>
|
| 115 |
+
<div id="clock">00:00:00</div>
|
| 116 |
+
</div>
|
| 117 |
+
<!-- Mobile Menu Placeholder / Status dot for mobile -->
|
| 118 |
+
<div class="md:hidden">
|
| 119 |
+
<span class="w-2 h-2 rounded-full bg-cyan-500 status-pulse block"></span>
|
| 120 |
+
</div>
|
| 121 |
+
</header>
|
| 122 |
+
|
| 123 |
+
<main class="flex-1 flex flex-col lg:flex-row overflow-hidden relative">
|
| 124 |
+
<!-- Left Column: 3D Viewport -->
|
| 125 |
+
<!-- Mobile: Fixed height (45% of viewport), Desktop: Flex grow -->
|
| 126 |
+
<div class="relative w-full h-[40vh] lg:h-full lg:flex-1 order-1 shrink-0">
|
| 127 |
+
<div id="canvas-container" class="w-full h-full bg-[#030507]"></div>
|
| 128 |
+
|
| 129 |
+
<!-- HUD Overlays - Adjusted positioning for mobile -->
|
| 130 |
+
<div class="absolute top-2 left-2 lg:top-6 lg:left-6 pointer-events-none z-10">
|
| 131 |
+
<div class="glass-panel p-2 lg:p-4 rounded-xl border-l-4 border-l-cyan-500 min-w-[140px] lg:min-w-[200px]">
|
| 132 |
+
<h3 class="text-white/40 text-[8px] lg:text-[10px] uppercase mb-1">Training Status</h3>
|
| 133 |
+
<div id="iteration-display" class="text-xl lg:text-3xl font-bold font-mono">TREE: 00</div>
|
| 134 |
+
<div id="loss-display" class="text-cyan-400 text-[10px] lg:text-xs mt-1">MSE: 0.0000</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<div class="absolute bottom-2 left-2 lg:bottom-6 lg:left-6 pointer-events-none hidden sm:block">
|
| 139 |
+
<div class="glass-panel p-2 lg:p-3 rounded-xl text-[10px] text-white/40 uppercase leading-relaxed">
|
| 140 |
+
<p>Vector Map: <span class="text-white">Active</span></p>
|
| 141 |
+
<p>Subsampling: <span class="text-white">1.0 (Exact)</span></p>
|
| 142 |
+
<p>View: <span class="text-white">User Controlled</span></p>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<div id="coord-hud" class="absolute top-2 right-2 lg:top-6 lg:right-6 pointer-events-none glass-panel p-2 lg:p-3 rounded-xl text-right z-10">
|
| 147 |
+
<div class="text-[8px] lg:text-[10px] text-white/40 mb-1">CURSOR DATA</div>
|
| 148 |
+
<div id="coord-readout" class="font-mono text-[10px] lg:text-xs text-cyan-500">X: 0.00 | Y: 0.00 | Z: 0.00</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- Right Column: Sidebar -->
|
| 153 |
+
<!-- Mobile: Scrollable remaining space, Desktop: Fixed width sidebar -->
|
| 154 |
+
<aside class="w-full lg:w-[400px] bg-[var(--bg-color)] lg:bg-transparent glass-panel border-t lg:border-t-0 lg:border-l border-white/10 p-4 lg:p-6 flex flex-col gap-4 lg:gap-6 overflow-y-auto flex-1 lg:flex-none order-2 pb-20 lg:pb-6">
|
| 155 |
+
|
| 156 |
+
<!-- Controls Section -->
|
| 157 |
+
<section>
|
| 158 |
+
<h2 class="text-white/40 text-[10px] font-bold uppercase tracking-widest mb-4">Hyper-Parameters</h2>
|
| 159 |
+
<div class="flex flex-col gap-5">
|
| 160 |
+
<!-- Added Speed Controller -->
|
| 161 |
+
<div class="space-y-2">
|
| 162 |
+
<div class="flex justify-between text-xs">
|
| 163 |
+
<span class="text-white/80">Training Delay (ms)</span>
|
| 164 |
+
<span id="speed-val" class="text-cyan-400 font-mono">100</span>
|
| 165 |
+
</div>
|
| 166 |
+
<input type="range" id="speed-input" min="10" max="1000" step="10" value="100" class="w-full">
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div class="space-y-2">
|
| 170 |
+
<div class="flex justify-between text-xs">
|
| 171 |
+
<span class="text-white/80">Learning Rate (η)</span>
|
| 172 |
+
<span id="lr-val" class="text-cyan-400 font-mono">0.1</span>
|
| 173 |
+
</div>
|
| 174 |
+
<input type="range" id="lr-input" min="0.01" max="1.0" step="0.01" value="0.1" class="w-full">
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<div class="space-y-2">
|
| 178 |
+
<div class="flex justify-between text-xs">
|
| 179 |
+
<span class="text-white/80">Max Tree Depth</span>
|
| 180 |
+
<span id="depth-val" class="text-cyan-400 font-mono">3</span>
|
| 181 |
+
</div>
|
| 182 |
+
<input type="range" id="depth-input" min="1" max="6" step="1" value="3" class="w-full">
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<div class="space-y-2">
|
| 186 |
+
<div class="flex justify-between text-xs">
|
| 187 |
+
<span class="text-white/80">Regularization (λ)</span>
|
| 188 |
+
<span id="lambda-val" class="text-cyan-400 font-mono">1.0</span>
|
| 189 |
+
</div>
|
| 190 |
+
<input type="range" id="lambda-input" min="0" max="10" step="0.5" value="1.0" class="w-full">
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
</section>
|
| 194 |
+
|
| 195 |
+
<!-- Playback -->
|
| 196 |
+
<section class="bg-white/5 rounded-xl p-4 border border-white/10">
|
| 197 |
+
<div class="grid grid-cols-2 gap-3">
|
| 198 |
+
<button id="start-btn" class="btn-action bg-cyan-600 hover:bg-cyan-500 text-black font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 text-sm lg:text-base">
|
| 199 |
+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M4.5 3.5v13l11-6.5-11-6.5z"/></svg>
|
| 200 |
+
TRAIN
|
| 201 |
+
</button>
|
| 202 |
+
<button id="step-btn" class="btn-action bg-white/10 hover:bg-white/20 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 text-sm lg:text-base">
|
| 203 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>
|
| 204 |
+
STEP
|
| 205 |
+
</button>
|
| 206 |
+
<button id="reset-btn" class="btn-action col-span-2 bg-pink-600/20 hover:bg-pink-600 text-pink-500 hover:text-white border border-pink-500/50 py-2 rounded-lg mt-1 font-bold text-sm lg:text-base">
|
| 207 |
+
RESET KERNEL
|
| 208 |
+
</button>
|
| 209 |
+
</div>
|
| 210 |
+
</section>
|
| 211 |
+
|
| 212 |
+
<!-- Scenario Selector -->
|
| 213 |
+
<section>
|
| 214 |
+
<h2 class="text-white/40 text-[10px] font-bold uppercase tracking-widest mb-4">Manifold Scenario</h2>
|
| 215 |
+
<div class="grid grid-cols-1 gap-2">
|
| 216 |
+
<button class="scenario-btn active border-glow-cyan bg-cyan-500/10 text-cyan-400 p-3 rounded-lg text-left text-xs transition-all" data-scene="sine">
|
| 217 |
+
<div class="font-bold">Interlocking Sine Wave</div>
|
| 218 |
+
<div class="opacity-60">High frequency non-linearity</div>
|
| 219 |
+
</button>
|
| 220 |
+
<button class="scenario-btn border border-white/5 hover:border-white/20 bg-white/5 p-3 rounded-lg text-left text-xs transition-all" data-scene="saddle">
|
| 221 |
+
<div class="font-bold">Hyperbolic Paraboloid</div>
|
| 222 |
+
<div class="opacity-60">Saddle-point gradient challenge</div>
|
| 223 |
+
</button>
|
| 224 |
+
<button class="scenario-btn border border-white/5 hover:border-white/20 bg-white/5 p-3 rounded-lg text-left text-xs transition-all" data-scene="stairs">
|
| 225 |
+
<div class="font-bold">Step Discontinuity</div>
|
| 226 |
+
<div class="opacity-60">Sharp edges, axis-aligned splits</div>
|
| 227 |
+
</button>
|
| 228 |
+
</div>
|
| 229 |
+
</section>
|
| 230 |
+
|
| 231 |
+
<!-- Real-time Stats -->
|
| 232 |
+
<section class="mt-auto pb-4 lg:pb-0">
|
| 233 |
+
<div class="grid grid-cols-2 gap-2 text-[10px] uppercase font-mono">
|
| 234 |
+
<div class="p-2 bg-white/5 rounded border border-white/10">
|
| 235 |
+
<div class="text-white/40">Trees Fit</div>
|
| 236 |
+
<div id="stat-trees" class="text-white text-lg font-bold">0</div>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="p-2 bg-white/5 rounded border border-white/10">
|
| 239 |
+
<div class="text-white/40">Convergence</div>
|
| 240 |
+
<div id="stat-conv" class="text-white text-lg font-bold">---</div>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</section>
|
| 244 |
+
</aside>
|
| 245 |
+
</main>
|
| 246 |
+
|
| 247 |
+
<!-- Bottom Panel - Modified to stack on mobile, hidden details on tiny screens -->
|
| 248 |
+
<footer class="glass-panel border-t border-white/10 p-4 lg:px-8 flex flex-col lg:flex-row items-center justify-between text-xs z-50 shrink-0 gap-4 lg:gap-0">
|
| 249 |
+
<div class="flex flex-wrap justify-center lg:justify-start gap-4 lg:gap-12 w-full lg:w-auto">
|
| 250 |
+
<div class="flex flex-col items-center lg:items-start">
|
| 251 |
+
<span class="text-white/40 uppercase text-[9px] mb-1">Mechanism</span>
|
| 252 |
+
<span class="text-cyan-400">Additive Basis Expansion</span>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="flex flex-col items-center lg:items-start">
|
| 255 |
+
<span class="text-white/40 uppercase text-[9px] mb-1">Loss Function</span>
|
| 256 |
+
<span class="text-purple-400">Sum of Squares (L2)</span>
|
| 257 |
+
</div>
|
| 258 |
+
<div class="flex flex-col items-center lg:items-start">
|
| 259 |
+
<span class="text-white/40 uppercase text-[9px] mb-1">Regularizer</span>
|
| 260 |
+
<span class="text-pink-400">Shrinkage + Complexity Penalties</span>
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="max-w-md text-white/50 italic text-[11px] leading-tight text-center lg:text-right hidden sm:block">
|
| 264 |
+
<strong class="text-white not-italic uppercase text-[9px]">Pro Tip:</strong>
|
| 265 |
+
Adjust **Training Delay** to slow down or speed up the recursive gradient minimization process.
|
| 266 |
+
</div>
|
| 267 |
+
</footer>
|
| 268 |
+
|
| 269 |
+
<!-- Victory Modal -->
|
| 270 |
+
<div id="victory-modal" class="fixed inset-0 bg-black/80 backdrop-blur-md z-[100] hidden items-center justify-center p-6">
|
| 271 |
+
<div class="glass-panel max-w-lg w-full rounded-2xl border-2 border-cyan-500 p-8 text-center relative">
|
| 272 |
+
<button id="close-modal" class="absolute top-4 right-4 text-white/40 hover:text-white">
|
| 273 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
| 274 |
+
</button>
|
| 275 |
+
<div class="w-16 h-16 bg-cyan-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
| 276 |
+
<svg class="w-8 h-8 text-cyan-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5z"/></svg>
|
| 277 |
+
</div>
|
| 278 |
+
<h2 class="text-2xl lg:text-3xl font-bold mb-2 tracking-tighter italic">OPTIMIZATION COMPLETE</h2>
|
| 279 |
+
<p class="text-white/60 mb-8 text-sm lg:text-base">The XGB-CORE engine has successfully minimized residuals for the current manifold.</p>
|
| 280 |
+
<div class="grid grid-cols-3 gap-4 mb-8">
|
| 281 |
+
<div class="p-3 bg-white/5 rounded-xl border border-white/10">
|
| 282 |
+
<div class="text-[9px] text-white/40 uppercase">Total Estimators</div>
|
| 283 |
+
<div id="final-trees" class="text-xl font-bold text-cyan-400">50</div>
|
| 284 |
+
</div>
|
| 285 |
+
<div id="final-loss-container" class="p-3 bg-white/5 rounded-xl border border-white/10">
|
| 286 |
+
<div class="text-[9px] text-white/40 uppercase">Global Loss</div>
|
| 287 |
+
<div id="final-loss" class="text-xl font-bold text-pink-400">0.0021</div>
|
| 288 |
+
</div>
|
| 289 |
+
<div class="p-3 bg-white/5 rounded-xl border border-white/10">
|
| 290 |
+
<div class="text-[9px] text-white/40 uppercase">Score</div>
|
| 291 |
+
<div class="text-xl font-bold text-white">99.8%</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
<button onclick="document.getElementById('victory-modal').style.display='none'" class="w-full bg-cyan-600 hover:bg-cyan-500 text-black font-bold py-4 rounded-xl shadow-lg shadow-cyan-500/20 transition-all">
|
| 295 |
+
ACKNOWLEDGE
|
| 296 |
+
</button>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
|
| 300 |
+
<script>
|
| 301 |
+
/**
|
| 302 |
+
* XGBOOST REGRESSION CORE LOGIC
|
| 303 |
+
*/
|
| 304 |
+
class DecisionTree {
|
| 305 |
+
constructor(maxDepth = 3) {
|
| 306 |
+
this.maxDepth = maxDepth;
|
| 307 |
+
this.tree = null;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
fit(X, y) {
|
| 311 |
+
this.tree = this.buildTree(X, y, 0);
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
buildTree(X, y, depth) {
|
| 315 |
+
if (depth >= this.maxDepth || X.length <= 2) {
|
| 316 |
+
return { leaf: true, value: y.reduce((a, b) => a + b, 0) / y.length };
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
let bestSplit = null;
|
| 320 |
+
let minMse = Infinity;
|
| 321 |
+
|
| 322 |
+
for (let featureIdx of [0, 1]) {
|
| 323 |
+
const featureValues = X.map(p => p[featureIdx]);
|
| 324 |
+
const uniqueValues = [...new Set(featureValues)].sort((a,b) => a-b);
|
| 325 |
+
const candidates = uniqueValues.filter((_, i) => i % Math.max(1, Math.floor(uniqueValues.length / 10)) === 0);
|
| 326 |
+
|
| 327 |
+
for (let threshold of candidates) {
|
| 328 |
+
const leftIndices = X.map((p, i) => p[featureIdx] < threshold ? i : null).filter(i => i !== null);
|
| 329 |
+
const rightIndices = X.map((p, i) => p[featureIdx] >= threshold ? i : null).filter(i => i !== null);
|
| 330 |
+
|
| 331 |
+
if (leftIndices.length === 0 || rightIndices.length === 0) continue;
|
| 332 |
+
|
| 333 |
+
const leftY = leftIndices.map(i => y[i]);
|
| 334 |
+
const rightY = rightIndices.map(i => y[i]);
|
| 335 |
+
const mse = this.calculateMSE(leftY) * leftY.length + this.calculateMSE(rightY) * rightY.length;
|
| 336 |
+
|
| 337 |
+
if (mse < minMse) {
|
| 338 |
+
minMse = mse;
|
| 339 |
+
bestSplit = { featureIdx, threshold, leftIndices, rightIndices };
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
if (!bestSplit) return { leaf: true, value: y.reduce((a, b) => a + b, 0) / y.length };
|
| 345 |
+
|
| 346 |
+
return {
|
| 347 |
+
leaf: false,
|
| 348 |
+
featureIdx: bestSplit.featureIdx,
|
| 349 |
+
threshold: bestSplit.threshold,
|
| 350 |
+
left: this.buildTree(bestSplit.leftIndices.map(i => X[i]), bestSplit.leftIndices.map(i => y[i]), depth + 1),
|
| 351 |
+
right: this.buildTree(bestSplit.rightIndices.map(i => X[i]), bestSplit.rightIndices.map(i => y[i]), depth + 1)
|
| 352 |
+
};
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
calculateMSE(y) {
|
| 356 |
+
if (y.length === 0) return 0;
|
| 357 |
+
const mean = y.reduce((a, b) => a + b, 0) / y.length;
|
| 358 |
+
return y.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / y.length;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
predictSingle(p) {
|
| 362 |
+
let node = this.tree;
|
| 363 |
+
while (!node.leaf) {
|
| 364 |
+
if (p[node.featureIdx] < node.threshold) node = node.left;
|
| 365 |
+
else node = node.right;
|
| 366 |
+
}
|
| 367 |
+
return node.value;
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
class XGBoost {
|
| 372 |
+
constructor() {
|
| 373 |
+
this.trees = [];
|
| 374 |
+
this.learningRate = 0.1;
|
| 375 |
+
this.maxDepth = 3;
|
| 376 |
+
this.basePrediction = 0;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
init(y) {
|
| 380 |
+
this.trees = [];
|
| 381 |
+
this.basePrediction = y.reduce((a, b) => a + b, 0) / y.length;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
trainStep(X, y) {
|
| 385 |
+
const currentPredictions = this.predictAll(X);
|
| 386 |
+
const residuals = y.map((val, i) => val - currentPredictions[i]);
|
| 387 |
+
const tree = new DecisionTree(this.maxDepth);
|
| 388 |
+
tree.fit(X, residuals);
|
| 389 |
+
this.trees.push(tree);
|
| 390 |
+
return this.calculateMSE(y, this.predictAll(X));
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
predict(p) {
|
| 394 |
+
let pred = this.basePrediction;
|
| 395 |
+
for (let tree of this.trees) {
|
| 396 |
+
pred += this.learningRate * tree.predictSingle(p);
|
| 397 |
+
}
|
| 398 |
+
return pred;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
predictAll(X) {
|
| 402 |
+
return X.map(p => this.predict(p));
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
calculateMSE(yTrue, yPred) {
|
| 406 |
+
return yTrue.reduce((acc, val, i) => acc + Math.pow(val - yPred[i], 2), 0) / yTrue.length;
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/**
|
| 411 |
+
* THREE.JS VISUALIZATION ENGINE
|
| 412 |
+
*/
|
| 413 |
+
const SCENE_CONFIG = {
|
| 414 |
+
sine: (x, z) => Math.sin(x * 2) * Math.cos(z * 2) * 0.5,
|
| 415 |
+
saddle: (x, z) => (x*x - z*z) * 0.4,
|
| 416 |
+
stairs: (x, z) => {
|
| 417 |
+
const stepX = Math.floor(x * 3) / 3;
|
| 418 |
+
const stepZ = Math.floor(z * 3) / 3;
|
| 419 |
+
return (stepX + stepZ) * 0.3;
|
| 420 |
+
}
|
| 421 |
+
};
|
| 422 |
+
|
| 423 |
+
let scene, camera, renderer, surface, dataPoints, pointsGroup, controls;
|
| 424 |
+
let container = document.getElementById('canvas-container');
|
| 425 |
+
let xgb = new XGBoost();
|
| 426 |
+
let trainingData = { X: [], y: [] };
|
| 427 |
+
let isTraining = false;
|
| 428 |
+
let currentScene = 'sine';
|
| 429 |
+
let trainingDelay = 100; // Delay between steps
|
| 430 |
+
|
| 431 |
+
function initThree() {
|
| 432 |
+
scene = new THREE.Scene();
|
| 433 |
+
camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
| 434 |
+
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
| 435 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 436 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
| 437 |
+
container.appendChild(renderer.domElement);
|
| 438 |
+
|
| 439 |
+
// Added OrbitControls
|
| 440 |
+
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
| 441 |
+
controls.enableDamping = true;
|
| 442 |
+
controls.dampingFactor = 0.05;
|
| 443 |
+
|
| 444 |
+
const ambientLight = new THREE.AmbientLight(0x404040, 2);
|
| 445 |
+
scene.add(ambientLight);
|
| 446 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
| 447 |
+
directionalLight.position.set(5, 10, 7.5);
|
| 448 |
+
scene.add(directionalLight);
|
| 449 |
+
|
| 450 |
+
const grid = new THREE.GridHelper(4, 20, 0x1a1a1a, 0x1a1a1a);
|
| 451 |
+
grid.position.y = -0.5;
|
| 452 |
+
scene.add(grid);
|
| 453 |
+
|
| 454 |
+
setupData();
|
| 455 |
+
createSurface();
|
| 456 |
+
|
| 457 |
+
camera.position.set(3, 2.5, 3);
|
| 458 |
+
camera.lookAt(0, 0, 0);
|
| 459 |
+
|
| 460 |
+
animate();
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
function setupData() {
|
| 464 |
+
if (pointsGroup) scene.remove(pointsGroup);
|
| 465 |
+
pointsGroup = new THREE.Group();
|
| 466 |
+
|
| 467 |
+
trainingData.X = [];
|
| 468 |
+
trainingData.y = [];
|
| 469 |
+
|
| 470 |
+
const count = 400;
|
| 471 |
+
const geom = new THREE.BufferGeometry();
|
| 472 |
+
const pos = new Float32Array(count * 3);
|
| 473 |
+
const colors = new Float32Array(count * 3);
|
| 474 |
+
|
| 475 |
+
for (let i = 0; i < count; i++) {
|
| 476 |
+
const x = (Math.random() - 0.5) * 4;
|
| 477 |
+
const z = (Math.random() - 0.5) * 4;
|
| 478 |
+
const y = SCENE_CONFIG[currentScene](x, z) + (Math.random() - 0.5) * 0.1;
|
| 479 |
+
|
| 480 |
+
trainingData.X.push([x, z]);
|
| 481 |
+
trainingData.y.push(y);
|
| 482 |
+
|
| 483 |
+
pos[i * 3] = x;
|
| 484 |
+
pos[i * 3 + 1] = y;
|
| 485 |
+
pos[i * 3 + 2] = z;
|
| 486 |
+
|
| 487 |
+
colors[i * 3] = 1;
|
| 488 |
+
colors[i * 3 + 1] = 0.5 + y;
|
| 489 |
+
colors[i * 3 + 2] = 1;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
| 493 |
+
geom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
| 494 |
+
|
| 495 |
+
const mat = new THREE.PointsMaterial({ size: 0.05, vertexColors: true, transparent: true, opacity: 0.8 });
|
| 496 |
+
dataPoints = new THREE.Points(geom, mat);
|
| 497 |
+
pointsGroup.add(dataPoints);
|
| 498 |
+
scene.add(pointsGroup);
|
| 499 |
+
|
| 500 |
+
xgb.init(trainingData.y);
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
function createSurface() {
|
| 504 |
+
if (surface) scene.remove(surface);
|
| 505 |
+
|
| 506 |
+
const segments = 40;
|
| 507 |
+
const geom = new THREE.PlaneGeometry(4, 4, segments, segments);
|
| 508 |
+
geom.rotateX(-Math.PI / 2);
|
| 509 |
+
|
| 510 |
+
const mat = new THREE.MeshPhongMaterial({
|
| 511 |
+
color: 0x00f3ff,
|
| 512 |
+
wireframe: true,
|
| 513 |
+
transparent: true,
|
| 514 |
+
opacity: 0.3,
|
| 515 |
+
side: THREE.DoubleSide
|
| 516 |
+
});
|
| 517 |
+
|
| 518 |
+
surface = new THREE.Mesh(geom, mat);
|
| 519 |
+
scene.add(surface);
|
| 520 |
+
updateSurface();
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function updateSurface() {
|
| 524 |
+
const positions = surface.geometry.attributes.position.array;
|
| 525 |
+
for (let i = 0; i < positions.length; i += 3) {
|
| 526 |
+
const x = positions[i];
|
| 527 |
+
const z = positions[i + 2];
|
| 528 |
+
positions[i + 1] = xgb.predict([x, z]);
|
| 529 |
+
}
|
| 530 |
+
surface.geometry.attributes.position.needsUpdate = true;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function animate() {
|
| 534 |
+
requestAnimationFrame(animate);
|
| 535 |
+
controls.update(); // Required for damping
|
| 536 |
+
renderer.render(scene, camera);
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
/**
|
| 540 |
+
* APP UI & STATE
|
| 541 |
+
*/
|
| 542 |
+
const ui = {
|
| 543 |
+
speedInput: document.getElementById('speed-input'),
|
| 544 |
+
speedVal: document.getElementById('speed-val'),
|
| 545 |
+
lrInput: document.getElementById('lr-input'),
|
| 546 |
+
lrVal: document.getElementById('lr-val'),
|
| 547 |
+
depthInput: document.getElementById('depth-input'),
|
| 548 |
+
depthVal: document.getElementById('depth-val'),
|
| 549 |
+
lambdaInput: document.getElementById('lambda-input'),
|
| 550 |
+
lambdaVal: document.getElementById('lambda-val'),
|
| 551 |
+
startBtn: document.getElementById('start-btn'),
|
| 552 |
+
stepBtn: document.getElementById('step-btn'),
|
| 553 |
+
resetBtn: document.getElementById('reset-btn'),
|
| 554 |
+
iterDisplay: document.getElementById('iteration-display'),
|
| 555 |
+
lossDisplay: document.getElementById('loss-display'),
|
| 556 |
+
statTrees: document.getElementById('stat-trees'),
|
| 557 |
+
statConv: document.getElementById('stat-conv'),
|
| 558 |
+
finalTrees: document.getElementById('final-trees'),
|
| 559 |
+
finalLoss: document.getElementById('final-loss'),
|
| 560 |
+
victoryModal: document.getElementById('victory-modal'),
|
| 561 |
+
scenarioBtns: document.querySelectorAll('.scenario-btn'),
|
| 562 |
+
coordReadout: document.getElementById('coord-readout'),
|
| 563 |
+
clock: document.getElementById('clock')
|
| 564 |
+
};
|
| 565 |
+
|
| 566 |
+
// Speed Control logic
|
| 567 |
+
ui.speedInput.addEventListener('input', (e) => {
|
| 568 |
+
trainingDelay = parseInt(e.target.value);
|
| 569 |
+
ui.speedVal.innerText = e.target.value;
|
| 570 |
+
if (isTraining) {
|
| 571 |
+
// Restart interval with new speed
|
| 572 |
+
clearInterval(trainInterval);
|
| 573 |
+
trainInterval = setInterval(performStep, trainingDelay);
|
| 574 |
+
}
|
| 575 |
+
});
|
| 576 |
+
|
| 577 |
+
ui.lrInput.addEventListener('input', (e) => {
|
| 578 |
+
xgb.learningRate = parseFloat(e.target.value);
|
| 579 |
+
ui.lrVal.innerText = e.target.value;
|
| 580 |
+
});
|
| 581 |
+
|
| 582 |
+
ui.depthInput.addEventListener('input', (e) => {
|
| 583 |
+
xgb.maxDepth = parseInt(e.target.value);
|
| 584 |
+
ui.depthVal.innerText = e.target.value;
|
| 585 |
+
});
|
| 586 |
+
|
| 587 |
+
ui.lambdaInput.addEventListener('input', (e) => {
|
| 588 |
+
ui.lambdaVal.innerText = e.target.value;
|
| 589 |
+
});
|
| 590 |
+
|
| 591 |
+
ui.scenarioBtns.forEach(btn => {
|
| 592 |
+
btn.addEventListener('click', () => {
|
| 593 |
+
ui.scenarioBtns.forEach(b => {
|
| 594 |
+
b.classList.remove('active', 'border-glow-cyan', 'text-cyan-400');
|
| 595 |
+
b.classList.add('border-white/5', 'bg-white/5');
|
| 596 |
+
});
|
| 597 |
+
btn.classList.add('active', 'border-glow-cyan', 'text-cyan-400');
|
| 598 |
+
btn.classList.remove('border-white/5', 'bg-white/5');
|
| 599 |
+
|
| 600 |
+
currentScene = btn.dataset.scene;
|
| 601 |
+
resetSimulation();
|
| 602 |
+
});
|
| 603 |
+
});
|
| 604 |
+
|
| 605 |
+
function performStep() {
|
| 606 |
+
if (xgb.trees.length >= 60) {
|
| 607 |
+
stopTraining();
|
| 608 |
+
showVictory();
|
| 609 |
+
return;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
const loss = xgb.trainStep(trainingData.X, trainingData.y);
|
| 613 |
+
updateSurface();
|
| 614 |
+
|
| 615 |
+
ui.iterDisplay.innerText = `TREE: ${xgb.trees.length.toString().padStart(2, '0')}`;
|
| 616 |
+
ui.lossDisplay.innerText = `MSE: ${loss.toFixed(6)}`;
|
| 617 |
+
ui.statTrees.innerText = xgb.trees.length;
|
| 618 |
+
|
| 619 |
+
const conv = (1 / (1 + loss)).toFixed(2);
|
| 620 |
+
ui.statConv.innerText = `${(conv * 100).toFixed(0)}%`;
|
| 621 |
+
|
| 622 |
+
if (loss < 0.0005) {
|
| 623 |
+
stopTraining();
|
| 624 |
+
showVictory();
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
let trainInterval;
|
| 629 |
+
function startTraining() {
|
| 630 |
+
if (isTraining) {
|
| 631 |
+
stopTraining();
|
| 632 |
+
} else {
|
| 633 |
+
isTraining = true;
|
| 634 |
+
ui.startBtn.innerHTML = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> PAUSE`;
|
| 635 |
+
ui.startBtn.classList.replace('bg-cyan-600', 'bg-purple-600');
|
| 636 |
+
trainInterval = setInterval(performStep, trainingDelay);
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
function stopTraining() {
|
| 641 |
+
isTraining = false;
|
| 642 |
+
ui.startBtn.innerHTML = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M4.5 3.5v13l11-6.5-11-6.5z"/></svg> TRAIN`;
|
| 643 |
+
ui.startBtn.classList.replace('bg-purple-600', 'bg-cyan-600');
|
| 644 |
+
clearInterval(trainInterval);
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
function resetSimulation() {
|
| 648 |
+
stopTraining();
|
| 649 |
+
setupData();
|
| 650 |
+
updateSurface();
|
| 651 |
+
ui.iterDisplay.innerText = `TREE: 00`;
|
| 652 |
+
ui.lossDisplay.innerText = `MSE: 0.0000`;
|
| 653 |
+
ui.statTrees.innerText = '0';
|
| 654 |
+
ui.statConv.innerText = '---';
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
function showVictory() {
|
| 658 |
+
const lastLoss = ui.lossDisplay.innerText.split(': ')[1];
|
| 659 |
+
ui.finalTrees.innerText = xgb.trees.length;
|
| 660 |
+
ui.finalLoss.innerText = lastLoss;
|
| 661 |
+
ui.victoryModal.style.display = 'flex';
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
ui.startBtn.addEventListener('click', startTraining);
|
| 665 |
+
ui.stepBtn.addEventListener('click', performStep);
|
| 666 |
+
ui.resetBtn.addEventListener('click', resetSimulation);
|
| 667 |
+
document.getElementById('close-modal').addEventListener('click', () => {
|
| 668 |
+
ui.victoryModal.style.display = 'none';
|
| 669 |
+
});
|
| 670 |
+
|
| 671 |
+
window.addEventListener('resize', () => {
|
| 672 |
+
camera.aspect = container.clientWidth / container.clientHeight;
|
| 673 |
+
camera.updateProjectionMatrix();
|
| 674 |
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 675 |
+
});
|
| 676 |
+
|
| 677 |
+
container.addEventListener('mousemove', (e) => {
|
| 678 |
+
const rect = container.getBoundingClientRect();
|
| 679 |
+
const x = ((e.clientX - rect.left) / container.clientWidth) * 4 - 2;
|
| 680 |
+
const z = ((e.clientY - rect.top) / container.clientHeight) * 4 - 2;
|
| 681 |
+
const y = xgb.predict([x, z]);
|
| 682 |
+
ui.coordReadout.innerText = `X: ${x.toFixed(2)} | Y: ${y.toFixed(2)} | Z: ${z.toFixed(2)}`;
|
| 683 |
+
});
|
| 684 |
+
|
| 685 |
+
setInterval(() => {
|
| 686 |
+
const now = new Date();
|
| 687 |
+
ui.clock.innerText = now.toTimeString().split(' ')[0];
|
| 688 |
+
}, 1000);
|
| 689 |
+
|
| 690 |
+
window.onload = initThree;
|
| 691 |
+
</script>
|
| 692 |
+
</body>
|
| 693 |
+
</html>
|